Skip to main content
localization **Domain**: Software localization, internationalization, multilingual application development
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/fabioc-aloha/Alex_Plug_In --skill localizationThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository name localization description **Domain**: Software localization, internationalization, multilingual application development tier extended applyTo **/*i18n*,**/*l10n*,**/*localization*,**/*translation*
Localization & Internationalization (i18n/l10n)
Domain : Software localization, internationalization, multilingual application development
Activation : Language detection, translation systems, RTL support, ICU MessageFormat, LQA, dialect inheritance
Version : 1.1.0
Activation Triggers
"localization", "internationalization", "i18n", "l10n"
"translate", "translation", "multilingual"
"language detection", "Accept-Language", "locale"
"RTL", "right-to-left", "Arabic", "Hebrew"
"ICU MessageFormat", "pluralization"
"font loading", "CJK", "Unicode"
"translation quality", "LQA"
"dialect", "dialect inheritance", "regional variant"
"Açoriano", "Azores Portuguese", "Manezinho", "Florianópolis"
"Portuguese dialects", "pt-BR variant", "language genealogy"
Core Concepts
Terminology Distinction
Engineering architecture to support multiple locales
l10n Localization Adapting content for specific locales
Locale Language + Region en-US, pt-BR, zh-Hans
BCP 47 Language tag standard RFC 5646 compliant tags
BCP 47 Language Tags language[-script][-region][-variant]
Examples:
- en → English (generic)
- en-US → English (United States)
- en-GB → English (United Kingdom)
- pt-BR → Portuguese (Brazil)
- zh-Hans → Chinese (Simplified script)
- zh-Hant → Chinese (Traditional script)
- sr-Latn → Serbian (Latin script)
- sr-Cyrl → Serbian (Cyrillic script)
Use hyphen (-), not underscore (_)
Lowercase language, titlecase script, uppercase region
Keep tags minimal (no script unless disambiguating)
Language Detection
Detection Cascade (Priority Order) ┌─────────────────────────────────────────────┐
│ 1. User Explicit Selection (highest) │
│ └── Stored preference in database │
├─────────────────────────────────────────────┤
│ 2. Identity Provider Profile │
│ └── MS Graph preferredLanguage │
│ └── OIDC locale claim │
├─────────────────────────────────────────────┤
│ 3. Organization/Tenant Default │
│ └── Admin-configured default │
├─────────────────────────────────────────────┤
│ 4. Accept-Language Header │
│ └── Browser/OS setting (privacy-safe) │
├─────────────────────────────────────────────┤
│ 5. navigator.language (client-side) │
│ └── Fallback for SPA interactions │
├─────────────────────────────────────────────┤
│ 6. Default Locale (lowest) │
│ └── Application default (usually 'en') │
└─────────────────────────────────────────────┘
Accept-Language Parsing import { match } from '@formatjs/intl-localematcher' ;
import Negotiator from 'negotiator' ;
const SUPPORTED_LOCALES = ['en' , 'en-GB' , 'es' , 'pt-BR' , 'fr' , 'de' , 'ja' , 'ko' , 'zh-Hans' , 'zh-Hant' ];
const DEFAULT_LOCALE = 'en' ;
function detectLanguageFromHeader (acceptLanguage : string | null ): string {
if (!acceptLanguage) return DEFAULT_LOCALE ;
try {
const negotiator = new Negotiator ({
headers : { 'accept-language' : acceptLanguage }
});
const languages = negotiator.languages ();
return match (languages, SUPPORTED_LOCALES , DEFAULT_LOCALE );
} catch {
return DEFAULT_LOCALE ;
}
}
CLDR Fallback Chains const LOCALE_FALLBACKS : Record <string , string []> = {
'pt-BR' : ['pt-BR' , 'pt' , 'en' ],
'pt-PT' : ['pt-PT' , 'pt' , 'en' ],
'zh-Hans' : ['zh-Hans' , 'zh' , 'en' ],
'zh-Hant' : ['zh-Hant' , 'zh' , 'en' ],
'en-GB' : ['en-GB' , 'en' ],
'fr-CA' : ['fr-CA' , 'fr' , 'en' ],
'es-419' : ['es-419' , 'es' , 'en' ],
};
function getWithFallback<T>(
key : string ,
locale : string ,
translations : Record <string , Record <string , T>>
): T | undefined {
const chain = LOCALE_FALLBACKS [locale] || [locale, 'en' ];
for (const loc of chain) {
if (translations[loc]?.[key]) {
return translations[loc][key];
}
}
return undefined ;
}
Dialect Inheritance Architecture
Cross-Domain Insight: Dialects as Inheritance Trees Regional dialect variants mirror software inheritance patterns . Understanding this enables elegant multi-variant localization:
OOP Concept Dialect Analog Example Base class Parent dialect Açoriano (Azores Portuguese) Derived class Regional variant Manezinho (Florianópolis) Inherited members Preserved features "tu" pronoun, maritime vocabulary Method overrides Modified features Simplified verb conjugations New members Local additions Gaúcho influences Abstract class Proto-language Common ancestor
Real Example: Portuguese Dialect Genealogy pt (Portuguese)
├── pt-PT (European Portuguese)
│ ├── pt-PT-azores (Açoriano)
│ │ └── pt-BR-floripa (Manezinho) ← Migration 1748-1756
│ └── pt-PT-madeira (Madeirense)
└── pt-BR (Brazilian Portuguese)
├── pt-BR-floripa (Manezinho) ← Also inherits from Açoriano!
├── pt-BR-nordeste (Nordestino)
└── pt-BR-gaucho (Gaúcho)
Key Insight : Manezinho demonstrates multiple inheritance — it inherits from both Açoriano (historical migration) and Brazilian Portuguese (geographic context).
Dialect-Aware Fallback Chain
const DIALECT_FALLBACKS : Record <string , string []> = {
'pt-BR-floripa' : ['pt-BR-floripa' , 'pt-PT-azores' , 'pt-BR' , 'pt' , 'en' ],
'pt-PT-azores' : ['pt-PT-azores' , 'pt-PT' , 'pt' , 'en' ],
'pt-BR-nordeste' : ['pt-BR-nordeste' , 'pt-BR' , 'pt' , 'en' ],
'es-419' : ['es-419' , 'es' , 'en' ],
'es-MX' : ['es-MX' , 'es-419' , 'es' , 'en' ],
'es-AR' : ['es-AR' , 'es-419' , 'es' , 'en' ],
'de-AT' : ['de-AT' , 'de' , 'en' ],
'de-CH' : ['de-CH' , 'de' , 'en' ],
'zh-Hans' : ['zh-Hans' , 'zh' , 'en' ],
'zh-Hant' : ['zh-Hant' , 'zh' , 'en' ],
'zh-Hant-HK' : ['zh-Hant-HK' , 'zh-Hant' , 'zh' , 'en' ],
};
Dialect Feature Overrides interface DialectFeatures {
pronouns : {
informal : string ;
formalAddress : string ;
};
verbConjugation : 'full' | 'simplified' | 'merged' ;
vocabulary : Record <string , string >;
prosody : string ;
}
const DIALECT_FEATURES : Record <string , Partial <DialectFeatures >> = {
'pt' : {
pronouns : { informal : 'tu' , formalAddress : 'o senhor' },
verbConjugation : 'full' ,
},
'pt-BR' : {
pronouns : { informal : 'você' , formalAddress : 'o senhor' },
verbConjugation : 'simplified' ,
},
'pt-PT-azores' : {
pronouns : { informal : 'tu' , formalAddress : 'vossemecê' },
verbConjugation : 'full' ,
vocabulary : {
'ocean' : 'mar alto' ,
'storm' : 'temporal' ,
'boat' : 'canoa' ,
},
prosody : 'slower cadence, vowel reduction' ,
},
'pt-BR-floripa' : {
pronouns : { informal : 'tu' , formalAddress : 'o senhor' },
verbConjugation : 'merged' ,
vocabulary : {
'ocean' : 'mar alto' ,
'beach' : 'praia' ,
'bus' : 'baleeira' ,
},
prosody : 'sing-song intonation (from Azorean heritage)' ,
},
};
function getDialectFeatures (locale : string ): DialectFeatures {
const chain = DIALECT_FALLBACKS [locale] || [locale, 'en' ];
const merged : Partial <DialectFeatures > = {};
for (const loc of [...chain].reverse ()) {
const features = DIALECT_FEATURES [loc];
if (features) {
Object .assign (merged, features);
if (features.vocabulary ) {
merged.vocabulary = { ...merged.vocabulary , ...features.vocabulary };
}
}
}
return merged as DialectFeatures ;
}
When to Use Dialect-Level Localization Use Case Recommendation Marketing/brand voice ✅ Dialect-specific for authenticity Legal/compliance text ❌ Use standard regional variant UI labels ⚠️ Only if market requires (Catalan in Barcelona) User-generated content ✅ Let users express naturally Documentation ⚠️ Standard variant, note dialect differences
Historical Linguistics as Requirements Dialect inheritance isn't arbitrary — it reflects real migration patterns :
1748-1756 : Azorean migration to Santa Catarina, Brazil → Manezinho
16th century : Portuguese colonization → Brazilian Portuguese branches
19th century : Italian/German immigration to Southern Brazil → Gaúcho influences
Understanding these migrations helps predict which features a dialect inherits vs. develops independently.
Translation Patterns
Basic Translation Function type TranslationKey = keyof typeof translations.en ;
function t (
key : TranslationKey ,
locale : string ,
replacements ?: Record <string , string | number >
): string {
const chain = LOCALE_FALLBACKS [locale] || [locale, 'en' ];
for (const loc of chain) {
const translation = translations[loc]?.[key];
if (translation) {
return applyReplacements (translation, replacements);
}
}
console .warn (`Missing translation: ${key} for ${locale} ` );
return key;
}
function applyReplacements (
text : string ,
replacements ?: Record <string , string | number >
): string {
if (!replacements) return text;
return Object .entries (replacements).reduce (
(result, [key, value] ) => result.replace (`{${key} }` , String (value)),
text
);
}
ICU MessageFormat (Pluralization) import IntlMessageFormat from 'intl-messageformat' ;
const PLURAL_MESSAGES = {
en : {
items : '{count, plural, one {# item} other {# items}}' ,
members : '{count, plural, one {# member} other {# members}}' ,
},
pl : {
items : '{count, plural, one {# element} few {# elementy} many {# elementów} other {# elementu}}' ,
},
ar : {
items : '{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}' ,
},
};
function formatPlural (key : string , count : number , locale : string ): string {
const messages = PLURAL_MESSAGES [locale] || PLURAL_MESSAGES .en ;
const message = messages[key] || PLURAL_MESSAGES .en [key];
const formatter = new IntlMessageFormat (message, locale);
return formatter.format ({ count }) as string ;
}
Date/Time/Number Formatting
function formatDate (date : Date , locale : string , style : 'full' | 'long' | 'medium' | 'short' = 'long' ): string {
return new Intl .DateTimeFormat (locale, { dateStyle : style }).format (date);
}
function formatTime (date : Date , locale : string , style : 'full' | 'long' | 'medium' | 'short' = 'short' ): string {
return new Intl .DateTimeFormat (locale, { timeStyle : style }).format (date);
}
function formatNumber (value : number , locale : string , options ?: Intl .NumberFormatOptions ): string {
return new Intl .NumberFormat (locale, options).format (value);
}
function formatPercent (value : number , locale : string ): string {
return new Intl .NumberFormat (locale, {
style : 'percent' ,
maximumFractionDigits : 0 ,
}).format (value / 100 );
}
function formatCurrency (value : number , locale : string , currency : string ): string {
return new Intl .NumberFormat (locale, {
style : 'currency' ,
currency,
}).format (value);
}
RTL (Right-to-Left) Support
RTL Languages const RTL_LANGUAGES = ['ar' , 'he' , 'fa' , 'ur' , 'yi' , 'ps' , 'sd' ];
function isRTL (locale : string ): boolean {
const lang = locale.split ('-' )[0 ];
return RTL_LANGUAGES .includes (lang);
}
function getDirection (locale : string ): 'ltr' | 'rtl' {
return isRTL (locale) ? 'rtl' : 'ltr' ;
}
function getTextAlign (locale : string ): 'left' | 'right' | 'start' {
return 'start' ;
}
CSS Logical Properties (RTL-Safe)
.container {
margin-left : 1rem ;
padding-right : 2rem ;
text-align : left;
border-left : 1px solid;
}
.container {
margin-inline-start : 1rem ;
padding-inline-end : 2rem ;
text-align : start;
border-inline-start : 1px solid;
}
Tailwind RTL Plugin
module .exports = {
plugins : [
require ('tailwindcss-rtl' ),
],
}
<div className="ml-4 rtl:mr-4 rtl:ml-0" >
Content with RTL -aware margins
</div>
Document Direction
function setDocumentDirection (locale : string ): void {
document .documentElement .dir = getDirection (locale);
document .documentElement .lang = locale;
}
Font Handling
Unicode Coverage by Script Script Languages Recommended Font Latin en, es, fr, de, pt, it, nl, pl, sv, etc. Inter, Noto Sans Latin Extended cs, hu, hr, ro, tr, vi Noto Sans Cyrillic ru, uk, bg, sr-Cyrl Noto Sans CJK - Japanese ja Noto Sans JP CJK - Korean ko Noto Sans KR CJK - Simplified zh-Hans Noto Sans SC CJK - Traditional zh-Hant Noto Sans TC Thai th Noto Sans Thai Arabic ar Noto Sans Arabic Hebrew he Noto Sans Hebrew Devanagari hi, mr, ne Noto Sans Devanagari
PDF Font Registration (react-pdf) import { Font } from '@react-pdf/renderer' ;
Font .register ({
family : 'Noto Sans' ,
fonts : [
{ src : 'https://fonts.gstatic.com/s/notosans/v36/...-Regular.ttf' , fontWeight : 400 },
{ src : 'https://fonts.gstatic.com/s/notosans/v36/...-Bold.ttf' , fontWeight : 700 },
],
});
Font .register ({
family : 'Noto Sans JP' ,
src : 'https://fonts.gstatic.com/s/notosansjp/v52/...-Regular.otf' ,
});
Font .register ({
family : 'Noto Sans SC' ,
src : 'https://fonts.gstatic.com/s/notosanssc/v36/...-Regular.otf' ,
});
function getFontFamily (locale : string ): string {
const lang = locale.split ('-' )[0 ];
switch (lang) {
case 'ja' : return 'Noto Sans JP' ;
case 'ko' : return 'Noto Sans KR' ;
case 'zh' : return locale.includes ('Hant' ) ? 'Noto Sans TC' : 'Noto Sans SC' ;
case 'th' : return 'Noto Sans Thai' ;
case 'ar' : return 'Noto Sans Arabic' ;
case 'he' : return 'Noto Sans Hebrew' ;
case 'hi' : return 'Noto Sans Devanagari' ;
default : return 'Noto Sans' ;
}
}
Web Font Loading Strategy
<link rel="preload" href="/fonts/NotoSans-Regular.woff2" as ="font" type ="font/woff2" crossOrigin="anonymous" />
@font -face {
font-family : 'Noto Sans' ;
src : url ('/fonts/NotoSans-Regular.woff2' ) format ('woff2' );
font-display : swap;
}
Text Expansion & Contraction
Expansion Ratios by Language Language vs English Example German +30% "Settings" → "Einstellungen" Finnish +30-40% Complex compound words French +15-20% Longer phrases Spanish +20-25% Gender agreement Italian +15% Russian +15% Arabic +25% Plus RTL Chinese -30% "Download" → "下载" Japanese -10-20% Korean -10%
UI Design Guidelines
.button { width : 100px ; }
.button {
min-width : 80px ;
max-width : 200px ;
padding-inline : 1rem ;
}
.label {
max-width : 100% ;
overflow : hidden;
text-overflow : ellipsis;
white-space : nowrap;
}
Pseudo-Localization (Testing) function pseudoLocalize (text : string ): string {
const placeholders : string [] = [];
let processed = text.replace (/\{[^}]+\}/g , (match ) => {
placeholders.push (match);
return `__PH${placeholders.length - 1 } __` ;
});
processed = processed
.replace (/a/g , 'à' ).replace (/A/g , 'À' )
.replace (/e/g , 'é' ).replace (/E/g , 'É' )
.replace (/i/g , 'í' ).replace (/I/g , 'Í' )
.replace (/o/g , 'ó' ).replace (/O/g , 'Ó' )
.replace (/u/g , 'ú' ).replace (/U/g , 'Ú' );
const padding = '~' .repeat (Math .ceil (processed.length * 0.3 ));
processed = `[${processed} ${padding} ]` ;
placeholders.forEach ((ph, i ) => {
processed = processed.replace (`__PH${i} __` , ph);
});
return processed;
}
Reveals untranslated/hardcoded strings (missing [...] markers)
Tests text expansion before real translations
Works in development without translation files
Identifies layout issues early
Next.js i18n Setup
Middleware Configuration
import createMiddleware from 'next-intl/middleware' ;
export default createMiddleware ({
locales : ['en' , 'en-GB' , 'es' , 'pt-BR' , 'fr' , 'de' , 'ja' , 'ko' , 'zh-Hans' , 'zh-Hant' ],
defaultLocale : 'en' ,
localeDetection : true ,
});
export const config = {
matcher : ['/((?!api|_next|_vercel|.*\\..*).*)' ],
};
Request Configuration
import { getRequestConfig } from 'next-intl/server' ;
import { headers } from 'next/headers' ;
export default getRequestConfig (async () => {
const headersList = headers ();
const acceptLanguage = headersList.get ('accept-language' );
const locale = detectLanguageFromHeader (acceptLanguage);
return {
locale,
messages : (await import (`@/messages/${locale} .json` )).default ,
};
});
URL Strategy Options Strategy Example Pros Cons Subpath /es/dashboardSEO-friendly, shareable Routing complexity Query param /dashboard?lang=esSimple Not SEO-friendly Cookie/Header Auto-detect Seamless Can't share language links Subdomain es.example.comClear separation DNS/hosting complexity
Recommendation: Subpath for public pages, cookie for app pages.
Localization Quality Assurance (LQA)
LQA Dimensions Dimension Focus Example Issues Linguistic Grammar, terminology "Guardar" vs "Salvar" (regional Spanish) Functional Layout, encoding Text truncation, broken characters Cultural Appropriateness Colors, icons, date formats Legal Compliance GDPR wording, privacy notices Technical Performance RTL rendering, font loading
Automated Testing describe ('Localization QA' , () => {
const SUPPORTED_LOCALES = ['en' , 'es' , 'pt-BR' , 'fr' , 'de' , 'ja' ];
test.each (SUPPORTED_LOCALES )('%s has all translation keys' , (locale ) => {
const baseKeys = Object .keys (translations.en );
const localeKeys = Object .keys (translations[locale] || {});
const missing = baseKeys.filter (key => !localeKeys.includes (key));
expect (missing).toEqual ([]);
});
test.each (SUPPORTED_LOCALES )('%s has no empty strings' , (locale ) => {
const empty = Object .entries (translations[locale] || {})
.filter (([_, v] ) => !v?.trim ())
.map (([k] ) => k);
expect (empty).toEqual ([]);
});
test.each (SUPPORTED_LOCALES )('%s has matching placeholders' , (locale ) => {
const issues : string [] = [];
Object .entries (translations.en ).forEach (([key, enValue] ) => {
const enPlaceholders = (enValue.match (/\{[^}]+\}/g ) || []).sort ();
const locValue = translations[locale]?.[key] || '' ;
const locPlaceholders = (locValue.match (/\{[^}]+\}/g ) || []).sort ();
if (JSON .stringify (enPlaceholders) !== JSON .stringify (locPlaceholders)) {
issues.push (key);
}
});
expect (issues).toEqual ([]);
});
});
Review Levels Level Reviewer When L1: Automated CI/CD Every PR L2: Developer Engineer Before merge L3: Linguistic Native speaker Before release L4: Professional Translation agency Major releases L5: In-market Local users Post-release
Bug Severity Classification Severity Examples SLA Critical Legal text incorrect, offensive content Fix immediately Major UI broken, text truncated, missing strings Fix before release Minor Typo, awkward phrasing Fix in next release Preferential Style choice, wording preference Consider for future
Terminology Management
Glossary Pattern const GLOSSARY = {
en : {
assessment : 'Assessment' ,
dashboard : 'Dashboard' ,
settings : 'Settings' ,
},
'pt-BR' : {
assessment : 'Avaliação' ,
dashboard : 'Painel' ,
settings : 'Configurações' ,
},
es : {
assessment : 'Evaluación' ,
dashboard : 'Panel' ,
settings : 'Configuración' ,
},
};
const DO_NOT_TRANSLATE = [
'Microsoft' ,
'Azure' ,
'Copilot' ,
'AIRS' ,
'AIRS-16' ,
'PDF' ,
'API' ,
];
Recommended Libraries Package Purpose Size next-intlNext.js App Router i18n ~8KB @formatjs/intl-localematcherBCP 47 locale matching ~2KB negotiatorAccept-Language parsing ~3KB intl-messageformatICU MessageFormat (plurals) ~15KB rtl-detectDetect RTL languages ~1KB tailwindcss-rtlTailwind RTL utilities Dev only
File Structure src/
├── i18n/
│ ├── config.ts # Supported locales, defaults
│ ├── request.ts # next-intl request config
│ ├── detect-language.ts # Detection utilities
│ ├── formatters.ts # Date/number/currency formatters
│ └── rtl.ts # RTL utilities
├── messages/
│ ├── en.json # English (base)
│ ├── en-GB.json # British English
│ ├── es.json # Spanish
│ ├── pt-BR.json # Portuguese (Brazil)
│ ├── fr.json # French
│ ├── de.json # German
│ ├── ja.json # Japanese
│ ├── ko.json # Korean
│ ├── zh-Hans.json # Chinese (Simplified)
│ └── zh-Hant.json # Chinese (Traditional)
└── lib/
└── pdf/
├── fonts.ts # Font registration
└── translations.ts # PDF-specific translations
Enterprise Localization Architecture
4-Phase Implementation Localization effort scales non-linearly. Plan accordingly:
Phase Scope Effort Output 1. Foundation i18n architecture, string extraction 10-15 hours Translatable codebase 2. First Language One target language end-to-end 20-40 hours Proof of concept 3. Scale 5-10 languages, professional translation 80-120 hours Market coverage 4. Maintenance Ongoing string management, LQA 200+ hours/year Sustained quality
Key insight : Phase 1 (foundation) is the critical path. A well-architected Phase 1 makes Phases 2-4 incremental. A poor Phase 1 requires rework at every subsequent phase.
Text Expansion Ratios Translations are almost never the same length as English source:
Target Language Expansion Example German +30% "Settings" → "Einstellungen" French +20% "File" → "Fichier" Japanese -30% "Settings" → "設定" Arabic +25% Variable depending on formality Portuguese (BR) +20% "Dashboard" → "Painel de Controle" Chinese -50% "Configuration" → "配置"
UI Impact : Design with 40% expansion budget for Western languages. Use min-width not width for buttons and labels.
Browser Language Detection with sessionStorage Bridge For SPAs that need consistent language across page navigations:
function detectAndPersistLanguage ( ): string {
const stored = sessionStorage .getItem ('detected-language' );
if (stored && SUPPORTED_LOCALES .includes (stored)) return stored;
const browserLang = navigator.language || navigator.languages ?.[0 ] || 'en' ;
const matched = match ([browserLang], SUPPORTED_LOCALES , DEFAULT_LOCALE );
sessionStorage .setItem ('detected-language' , matched);
return matched;
}
Why sessionStorage : navigator.language can differ between tabs (user changed browser settings). sessionStorage keeps the session consistent while allowing fresh detection on new sessions.
Translation Quality Assurance (LQA)
Severity Tiers Tier Severity Example SLA Critical Meaning changed or lost "Delete" translated as "Save" Immediate fix Major Confusing or misleading Gender/formality wrong for audience Before release Minor Grammatically awkward but understandable Unnatural word order Next sprint Preferential Style/tone preference Formal vs informal register Backlog
Language-Specific Intelligence Each language has unique QA concerns:
Language Common Issues Check For German Compound nouns, gendered articles Noun capitalization, article agreement French Formal/informal "you" (vous/tu) Consistent register throughout Japanese Honorific levels (keigo) Appropriate formality for context Arabic Dual plural form, RTL numbers Mixed LTR/RTL in technical content Portuguese BR vs PT vocabulary "Tela" (BR) vs "Ecrã" (PT) for "screen" Chinese Simplified vs Traditional Wrong variant for target market
Pseudo-Localization Testing Before real translations, use pseudo-loc to find i18n bugs:
function pseudoLocalize (text : string ): string {
const charMap : Record <string , string > = {
'a' : 'á' , 'e' : 'é' , 'i' : 'í' , 'o' : 'ó' , 'u' : 'ú' ,
'A' : 'Á' , 'E' : 'É' , 'I' : 'Í' , 'O' : 'Ó' , 'U' : 'Ú' ,
};
const accented = text.split ('' ).map (c => charMap[c] || c).join ('' );
const expanded = accented + ' +++' ;
return `[${expanded} ]` ;
}
Hardcoded strings (no brackets = not going through i18n)
Truncated text (expansion breaks UI)
Encoding issues (accented chars display wrong)
Concatenated strings (brackets in wrong place)
Quick Reference
Language Switcher UX
Show native language names ("Deutsch" not "German")
Use flag + name (flags alone are problematic)
Group by region for long lists
Make switcher visible in header
Instant switch (no page reload)
Hide in footer or settings
Use flags alone (languages ≠ countries)
Auto-switch without confirmation
Use English names for languages
Privacy Considerations All detection methods should be GDPR/privacy compliant:
✅ User selection (explicit consent)
✅ Accept-Language header (browser setting, not PII)
✅ Organization default (no personal data)
❌ IP-based geolocation (PII, avoid)
Related occupations SOC
Based on SOC occupation classification