Software Localisation - Quick Reference Production patterns for internationalisation (i18n) and localisation (l10n) in modern web applications. Covers library selection, translation management, ICU message format, RTL support, and CI/CD workflows. Snapshot (2026-02) : i18next 25.x, react-i18next 16.x, react-intl 8.x, vue-i18n 11.x, next-intl 4.x, @angular/localize 21.x. Always verify current versions in the target repo (see Currency Check Protocol). Authoritative References : - i18next Documentation - FormatJS/react-intl - ICU Message Format - MDN Intl API Quick Reference | Task | Tool/Librar…

+ amount.toFixed(2);\n\n// PASS Intl formatting\nconst price = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n}).format(amount);\n```\n\n### Cache Formatter Instances\n\n```typescript\n// FAIL Creating new formatter each render\nfunction formatDate(date: Date) {\n return new Intl.DateTimeFormat('en-US').format(date);\n}\n\n// PASS Cache formatter\nconst dateFormatter = new Intl.DateTimeFormat('en-US');\nfunction formatDate(date: Date) {\n return dateFormatter.format(date);\n}\n\n// PASS Or use a cache map for multiple locales\nconst formatters = new Map\u003cstring, Intl.DateTimeFormat>();\n\nfunction getDateFormatter(locale: string): Intl.DateTimeFormat {\n if (!formatters.has(locale)) {\n formatters.set(locale, new Intl.DateTimeFormat(locale));\n }\n return formatters.get(locale)!;\n}\n```\n\n### Handle Timezone Consistently\n\n```typescript\n// Store dates in UTC (ISO 8601)\nconst storedDate = '2025-01-15T14:30:00Z';\n\n// Display in user's timezone\nnew Intl.DateTimeFormat('en-US', {\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n dateStyle: 'medium',\n timeStyle: 'short',\n}).format(new Date(storedDate));\n```\n\n### Test with Multiple Locales\n\n```typescript\nconst testLocales = ['en-US', 'de-DE', 'ar-SA', 'ja-JP', 'he-IL'];\n\ntestLocales.forEach((locale) => {\n describe(`Locale: ${locale}`, () => {\n it('formats currency correctly', () => {\n const formatted = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency: 'USD',\n }).format(1234.56);\n\n expect(formatted).toBeTruthy();\n expect(formatted.length).toBeGreaterThan(0);\n });\n });\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17723,"content_sha256":"9431e1deb1886e0669c1946ebcf3cc669812fad903c08f453b3dd49c81ccf2d9"},{"filename":"references/ops-runbook.md","content":"# Ops Runbook: Large Locale Catalogs (LLM-Safe)\n\nUse this when locale catalogs are too large for single reads, mixed-language UI appears, or missing keys are reported.\n\n## 90-Second Triage\n\n```bash\n# 1) Confirm locale file layout\nrg --files src/messages | sort\n\n# 2) Detect oversized catalogs before reading\nwc -l src/messages/en/*.json src/messages/*/*.json | sort -nr | head\n\n# 3) Chunk reads for large files (avoid tool limits)\nsed -n '1,200p' src/messages/en/landing.json\nsed -n '201,400p' src/messages/en/landing.json\n```\n\n## Key Parity Check (Base vs Target Locale)\n\n```bash\nBASE=en\nTARGET=ru\n\njq -r 'paths(scalars) | join(\".\")' src/messages/$BASE/*.json | sort -u > /tmp/$BASE.keys\njq -r 'paths(scalars) | join(\".\")' src/messages/$TARGET/*.json | sort -u > /tmp/$TARGET.keys\n\n# Missing in target\ncomm -23 /tmp/$BASE.keys /tmp/$TARGET.keys\n\n# Extra in target\ncomm -13 /tmp/$BASE.keys /tmp/$TARGET.keys\n```\n\n## Hardcoded UI String Sweep\n\n```bash\n# TSX/TS hardcoded literals (quick heuristic)\nrg -n --pcre2 '\"[A-Za-z][^\"\\n]{2,}\"' src --glob '*.tsx' --glob '*.ts'\n\n# JSX text nodes\nrg -n --pcre2 '>[A-Za-z][^\u003c]{2,}\u003c' src --glob '*.tsx'\n```\n\n## CI Gate Pattern (No Mixed Language)\n\n```bash\n# Fail build if known missing-key sentinel appears\nrg -n '__MISSING_I18N__|TODO_TRANSLATE' src/messages && exit 1 || true\n\n# Optional: block English fallback on localized, indexable routes\nrg -n 'fallback.*en|defaultLocale.*en' src/app src/lib\n```\n\n## Operational Rules\n\n- Never read large locale files in one shot; always chunk.\n- Use key diff first, translation pass second.\n- Treat marketing/SEO locale key gaps as release blockers.\n- Do not auto-insert machine translations without a tracked review pass.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1701,"content_sha256":"2110007a23f703cfccabefe49a15dbcf58af9f9123feb9fe77ecf9933ad00d70"},{"filename":"references/rtl-support.md","content":"# Right-to-Left (RTL) Language Support\n\nProduction patterns for Arabic, Hebrew, Persian, and other RTL languages.\n\n---\n\n## Core Concepts\n\n### RTL Languages\n\n| Language | Code | Script Direction | Notes |\n|----------|------|------------------|-------|\n| Arabic | ar | RTL | Most common RTL language |\n| Hebrew | he | RTL | Israel, Jewish communities |\n| Persian (Farsi) | fa | RTL | Iran, Afghanistan |\n| Urdu | ur | RTL | Pakistan, India |\n| Pashto | ps | RTL | Afghanistan, Pakistan |\n| Kurdish (Sorani) | ckb | RTL | Iraq, Iran |\n| Yiddish | yi | RTL | Jewish diaspora |\n| Sindhi | sd | RTL | Pakistan, India |\n\n### Bidirectional (BiDi) Text\n\nMixed LTR and RTL content in the same document:\n\n```text\nRTL sentence: \"مرحبا بك في موقعنا\"\nLTR in RTL: \"مرحبا بك في React 19\"\nNumbers: \"السعر: $99.99\" (numbers stay LTR)\n```\n\n---\n\n## CSS Logical Properties\n\nReplace physical properties (left/right) with logical ones (start/end).\n\n### Property Mapping\n\n| Physical (LTR) | Logical | RTL Equivalent |\n|----------------|---------|----------------|\n| `margin-left` | `margin-inline-start` | `margin-right` |\n| `margin-right` | `margin-inline-end` | `margin-left` |\n| `padding-left` | `padding-inline-start` | `padding-right` |\n| `padding-right` | `padding-inline-end` | `padding-left` |\n| `left` | `inset-inline-start` | `right` |\n| `right` | `inset-inline-end` | `left` |\n| `text-align: left` | `text-align: start` | `text-align: right` |\n| `text-align: right` | `text-align: end` | `text-align: left` |\n| `border-left` | `border-inline-start` | `border-right` |\n| `float: left` | `float: inline-start` | `float: right` |\n\n### Implementation\n\n```css\n/* FAIL Physical properties (breaks RTL) */\n.sidebar {\n margin-left: 1rem;\n padding-right: 2rem;\n border-left: 1px solid #ccc;\n text-align: left;\n}\n\n/* PASS Logical properties (works for LTR and RTL) */\n.sidebar {\n margin-inline-start: 1rem;\n padding-inline-end: 2rem;\n border-inline-start: 1px solid #ccc;\n text-align: start;\n}\n```\n\n### Flexbox and Grid\n\n```css\n/* Flexbox automatically respects direction */\n.nav {\n display: flex;\n gap: 1rem;\n}\n\n/* Grid with logical alignment */\n.grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n justify-items: start; /* Logical, respects RTL */\n}\n```\n\n---\n\n## HTML dir Attribute\n\n### Document-Level\n\n```html\n\u003c!DOCTYPE html>\n\u003chtml lang=\"ar\" dir=\"rtl\">\n \u003chead>\n \u003cmeta charset=\"UTF-8\" />\n \u003c/head>\n \u003cbody>\n \u003c!-- All content flows RTL -->\n \u003c/body>\n\u003c/html>\n```\n\n### Dynamic Direction (React)\n\n```tsx\n// components/RootLayout.tsx\nimport { useLocale } from 'next-intl';\n\nconst rtlLocales = ['ar', 'he', 'fa', 'ur'];\n\nexport function RootLayout({ children }: { children: React.ReactNode }) {\n const locale = useLocale();\n const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr';\n\n return (\n \u003chtml lang={locale} dir={dir}>\n \u003cbody>{children}\u003c/body>\n \u003c/html>\n );\n}\n```\n\n### Dynamic Direction (Vue)\n\n```vue\n\u003cscript setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n';\nimport { computed, watchEffect } from 'vue';\n\nconst { locale } = useI18n();\nconst rtlLocales = ['ar', 'he', 'fa'];\n\nconst dir = computed(() => (rtlLocales.includes(locale.value) ? 'rtl' : 'ltr'));\n\nwatchEffect(() => {\n document.documentElement.dir = dir.value;\n document.documentElement.lang = locale.value;\n});\n\u003c/script>\n```\n\n### Element-Level Override\n\n```html\n\u003c!-- RTL document with LTR code block -->\n\u003chtml dir=\"rtl\">\n \u003cbody>\n \u003cp>هذا نص عربي\u003c/p>\n \u003cpre dir=\"ltr\">\u003ccode>const x = 1;\u003c/code>\u003c/pre>\n \u003c/body>\n\u003c/html>\n```\n\n---\n\n## Tailwind CSS RTL Support\n\n### Built-in RTL Variants (v3.3+)\n\n```html\n\u003c!-- Automatic with dir=\"rtl\" on parent -->\n\u003cdiv class=\"ml-4 rtl:mr-4 rtl:ml-0\">\n Content with RTL-aware margins\n\u003c/div>\n\n\u003c!-- Or use logical utilities -->\n\u003cdiv class=\"ms-4\">\n \u003c!-- margin-inline-start: 1rem -->\n\u003c/div>\n```\n\n### Logical Utility Classes\n\n```html\n\u003c!-- Logical spacing -->\n\u003cdiv class=\"ps-4 pe-2 ms-auto me-0\">\n \u003c!-- padding-inline-start/end, margin-inline-start/end -->\n\u003c/div>\n\n\u003c!-- Logical borders -->\n\u003cdiv class=\"border-s-2 border-e-0 rounded-s-lg\">\n \u003c!-- border-inline-start/end, border-radius -->\n\u003c/div>\n\n\u003c!-- Logical positioning -->\n\u003cdiv class=\"start-0 end-auto\">\n \u003c!-- inset-inline-start/end -->\n\u003c/div>\n```\n\n### Custom Tailwind Config\n\n```javascript\n// tailwind.config.js\nmodule.exports = {\n theme: {\n extend: {\n // Add custom RTL-aware utilities if needed\n },\n },\n plugins: [\n // Plugin for additional RTL utilities\n function ({ addUtilities }) {\n addUtilities({\n '.flip-horizontal': {\n transform: 'scaleX(-1)',\n },\n '.rtl\\\\:flip-horizontal': {\n '[dir=\"rtl\"] &': {\n transform: 'scaleX(-1)',\n },\n },\n });\n },\n ],\n};\n```\n\n---\n\n## Icons and Images\n\n### When to Mirror\n\n| Element | Mirror? | Example |\n|---------|---------|---------|\n| Directional arrows | Yes | \u003c- -> navigation arrows |\n| Back/forward icons | Yes | Browser navigation |\n| Progress indicators | Yes | Step wizards, progress bars |\n| Sliders | Yes | Range inputs, carousels |\n| Checkmarks | No | Checkmarks are widely understood |\n| Logos | No | Brand identity stays fixed |\n| Photos | No | Real-world images stay fixed |\n| Icons with text | Depends | Clock with numbers? No. Arrow with \"Next\"? Yes |\n\n### CSS Mirroring\n\n```css\n/* Mirror specific icons in RTL */\n[dir='rtl'] .icon-arrow-right {\n transform: scaleX(-1);\n}\n\n/* Or use a utility class */\n.mirror-rtl {\n [dir='rtl'] & {\n transform: scaleX(-1);\n }\n}\n```\n\n### React Component\n\n```tsx\ninterface DirectionalIconProps {\n icon: React.ComponentType\u003c{ className?: string }>;\n className?: string;\n}\n\nexport function DirectionalIcon({ icon: Icon, className }: DirectionalIconProps) {\n return (\n \u003cIcon\n className={cn(\n className,\n 'rtl:scale-x-[-1]' // Tailwind RTL variant\n )}\n />\n );\n}\n\n// Usage\n\u003cDirectionalIcon icon={ArrowRightIcon} className=\"w-5 h-5\" />\n```\n\n---\n\n## Form Inputs\n\n### Input Direction\n\n```html\n\u003c!-- Email/URL always LTR (even in RTL context) -->\n\u003cinput type=\"email\" dir=\"ltr\" class=\"text-left\" />\n\u003cinput type=\"url\" dir=\"ltr\" class=\"text-left\" />\n\n\u003c!-- Phone numbers LTR -->\n\u003cinput type=\"tel\" dir=\"ltr\" class=\"text-left\" />\n\n\u003c!-- Text inputs follow document direction -->\n\u003cinput type=\"text\" />\n\u003c!-- dir inherited from parent -->\n```\n\n### React Form Component\n\n```tsx\ninterface InputProps extends React.InputHTMLAttributes\u003cHTMLInputElement> {\n type?: string;\n}\n\nexport function Input({ type = 'text', ...props }: InputProps) {\n // Force LTR for specific input types\n const forceLtr = ['email', 'url', 'tel', 'number'].includes(type);\n\n return (\n \u003cinput\n type={type}\n dir={forceLtr ? 'ltr' : undefined}\n className={cn(\n 'w-full rounded-md border px-3 py-2',\n forceLtr && 'text-left'\n )}\n {...props}\n />\n );\n}\n```\n\n### Placeholder Direction\n\n```css\n/* RTL placeholder styling */\n[dir='rtl'] input::placeholder {\n text-align: right;\n}\n\n/* LTR inputs in RTL context */\n[dir='rtl'] input[dir='ltr']::placeholder {\n text-align: left;\n}\n```\n\n---\n\n## Tables\n\n### RTL Table Layout\n\n```css\n/* Tables automatically reverse in RTL */\n[dir='rtl'] table {\n /* Columns flow right-to-left automatically */\n}\n\n/* Explicit text alignment */\n[dir='rtl'] th,\n[dir='rtl'] td {\n text-align: right; /* Or use text-align: start */\n}\n\n/* Numbers stay LTR */\n[dir='rtl'] .numeric-column {\n direction: ltr;\n text-align: right; /* Align to start of RTL flow */\n}\n```\n\n### React Table Component\n\n```tsx\ninterface TableCellProps {\n children: React.ReactNode;\n numeric?: boolean;\n}\n\nexport function TableCell({ children, numeric }: TableCellProps) {\n return (\n \u003ctd\n className={cn('px-4 py-2 text-start', numeric && 'font-mono')}\n dir={numeric ? 'ltr' : undefined}\n >\n {children}\n \u003c/td>\n );\n}\n```\n\n---\n\n## Testing RTL\n\n### Browser DevTools\n\n```javascript\n// Toggle RTL in browser console\ndocument.documentElement.dir = 'rtl';\ndocument.documentElement.lang = 'ar';\n\n// Toggle back\ndocument.documentElement.dir = 'ltr';\ndocument.documentElement.lang = 'en';\n```\n\n### Storybook RTL Decorator\n\n```tsx\n// .storybook/preview.tsx\nimport { useEffect } from 'react';\n\nconst withRTL = (Story, context) => {\n const { globals } = context;\n const dir = globals.locale === 'ar' ? 'rtl' : 'ltr';\n\n useEffect(() => {\n document.documentElement.dir = dir;\n }, [dir]);\n\n return \u003cStory />;\n};\n\nexport const decorators = [withRTL];\n\nexport const globalTypes = {\n locale: {\n name: 'Locale',\n defaultValue: 'en',\n toolbar: {\n icon: 'globe',\n items: ['en', 'ar', 'he'],\n },\n },\n};\n```\n\n### Playwright RTL Tests\n\n```typescript\n// tests/rtl.spec.ts\nimport { test, expect } from '@playwright/test';\n\ntest.describe('RTL Support', () => {\n test('layout mirrors correctly in Arabic', async ({ page }) => {\n await page.goto('/ar/dashboard');\n\n // Check document direction\n const html = page.locator('html');\n await expect(html).toHaveAttribute('dir', 'rtl');\n\n // Check sidebar is on the right\n const sidebar = page.locator('[data-testid=\"sidebar\"]');\n const sidebarBox = await sidebar.boundingBox();\n const viewportSize = page.viewportSize();\n\n expect(sidebarBox?.x).toBeGreaterThan(viewportSize!.width / 2);\n });\n\n test('navigation arrows are mirrored', async ({ page }) => {\n await page.goto('/ar/products');\n\n const nextButton = page.locator('[data-testid=\"next-button\"] svg');\n const transform = await nextButton.evaluate((el) => {\n return window.getComputedStyle(el).transform;\n });\n\n // Check for scaleX(-1) transform\n expect(transform).toContain('-1');\n });\n});\n```\n\n### Visual Regression Testing\n\n```typescript\n// tests/visual-rtl.spec.ts\nimport { test, expect } from '@playwright/test';\n\nconst locales = ['en', 'ar'];\n\nfor (const locale of locales) {\n test(`dashboard visual regression - ${locale}`, async ({ page }) => {\n await page.goto(`/${locale}/dashboard`);\n await expect(page).toHaveScreenshot(`dashboard-${locale}.png`);\n });\n}\n```\n\n---\n\n## Common Issues and Fixes\n\n### Issue: Scrollbar Position\n\n```css\n/* Move scrollbar to left in RTL */\n[dir='rtl'] {\n /* Most browsers handle this automatically */\n /* For custom scrollbars: */\n &::-webkit-scrollbar {\n /* Scrollbar styling */\n }\n}\n```\n\n### Issue: Absolute Positioning\n\n```css\n/* FAIL Breaks in RTL */\n.tooltip {\n position: absolute;\n left: 100%;\n}\n\n/* PASS Works in both directions */\n.tooltip {\n position: absolute;\n inset-inline-start: 100%;\n}\n```\n\n### Issue: Border Radius\n\n```css\n/* FAIL Physical corners */\n.card {\n border-radius: 8px 0 0 8px;\n}\n\n/* PASS Logical corners */\n.card {\n border-start-start-radius: 8px;\n border-end-start-radius: 8px;\n}\n```\n\n### Issue: Transitions and Animations\n\n```css\n/* Reverse animation direction for RTL */\n@keyframes slide-in {\n from {\n transform: translateX(-100%);\n }\n to {\n transform: translateX(0);\n }\n}\n\n[dir='rtl'] .slide-in {\n animation-direction: reverse;\n}\n\n/* Or use logical values */\n@keyframes slide-in-logical {\n from {\n inset-inline-start: -100%;\n }\n to {\n inset-inline-start: 0;\n }\n}\n```\n\n---\n\n## RTL Checklist\n\n### Initial Setup\n\n- REQUIRED: Add `dir` attribute to `\u003chtml>` element dynamically\n- REQUIRED: Use CSS logical properties throughout codebase\n- REQUIRED: Configure Tailwind RTL variants (if using Tailwind)\n- REQUIRED: Set up RTL toggle in Storybook/dev environment\n\n### Components\n\n- REQUIRED: Replace `left`/`right` with `start`/`end` in CSS\n- REQUIRED: Mirror directional icons (arrows, chevrons)\n- REQUIRED: Keep logos and photos non-mirrored\n- REQUIRED: Force LTR for email, URL, phone inputs\n- REQUIRED: Handle numeric data direction\n\n### Testing\n\n- REQUIRED: Visual regression tests for RTL locales\n- REQUIRED: Manual testing with actual Arabic/Hebrew content\n- REQUIRED: Check all interactive elements (dropdowns, modals)\n- REQUIRED: Verify form validation message alignment\n- REQUIRED: Test keyboard navigation (Tab order reverses)\n\n### Content\n\n- REQUIRED: Professional translation (not just mirrored English)\n- REQUIRED: Proper RTL punctuation and formatting\n- REQUIRED: Number formatting for RTL locales\n- REQUIRED: Date formatting (varies by region)\n\n---\n\n## Browser Support\n\n| Feature | Chrome | Firefox | Safari | Edge |\n|---------|--------|---------|--------|------|\n| CSS Logical Properties | 89+ | 66+ | 15+ | 89+ |\n| `dir` attribute | All | All | All | All |\n| BiDi algorithm | All | All | All | All |\n| `:dir()` pseudo-class | 120+ | 49+ | 16.4+ | 120+ |\n\n### Fallback for Older Browsers\n\n```css\n/* Fallback pattern */\n.element {\n margin-left: 1rem; /* Fallback */\n margin-inline-start: 1rem; /* Modern */\n}\n\n/* Or use PostCSS plugin */\n/* postcss-logical handles this automatically */\n```\n\n```bash\nnpm install postcss-logical\n```\n\n```javascript\n// postcss.config.js\nmodule.exports = {\n plugins: [require('postcss-logical')({ dir: 'ltr' })],\n};\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12988,"content_sha256":"9813f9424a0fbc34822b4373da530fe118d1db39d6dccc3761406d189ee2351b"},{"filename":"references/testing-i18n.md","content":"# Testing Localised Applications\n\nSystematic testing strategies for internationalised applications. Covers pseudo-localisation, visual regression, RTL automation, pluralisation edge cases, format testing, missing translation detection, and CI integration.\n\n---\n\n## Why i18n Testing Is Different\n\nLocalisation bugs are invisible to monolingual development teams. A passing English test suite says nothing about Arabic layout, Polish pluralisation, or Japanese character truncation. i18n testing requires intentional, locale-aware test design.\n\n| Bug Category | Impact | Detection Method |\n|-------------|--------|-----------------|\n| Truncated text | UI overflow, hidden CTAs | Visual regression, pseudo-loc |\n| Wrong plural form | Grammatical errors | Plural rule testing per locale |\n| Broken RTL layout | Unusable UI for ~400M users | RTL screenshot comparison |\n| Hardcoded strings | Untranslated UI fragments | Static analysis, pseudo-loc |\n| Format errors | Wrong date/currency display | Format assertion tests |\n| Missing translations | English leaking into locale pages | CI extraction diff |\n\n---\n\n## Pseudo-Localisation\n\nPseudo-localisation replaces English strings with accented, expanded versions to expose i18n issues without waiting for real translations.\n\n### What It Detects\n\n- **Hardcoded strings** — untouched text stands out against accented pseudo text\n- **Truncation** — expanded strings (30-50% longer) reveal overflow\n- **Concatenation bugs** — broken pseudo strings expose concatenated segments\n- **Character encoding** — accented characters expose UTF-8 failures\n- **BiDi issues** — bracketed pseudo text reveals embedding problems\n\n### Pseudo-Locale Formats\n\n| Style | Example | Best For |\n|-------|---------|----------|\n| Accented | `[Ĥéĺĺö Ŵöŕĺð]` | Hardcoded string detection |\n| Expanded | `[Heeellloooo Wooorrrllldd]` | Truncation testing |\n| Mirrored | `[dlroW olleH]` | RTL layout simulation |\n| Bracketed | `[Hello World]` | Missing translation detection |\n\n### Tools and Integration\n\n```bash\n# i18next pseudo-locale plugin\nnpm install i18next-pseudo\n\n# formatjs pseudo-locale generation\nnpx formatjs compile --pseudo-locale en-XA messages/en.json -o messages/en-XA.json\n```\n\n```typescript\n// i18next configuration with pseudo-locale\nimport i18next from 'i18next';\nimport pseudo from 'i18next-pseudo';\n\ni18next.use(pseudo).init({\n lng: 'en',\n postProcess: ['pseudo'],\n pseudo: {\n enabled: process.env.NODE_ENV === 'development',\n languageToPseudo: 'en-XA',\n letterMultiplier: 2, // 2x expansion for truncation testing\n repeatedLetters: ['a', 'e', 'i', 'o', 'u'],\n wrapped: true, // Add brackets [...]\n },\n});\n```\n\n### CI Integration\n\n```yaml\n# GitHub Actions: run pseudo-locale visual tests\n- name: Pseudo-locale visual regression\n run: |\n NEXT_PUBLIC_PSEUDO_LOCALE=true npx playwright test --project=pseudo-loc\n env:\n CI: true\n```\n\n---\n\n## Visual Testing for Truncation and Layout Overflow\n\n### Expansion Ratios by Language\n\n| Source Length | Typical Expansion | Worst Case Languages |\n|-------------|-------------------|---------------------|\n| 1-10 chars | +200-300% | German, Finnish, Greek |\n| 11-20 chars | +80-200% | German, Russian, French |\n| 21-70 chars | +40-80% | German, Portuguese, Dutch |\n| 70+ chars | +30-40% | Most European languages |\n\nCJK languages (Chinese, Japanese, Korean) are typically shorter in character count but may be wider in pixel width due to full-width characters.\n\n### Playwright Visual Regression Per Locale\n\n```typescript\n// tests/visual-i18n.spec.ts\nimport { test, expect } from '@playwright/test';\n\nconst VISUAL_TEST_LOCALES = ['en', 'de', 'ar', 'ja', 'ru'];\nconst CRITICAL_PAGES = ['/dashboard', '/settings', '/checkout'];\n\nfor (const locale of VISUAL_TEST_LOCALES) {\n for (const page of CRITICAL_PAGES) {\n test(`visual: ${page} in ${locale}`, async ({ page: p }) => {\n await p.goto(`/${locale}${page}`);\n await p.waitForLoadState('networkidle');\n\n await expect(p).toHaveScreenshot(\n `${page.replace('/', '')}-${locale}.png`,\n {\n maxDiffPixelRatio: 0.01,\n fullPage: true,\n }\n );\n });\n }\n}\n```\n\n### Overflow Detection Script\n\n```typescript\n// tests/helpers/overflow-detector.ts\nexport async function detectOverflow(page: Page): Promise\u003cOverflowResult[]> {\n return page.evaluate(() => {\n const overflows: Array\u003c{ selector: string; text: string; overflow: string }> = [];\n\n document.querySelectorAll('*').forEach((el) => {\n const style = window.getComputedStyle(el);\n if (\n el.scrollWidth > el.clientWidth &&\n style.overflow !== 'scroll' &&\n style.overflow !== 'auto' &&\n style.overflow !== 'hidden'\n ) {\n overflows.push({\n selector: el.tagName + (el.id ? `#${el.id}` : ''),\n text: (el.textContent || '').slice(0, 50),\n overflow: `${el.scrollWidth - el.clientWidth}px`,\n });\n }\n });\n\n return overflows;\n });\n}\n```\n\n---\n\n## RTL Layout Testing Automation\n\n### Automated RTL Structural Tests\n\n```typescript\n// tests/rtl-layout.spec.ts\nimport { test, expect } from '@playwright/test';\n\ntest.describe('RTL Layout Validation', () => {\n test.beforeEach(async ({ page }) => {\n await page.goto('/ar/dashboard');\n });\n\n test('document direction is set', async ({ page }) => {\n await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');\n await expect(page.locator('html')).toHaveAttribute('lang', 'ar');\n });\n\n test('sidebar is on the right side', async ({ page }) => {\n const sidebar = page.locator('[data-testid=\"sidebar\"]');\n const box = await sidebar.boundingBox();\n const viewport = page.viewportSize()!;\n expect(box!.x).toBeGreaterThan(viewport.width * 0.5);\n });\n\n test('no physical CSS properties leak', async ({ page }) => {\n const violations = await page.evaluate(() => {\n const issues: string[] = [];\n document.querySelectorAll('*').forEach((el) => {\n const style = window.getComputedStyle(el);\n // Check for non-zero margin-left that should be margin-inline-start\n if (el.getAttribute('style')?.includes('margin-left')) {\n issues.push(`${el.tagName}: inline style uses margin-left`);\n }\n });\n return issues;\n });\n expect(violations).toHaveLength(0);\n });\n\n test('directional icons are mirrored', async ({ page }) => {\n const arrows = page.locator('[data-directional=\"true\"]');\n const count = await arrows.count();\n\n for (let i = 0; i \u003c count; i++) {\n const transform = await arrows.nth(i).evaluate((el) =>\n window.getComputedStyle(el).transform\n );\n expect(transform).toContain('-1'); // scaleX(-1)\n }\n });\n});\n```\n\n### Storybook RTL Testing\n\n```typescript\n// .storybook/test-runner.ts\nimport { getStoryContext } from '@storybook/test-runner';\n\nexport async function postRender(page, context) {\n const storyContext = await getStoryContext(page, context);\n\n if (storyContext.globals.locale === 'ar') {\n const dir = await page.evaluate(() => document.documentElement.dir);\n expect(dir).toBe('rtl');\n }\n}\n```\n\n---\n\n## Pluralisation Testing\n\n### Language Plural Rule Categories\n\nLanguages have different plural rules defined by CLDR. Testing with just 0, 1, 2 is insufficient.\n\n| Language | Forms | Categories | Test Values |\n|----------|-------|------------|-------------|\n| English | 2 | one, other | 0, 1, 2, 5, 100 |\n| French | 2 | one, other | 0, 1, 2, 1000000 |\n| Polish | 4 | one, few, many, other | 1, 2, 5, 12, 22, 0.5 |\n| Arabic | 6 | zero, one, two, few, many, other | 0, 1, 2, 3, 11, 100 |\n| Russian | 3 | one, few, many | 1, 2, 5, 21, 11, 111 |\n| Japanese | 1 | other | 0, 1, 100 |\n| Czech | 3 | one, few, other | 1, 2, 5 |\n\n### Plural Test Matrix Generator\n\n```typescript\n// tests/helpers/plural-test-data.ts\nexport const PLURAL_TEST_CASES: Record\u003cstring, number[]> = {\n en: [0, 1, 2, 5, 21, 100],\n ar: [0, 1, 2, 3, 11, 100],\n pl: [0, 1, 2, 5, 12, 22],\n ru: [0, 1, 2, 5, 11, 21, 101, 111],\n ja: [0, 1, 5, 100],\n fr: [0, 1, 2, 1000000],\n cs: [0, 1, 2, 5],\n};\n\nexport function getPluralTestValues(locale: string): number[] {\n const lang = locale.split('-')[0];\n return PLURAL_TEST_CASES[lang] || PLURAL_TEST_CASES['en'];\n}\n```\n\n### Automated Plural Validation\n\n```typescript\n// tests/plurals.spec.ts\nimport { test, expect } from '@playwright/test';\nimport { PLURAL_TEST_CASES } from './helpers/plural-test-data';\n\nfor (const [locale, values] of Object.entries(PLURAL_TEST_CASES)) {\n for (const count of values) {\n test(`plural: ${locale} with count=${count}`, async ({ page }) => {\n await page.goto(`/${locale}/items?count=${count}`);\n\n const text = await page.locator('[data-testid=\"item-count\"]').textContent();\n\n // No raw ICU syntax should leak\n expect(text).not.toContain('{count');\n expect(text).not.toContain('plural,');\n\n // No empty or undefined text\n expect(text?.trim().length).toBeGreaterThan(0);\n });\n }\n}\n```\n\n---\n\n## Date, Number, and Currency Format Testing\n\n### Format Expectations by Locale\n\n| Locale | Date (medium) | Number (1234.5) | Currency ($1234.50) |\n|--------|--------------|-----------------|---------------------|\n| en-US | Jan 15, 2026 | 1,234.5 | $1,234.50 |\n| de-DE | 15.01.2026 | 1.234,5 | 1.234,50 $ |\n| fr-FR | 15 janv. 2026 | 1 234,5 | 1 234,50 $ |\n| ja-JP | 2026/01/15 | 1,234.5 | $1,234 |\n| ar-SA | 15/01/2026 | 1,234.5 | 1,234.50 $ |\n| hi-IN | 15 Jan 2026 | 1,234.5 | $1,234.50 |\n\n### Format Assertion Tests\n\n```typescript\n// tests/formatting.spec.ts\nimport { test, expect } from '@playwright/test';\n\nconst FORMAT_CASES = [\n {\n locale: 'en-US',\n date: /Jan\\s+15,\\s+2026/,\n number: /1,234/,\n currency: /\\$1,234\\.50/,\n },\n {\n locale: 'de-DE',\n date: /15\\.\\s*01\\.\\s*2026|15\\.\\s*Jan/,\n number: /1\\.234/,\n currency: /1\\.234,50/,\n },\n {\n locale: 'ar-SA',\n date: /١٥|15/,\n number: /١٬٢٣٤|1,234/,\n currency: /١٬٢٣٤|1,234/,\n },\n];\n\nfor (const { locale, date, number, currency } of FORMAT_CASES) {\n test(`formatting: ${locale}`, async ({ page }) => {\n await page.goto(`/${locale}/formatting-test`);\n\n const dateText = await page.locator('[data-testid=\"date-display\"]').textContent();\n expect(dateText).toMatch(date);\n\n const numberText = await page.locator('[data-testid=\"number-display\"]').textContent();\n expect(numberText).toMatch(number);\n\n const currencyText = await page.locator('[data-testid=\"currency-display\"]').textContent();\n expect(currencyText).toMatch(currency);\n });\n}\n```\n\n---\n\n## Screenshot Comparison Across Locales\n\n### Multi-Locale Screenshot Pipeline\n\n```typescript\n// playwright.config.ts — locale-specific projects\nimport { defineConfig } from '@playwright/test';\n\nconst SCREENSHOT_LOCALES = ['en', 'de', 'ar', 'ja', 'pt-BR'];\n\nexport default defineConfig({\n projects: SCREENSHOT_LOCALES.map((locale) => ({\n name: `screenshots-${locale}`,\n use: {\n locale,\n baseURL: `http://localhost:3000/${locale}`,\n },\n })),\n expect: {\n toHaveScreenshot: {\n maxDiffPixelRatio: 0.02,\n animations: 'disabled',\n },\n },\n});\n```\n\n### Targeted Component Screenshots\n\n```typescript\n// tests/component-screenshots.spec.ts\nconst COMPONENTS = [\n { testId: 'header', name: 'header' },\n { testId: 'pricing-table', name: 'pricing' },\n { testId: 'footer', name: 'footer' },\n { testId: 'checkout-form', name: 'checkout' },\n];\n\nfor (const { testId, name } of COMPONENTS) {\n test(`screenshot: ${name}`, async ({ page }) => {\n await page.goto('/');\n const component = page.locator(`[data-testid=\"${testId}\"]`);\n await expect(component).toHaveScreenshot(`${name}.png`);\n });\n}\n```\n\n---\n\n## Missing Translation Detection in CI\n\n### i18next-parser: Extract and Diff\n\n```bash\n# Extract all translation keys from source code\nnpx i18next-parser\n\n# Compare extracted keys with existing translations\n# Missing keys appear in the output JSON with empty values\n```\n\n```javascript\n// i18next-parser.config.js\nmodule.exports = {\n locales: ['en', 'de', 'ar', 'ja', 'fr', 'pt-BR'],\n output: 'locales/$LOCALE/$NAMESPACE.json',\n input: ['src/**/*.{ts,tsx}'],\n keepRemoved: false,\n failOnWarnings: true, // CI will fail on missing keys\n defaultValue: '__MISSING__',\n};\n```\n\n### FormatJS: Extract and Compile\n\n```bash\n# Extract message descriptors\nnpx formatjs extract 'src/**/*.tsx' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'\n\n# Compile with missing check\nnpx formatjs compile lang/de.json --out-file compiled/de.json --ast\n```\n\n### CI Pipeline for Missing Translations\n\n```yaml\n# .github/workflows/i18n-check.yml\nname: i18n Quality Check\non: [pull_request]\n\njobs:\n check-translations:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Install dependencies\n run: npm ci\n\n - name: Extract translation keys\n run: npx i18next-parser\n\n - name: Check for missing translations\n run: |\n node scripts/check-missing-translations.js\n # Exit 1 if any locale has missing keys\n```\n\n```javascript\n// scripts/check-missing-translations.js\nconst fs = require('fs');\nconst path = require('path');\n\nconst LOCALES_DIR = path.join(__dirname, '..', 'locales');\nconst REQUIRED_LOCALES = ['de', 'ar', 'ja', 'fr'];\nlet hasErrors = false;\n\nfor (const locale of REQUIRED_LOCALES) {\n const filePath = path.join(LOCALES_DIR, locale, 'common.json');\n const translations = JSON.parse(fs.readFileSync(filePath, 'utf-8'));\n\n const missing = Object.entries(translations)\n .filter(([, value]) => value === '__MISSING__' || value === '')\n .map(([key]) => key);\n\n if (missing.length > 0) {\n console.error(`[${locale}] Missing ${missing.length} translations:`);\n missing.forEach((key) => console.error(` - ${key}`));\n hasErrors = true;\n }\n}\n\nif (hasErrors) process.exit(1);\n```\n\n---\n\n## End-to-End Locale Switching Tests\n\n```typescript\n// tests/locale-switching.spec.ts\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Locale Switching', () => {\n test('switches from English to German', async ({ page }) => {\n await page.goto('/en/dashboard');\n\n // Switch locale via UI\n await page.click('[data-testid=\"locale-switcher\"]');\n await page.click('[data-testid=\"locale-de\"]');\n\n // URL should update\n await expect(page).toHaveURL(/\\/de\\/dashboard/);\n\n // Content should be in German\n const heading = await page.locator('h1').textContent();\n expect(heading).not.toBe('Dashboard'); // Should be translated\n });\n\n test('persists locale preference across navigation', async ({ page }) => {\n await page.goto('/de/dashboard');\n\n // Navigate to another page\n await page.click('[data-testid=\"nav-settings\"]');\n await expect(page).toHaveURL(/\\/de\\/settings/);\n\n // Reload — locale should persist\n await page.reload();\n await expect(page).toHaveURL(/\\/de\\/settings/);\n });\n\n test('respects Accept-Language header', async ({ page, context }) => {\n await context.setExtraHTTPHeaders({\n 'Accept-Language': 'de-DE,de;q=0.9,en;q=0.8',\n });\n\n await page.goto('/');\n await expect(page).toHaveURL(/\\/de\\//);\n });\n\n test('falls back gracefully for unsupported locale', async ({ page }) => {\n await page.goto('/zz/dashboard');\n // Should redirect to default locale\n await expect(page).toHaveURL(/\\/en\\/dashboard/);\n });\n});\n```\n\n---\n\n## Accessibility Testing Across Locales\n\n### Screen Reader Validation\n\n```typescript\n// tests/a11y-i18n.spec.ts\nimport { test, expect } from '@playwright/test';\nimport AxeBuilder from '@axe-core/playwright';\n\nconst A11Y_LOCALES = ['en', 'ar', 'ja'];\n\nfor (const locale of A11Y_LOCALES) {\n test(`accessibility: ${locale} dashboard`, async ({ page }) => {\n await page.goto(`/${locale}/dashboard`);\n\n const results = await new AxeBuilder({ page })\n .withTags(['wcag2a', 'wcag2aa'])\n .analyze();\n\n expect(results.violations).toHaveLength(0);\n });\n\n test(`lang attribute: ${locale}`, async ({ page }) => {\n await page.goto(`/${locale}/dashboard`);\n const lang = await page.locator('html').getAttribute('lang');\n expect(lang).toBe(locale);\n });\n}\n```\n\n---\n\n## Test Matrix Design\n\n### Locale Coverage Strategy\n\nNot every locale needs every test. Categorise locales by risk profile.\n\n| Tier | Locales | Test Coverage | Rationale |\n|------|---------|---------------|-----------|\n| **Tier 1: Full** | en, primary market locale | All tests: visual, functional, a11y | Revenue critical |\n| **Tier 2: RTL** | ar (or he) | Full + RTL-specific | Layout direction change |\n| **Tier 3: Complex script** | ja (or zh, ko) | Visual + formatting + truncation | CJK character width |\n| **Tier 4: Complex plural** | pl (or ar, ru) | Plural rules + formatting | Most plural categories |\n| **Tier 5: Spot check** | Remaining locales | Missing translation CI + smoke | Coverage without cost |\n\n### Recommended Minimum Test Set\n\n```text\nLocale selection for maximum coverage with minimum tests:\n en — baseline (LTR, simple plurals, Latin script)\n de — longest strings (expansion testing)\n ar — RTL + complex plurals (6 forms) + different script\n ja — CJK characters, single plural form, different formatting\n pl — complex plural rules (4 forms)\n pt-BR — emerging market, different from pt-PT\n```\n\n---\n\n## Anti-Patterns\n\n| Anti-Pattern | Problem | Fix |\n|-------------|---------|-----|\n| Testing only in English | Zero locale coverage | Add locale dimension to test matrix |\n| Pixel-perfect screenshot thresholds | Constant false positives | Use 1-2% diff ratio, review diffs |\n| Hardcoded expected strings in tests | Breaks when translations update | Test structure, not exact text |\n| Testing all locales in every run | Slow CI, wasted resources | Tier-based coverage strategy |\n| Skipping pseudo-localisation | Hardcoded strings ship to production | Enable pseudo-loc in dev and CI |\n| Manual RTL testing only | Regressions between releases | Automate structural RTL checks |\n| Ignoring format differences | Wrong dates/currencies in production | Locale-specific format assertions |\n\n---\n\n## Cross-References\n\n- [rtl-support.md](rtl-support.md) — CSS logical properties, Tailwind RTL, icon mirroring\n- [icu-message-format.md](icu-message-format.md) — Plural rules, select, number/date formatting\n- [locale-handling.md](locale-handling.md) — Date, number, currency formatting by locale\n- [translation-workflows.md](translation-workflows.md) — CI/CD pipelines, string extraction, TMS integration\n- [framework-guides.md](framework-guides.md) — Framework-specific i18n setup (React, Vue, Angular, Next.js)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18716,"content_sha256":"9943a4c4bc13ad80f67c9c2026975641133f5cc0098f34cca134b1e45af76efe"},{"filename":"references/translation-workflows.md","content":"# Translation Workflows\n\nProduction patterns for string extraction, TMS integration, and CI/CD pipelines.\n\n---\n\n## String Extraction\n\n### i18next-parser\n\nExtract translation keys from React, Vue, and TypeScript codebases.\n\n```bash\nnpm install -D i18next-parser\n```\n\n```javascript\n// i18next-parser.config.js\nmodule.exports = {\n locales: ['en', 'de', 'fr', 'ar'],\n output: 'public/locales/$LOCALE/$NAMESPACE.json',\n input: ['src/**/*.{ts,tsx,js,jsx}'],\n\n // Key extraction patterns\n lexers: {\n tsx: ['JsxLexer'],\n ts: ['JavascriptLexer'],\n },\n\n // Namespace from file path\n namespaceSeparator: ':',\n keySeparator: '.',\n\n // Keep existing translations\n keepRemoved: false,\n\n // Sort keys alphabetically\n sort: true,\n\n // Default value for new keys\n defaultValue: (locale, namespace, key) => {\n return locale === 'en' ? key : '';\n },\n};\n```\n\n```bash\n# Run extraction\nnpx i18next-parser\n\n# Watch mode\nnpx i18next-parser --watch\n```\n\n### FormatJS CLI (@formatjs/cli)\n\nExtract and compile ICU messages for react-intl.\n\n```bash\nnpm install -D @formatjs/cli\n```\n\n```bash\n# Extract messages\nnpx formatjs extract 'src/**/*.tsx' \\\n --out-file lang/en.json \\\n --id-interpolation-pattern '[sha512:contenthash:base64:6]' \\\n --format simple\n\n# Compile for production (AST)\nnpx formatjs compile lang/en.json \\\n --out-file compiled-lang/en.json \\\n --ast\n```\n\n### Lingui CLI\n\nExtract and compile for LinguiJS.\n\n```bash\n# Extract messages\nnpx lingui extract\n\n# Compile catalogs\nnpx lingui compile\n\n# Extract and compile\nnpx lingui extract && npx lingui compile\n```\n\n---\n\n## Translation Management Systems (TMS)\n\n### Phrase Integration\n\nEnterprise TMS with GitHub/GitLab sync.\n\n```yaml\n# .phrase.yml\nphrase:\n access_token: ${PHRASE_ACCESS_TOKEN}\n project_id: your-project-id\n push:\n sources:\n - file: ./public/locales/en/*.json\n params:\n locale_id: en\n file_format: simple_json\n pull:\n targets:\n - file: ./public/locales/\u003clocale_name>/\u003ctag>.json\n params:\n file_format: simple_json\n```\n\n```bash\n# Push source strings\nphrase push\n\n# Pull translations\nphrase pull\n\n# Install CLI\nbrew install phrase-cli\n```\n\n**GitHub Action:**\n\n```yaml\n# .github/workflows/phrase-sync.yml\nname: Phrase Sync\non:\n push:\n branches: [main]\n paths:\n - 'public/locales/en/**'\n\njobs:\n sync:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Push to Phrase\n uses: phrase/phrase-cli-action@v2\n with:\n command: push\n env:\n PHRASE_ACCESS_TOKEN: ${{ secrets.PHRASE_ACCESS_TOKEN }}\n```\n\n### Lokalise Integration\n\nDeveloper-friendly TMS with Figma plugin.\n\n```bash\n# Install CLI\nnpm install -g @lokalise/cli\n```\n\n```yaml\n# lokalise.yml\nlokalise:\n token: ${LOKALISE_API_TOKEN}\n project_id: your-project-id\n\nupload:\n file: ./public/locales/en/*.json\n lang_iso: en\n replace_modified: true\n convert_placeholders: true\n\ndownload:\n format: json\n dest: ./public/locales/%LANG_ISO%/\n export_empty_as: skip\n placeholder_format: icu\n```\n\n```bash\n# Push source\nlokalise2 file upload --config lokalise.yml\n\n# Pull translations\nlokalise2 file download --config lokalise.yml\n```\n\n**GitHub Action:**\n\n```yaml\n# .github/workflows/lokalise-sync.yml\nname: Lokalise Sync\non:\n push:\n branches: [main]\n paths:\n - 'public/locales/en/**'\n\njobs:\n upload:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Upload to Lokalise\n uses: lokalise/lokalise-upload-action@v1\n with:\n api_token: ${{ secrets.LOKALISE_API_TOKEN }}\n project_id: ${{ secrets.LOKALISE_PROJECT_ID }}\n file: public/locales/en/common.json\n lang_iso: en\n```\n\n### Crowdin Integration\n\nCommunity-focused with open source free tier.\n\n```yaml\n# crowdin.yml\nproject_id: your-project-id\napi_token: ${CROWDIN_API_TOKEN}\nbase_path: .\nbase_url: https://api.crowdin.com\n\npreserve_hierarchy: true\n\nfiles:\n - source: /public/locales/en/*.json\n translation: /public/locales/%two_letters_code%/%original_file_name%\n type: json\n```\n\n```bash\n# Install CLI\nnpm install -g @crowdin/cli\n\n# Upload sources\ncrowdin upload sources\n\n# Download translations\ncrowdin download\n```\n\n**GitHub Action:**\n\n```yaml\n# .github/workflows/crowdin.yml\nname: Crowdin Sync\non:\n push:\n branches: [main]\n\njobs:\n synchronize:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Crowdin Sync\n uses: crowdin/github-action@v1\n with:\n upload_sources: true\n download_translations: true\n create_pull_request: true\n env:\n CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}\n CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}\n```\n\n---\n\n## CI/CD Pipelines\n\n### GitHub Actions: Full Workflow\n\n```yaml\n# .github/workflows/i18n.yml\nname: i18n Pipeline\non:\n push:\n branches: [main, develop]\n pull_request:\n branches: [main]\n\njobs:\n # 1. Validate translations\n validate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n - name: Install dependencies\n run: npm ci\n\n - name: Extract and check for new keys\n run: |\n npx i18next-parser\n git diff --exit-code public/locales/en/ || echo \"::warning::New translation keys detected\"\n\n - name: Validate ICU syntax\n run: npx formatjs compile-folder public/locales/en --ast\n\n - name: Check for missing translations\n run: node scripts/check-translations.js\n\n # 2. Push to TMS (main branch only)\n push-translations:\n runs-on: ubuntu-latest\n needs: validate\n if: github.ref == 'refs/heads/main'\n steps:\n - uses: actions/checkout@v4\n\n - name: Push to Phrase\n uses: phrase/phrase-cli-action@v2\n with:\n command: push\n env:\n PHRASE_ACCESS_TOKEN: ${{ secrets.PHRASE_ACCESS_TOKEN }}\n\n # 3. Pull translations (scheduled)\n pull-translations:\n runs-on: ubuntu-latest\n if: github.event_name == 'schedule'\n steps:\n - uses: actions/checkout@v4\n\n - name: Pull from Phrase\n uses: phrase/phrase-cli-action@v2\n with:\n command: pull\n env:\n PHRASE_ACCESS_TOKEN: ${{ secrets.PHRASE_ACCESS_TOKEN }}\n\n - name: Create PR with translations\n uses: peter-evans/create-pull-request@v5\n with:\n title: '[i18n] Update translations'\n commit-message: 'chore(i18n): update translations from Phrase'\n branch: i18n/update-translations\n labels: i18n, automated\n```\n\n### Translation Validation Script\n\n```javascript\n// scripts/check-translations.js\nconst fs = require('fs');\nconst path = require('path');\n\nconst localesDir = './public/locales';\nconst sourceLocale = 'en';\nconst targetLocales = ['de', 'fr', 'ar'];\n\nconst sourceFiles = fs.readdirSync(path.join(localesDir, sourceLocale));\nconst issues = [];\n\nfor (const file of sourceFiles) {\n const sourcePath = path.join(localesDir, sourceLocale, file);\n const sourceKeys = Object.keys(JSON.parse(fs.readFileSync(sourcePath, 'utf8')));\n\n for (const locale of targetLocales) {\n const targetPath = path.join(localesDir, locale, file);\n\n if (!fs.existsSync(targetPath)) {\n issues.push(`Missing file: ${locale}/${file}`);\n continue;\n }\n\n const targetKeys = Object.keys(JSON.parse(fs.readFileSync(targetPath, 'utf8')));\n const missingKeys = sourceKeys.filter((key) => !targetKeys.includes(key));\n\n if (missingKeys.length > 0) {\n issues.push(`${locale}/${file}: Missing ${missingKeys.length} keys`);\n missingKeys.forEach((key) => console.log(` - ${key}`));\n }\n }\n}\n\nif (issues.length > 0) {\n console.error('\\nFAIL Translation issues found:');\n issues.forEach((issue) => console.error(` - ${issue}`));\n process.exit(1);\n} else {\n console.log('PASS All translations complete');\n}\n```\n\n### GitLab CI Pipeline\n\n```yaml\n# .gitlab-ci.yml\nstages:\n - validate\n - sync\n\nvariables:\n NODE_VERSION: '20'\n\nvalidate-i18n:\n stage: validate\n image: node:${NODE_VERSION}\n script:\n - npm ci\n - npx i18next-parser\n - node scripts/check-translations.js\n rules:\n - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\n\npush-to-tms:\n stage: sync\n image: node:${NODE_VERSION}\n script:\n - npm install -g phrase-cli\n - phrase push\n rules:\n - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH\n changes:\n - public/locales/en/**/*\n```\n\n---\n\n## Git Workflow Patterns\n\n### Branch-Based Translation Workflow\n\n```text\nmain\n├── feature/add-checkout-flow\n│ └── Extract new keys -> Push to TMS\n│\n└── i18n/update-translations (automated)\n └── Pull from TMS -> Create PR\n```\n\n### Monorepo Pattern\n\n```yaml\n# turbo.json (Turborepo)\n{\n \"pipeline\": {\n \"i18n:extract\": {\n \"dependsOn\": [\"^build\"],\n \"outputs\": [\"public/locales/**\"]\n },\n \"i18n:push\": {\n \"dependsOn\": [\"i18n:extract\"]\n }\n }\n}\n```\n\n```bash\n# Extract from all packages\nturbo run i18n:extract\n\n# Push to TMS\nturbo run i18n:push\n```\n\n### Pre-commit Hook\n\n```bash\n# .husky/pre-commit\n#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n# Extract and stage new translation keys\nnpx i18next-parser\ngit add public/locales/en/\n```\n\n---\n\n## Missing Key Detection\n\n### Development Warning\n\n```typescript\n// i18n/config.ts (i18next)\ni18n.init({\n // ...\n saveMissing: process.env.NODE_ENV === 'development',\n missingKeyHandler: (lng, ns, key, fallbackValue) => {\n console.warn(` Missing translation: [${lng}] ${ns}:${key}`);\n\n // Optional: Send to logging service\n if (process.env.SENTRY_DSN) {\n Sentry.captureMessage(`Missing i18n key: ${key}`, {\n level: 'warning',\n extra: { locale: lng, namespace: ns },\n });\n }\n },\n});\n```\n\n### Production Fallback Strategy\n\n```typescript\ni18n.init({\n fallbackLng: {\n 'de-AT': ['de', 'en'],\n 'de-CH': ['de', 'en'],\n 'zh-TW': ['zh-Hant', 'zh', 'en'],\n default: ['en'],\n },\n\n // Return key as fallback (debugging)\n returnEmptyString: false,\n\n // Custom fallback value\n parseMissingKeyHandler: (key) => {\n return `WARNING: ${key}`;\n },\n});\n```\n\n---\n\n## Translation File Organisation\n\n### Namespace Strategy\n\n```text\nlocales/\n├── en/\n│ ├── common.json # 50-100 keys: buttons, errors, nav\n│ ├── auth.json # 30-50 keys: login, register\n│ ├── dashboard.json # 100+ keys: dashboard-specific\n│ ├── validation.json # 20-40 keys: form validation\n│ ├── emails.json # Email templates (if needed)\n│ └── legal.json # Terms, privacy (rarely changes)\n└── de/\n └── ... (mirror structure)\n```\n\n### Key Naming Conventions\n\n```json\n{\n // Feature.component.element pattern\n \"auth.login.title\": \"Sign In\",\n \"auth.login.email_label\": \"Email Address\",\n \"auth.login.submit_button\": \"Sign In\",\n \"auth.login.forgot_password_link\": \"Forgot password?\",\n\n // Action-based for buttons\n \"common.actions.save\": \"Save\",\n \"common.actions.cancel\": \"Cancel\",\n \"common.actions.delete\": \"Delete\",\n\n // Error messages with context\n \"validation.email.required\": \"Email is required\",\n \"validation.email.invalid\": \"Please enter a valid email\",\n \"validation.password.min_length\": \"Password must be at least {min} characters\"\n}\n```\n\n### Avoid\n\n```json\n{\n // FAIL Too generic\n \"button1\": \"Click here\",\n\n // FAIL Hardcoded values\n \"items_5\": \"You have 5 items\",\n\n // FAIL Concatenation-dependent\n \"hello\": \"Hello\",\n \"world\": \"World\",\n // Used as: t('hello') + ' ' + t('world') FAIL\n}\n```\n\n---\n\n## TMS Selection Guide\n\n| TMS | Best For | Pricing | Key Features |\n|-----|----------|---------|--------------|\n| **Phrase** | Enterprise, large teams | $$ | GitHub sync, CLI, over-the-air |\n| **Lokalise** | Dev-focused teams | $ | Figma plugin, branching, AI |\n| **Crowdin** | Open source, community | $ - Free tier | Crowdsourcing, 600+ integrations |\n| **Transifex** | API-first workflows | $ | String detection, webhooks |\n| **POEditor** | Small teams | $ | Simple UI, reasonable pricing |\n| **Locize** | i18next projects | $ | Real-time sync, versioning |\n\n### Decision Criteria\n\n1. **Team size**: Solo/small -> POEditor; Enterprise -> Phrase\n2. **Budget**: Free tier needed -> Crowdin; Cost-flexible -> Lokalise\n3. **i18n library**: i18next ecosystem -> Locize; Any -> Phrase/Lokalise\n4. **Workflow**: Community translations -> Crowdin; Professional -> Phrase\n5. **Integrations**: Figma-heavy -> Lokalise; GitHub-first -> Phrase/Crowdin\n\n---\n\n## AI-Powered Translation (2026)\n\nAI is reshaping localization workflows beyond basic machine translation. Modern approaches use multiple AI engines, agentic automation, and deep CI/CD integration.\n\n### Consensus-Based Translation\n\nMultiple independent AI engines verify translations, reducing errors by ~22%.\n\n```text\nTranslation Request\n │\n ├─ Engine 1 (GPT-4) ──────┐\n ├─ Engine 2 (Claude) ─────┼─ Consensus Check -> Final Translation\n ├─ Engine 3 (Gemini) ─────┘\n │\n └─ If disagreement -> Human review queue\n```\n\n**Benefits**:\n- Higher accuracy than single-engine MT\n- Automatic flagging of uncertain translations\n- Reduced post-editing workload\n\n### AI Translation Tools\n\n| Tool | Type | Best For |\n|------|------|----------|\n| **i18n-ai-translate** | CLI | i18next JSON with ChatGPT/Gemini/Claude |\n| **i18nexus** | Platform | React/Next.js with AI management |\n| **Phrase Language AI** | Enterprise | Large-scale AI translation |\n| **Locize** | i18next-native | Real-time AI-assisted translation |\n\n### i18n-ai-translate Example\n\n```bash\n# Install\nnpm install -g i18n-ai-translate\n\n# Translate single file\ni18n-ai-translate --input locales/en/common.json --output locales/de/common.json --target de --provider openai\n\n# Translate entire directory\ni18n-ai-translate --input locales/en --output locales/de --target de --provider anthropic\n```\n\n**Features**:\n- Preserves file structure (clean Git diffs)\n- Keeps variables intact (`{name}`, `{{count}}`)\n- Supports ChatGPT, Gemini, Claude, or local Ollama\n\n### CI/CD AI Translation Pipeline\n\n```yaml\n# .github/workflows/ai-translate.yml\nname: AI Translation\non:\n push:\n branches: [main]\n paths:\n - 'locales/en/**'\n\njobs:\n translate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n - name: Install AI translator\n run: npm install -g i18n-ai-translate\n\n - name: Detect new keys\n id: detect\n run: |\n git diff HEAD~1 --name-only -- locales/en/ > changed_files.txt\n echo \"files=$(cat changed_files.txt | tr '\\n' ' ')\" >> $GITHUB_OUTPUT\n\n - name: AI translate new keys\n if: steps.detect.outputs.files != ''\n run: |\n for lang in de fr es; do\n i18n-ai-translate \\\n --input locales/en \\\n --output locales/$lang \\\n --target $lang \\\n --provider openai \\\n --only-missing\n done\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n\n - name: Create PR with translations\n uses: peter-evans/create-pull-request@v5\n with:\n title: '[i18n] AI-generated translations for review'\n commit-message: 'chore(i18n): AI-translate new keys'\n branch: i18n/ai-translations\n labels: i18n, ai-generated, needs-review\n```\n\n### Agentic Translation Workflows\n\nAI agents automate the full translation lifecycle:\n\n```text\n1. Extract -> Agent detects new strings in code\n2. Translate -> AI generates translations per locale\n3. Review -> Agent routes to human reviewers\n4. Integrate -> Agent commits approved translations\n5. Monitor -> Agent tracks translation coverage\n```\n\n**TMS platforms with AI agents** (2025-2026):\n- Phrase: AI workflow automation\n- Lokalise: AI-assisted QA and review\n- Crowdin: AI translation suggestions\n- Smartling: Agentic content adaptation\n\n### Best Practices for AI Translation\n\n| Practice | Why |\n|----------|-----|\n| Always flag AI translations for review | Catch context errors |\n| Use `--only-missing` flag | Don't overwrite human translations |\n| Set up domain-specific glossaries | Improve technical accuracy |\n| Monitor translation quality metrics | Track AI performance over time |\n| Keep humans in the loop | Final approval for production |\n\n### When to Use AI vs Human Translation\n\n| Content Type | AI Suitability | Recommendation |\n|--------------|----------------|----------------|\n| UI strings | High | AI + light review |\n| Marketing copy | Medium | AI draft + human polish |\n| Legal/compliance | Low | Human translation |\n| Technical docs | High | AI + technical review |\n| User-generated content | High | AI-only acceptable |\n\n---\n\n## Edge Translation (On-Device)\n\nEmerging in 2025-2026: on-device translation for offline-first apps.\n\n```typescript\n// Example: On-device translation with Web AI\nif ('translation' in navigator) {\n const translator = await navigator.translation.createTranslator({\n sourceLanguage: 'en',\n targetLanguage: 'de',\n });\n\n const translated = await translator.translate('Hello, world!');\n // \"Hallo, Welt!\" - no network request\n}\n```\n\n**Current status**:\n- Chrome Origin Trial for Translation API\n- Safari exploring similar APIs\n- Useful for: privacy-sensitive apps, offline-first, low-latency requirements\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17668,"content_sha256":"c6a417d7dd7e364904a40b219dc89919ef40fbe9a056676c77f70582586c9f07"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Software Localisation - Quick Reference","type":"text"}]},{"type":"paragraph","content":[{"text":"Production patterns for internationalisation (i18n) and localisation (l10n) in modern web applications. Covers library selection, translation management, ICU message format, RTL support, and CI/CD workflows.","type":"text"}]},{"type":"paragraph","content":[{"text":"Snapshot (2026-02)","type":"text","marks":[{"type":"strong"}]},{"text":": i18next 25.x, react-i18next 16.x, react-intl 8.x, vue-i18n 11.x, next-intl 4.x, @angular/localize 21.x. Always verify current versions in the target repo (see Currency Check Protocol).","type":"text"}]},{"type":"paragraph","content":[{"text":"Authoritative References","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"i18next Documentation","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.i18next.com/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FormatJS/react-intl","type":"text","marks":[{"type":"link","attrs":{"href":"https://formatjs.github.io/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ICU Message Format","type":"text","marks":[{"type":"link","attrs":{"href":"https://unicode-org.github.io/icu/userguide/format_parse/messages/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MDN Intl API","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool/Library","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When to Use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"React i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"react-i18next","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i i18next react-i18next","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Most React apps, flexibility","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"React i18n (ICU)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"react-intl (FormatJS)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i react-intl","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ICU-first message catalog + tooling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vue i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"vue-i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i vue-i18n","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vue 3 apps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Angular i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@angular/localize","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ng add @angular/localize","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Angular apps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next.js i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"next-intl","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i next-intl","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next.js App Router","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Minimal bundle","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LinguiJS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i @lingui/core @lingui/react","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bundle size critical","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type-safe","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"typesafe-i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm i typesafe-i18n","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TypeScript-first projects","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String extraction","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"i18next-parser","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npx i18next-parser","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract keys from code","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ICU linting","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@formatjs/cli","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npx formatjs extract","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validate ICU messages","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Decision Tree: Library Selection","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Project requirements:\n │\n ├─ React/Next.js project?\n │ ├─ ICU-first message catalogs + FormatJS tooling?\n │ │ └─ react-intl (FormatJS)\n │ │\n │ ├─ Flexibility, plugins, lazy loading?\n │ │ └─ react-i18next\n │ │\n │ ├─ Bundle size critical?\n │ │ └─ LinguiJS (ICU syntax)\n │ │\n │ └─ TypeScript-first, compile-time safety?\n │ └─ typesafe-i18n\n │\n ├─ Vue/Nuxt project?\n │ └─ vue-i18n (Composition API)\n │\n ├─ Angular project?\n │ ├─ Built-in solution preferred?\n │ │ └─ @angular/localize (first-party, AOT support)\n │ │\n │ └─ Need i18next ecosystem?\n │ └─ angular-i18next (wrapper)\n │\n └─ Framework-agnostic / Node.js?\n └─ i18next core (works everywhere)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Library Comparison","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Library","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ICU Support","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lazy Loading","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TypeScript","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Best For","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"react-i18next","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plugin/optional","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Good","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flexible, popular React choice","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"react-intl","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Good","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ICU-first catalogs + tooling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LinguiJS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Excellent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bundle-conscious apps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"typesafe-i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Limited","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Excellent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Compile-time key safety","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"vue-i18n","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Good","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Vue 3 apps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@angular/localize","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AOT","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Native","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Angular apps","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Core Concepts","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Character Encoding (Critical)","type":"text"}]},{"type":"paragraph","content":[{"text":"Always use UTF-8","type":"text","marks":[{"type":"strong"}]},{"text":" across your entire stack to prevent text corruption:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"PASS Required: UTF-8 everywhere\n- Database: utf8mb4 (MySQL) or UTF-8 (PostgreSQL)\n- HTML: \u003cmeta charset=\"UTF-8\">\n- HTTP headers: Content-Type: text/html; charset=utf-8\n- File encoding: Save all source files as UTF-8\n- API responses: JSON with UTF-8 encoding","type":"text"}]},{"type":"paragraph","content":[{"text":"UTF-8 supports all Unicode characters including emojis, mathematical symbols, and all language scripts. Inconsistent encoding causes: corrupted characters (�), failed searches for accented names, and rejected international input.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Translation Key Patterns","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// Flat keys (simple)\n\"welcome\": \"Welcome to our app\"\n\"user.greeting\": \"Hello, {name}\"\n\n// Nested keys (organised)\n{\n \"user\": {\n \"greeting\": \"Hello, {name}\",\n \"profile\": {\n \"title\": \"Your Profile\"\n }\n }\n}\n\n// Namespace separation (scalable)\n// common.json, auth.json, dashboard.json","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"ICU Message Format Essentials","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"// Simple interpolation\n\"Hello, {name}!\"\n\n// Pluralisation\n\"{count, plural, one {# item} other {# items}}\"\n\n// Select (gender, category)\n\"{gender, select, male {He} female {She} other {They}} liked your post\"\n\n// Number formatting\n\"Price: {price, number, currency}\"\n\n// Date formatting\n\"Posted: {date, date, medium}\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Locale Detection Strategy","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Priority order:\n1. User preference (stored in profile/localStorage)\n2. URL parameter or path (/en/about, ?lang=de)\n3. Cookie (NEXT_LOCALE, i18next)\n4. Accept-Language header\n5. Default locale fallback","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Locale Quality Gates (SEO/AEO-Safe)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use these gates for locale-routed, indexable pages (for example ","type":"text"},{"text":"/vi/*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/de/*","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not ship mixed-language content on a single locale route.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not silently fall back to English for indexable page content.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep metadata, breadcrumbs, and JSON-LD in the same locale as visible content.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer explicit missing-key handling in CI over runtime fallback in production SEO pages.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If fallback is unavoidable, use locale-safe neutral copy and track missing keys.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Missing Translation Decision Rule","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Marketing/SEO pages: block publish or replace with locale-safe copy; never inject English fragments.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Product UI (non-indexed surfaces): fallback is acceptable with telemetry and follow-up fix.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"EN/RU Mixed-Language Regression Protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this when users report locale mixing (for example RU screens showing EN fragments).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1) Key-Parity Diff (Base vs Target Locale)","type":"text"}]},{"type":"paragraph","content":[{"text":"Compare key sets between source and target locale files; treat missing keys as release blockers on user-facing pages.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"jq -r 'paths(scalars) | join(\".\")' app/src/messages/en/*.json | sort -u > /tmp/en.keys\njq -r 'paths(scalars) | join(\".\")' app/src/messages/ru/*.json | sort -u > /tmp/ru.keys\ncomm -23 /tmp/en.keys /tmp/ru.keys # present in EN, missing in RU","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2) Hardcoded-String Sweep in UI","type":"text"}]},{"type":"paragraph","content":[{"text":"Search for user-visible literals in components/pages that should use i18n keys.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"rg -n '>[A-Za-z][^\u003c]{2,}\u003c' app/src -g '*.tsx'\nrg -n '\"[A-Za-z][^\"]{2,}\"' app/src -g '*.tsx' -g '*.ts'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3) Route-Level Locale Smoke Check","type":"text"}]},{"type":"paragraph","content":[{"text":"For target locale routes, verify rendered text is consistently localized and no fallback EN fragments appear in critical UI regions.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4) Engine Text Audit","type":"text"}]},{"type":"paragraph","content":[{"text":"Ensure computed/engine-driven messages (not just static labels) pass through translation mapping instead of returning raw EN strings.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5) CI Gate","type":"text"}]},{"type":"paragraph","content":[{"text":"Add a lightweight gate that fails when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"required target-locale keys are missing,","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"newly added UI literals bypass the i18n layer,","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"locale-routed smoke pages include mixed-language sentinel terms.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Runtime Constraint Note","type":"text"}]},{"type":"paragraph","content":[{"text":"If an agent runtime has no external translation connector, do not block on auto-translation tools. Enforce key completeness + placeholder strategy, then backfill approved translations in a separate tracked pass.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Engine Output i18n (","type":"text"},{"text":"_i18n","type":"text","marks":[{"type":"code_inline"}]},{"text":" Metadata Pattern)","type":"text"}]},{"type":"paragraph","content":[{"text":"Server-generated engine content (astrology calculations, ML outputs, computed reports) needs localisation without making the engine locale-aware.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pattern","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Engine attaches ","type":"text"},{"text":"_i18n: { key: \"transits.neptune_trine.description\", params: { planet: \"Neptune\" } }","type":"text","marks":[{"type":"code_inline"}]},{"text":" alongside the English string","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Client resolves: ","type":"text"},{"text":"_i18n ? t(_i18n.key, _i18n.params) : englishFallback","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Backward-compatible: old cached responses without ","type":"text"},{"text":"_i18n","type":"text","marks":[{"type":"code_inline"}]},{"text":" gracefully degrade to English","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Server caches once; every locale resolves on the client","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"t.has(key)","type":"text","marks":[{"type":"code_inline"}]},{"text":" before ","type":"text"},{"text":"t(key)","type":"text","marks":[{"type":"code_inline"}]},{"text":" for graceful fallback","type":"text"}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{ text: \"Neptune trine Jupiter\", _i18n: { key: \"transits.neptune_trine\", params: { p1: \"Neptune\", p2: \"Jupiter\" } } }","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PASS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Client resolves per locale; server caches once","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{ text_en: \"...\", text_ru: \"...\", text_de: \"...\" }","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FAIL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server bloat, cache per locale","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"t(meaning.theme)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FAIL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Using raw engine output as translation key","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"t.has('meanings.4.theme') ? t('meanings.4.theme') : meaning.theme","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PASS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Graceful fallback when key missing","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Locale Key Design Anti-Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"\"1 Field, N Slots\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Using one locale key for multiple distinct UI purposes. Each UI slot (badge, card title, modal description, affirmation) needs its own semantically distinct key, even if the English text happens to be similar.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"meanings.4.advice","type":"text","marks":[{"type":"code_inline"}]},{"text":" used for karmic debt, life path, birthday guidance, and advanced cycles (same text 6x)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FAIL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Coupling breaks when any slot needs a different translation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"meanings.4.advice","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"karmicDebt.4.lifeLesson","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"lifePath.4.affirmation","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"birthday.4.guidance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PASS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Distinct keys per slot — independent translation","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Short vs. Long Variants","type":"text"}]},{"type":"paragraph","content":[{"text":"Plan for both from the start.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"nodeGrowth","type":"text","marks":[{"type":"code_inline"}]},{"text":" (full: \"North Node — growth and new beginnings\") + ","type":"text"},{"text":"nodeGrowthShort","type":"text","marks":[{"type":"code_inline"}]},{"text":" (badge: \"Growth\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PASS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Each UI context gets appropriate length","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Single ","type":"text"},{"text":"nodeGrowth","type":"text","marks":[{"type":"code_inline"}]},{"text":" key that's too long for badge UI, requires substring hacks","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FAIL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Substring breaks in non-English locales","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Static Key Maps over String Transforms","type":"text"}]},{"type":"paragraph","content":[{"text":"When API output format doesn't match locale key naming, use a hardcoded map instead of string manipulation.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"// PASS: Explicit map — handles all edge cases\nconst PHASE_TO_KEY: Record\u003cstring, string> = {\n \"New Moon\": \"new\", \"Waxing Crescent\": \"waxingCrescent\",\n \"First Quarter\": \"firstQuarter\", \"Full Moon\": \"full\"\n};\n\n// FAIL: String transform — breaks on \"New Moon\" → \"newMoon\" vs actual key \"new\"\nconst key = phaseName.replace(/\\s+/g, '').replace(/^./, c => c.toLowerCase());","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Machine Translation Quality Gates","type":"text"}]},{"type":"paragraph","content":[{"text":"Short, domain-specific terms trip up automated MT. Known examples:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Locale","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Expected","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MT Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Term","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Arabic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"أرض","type":"text","marks":[{"type":"code_inline"}]},{"text":" (earth as element)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"أذن","type":"text","marks":[{"type":"code_inline"}]},{"text":" (ear)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"earth\"","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Japanese","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"火","type":"text","marks":[{"type":"code_inline"}]},{"text":" (fire as element)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"樅","type":"text","marks":[{"type":"code_inline"}]},{"text":" (fir tree)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"fire\"","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hindi","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"भू","type":"text","marks":[{"type":"code_inline"}]},{"text":" (earth)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"कान","type":"text","marks":[{"type":"code_inline"}]},{"text":" (ear)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"earth\"","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rules","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Maintain a curated dictionary of domain terms (zodiac signs, elements, planetary names, astronomical terms) per locale","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never auto-translate terms shorter than 3 words without dictionary lookup","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Post-MT audit: grep for known bad translations (compile a blocklist per locale)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For new locales, translate domain terms first, then use them as glossary constraints for MT","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Locale Propagation Protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"Every commit adding EN keys MUST propagate to all target locales. This is the #1 recurring i18n bug — hit in 4+ independent sessions.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Steps","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before commit","type":"text","marks":[{"type":"strong"}]},{"text":": diff EN locale files against target locales for missing keys","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Script-based propagation","type":"text","marks":[{"type":"strong"}]},{"text":": inject missing keys from EN into all other locales with EN fallback values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CI gate","type":"text","marks":[{"type":"strong"}]},{"text":": fail builds when target locale files have fewer keys than EN (configurable threshold)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pre-commit hook","type":"text","marks":[{"type":"strong"}]},{"text":" (optional): auto-run propagation script on staged locale files","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Quick Key-Parity Check","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"jq -r 'paths(scalars) | join(\".\")' messages/en/*.json | sort -u > /tmp/en.keys\nfor locale in ar de es fr hi it ja ko pt-BR ru tr vi zh; do\n jq -r 'paths(scalars) | join(\".\")' messages/$locale/*.json | sort -u > /tmp/$locale.keys\n echo \"=== $locale missing ===\"\n comm -23 /tmp/en.keys /tmp/$locale.keys | head -20\ndone","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Batch Translation Approach","type":"text"}]},{"type":"paragraph","content":[{"text":"Find all gaps first (diff-based), then translate systematically file-by-file. One-at-a-time discovery is 5x slower than batching:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run key-parity check across all locales to produce the full gap list","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Group missing keys by namespace/file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Translate one file at a time for each locale, using existing translations as glossary context","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify parity after the batch completes","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Duplicate JSON Key Detection","type":"text"}]},{"type":"paragraph","content":[{"text":"Large hand-edited JSON locale files can have duplicate keys. Per JSON spec, last-writer-wins — keys are silently dropped.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"CI Check","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Detect duplicate keys in JSON locale files\nnode -e \"\nconst fs = require('fs');\nconst file = process.argv[1];\nconst text = fs.readFileSync(file, 'utf8');\nconst keys = [];\nJSON.parse(text, (key, value) => { if (key) keys.push(key); return value; });\nconst dupes = keys.filter((k, i) => keys.indexOf(k) !== i);\nif (dupes.length) { console.error('DUPLICATE KEYS in', file, ':', [...new Set(dupes)]); process.exit(1); }\n\" \"$FILE\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Run this on every locale file in CI to catch silent key collisions before they reach production.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Navigation","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Resources (Deep Dives)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/framework-guides.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/framework-guides.md","title":null}}]},{"text":" - React, Vue, Angular, Next.js implementation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/icu-message-format.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/icu-message-format.md","title":null}}]},{"text":" - Pluralisation, select, formatting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/translation-workflows.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/translation-workflows.md","title":null}}]},{"text":" - TMS, CI/CD, string extraction","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/rtl-support.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/rtl-support.md","title":null}}]},{"text":" - Right-to-left language support","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/locale-handling.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/locale-handling.md","title":null}}]},{"text":" - Dates, numbers, currencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/testing-i18n.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/testing-i18n.md","title":null}}]},{"text":" - Pseudo-localisation, visual regression, plural testing, missing translation CI detection","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/accessibility-i18n.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/accessibility-i18n.md","title":null}}]},{"text":" - Screen readers across languages, ARIA in multilingual contexts, BiDi accessibility, IME","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/content-management-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/content-management-patterns.md","title":null}}]},{"text":" - Translation memory, glossaries, context for translators, MTPE workflows, cost optimisation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/ops-runbook.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ops-runbook.md","title":null}}]},{"text":" - LLM-safe triage scripts for large catalogs, key-parity checks, CI gate patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Templates (Production Starters)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/react-i18next-setup.md","type":"text","marks":[{"type":"link","attrs":{"href":"assets/react-i18next-setup.md","title":null}}]},{"text":" - React + i18next complete setup","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/vue-i18n-setup.md","type":"text","marks":[{"type":"link","attrs":{"href":"assets/vue-i18n-setup.md","title":null}}]},{"text":" - Vue 3 + vue-i18n setup","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/nextjs-i18n-setup.md","type":"text","marks":[{"type":"link","attrs":{"href":"assets/nextjs-i18n-setup.md","title":null}}]},{"text":" - Next.js App Router i18n","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data/sources.json","type":"text","marks":[{"type":"link","attrs":{"href":"data/sources.json","title":null}}]},{"text":" - 60+ curated external references","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"../software-frontend/SKILL.md","type":"text","marks":[{"type":"link","attrs":{"href":"../software-frontend/SKILL.md","title":null}}]},{"text":" - Frontend architecture patterns (React, Vue, Angular, Next.js)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"../marketing-seo/SKILL.md","type":"text","marks":[{"type":"link","attrs":{"href":"../marketing-seo/SKILL.md","title":null}}]},{"text":" - Hreflang, international SEO","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Namespace Organisation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"locales/\n├── en/\n│ ├── common.json # Shared: buttons, errors, nav\n│ ├── auth.json # Login, register, password\n│ ├── dashboard.json # Dashboard-specific\n│ └── validation.json # Form validation messages\n├── de/\n│ └── ... (same structure)\n└── ar/\n └── ... (same structure)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Lazy Loading and TypeScript Integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Load namespaces on demand (","type":"text"},{"text":"i18n.loadNamespaces","type":"text","marks":[{"type":"code_inline"}]},{"text":") and use ","type":"text"},{"text":"CustomTypeOptions","type":"text","marks":[{"type":"code_inline"}]},{"text":" in i18next to get compile-time key safety. See ","type":"text"},{"text":"references/framework-guides.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/framework-guides.md","title":null}}]},{"text":" for per-framework setup with code examples.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Anti-Patterns to Avoid","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Anti-Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Problem","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hardcoded strings","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not translatable","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extract all user-facing text","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"String concatenation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Breaks translation context","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use interpolation ","type":"text"},{"text":"{name}","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manual pluralisation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wrong for many languages","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ICU plural rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inline styles for RTL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Doesn't scale","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use CSS logical properties","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Storing locale in URL only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lost on navigation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Also persist to cookie/storage","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No fallback locale","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Blank text for missing keys","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Always set ","type":"text"},{"text":"fallbackLng","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Silent English fallback on indexable non-English pages","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mixed-language output harms UX and can weaken locale SEO/AEO quality","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use locale-safe copy or fail build on missing keys for indexable routes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Loading all locales upfront","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Slow initial load","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lazy load per namespace/locale","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Operational Checklist","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Initial Setup","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Choose i18n library based on decision tree","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Set up directory structure for translations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Configure fallback locale chain","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Set up locale detection strategy","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Add TypeScript types for translation keys","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Configure lazy loading for namespaces","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Translation Workflow","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Set up string extraction (i18next-parser, formatjs, Lingui)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Integrate with a TMS when needed (Phrase, Lokalise, Crowdin, Locize)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Configure CI/CD for translation sync","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Set up translation review process (glossary + style guide + QA gates)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Add missing key detection in development","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Add hardcoded string detection for locale-routed pages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Verify metadata + JSON-LD locale parity with visible content","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Add locale QA for mixed-language regressions on high-intent pages","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"i18n Key Validation (Per-Change Gate)","type":"text"}]},{"type":"paragraph","content":[{"text":"When adding new ","type":"text"},{"text":"t()","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"useTranslations()","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls or new message keys:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify the key exists in the base locale file (e.g., ","type":"text"},{"text":"messages/en/*.json","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add the key to the base locale file before using it in code.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For multi-locale projects, add placeholder entries in all locale files or confirm the fallback chain handles missing keys gracefully.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the project's missing-key detection (e.g., ","type":"text"},{"text":"npm run build","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"next-intl","type":"text","marks":[{"type":"code_inline"}]},{"text":" compile check) before committing.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Missing i18n keys cause blank text or fallback-language bleed on localized pages — a silent, user-facing regression.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"RTL Support","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Use CSS logical properties (margin-inline-start)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Set ","type":"text"},{"text":"dir=\"rtl\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" for RTL locales","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test with real RTL content (Arabic, Hebrew)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Handle bidirectional text (BiDi) in mixed strings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Mirror directional icons and images where appropriate","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test pluralisation with 0, 1, 2, 5, 21 (language-specific)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test date/number/currency formatting per locale","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test RTL layout in key screens/components","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test missing translation key handling (dev-only warnings)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"REQUIRED: Test locale switching and persistence (cookie/storage/url)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Currency Check Protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"When recommending libraries, versions, or tooling, verify what is current for the target ecosystem and project constraints. Prefer package registries and release notes over stale hard-coded numbers.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Package versions (Node/npm)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"npm view i18next version\nnpm view react-i18next version\nnpm view react-intl version\nnpm view vue-i18n version\nnpm view next-intl version\nnpm view @angular/localize version\nnpm view @lingui/core version\nnpm view typesafe-i18n version","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"\"Is X still recommended?\" checks","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check the project's last release date, open issues, and maintenance activity (GitHub releases/issues).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check framework compatibility (Next.js App Router/RSC, React 19, Vue 3, Angular current major).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For bundle concerns, measure in the real app with a bundle analyzer instead of relying on published size claims.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Ops Runbook: Large Locale Catalogs (LLM-Safe)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this when locale catalogs are too large for single reads, mixed-language UI appears, or missing keys are reported. See ","type":"text"},{"text":"references/ops-runbook.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ops-runbook.md","title":null}}]},{"text":" for triage scripts, key-parity checks, hardcoded string sweeps, and CI gate patterns.","type":"text"}]},{"type":"paragraph","content":[{"text":"Operational Rules","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never read large locale files in one shot; always chunk.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use key diff first, translation pass second.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat marketing/SEO locale key gaps as release blockers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not auto-insert machine translations without a tracked review pass.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fact-Checking","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer primary sources; report source links and dates for volatile information.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If web access is unavailable, state the limitation and mark guidance as unverified.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"software-localisation","author":"@skillopedia","source":{"stars":60,"repo_name":"ai-agents-public","origin_url":"https://github.com/vasilyu1983/ai-agents-public/blob/HEAD/frameworks/shared-skills/skills/software-localisation/SKILL.md","repo_owner":"vasilyu1983","body_sha256":"fa54551a9c36a96e049a43161c2c9cbb6f3978083c84d1f190a131cb9c81b496","cluster_key":"063f3a834bc760d21f20ba5bf2dc72d743c2495ab77755df1073e9752f189965","clean_bundle":{"format":"clean-skill-bundle-v1","source":"vasilyu1983/ai-agents-public/frameworks/shared-skills/skills/software-localisation/SKILL.md","attachments":[{"id":"70006678-3171-5c23-b7e6-d98f24286326","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70006678-3171-5c23-b7e6-d98f24286326/attachment.md","path":"assets/nextjs-i18n-setup.md","size":15489,"sha256":"3e83abc02c95eba58aea4ea7cd218d40eeea400a69b1d90c388e91041fe4612a","contentType":"text/markdown; charset=utf-8"},{"id":"47c25f94-7048-52b7-828a-19a2381a1320","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/47c25f94-7048-52b7-828a-19a2381a1320/attachment.md","path":"assets/react-i18next-setup.md","size":11885,"sha256":"0d185d0ca101792c68c1a16ed0bda1b82bea426e700d139a5afcb98b3c9a0e0e","contentType":"text/markdown; charset=utf-8"},{"id":"c53f3055-4b8b-5aea-a240-bc5d5cddee41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c53f3055-4b8b-5aea-a240-bc5d5cddee41/attachment.md","path":"assets/vue-i18n-setup.md","size":12854,"sha256":"343f85db700744f558b93ae40f82c854e1400e4f8c2eb9f2b1cc5de6cb8517da","contentType":"text/markdown; charset=utf-8"},{"id":"cfd7122d-b3da-5962-8c10-24a6368eb311","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cfd7122d-b3da-5962-8c10-24a6368eb311/attachment.json","path":"data/sources.json","size":19544,"sha256":"18552193afd9780aea62ab9c82821b0fc2c9bf5d7d749011050fd8f6a3fb31f6","contentType":"application/json; charset=utf-8"},{"id":"7fd60615-17f5-5c62-8048-06602134c4b2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7fd60615-17f5-5c62-8048-06602134c4b2/attachment.md","path":"references/accessibility-i18n.md","size":16850,"sha256":"b68237cd6c6ca3539eee5f68b4cbc1bb4a432741b6fde86ae387407acfde3674","contentType":"text/markdown; charset=utf-8"},{"id":"6d25d9d8-2537-500b-a8ee-a933db2b1682","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6d25d9d8-2537-500b-a8ee-a933db2b1682/attachment.md","path":"references/content-management-patterns.md","size":17702,"sha256":"7cd32f184280ecd413019cd845303687ec3c07cf6d21f83bad734fafddcacc0b","contentType":"text/markdown; charset=utf-8"},{"id":"f21ec757-51bb-5692-a21c-38cd0062b6a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f21ec757-51bb-5692-a21c-38cd0062b6a9/attachment.md","path":"references/framework-guides.md","size":16533,"sha256":"c447bdc2b3076c883f58e03425eda43030c55096d925b955c0cb91d1fbcf02e6","contentType":"text/markdown; charset=utf-8"},{"id":"a96146aa-d364-53ba-a635-8d949bd6ce9f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a96146aa-d364-53ba-a635-8d949bd6ce9f/attachment.md","path":"references/icu-message-format.md","size":10947,"sha256":"f0d002a7940e06b54e467ce0a53b8cfa35f35ca0aaa5e184183909277deb8845","contentType":"text/markdown; charset=utf-8"},{"id":"5eb26612-0ff2-5598-83db-4a2ed70c2b1d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5eb26612-0ff2-5598-83db-4a2ed70c2b1d/attachment.md","path":"references/locale-handling.md","size":17723,"sha256":"9431e1deb1886e0669c1946ebcf3cc669812fad903c08f453b3dd49c81ccf2d9","contentType":"text/markdown; charset=utf-8"},{"id":"c24d6153-6f7e-5489-9494-bf6e1a1e60f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c24d6153-6f7e-5489-9494-bf6e1a1e60f2/attachment.md","path":"references/ops-runbook.md","size":1701,"sha256":"2110007a23f703cfccabefe49a15dbcf58af9f9123feb9fe77ecf9933ad00d70","contentType":"text/markdown; charset=utf-8"},{"id":"845ddf27-baab-59c9-bad3-6dc717dcf700","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/845ddf27-baab-59c9-bad3-6dc717dcf700/attachment.md","path":"references/rtl-support.md","size":12988,"sha256":"9813f9424a0fbc34822b4373da530fe118d1db39d6dccc3761406d189ee2351b","contentType":"text/markdown; charset=utf-8"},{"id":"22025dc2-f4ea-5916-8215-4c5bc7161845","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/22025dc2-f4ea-5916-8215-4c5bc7161845/attachment.md","path":"references/testing-i18n.md","size":18716,"sha256":"9943a4c4bc13ad80f67c9c2026975641133f5cc0098f34cca134b1e45af76efe","contentType":"text/markdown; charset=utf-8"},{"id":"e5b275e6-8208-5fe5-a8fa-d2d73b4277b0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5b275e6-8208-5fe5-a8fa-d2d73b4277b0/attachment.md","path":"references/translation-workflows.md","size":17668,"sha256":"c6a417d7dd7e364904a40b219dc89919ef40fbe9a056676c77f70582586c9f07","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"72be06f15e8da22d827462dc860c8fc78e30c29dee37b76622f8cfca0690390f","attachment_count":13,"text_attachments":13,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"frameworks/shared-skills/skills/software-localisation/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Production-grade i18n/l10n for React, Vue, Angular, and Next.js with ICU format and RTL support. Use when setting up or debugging localisation."}},"renderedAt":1782979460026}

Software Localisation - Quick Reference Production patterns for internationalisation (i18n) and localisation (l10n) in modern web applications. Covers library selection, translation management, ICU message format, RTL support, and CI/CD workflows. Snapshot (2026-02) : i18next 25.x, react-i18next 16.x, react-intl 8.x, vue-i18n 11.x, next-intl 4.x, @angular/localize 21.x. Always verify current versions in the target repo (see Currency Check Protocol). Authoritative References : - i18next Documentation - FormatJS/react-intl - ICU Message Format - MDN Intl API Quick Reference | Task | Tool/Librar…