specweave 1.0.239 → 1.0.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CLAUDE.md +31 -30
  2. package/README.md +1 -1
  3. package/bin/specweave.js +16 -0
  4. package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts.map +1 -1
  5. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js +17 -2
  6. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +7 -0
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  9. package/dist/plugins/specweave-github/lib/github-feature-sync.js +53 -0
  10. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  11. package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -1
  12. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +17 -2
  13. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -1
  14. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +1 -0
  15. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -1
  16. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +7 -3
  17. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -1
  18. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -1
  19. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +27 -19
  20. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -1
  21. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +8 -0
  22. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -1
  23. package/dist/plugins/specweave-testing/lib/playwright-routing.js +10 -7
  24. package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -1
  25. package/dist/src/adapters/agents-md-generator.js +1 -1
  26. package/dist/src/adapters/agents-md-generator.js.map +1 -1
  27. package/dist/src/adapters/claude/README.md +1 -1
  28. package/dist/src/adapters/claude-md-generator.js +1 -1
  29. package/dist/src/adapters/claude-md-generator.js.map +1 -1
  30. package/dist/src/cli/commands/init.d.ts.map +1 -1
  31. package/dist/src/cli/commands/init.js +10 -1
  32. package/dist/src/cli/commands/init.js.map +1 -1
  33. package/dist/src/cli/commands/refresh-marketplace.d.ts.map +1 -1
  34. package/dist/src/cli/commands/refresh-marketplace.js +7 -67
  35. package/dist/src/cli/commands/refresh-marketplace.js.map +1 -1
  36. package/dist/src/cli/commands/team.d.ts +20 -0
  37. package/dist/src/cli/commands/team.d.ts.map +1 -0
  38. package/dist/src/cli/commands/team.js +101 -0
  39. package/dist/src/cli/commands/team.js.map +1 -0
  40. package/dist/src/cli/helpers/init/claude-settings-env.d.ts +16 -0
  41. package/dist/src/cli/helpers/init/claude-settings-env.d.ts.map +1 -0
  42. package/dist/src/cli/helpers/init/claude-settings-env.js +44 -0
  43. package/dist/src/cli/helpers/init/claude-settings-env.js.map +1 -0
  44. package/dist/src/cli/helpers/init/plugin-installer.d.ts.map +1 -1
  45. package/dist/src/cli/helpers/init/plugin-installer.js +9 -13
  46. package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
  47. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  48. package/dist/src/cli/helpers/issue-tracker/index.js +12 -6
  49. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  50. package/dist/src/cli/helpers/issue-tracker/types.d.ts +2 -0
  51. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  52. package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
  53. package/dist/src/core/increment/discipline-checker.js +1 -1
  54. package/dist/src/core/increment/discipline-checker.js.map +1 -1
  55. package/dist/src/core/increment/status-commands.d.ts.map +1 -1
  56. package/dist/src/core/increment/status-commands.js +7 -0
  57. package/dist/src/core/increment/status-commands.js.map +1 -1
  58. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +2 -2
  59. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
  60. package/dist/src/core/lazy-loading/llm-plugin-detector.js +63 -25
  61. package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
  62. package/dist/src/core/reflection/reflect-handler.js +2 -2
  63. package/dist/src/core/reflection/reflect-handler.js.map +1 -1
  64. package/dist/src/core/session/handoff-context.js +2 -2
  65. package/dist/src/core/session/handoff-context.js.map +1 -1
  66. package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
  67. package/dist/src/sync/ado-reconciler.js +21 -2
  68. package/dist/src/sync/ado-reconciler.js.map +1 -1
  69. package/dist/src/sync/engine.d.ts.map +1 -1
  70. package/dist/src/sync/engine.js +2 -0
  71. package/dist/src/sync/engine.js.map +1 -1
  72. package/dist/src/sync/github-reconciler.d.ts.map +1 -1
  73. package/dist/src/sync/github-reconciler.js +52 -26
  74. package/dist/src/sync/github-reconciler.js.map +1 -1
  75. package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
  76. package/dist/src/sync/jira-reconciler.js +16 -3
  77. package/dist/src/sync/jira-reconciler.js.map +1 -1
  78. package/dist/src/sync/providers/ado.d.ts.map +1 -1
  79. package/dist/src/sync/providers/ado.js +4 -2
  80. package/dist/src/sync/providers/ado.js.map +1 -1
  81. package/dist/src/sync/providers/github.d.ts.map +1 -1
  82. package/dist/src/sync/providers/github.js +11 -0
  83. package/dist/src/sync/providers/github.js.map +1 -1
  84. package/dist/src/sync/providers/jira.d.ts.map +1 -1
  85. package/dist/src/sync/providers/jira.js +14 -2
  86. package/dist/src/sync/providers/jira.js.map +1 -1
  87. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  88. package/dist/src/sync/sync-coordinator.js +31 -6
  89. package/dist/src/sync/sync-coordinator.js.map +1 -1
  90. package/dist/src/utils/auto-install.js +4 -4
  91. package/dist/src/utils/auto-install.js.map +1 -1
  92. package/package.json +2 -2
  93. package/plugins/FINAL-AUDIT-RECOMMENDATIONS.md +3 -3
  94. package/plugins/SKILLS-VS-AGENTS.md +1 -1
  95. package/plugins/specweave/PLUGIN.md +0 -2
  96. package/plugins/specweave/commands/export-skills.md +1 -1
  97. package/plugins/specweave/commands/role-orchestrator.md +1 -1
  98. package/plugins/specweave/hooks/log-decision.sh +6 -0
  99. package/plugins/specweave/hooks/stop-auto-v5.sh +17 -1
  100. package/plugins/specweave/hooks/stop-reflect.sh +16 -2
  101. package/plugins/specweave/hooks/stop-sync.sh +17 -9
  102. package/plugins/specweave/hooks/user-prompt-submit.sh +119 -35
  103. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +52 -26
  104. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  105. package/plugins/specweave/scripts/read-grill-context.sh +149 -0
  106. package/plugins/specweave/skills/code-review/SKILL.md +608 -0
  107. package/plugins/specweave/skills/done/SKILL.md +1 -1
  108. package/plugins/specweave/skills/grill/SKILL.md +91 -0
  109. package/plugins/specweave/skills/performance/SKILL.md +6 -0
  110. package/plugins/specweave/skills/security/SKILL.md +7 -0
  111. package/plugins/specweave/skills/security-patterns/SKILL.md +6 -0
  112. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  113. package/plugins/specweave/skills/team-build/SKILL.md +1 -1
  114. package/plugins/specweave/skills/team-orchestrate/SKILL.md +1 -1
  115. package/plugins/specweave/skills/tech-lead/SKILL.md +7 -0
  116. package/plugins/specweave-ado/lib/ado-permission-gate.js +18 -2
  117. package/plugins/specweave-ado/lib/ado-permission-gate.ts +19 -2
  118. package/plugins/specweave-frontend/skills/frontend/SKILL.md +138 -2
  119. package/plugins/specweave-frontend/skills/i18n-expert/SKILL.md +989 -0
  120. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +23 -1
  121. package/plugins/specweave-github/lib/github-feature-sync.js +41 -0
  122. package/plugins/specweave-github/lib/github-feature-sync.ts +62 -0
  123. package/plugins/specweave-infrastructure/PLUGIN.md +2 -1
  124. package/plugins/specweave-infrastructure/skills/gcp-deep-dive/SKILL.md +1172 -0
  125. package/plugins/specweave-infrastructure/skills/observability/SKILL.md +6 -0
  126. package/plugins/specweave-infrastructure/skills/opentelemetry/SKILL.md +6 -0
  127. package/plugins/specweave-jira/lib/jira-permission-gate.js +18 -2
  128. package/plugins/specweave-jira/lib/jira-permission-gate.ts +19 -2
  129. package/plugins/specweave-mobile/PLUGIN.md +1 -2
  130. package/plugins/specweave-mobile/README.md +13 -12
  131. package/plugins/specweave-mobile/skills/capacitor-ionic/SKILL.md +4 -18
  132. package/plugins/specweave-mobile/skills/deep-linking-push/SKILL.md +4 -22
  133. package/plugins/specweave-mobile/skills/expo/SKILL.md +4 -24
  134. package/plugins/specweave-mobile/skills/mobile-testing/SKILL.md +4 -22
  135. package/plugins/specweave-mobile/skills/react-native-expert/SKILL.md +404 -47
  136. package/plugins/specweave-testing/PLUGIN.md +3 -11
  137. package/plugins/specweave-testing/lib/playwright-cli-detector.js +8 -3
  138. package/plugins/specweave-testing/lib/playwright-cli-detector.ts +8 -3
  139. package/plugins/specweave-testing/lib/playwright-cli-runner.js +25 -20
  140. package/plugins/specweave-testing/lib/playwright-cli-runner.ts +24 -19
  141. package/plugins/specweave-testing/lib/playwright-routing.js +1 -6
  142. package/plugins/specweave-testing/lib/playwright-routing.ts +11 -8
  143. package/plugins/specweave-testing/skills/accessibility-testing/SKILL.md +998 -0
  144. package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +29 -28
  145. package/plugins/specweave-testing/skills/mutation-testing/SKILL.md +769 -0
  146. package/plugins/specweave-testing/skills/performance-testing/SKILL.md +961 -0
  147. package/plugins/specweave-testing/skills/qa-engineer/SKILL.md +2 -0
  148. package/plugins/specweave/.specweave/logs/decisions.jsonl +0 -12
  149. package/plugins/specweave/.specweave/logs/reflect/reflect.log +0 -8
  150. package/plugins/specweave/.specweave/logs/stop-auto.log +0 -6
  151. package/plugins/specweave/.specweave/logs/stop-sync.log +0 -10
  152. package/plugins/specweave/.specweave/state/dashboard.json +0 -43
  153. package/plugins/specweave/skills/infrastructure/SKILL.md +0 -86
  154. package/plugins/specweave/skills/qa-lead/SKILL.md +0 -77
  155. package/plugins/specweave-mobile/skills/mobile-architect/SKILL.md +0 -30
  156. package/plugins/specweave-testing/commands/e2e-setup.md +0 -1103
  157. package/plugins/specweave-testing/commands/test-coverage.md +0 -983
  158. package/plugins/specweave-testing/commands/test-generate.md +0 -1160
  159. package/plugins/specweave-testing/commands/test-init.md +0 -413
  160. package/plugins/specweave-testing/commands/ui-automate.md +0 -182
  161. package/plugins/specweave-testing/commands/ui-inspect.md +0 -82
@@ -0,0 +1,989 @@
1
+ ---
2
+ description: Expert in frontend internationalization (i18n) and localization (l10n) covering i18next/react-i18next setup, Next.js i18n routing, RTL support, date/number/currency formatting with Intl APIs, translation management workflows, and performance optimization. Use when implementing multilingual apps, adding locale support, handling RTL layouts, or managing translation pipelines.
3
+ allowed-tools: Read, Write, Edit, Bash
4
+ model: opus
5
+ context: fork
6
+ ---
7
+
8
+ # i18n Expert
9
+
10
+ You are an expert in frontend internationalization (i18n) and localization (l10n). You help teams build multilingual applications that handle translations, locale-aware formatting, RTL layouts, and translation management workflows.
11
+
12
+ **Triggers**: i18n, internationalization, translation, localization, l10n, RTL, multilingual, locale, react-i18next, hreflang, Intl, pluralization, Crowdin, Lokalise
13
+
14
+ ## Core Expertise
15
+
16
+ ### 1. i18next / react-i18next Setup
17
+
18
+ **Installation**:
19
+ ```bash
20
+ npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
21
+ ```
22
+
23
+ **Configuration** (`src/i18n/index.ts`):
24
+ ```typescript
25
+ import i18n from 'i18next';
26
+ import { initReactI18next } from 'react-i18next';
27
+ import LanguageDetector from 'i18next-browser-languagedetector';
28
+ import HttpBackend from 'i18next-http-backend';
29
+
30
+ i18n
31
+ .use(HttpBackend)
32
+ .use(LanguageDetector)
33
+ .use(initReactI18next)
34
+ .init({
35
+ fallbackLng: 'en',
36
+ supportedLngs: ['en', 'de', 'fr', 'ar', 'ja', 'zh'],
37
+ defaultNS: 'common',
38
+ ns: ['common', 'auth', 'dashboard', 'errors'],
39
+
40
+ interpolation: {
41
+ escapeValue: false, // React already escapes
42
+ },
43
+
44
+ detection: {
45
+ order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
46
+ lookupQuerystring: 'lng',
47
+ lookupCookie: 'i18next',
48
+ lookupLocalStorage: 'i18nextLng',
49
+ caches: ['localStorage', 'cookie'],
50
+ },
51
+
52
+ backend: {
53
+ loadPath: '/locales/{{lng}}/{{ns}}.json',
54
+ },
55
+
56
+ react: {
57
+ useSuspense: true,
58
+ },
59
+ });
60
+
61
+ export default i18n;
62
+ ```
63
+
64
+ **Namespace Organization**:
65
+ ```
66
+ public/locales/
67
+ ├── en/
68
+ │ ├── common.json # Shared: buttons, labels, navigation
69
+ │ ├── auth.json # Login, signup, password reset
70
+ │ ├── dashboard.json # Dashboard-specific strings
71
+ │ ├── errors.json # Error messages
72
+ │ └── validation.json # Form validation messages
73
+ ├── de/
74
+ │ ├── common.json
75
+ │ └── ...
76
+ └── ar/
77
+ ├── common.json
78
+ └── ...
79
+ ```
80
+
81
+ **Namespace JSON structure** (`en/common.json`):
82
+ ```json
83
+ {
84
+ "nav": {
85
+ "home": "Home",
86
+ "about": "About",
87
+ "settings": "Settings"
88
+ },
89
+ "actions": {
90
+ "save": "Save",
91
+ "cancel": "Cancel",
92
+ "delete": "Delete",
93
+ "confirm": "Are you sure?"
94
+ },
95
+ "greeting": "Hello, {{name}}!"
96
+ }
97
+ ```
98
+
99
+ **Component Usage**:
100
+ ```tsx
101
+ import { useTranslation } from 'react-i18next';
102
+
103
+ function Dashboard() {
104
+ const { t } = useTranslation('dashboard');
105
+ const { t: tCommon } = useTranslation('common');
106
+
107
+ return (
108
+ <div>
109
+ <h1>{t('title')}</h1>
110
+ <p>{tCommon('greeting', { name: 'Alice' })}</p>
111
+ <button>{tCommon('actions.save')}</button>
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ **Pluralization** (`en/common.json`):
118
+ ```json
119
+ {
120
+ "items_count": "{{count}} item",
121
+ "items_count_other": "{{count}} items",
122
+ "items_count_zero": "No items"
123
+ }
124
+ ```
125
+
126
+ ```tsx
127
+ // Automatically selects the correct plural form
128
+ t('items_count', { count: 0 }); // "No items"
129
+ t('items_count', { count: 1 }); // "1 item"
130
+ t('items_count', { count: 5 }); // "5 items"
131
+ ```
132
+
133
+ **Context-based translations** (e.g., gendered text):
134
+ ```json
135
+ {
136
+ "friend": "A friend",
137
+ "friend_male": "A boyfriend",
138
+ "friend_female": "A girlfriend"
139
+ }
140
+ ```
141
+ ```tsx
142
+ t('friend', { context: 'male' }); // "A boyfriend"
143
+ ```
144
+
145
+ **Language Switcher Component**:
146
+ ```tsx
147
+ import { useTranslation } from 'react-i18next';
148
+
149
+ const languages = [
150
+ { code: 'en', label: 'English', dir: 'ltr' },
151
+ { code: 'de', label: 'Deutsch', dir: 'ltr' },
152
+ { code: 'ar', label: 'العربية', dir: 'rtl' },
153
+ { code: 'ja', label: '日本語', dir: 'ltr' },
154
+ ];
155
+
156
+ function LanguageSwitcher() {
157
+ const { i18n } = useTranslation();
158
+
159
+ const changeLanguage = (code: string) => {
160
+ i18n.changeLanguage(code);
161
+ const lang = languages.find((l) => l.code === code);
162
+ document.documentElement.dir = lang?.dir ?? 'ltr';
163
+ document.documentElement.lang = code;
164
+ };
165
+
166
+ return (
167
+ <select
168
+ value={i18n.language}
169
+ onChange={(e) => changeLanguage(e.target.value)}
170
+ aria-label="Select language"
171
+ >
172
+ {languages.map((lang) => (
173
+ <option key={lang.code} value={lang.code}>
174
+ {lang.label}
175
+ </option>
176
+ ))}
177
+ </select>
178
+ );
179
+ }
180
+ ```
181
+
182
+ ### 2. Next.js i18n (App Router)
183
+
184
+ **Directory-based i18n routing**:
185
+ ```
186
+ app/
187
+ ├── [locale]/
188
+ │ ├── layout.tsx
189
+ │ ├── page.tsx
190
+ │ ├── about/
191
+ │ │ └── page.tsx
192
+ │ └── dashboard/
193
+ │ └── page.tsx
194
+ ├── middleware.ts
195
+ └── i18n/
196
+ ├── config.ts
197
+ ├── request.ts
198
+ └── dictionaries.ts
199
+ ```
200
+
201
+ **i18n Config** (`i18n/config.ts`):
202
+ ```typescript
203
+ export const i18nConfig = {
204
+ defaultLocale: 'en',
205
+ locales: ['en', 'de', 'fr', 'ar', 'ja'],
206
+ } as const;
207
+
208
+ export type Locale = (typeof i18nConfig.locales)[number];
209
+ ```
210
+
211
+ **Middleware for Language Detection** (`middleware.ts`):
212
+ ```typescript
213
+ import { NextRequest, NextResponse } from 'next/server';
214
+ import { i18nConfig } from './i18n/config';
215
+
216
+ function getLocale(request: NextRequest): string {
217
+ // 1. Check cookie
218
+ const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
219
+ if (cookieLocale && i18nConfig.locales.includes(cookieLocale as any)) {
220
+ return cookieLocale;
221
+ }
222
+
223
+ // 2. Check Accept-Language header
224
+ const acceptLanguage = request.headers.get('accept-language');
225
+ if (acceptLanguage) {
226
+ const preferred = acceptLanguage
227
+ .split(',')
228
+ .map((lang) => lang.split(';')[0].trim().substring(0, 2))
229
+ .find((lang) => i18nConfig.locales.includes(lang as any));
230
+ if (preferred) return preferred;
231
+ }
232
+
233
+ return i18nConfig.defaultLocale;
234
+ }
235
+
236
+ export function middleware(request: NextRequest) {
237
+ const { pathname } = request.nextUrl;
238
+
239
+ // Skip static assets, API routes, and _next
240
+ if (
241
+ pathname.startsWith('/_next') ||
242
+ pathname.startsWith('/api') ||
243
+ pathname.includes('.')
244
+ ) {
245
+ return;
246
+ }
247
+
248
+ // Check if pathname has a locale prefix
249
+ const pathnameHasLocale = i18nConfig.locales.some(
250
+ (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
251
+ );
252
+
253
+ if (pathnameHasLocale) return;
254
+
255
+ // Redirect to locale-prefixed URL
256
+ const locale = getLocale(request);
257
+ const newUrl = new URL(`/${locale}${pathname}`, request.url);
258
+ return NextResponse.redirect(newUrl);
259
+ }
260
+
261
+ export const config = {
262
+ matcher: ['/((?!_next|api|favicon.ico|robots.txt|sitemap.xml).*)'],
263
+ };
264
+ ```
265
+
266
+ **Dictionary Loading** (`i18n/dictionaries.ts`):
267
+ ```typescript
268
+ import type { Locale } from './config';
269
+
270
+ const dictionaries = {
271
+ en: () => import('../dictionaries/en.json').then((m) => m.default),
272
+ de: () => import('../dictionaries/de.json').then((m) => m.default),
273
+ fr: () => import('../dictionaries/fr.json').then((m) => m.default),
274
+ ar: () => import('../dictionaries/ar.json').then((m) => m.default),
275
+ ja: () => import('../dictionaries/ja.json').then((m) => m.default),
276
+ };
277
+
278
+ export function getDictionary(locale: Locale) {
279
+ return dictionaries[locale]();
280
+ }
281
+ ```
282
+
283
+ **Locale Layout** (`app/[locale]/layout.tsx`):
284
+ ```tsx
285
+ import { i18nConfig, type Locale } from '@/i18n/config';
286
+ import { notFound } from 'next/navigation';
287
+
288
+ export function generateStaticParams() {
289
+ return i18nConfig.locales.map((locale) => ({ locale }));
290
+ }
291
+
292
+ export async function generateMetadata({
293
+ params,
294
+ }: {
295
+ params: Promise<{ locale: Locale }>;
296
+ }) {
297
+ const { locale } = await params;
298
+ return {
299
+ alternates: {
300
+ canonical: `https://example.com/${locale}`,
301
+ languages: Object.fromEntries(
302
+ i18nConfig.locales.map((l) => [l, `https://example.com/${l}`])
303
+ ),
304
+ },
305
+ };
306
+ }
307
+
308
+ export default async function LocaleLayout({
309
+ children,
310
+ params,
311
+ }: {
312
+ children: React.ReactNode;
313
+ params: Promise<{ locale: Locale }>;
314
+ }) {
315
+ const { locale } = await params;
316
+
317
+ if (!i18nConfig.locales.includes(locale)) {
318
+ notFound();
319
+ }
320
+
321
+ const dir = locale === 'ar' ? 'rtl' : 'ltr';
322
+
323
+ return (
324
+ <html lang={locale} dir={dir}>
325
+ <body>{children}</body>
326
+ </html>
327
+ );
328
+ }
329
+ ```
330
+
331
+ **SEO: hreflang Tags** (in root layout `<head>`):
332
+ ```tsx
333
+ import { i18nConfig } from '@/i18n/config';
334
+
335
+ function HreflangTags({ currentPath }: { currentPath: string }) {
336
+ const baseUrl = 'https://example.com';
337
+
338
+ return (
339
+ <>
340
+ {i18nConfig.locales.map((locale) => (
341
+ <link
342
+ key={locale}
343
+ rel="alternate"
344
+ hrefLang={locale}
345
+ href={`${baseUrl}/${locale}${currentPath}`}
346
+ />
347
+ ))}
348
+ <link
349
+ rel="alternate"
350
+ hrefLang="x-default"
351
+ href={`${baseUrl}/${i18nConfig.defaultLocale}${currentPath}`}
352
+ />
353
+ </>
354
+ );
355
+ }
356
+ ```
357
+
358
+ **Locale Page with Dictionary** (`app/[locale]/page.tsx`):
359
+ ```tsx
360
+ import { getDictionary } from '@/i18n/dictionaries';
361
+ import type { Locale } from '@/i18n/config';
362
+
363
+ export default async function HomePage({
364
+ params,
365
+ }: {
366
+ params: Promise<{ locale: Locale }>;
367
+ }) {
368
+ const { locale } = await params;
369
+ const dict = await getDictionary(locale);
370
+
371
+ return (
372
+ <main>
373
+ <h1>{dict.home.title}</h1>
374
+ <p>{dict.home.description}</p>
375
+ </main>
376
+ );
377
+ }
378
+ ```
379
+
380
+ ### 3. RTL Support
381
+
382
+ **CSS Logical Properties** (replace physical properties with logical equivalents):
383
+
384
+ | Physical (avoid) | Logical (prefer) |
385
+ |---|---|
386
+ | `margin-left` | `margin-inline-start` |
387
+ | `margin-right` | `margin-inline-end` |
388
+ | `padding-left` | `padding-inline-start` |
389
+ | `padding-right` | `padding-inline-end` |
390
+ | `text-align: left` | `text-align: start` |
391
+ | `text-align: right` | `text-align: end` |
392
+ | `float: left` | `float: inline-start` |
393
+ | `border-left` | `border-inline-start` |
394
+ | `left: 0` | `inset-inline-start: 0` |
395
+ | `right: 0` | `inset-inline-end: 0` |
396
+ | `width` | `inline-size` |
397
+ | `height` | `block-size` |
398
+
399
+ **RTL-safe CSS example**:
400
+ ```css
401
+ .sidebar {
402
+ /* Physical (breaks RTL): */
403
+ /* padding-left: 1rem; margin-right: 2rem; */
404
+
405
+ /* Logical (works in LTR and RTL): */
406
+ padding-inline-start: 1rem;
407
+ margin-inline-end: 2rem;
408
+ border-inline-start: 3px solid var(--accent);
409
+ inset-inline-start: 0;
410
+ }
411
+
412
+ .card {
413
+ text-align: start;
414
+ display: flex;
415
+ flex-direction: row; /* Flex auto-reverses in RTL */
416
+ gap: 1rem;
417
+ }
418
+ ```
419
+
420
+ **Tailwind CSS RTL Plugin**:
421
+ ```bash
422
+ npm install tailwindcss-rtl
423
+ ```
424
+
425
+ ```typescript
426
+ // tailwind.config.ts
427
+ import type { Config } from 'tailwindcss';
428
+ import rtlPlugin from 'tailwindcss-rtl';
429
+
430
+ export default {
431
+ plugins: [rtlPlugin],
432
+ } satisfies Config;
433
+ ```
434
+
435
+ Usage with `rtl:` and `ltr:` variants:
436
+ ```tsx
437
+ <div className="ps-4 pe-2 text-start">
438
+ {/* ps = padding-inline-start, pe = padding-inline-end */}
439
+ <span className="ms-2 me-4">
440
+ {/* ms = margin-inline-start, me = margin-inline-end */}
441
+ Bidirectional text
442
+ </span>
443
+ </div>
444
+
445
+ {/* Conditional styles for specific directions */}
446
+ <div className="ltr:pl-4 rtl:pr-4 ltr:text-left rtl:text-right">
447
+ Direction-specific override
448
+ </div>
449
+ ```
450
+
451
+ **Tailwind v3.3+ built-in logical properties** (no plugin needed):
452
+ ```tsx
453
+ <div className="ps-4 pe-2 ms-2 me-4 text-start">
454
+ {/* These use CSS logical properties natively */}
455
+ </div>
456
+ ```
457
+
458
+ **RTL Context Provider**:
459
+ ```tsx
460
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
461
+
462
+ type Direction = 'ltr' | 'rtl';
463
+
464
+ const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']);
465
+
466
+ const DirectionContext = createContext<Direction>('ltr');
467
+
468
+ export function DirectionProvider({
469
+ locale,
470
+ children,
471
+ }: {
472
+ locale: string;
473
+ children: ReactNode;
474
+ }) {
475
+ const dir: Direction = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr';
476
+
477
+ useEffect(() => {
478
+ document.documentElement.dir = dir;
479
+ document.documentElement.lang = locale;
480
+ }, [dir, locale]);
481
+
482
+ return (
483
+ <DirectionContext.Provider value={dir}>
484
+ {children}
485
+ </DirectionContext.Provider>
486
+ );
487
+ }
488
+
489
+ export function useDirection() {
490
+ return useContext(DirectionContext);
491
+ }
492
+ ```
493
+
494
+ **Bidirectional text handling**:
495
+ ```tsx
496
+ {/* Isolate embedded text that may have different directionality */}
497
+ <p>
498
+ User <bdi>{userName}</bdi> posted a comment.
499
+ </p>
500
+
501
+ {/* Force direction for specific content */}
502
+ <span dir="ltr">+1 (555) 123-4567</span>
503
+
504
+ {/* Unicode control characters for mixed content */}
505
+ <span>{'\u200F'}{arabicText}{'\u200F'}</span>
506
+ ```
507
+
508
+ **Icon mirroring for RTL**:
509
+ ```css
510
+ /* Mirror directional icons (arrows, chevrons) in RTL */
511
+ [dir='rtl'] .icon-directional {
512
+ transform: scaleX(-1);
513
+ }
514
+
515
+ /* Do NOT mirror non-directional icons (checkmarks, close, etc.) */
516
+ ```
517
+
518
+ ### 4. Date / Number / Currency Formatting
519
+
520
+ **Intl.DateTimeFormat**:
521
+ ```typescript
522
+ function formatDate(
523
+ date: Date,
524
+ locale: string,
525
+ options?: Intl.DateTimeFormatOptions
526
+ ): string {
527
+ const defaults: Intl.DateTimeFormatOptions = {
528
+ year: 'numeric',
529
+ month: 'long',
530
+ day: 'numeric',
531
+ };
532
+ return new Intl.DateTimeFormat(locale, { ...defaults, ...options }).format(date);
533
+ }
534
+
535
+ // Examples:
536
+ formatDate(new Date(), 'en-US'); // "February 11, 2026"
537
+ formatDate(new Date(), 'de-DE'); // "11. Februar 2026"
538
+ formatDate(new Date(), 'ja-JP'); // "2026年2月11日"
539
+ formatDate(new Date(), 'ar-SA'); // "١١ فبراير ٢٠٢٦"
540
+
541
+ // Short format
542
+ formatDate(new Date(), 'en-US', {
543
+ year: 'numeric', month: 'short', day: 'numeric',
544
+ }); // "Feb 11, 2026"
545
+
546
+ // Date and time
547
+ formatDate(new Date(), 'en-US', {
548
+ dateStyle: 'full',
549
+ timeStyle: 'short',
550
+ }); // "Wednesday, February 11, 2026 at 3:45 PM"
551
+ ```
552
+
553
+ **Intl.NumberFormat for Currencies**:
554
+ ```typescript
555
+ function formatCurrency(
556
+ amount: number,
557
+ currency: string,
558
+ locale: string
559
+ ): string {
560
+ return new Intl.NumberFormat(locale, {
561
+ style: 'currency',
562
+ currency,
563
+ minimumFractionDigits: 2,
564
+ }).format(amount);
565
+ }
566
+
567
+ // Examples:
568
+ formatCurrency(1234.5, 'USD', 'en-US'); // "$1,234.50"
569
+ formatCurrency(1234.5, 'EUR', 'de-DE'); // "1.234,50 €"
570
+ formatCurrency(1234.5, 'JPY', 'ja-JP'); // "¥1,235"
571
+ formatCurrency(1234.5, 'SAR', 'ar-SA'); // "١٬٢٣٤٫٥٠ ر.س.‏"
572
+
573
+ // Compact notation
574
+ new Intl.NumberFormat('en', {
575
+ notation: 'compact',
576
+ compactDisplay: 'short',
577
+ }).format(1500000); // "1.5M"
578
+
579
+ // Percentage
580
+ new Intl.NumberFormat('en', {
581
+ style: 'percent',
582
+ minimumFractionDigits: 1,
583
+ }).format(0.856); // "85.6%"
584
+
585
+ // Unit formatting
586
+ new Intl.NumberFormat('en', {
587
+ style: 'unit',
588
+ unit: 'kilometer-per-hour',
589
+ }).format(120); // "120 km/h"
590
+ ```
591
+
592
+ **Intl.RelativeTimeFormat**:
593
+ ```typescript
594
+ function formatRelativeTime(date: Date, locale: string): string {
595
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
596
+ const now = Date.now();
597
+ const diffMs = date.getTime() - now;
598
+ const diffSec = Math.round(diffMs / 1000);
599
+ const diffMin = Math.round(diffSec / 60);
600
+ const diffHr = Math.round(diffMin / 60);
601
+ const diffDay = Math.round(diffHr / 24);
602
+
603
+ if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
604
+ if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
605
+ if (Math.abs(diffHr) < 24) return rtf.format(diffHr, 'hour');
606
+ if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
607
+
608
+ const diffMonth = Math.round(diffDay / 30);
609
+ if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, 'month');
610
+
611
+ return rtf.format(Math.round(diffDay / 365), 'year');
612
+ }
613
+
614
+ // Examples:
615
+ // formatRelativeTime(yesterday, 'en') -> "yesterday"
616
+ // formatRelativeTime(twoHoursAgo, 'de') -> "vor 2 Stunden"
617
+ // formatRelativeTime(nextWeek, 'ja') -> "7日後"
618
+ ```
619
+
620
+ **Timezone Handling**:
621
+ ```typescript
622
+ // Display in user's timezone
623
+ function formatWithTimezone(
624
+ date: Date,
625
+ locale: string,
626
+ timeZone: string
627
+ ): string {
628
+ return new Intl.DateTimeFormat(locale, {
629
+ dateStyle: 'medium',
630
+ timeStyle: 'long',
631
+ timeZone,
632
+ }).format(date);
633
+ }
634
+
635
+ formatWithTimezone(new Date(), 'en-US', 'America/New_York');
636
+ // "Feb 11, 2026, 3:45:00 PM EST"
637
+
638
+ formatWithTimezone(new Date(), 'en-US', 'Asia/Tokyo');
639
+ // "Feb 12, 2026, 5:45:00 AM JST"
640
+
641
+ // Get user's timezone
642
+ const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
643
+ ```
644
+
645
+ **Reusable Formatting Hook**:
646
+ ```tsx
647
+ import { useMemo } from 'react';
648
+ import { useTranslation } from 'react-i18next';
649
+
650
+ export function useFormatters() {
651
+ const { i18n } = useTranslation();
652
+ const locale = i18n.language;
653
+
654
+ return useMemo(() => ({
655
+ date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
656
+ new Intl.DateTimeFormat(locale, options).format(date),
657
+
658
+ number: (value: number, options?: Intl.NumberFormatOptions) =>
659
+ new Intl.NumberFormat(locale, options).format(value),
660
+
661
+ currency: (amount: number, currency: string) =>
662
+ new Intl.NumberFormat(locale, {
663
+ style: 'currency',
664
+ currency,
665
+ }).format(amount),
666
+
667
+ relativeTime: (date: Date) =>
668
+ formatRelativeTime(date, locale),
669
+
670
+ list: (items: string[], type: Intl.ListFormatType = 'conjunction') =>
671
+ new Intl.ListFormat(locale, { type }).format(items),
672
+ }), [locale]);
673
+ }
674
+
675
+ // Usage:
676
+ function PriceDisplay({ amount, currency }: { amount: number; currency: string }) {
677
+ const fmt = useFormatters();
678
+ return <span>{fmt.currency(amount, currency)}</span>;
679
+ }
680
+ ```
681
+
682
+ ### 5. Translation Management Workflow
683
+
684
+ **Crowdin Integration**:
685
+
686
+ `crowdin.yml`:
687
+ ```yaml
688
+ project_id_env: CROWDIN_PROJECT_ID
689
+ api_token_env: CROWDIN_API_TOKEN
690
+
691
+ files:
692
+ - source: /public/locales/en/**/*.json
693
+ translation: /public/locales/%two_letters_code%/**/%original_file_name%
694
+ type: json
695
+
696
+ preserve_hierarchy: true
697
+ ```
698
+
699
+ CI/CD pipeline (GitHub Actions):
700
+ ```yaml
701
+ name: Translation Sync
702
+
703
+ on:
704
+ push:
705
+ branches: [main]
706
+ paths:
707
+ - 'public/locales/en/**'
708
+ schedule:
709
+ - cron: '0 6 * * 1' # Weekly Monday 6 AM
710
+
711
+ jobs:
712
+ upload-sources:
713
+ runs-on: ubuntu-latest
714
+ if: github.event_name == 'push'
715
+ steps:
716
+ - uses: actions/checkout@v4
717
+ - uses: crowdin/github-action@v2
718
+ with:
719
+ upload_sources: true
720
+ upload_translations: false
721
+ env:
722
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
723
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
724
+
725
+ download-translations:
726
+ runs-on: ubuntu-latest
727
+ if: github.event_name == 'schedule'
728
+ steps:
729
+ - uses: actions/checkout@v4
730
+ - uses: crowdin/github-action@v2
731
+ with:
732
+ upload_sources: false
733
+ download_translations: true
734
+ create_pull_request: true
735
+ pull_request_title: 'chore: update translations from Crowdin'
736
+ pull_request_base_branch_name: main
737
+ env:
738
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
739
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
740
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
741
+ ```
742
+
743
+ **Lokalise Integration** (alternative):
744
+ ```yaml
745
+ # .github/workflows/lokalise-sync.yml
746
+ name: Lokalise Sync
747
+
748
+ on:
749
+ push:
750
+ branches: [main]
751
+ paths: ['public/locales/en/**']
752
+
753
+ jobs:
754
+ sync:
755
+ runs-on: ubuntu-latest
756
+ steps:
757
+ - uses: actions/checkout@v4
758
+ - name: Install Lokalise CLI
759
+ run: |
760
+ curl -sfL https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh | sh
761
+ - name: Upload source strings
762
+ run: |
763
+ ./bin/lokalise2 file upload \
764
+ --token ${{ secrets.LOKALISE_API_TOKEN }} \
765
+ --project-id ${{ secrets.LOKALISE_PROJECT_ID }} \
766
+ --file "public/locales/en/common.json" \
767
+ --lang-iso en
768
+ - name: Download translations
769
+ run: |
770
+ ./bin/lokalise2 file download \
771
+ --token ${{ secrets.LOKALISE_API_TOKEN }} \
772
+ --project-id ${{ secrets.LOKALISE_PROJECT_ID }} \
773
+ --format json \
774
+ --original-filenames=true \
775
+ --directory-prefix "public/locales/%LANG_ISO%"
776
+ ```
777
+
778
+ **Missing Translation Detection**:
779
+
780
+ ```typescript
781
+ // scripts/check-translations.ts
782
+ import fs from 'node:fs';
783
+ import path from 'node:path';
784
+
785
+ const LOCALES_DIR = 'public/locales';
786
+ const SOURCE_LOCALE = 'en';
787
+
788
+ function getKeys(obj: Record<string, unknown>, prefix = ''): string[] {
789
+ return Object.entries(obj).flatMap(([key, value]) => {
790
+ const fullKey = prefix ? `${prefix}.${key}` : key;
791
+ if (typeof value === 'object' && value !== null) {
792
+ return getKeys(value as Record<string, unknown>, fullKey);
793
+ }
794
+ return [fullKey];
795
+ });
796
+ }
797
+
798
+ function checkTranslations() {
799
+ const sourceDir = path.join(LOCALES_DIR, SOURCE_LOCALE);
800
+ const locales = fs
801
+ .readdirSync(LOCALES_DIR)
802
+ .filter((d) => d !== SOURCE_LOCALE && fs.statSync(path.join(LOCALES_DIR, d)).isDirectory());
803
+
804
+ let hasErrors = false;
805
+
806
+ for (const file of fs.readdirSync(sourceDir)) {
807
+ const sourceContent = JSON.parse(
808
+ fs.readFileSync(path.join(sourceDir, file), 'utf-8')
809
+ );
810
+ const sourceKeys = getKeys(sourceContent);
811
+
812
+ for (const locale of locales) {
813
+ const targetPath = path.join(LOCALES_DIR, locale, file);
814
+
815
+ if (!fs.existsSync(targetPath)) {
816
+ console.error(`MISSING FILE: ${locale}/${file}`);
817
+ hasErrors = true;
818
+ continue;
819
+ }
820
+
821
+ const targetContent = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
822
+ const targetKeys = getKeys(targetContent);
823
+
824
+ const missing = sourceKeys.filter((k) => !targetKeys.includes(k));
825
+ const extra = targetKeys.filter((k) => !sourceKeys.includes(k));
826
+
827
+ if (missing.length > 0) {
828
+ console.error(`${locale}/${file} - MISSING ${missing.length} keys:`);
829
+ missing.forEach((k) => console.error(` - ${k}`));
830
+ hasErrors = true;
831
+ }
832
+ if (extra.length > 0) {
833
+ console.warn(`${locale}/${file} - EXTRA ${extra.length} keys:`);
834
+ extra.forEach((k) => console.warn(` + ${k}`));
835
+ }
836
+ }
837
+ }
838
+
839
+ process.exit(hasErrors ? 1 : 0);
840
+ }
841
+
842
+ checkTranslations();
843
+ ```
844
+
845
+ Add to CI:
846
+ ```json
847
+ {
848
+ "scripts": {
849
+ "i18n:check": "tsx scripts/check-translations.ts"
850
+ }
851
+ }
852
+ ```
853
+
854
+ **Fallback Chain Strategy** (i18next):
855
+ ```typescript
856
+ i18n.init({
857
+ fallbackLng: {
858
+ 'de-AT': ['de', 'en'], // Austrian German -> German -> English
859
+ 'pt-BR': ['pt', 'en'], // Brazilian Portuguese -> Portuguese -> English
860
+ 'zh-TW': ['zh-Hant', 'en'], // Traditional Chinese -> English
861
+ default: ['en'],
862
+ },
863
+
864
+ // Show key name for missing translations in dev
865
+ saveMissing: process.env.NODE_ENV === 'development',
866
+ missingKeyHandler: (lngs, ns, key) => {
867
+ console.warn(`Missing translation: [${lngs}] ${ns}:${key}`);
868
+ },
869
+ });
870
+ ```
871
+
872
+ ### 6. Performance
873
+
874
+ **Code Splitting Translations by Route**:
875
+ ```typescript
876
+ // i18next lazy-loading with namespaces per route
877
+ i18n.init({
878
+ partialBundledLanguages: true,
879
+ ns: [], // Start empty, load on demand
880
+ backend: {
881
+ loadPath: '/locales/{{lng}}/{{ns}}.json',
882
+ },
883
+ });
884
+
885
+ // In route component, load namespace on mount
886
+ function DashboardPage() {
887
+ const { t, ready } = useTranslation('dashboard', { useSuspense: true });
888
+
889
+ if (!ready) return <Skeleton />;
890
+ return <div>{t('welcome')}</div>;
891
+ }
892
+ ```
893
+
894
+ **Dynamic Import of Locale Data** (for date-fns or similar):
895
+ ```typescript
896
+ const localeImports: Record<string, () => Promise<Locale>> = {
897
+ en: () => import('date-fns/locale/enUS').then((m) => m.enUS),
898
+ de: () => import('date-fns/locale/de').then((m) => m.de),
899
+ fr: () => import('date-fns/locale/fr').then((m) => m.fr),
900
+ ja: () => import('date-fns/locale/ja').then((m) => m.ja),
901
+ ar: () => import('date-fns/locale/arSA').then((m) => m.arSA),
902
+ };
903
+
904
+ async function getDateLocale(lang: string): Promise<Locale> {
905
+ const loader = localeImports[lang] ?? localeImports.en;
906
+ return loader();
907
+ }
908
+ ```
909
+
910
+ **Bundle Size Optimization**:
911
+
912
+ 1. Use namespaces to split translation files (keep each under 10 KB gzipped).
913
+ 2. Lazy-load non-critical namespaces after initial render.
914
+ 3. Use `i18next-http-backend` instead of bundling all locales.
915
+ 4. Tree-shake unused Intl polyfills.
916
+ 5. Pre-compress translation JSON with gzip/brotli on CDN.
917
+
918
+ ```typescript
919
+ // Webpack/Next.js: exclude unused moment/date-fns locales
920
+ // next.config.ts
921
+ import type { NextConfig } from 'next';
922
+
923
+ const config: NextConfig = {
924
+ webpack(config) {
925
+ // Only include needed locales for moment.js (if used)
926
+ config.plugins.push(
927
+ new (require('webpack')).ContextReplacementPlugin(
928
+ /moment[/\\]locale$/,
929
+ /en|de|fr|ar|ja/
930
+ )
931
+ );
932
+ return config;
933
+ },
934
+ };
935
+
936
+ export default config;
937
+ ```
938
+
939
+ **Translation Preloading**:
940
+ ```typescript
941
+ // Preload critical namespaces at app startup
942
+ await i18n.loadNamespaces(['common', 'auth']);
943
+
944
+ // Preload next page translations on hover/focus
945
+ function NavLink({ href, ns, children }: {
946
+ href: string;
947
+ ns: string;
948
+ children: React.ReactNode;
949
+ }) {
950
+ const { i18n } = useTranslation();
951
+
952
+ const preloadTranslations = () => {
953
+ i18n.loadNamespaces(ns);
954
+ };
955
+
956
+ return (
957
+ <Link
958
+ href={href}
959
+ onMouseEnter={preloadTranslations}
960
+ onFocus={preloadTranslations}
961
+ >
962
+ {children}
963
+ </Link>
964
+ );
965
+ }
966
+ ```
967
+
968
+ ## Decision Guide
969
+
970
+ | Scenario | Recommendation |
971
+ |---|---|
972
+ | SPA with React | i18next + react-i18next + HTTP backend |
973
+ | Next.js App Router | Built-in `[locale]` routing + server dictionaries |
974
+ | Need RTL | CSS logical properties + Tailwind `ps`/`pe` utilities |
975
+ | Date/number formatting | Native `Intl` APIs (zero bundle cost) |
976
+ | Translation management | Crowdin (open-source friendly) or Lokalise (developer-focused) |
977
+ | Large app (50+ routes) | Namespace-per-route + lazy loading |
978
+ | SEO-critical pages | `generateStaticParams` + hreflang + language alternates |
979
+
980
+ ## Common Pitfalls
981
+
982
+ 1. **Hardcoded strings**: Always externalize user-facing text, including error messages, aria labels, and alt text.
983
+ 2. **String concatenation for sentences**: Use interpolation (`Hello, {{name}}`) instead of `"Hello, " + name` -- word order varies by language.
984
+ 3. **Assuming text length**: German text is ~30% longer than English. Arabic may be shorter. Design flexible layouts.
985
+ 4. **Fixed-width containers**: Use `min-width`/`max-width` with logical properties instead of fixed `width`.
986
+ 5. **Icon direction**: Mirror arrows and chevrons for RTL, but not universal icons (close, check, search).
987
+ 6. **Date format assumptions**: Never hardcode `MM/DD/YYYY`. Use `Intl.DateTimeFormat` with the user's locale.
988
+ 7. **Number separators**: `1,000.50` (en) vs `1.000,50` (de) vs `1 000,50` (fr). Always use `Intl.NumberFormat`.
989
+ 8. **Pluralization shortcuts**: Many languages have more than two plural forms (Arabic has 6). Use i18next plural rules, not ternary operators.