@mars-stack/cli 0.2.0 → 1.0.2

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 (175) hide show
  1. package/dist/index.js +137 -12
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  5. package/template/.cursor/rules/data-access.mdc +29 -0
  6. package/template/.cursor/rules/project-structure.mdc +34 -0
  7. package/template/.cursor/rules/security.mdc +25 -0
  8. package/template/.cursor/rules/testing.mdc +24 -0
  9. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  10. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  11. package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
  12. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  13. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  14. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  15. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  16. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  17. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  18. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  19. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  20. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  21. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  22. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  23. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  24. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  25. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  26. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  27. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  28. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  29. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  30. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  31. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  32. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  33. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  34. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  35. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  36. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  37. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  38. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  39. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  40. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  41. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  42. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  43. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  44. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  45. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  46. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  47. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  48. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  49. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  50. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  51. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  52. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  53. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  54. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  55. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  56. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  57. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  58. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  59. package/template/AGENTS.md +104 -0
  60. package/template/ARCHITECTURE.md +102 -0
  61. package/template/docs/QUALITY_SCORE.md +20 -0
  62. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  63. package/template/docs/design-docs/core-beliefs.md +43 -0
  64. package/template/docs/design-docs/index.md +8 -0
  65. package/template/docs/exec-plans/active/.gitkeep +0 -0
  66. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  67. package/template/docs/exec-plans/tech-debt.md +7 -0
  68. package/template/docs/generated/.gitkeep +0 -0
  69. package/template/docs/product-specs/index.md +7 -0
  70. package/template/docs/references/index.md +18 -0
  71. package/template/e2e/api.spec.ts +20 -0
  72. package/template/e2e/auth.spec.ts +24 -0
  73. package/template/e2e/public.spec.ts +25 -0
  74. package/template/eslint.config.mjs +24 -0
  75. package/template/next-env.d.ts +6 -0
  76. package/template/next.config.ts +45 -0
  77. package/template/package.json +80 -0
  78. package/template/playwright.config.ts +31 -0
  79. package/template/postcss.config.mjs +8 -0
  80. package/template/prisma/generated/prisma/browser.ts +49 -0
  81. package/template/prisma/generated/prisma/client.ts +73 -0
  82. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  83. package/template/prisma/generated/prisma/enums.ts +15 -0
  84. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  85. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  86. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  87. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  88. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  89. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  90. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  91. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  92. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  93. package/template/prisma/generated/prisma/models.ts +17 -0
  94. package/template/prisma/schema/auth.prisma +69 -0
  95. package/template/prisma/schema/base.prisma +8 -0
  96. package/template/prisma/schema/file.prisma +15 -0
  97. package/template/prisma/schema/subscription.prisma +17 -0
  98. package/template/prisma.config.ts +13 -0
  99. package/template/scripts/check-architecture.ts +221 -0
  100. package/template/scripts/check-doc-freshness.ts +242 -0
  101. package/template/scripts/ensure-db.mjs +291 -0
  102. package/template/scripts/generate-docs.ts +143 -0
  103. package/template/scripts/generate-env-example.ts +89 -0
  104. package/template/scripts/seed.ts +56 -0
  105. package/template/scripts/update-quality-score.ts +263 -0
  106. package/template/src/__tests__/architecture.test.ts +114 -0
  107. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  108. package/template/src/app/(auth)/layout.tsx +11 -0
  109. package/template/src/app/(auth)/register/page.tsx +162 -0
  110. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  111. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  112. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  113. package/template/src/app/(auth)/verify/page.tsx +56 -0
  114. package/template/src/app/(protected)/admin/page.tsx +108 -0
  115. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  116. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  117. package/template/src/app/(protected)/layout.tsx +262 -0
  118. package/template/src/app/(protected)/settings/page.tsx +370 -0
  119. package/template/src/app/api/auth/forgot/route.ts +63 -0
  120. package/template/src/app/api/auth/login/route.ts +121 -0
  121. package/template/src/app/api/auth/logout/route.ts +19 -0
  122. package/template/src/app/api/auth/me/route.ts +30 -0
  123. package/template/src/app/api/auth/reset/route.ts +45 -0
  124. package/template/src/app/api/auth/signup/route.ts +85 -0
  125. package/template/src/app/api/auth/verify/route.ts +46 -0
  126. package/template/src/app/api/csrf/route.ts +12 -0
  127. package/template/src/app/api/health/route.ts +10 -0
  128. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  129. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  130. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  131. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  132. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  133. package/template/src/app/api/protected/user/password/route.ts +63 -0
  134. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  135. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  136. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  137. package/template/src/app/api/readiness/route.ts +15 -0
  138. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  139. package/template/src/app/error.tsx +33 -0
  140. package/template/src/app/layout.tsx +29 -0
  141. package/template/src/app/not-found.tsx +20 -0
  142. package/template/src/app/page.tsx +136 -0
  143. package/template/src/app/privacy/page.tsx +178 -0
  144. package/template/src/app/providers.tsx +8 -0
  145. package/template/src/app/terms/page.tsx +139 -0
  146. package/template/src/config/app.config.ts +70 -0
  147. package/template/src/config/routes.ts +17 -0
  148. package/template/src/features/admin/index.ts +11 -0
  149. package/template/src/features/admin/permissions.ts +64 -0
  150. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  151. package/template/src/features/auth/context/index.ts +2 -0
  152. package/template/src/features/auth/index.ts +3 -0
  153. package/template/src/features/auth/server/consent.ts +66 -0
  154. package/template/src/features/auth/server/session-revocation.ts +20 -0
  155. package/template/src/features/auth/server/sessions.ts +66 -0
  156. package/template/src/features/auth/server/user.ts +166 -0
  157. package/template/src/features/auth/types.ts +19 -0
  158. package/template/src/features/auth/validators.ts +29 -0
  159. package/template/src/features/billing/server/index.ts +66 -0
  160. package/template/src/features/billing/types.ts +43 -0
  161. package/template/src/features/uploads/server/index.ts +49 -0
  162. package/template/src/features/uploads/types.ts +26 -0
  163. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  164. package/template/src/lib/core/email/templates/index.ts +4 -0
  165. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  166. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  167. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  168. package/template/src/lib/mars.ts +56 -0
  169. package/template/src/lib/prisma.ts +19 -0
  170. package/template/src/proxy.ts +92 -0
  171. package/template/src/styles/brand.css +15 -0
  172. package/template/src/styles/globals.css +7 -0
  173. package/template/tsconfig.json +59 -0
  174. package/template/vitest.config.ts +41 -0
  175. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,518 @@
1
+ # Skill: Configure Internationalization (i18n)
2
+
3
+ Set up multi-language support in a MARS application using `next-intl` with the App Router, including locale routing, translation files, and RTL support.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add translations, multi-language support, internationalization, i18n, localization, l10n, or locale switching.
8
+
9
+ ## Prerequisites
10
+
11
+ - Next.js App Router (MARS default)
12
+ - `appConfig.features.i18n` set to `true` (add it if missing)
13
+
14
+ ## Step 1: Install next-intl
15
+
16
+ ```bash
17
+ yarn add next-intl
18
+ ```
19
+
20
+ ## Step 2: Directory Structure
21
+
22
+ ```
23
+ src/
24
+ ├── i18n/
25
+ │ ├── request.ts # getRequestConfig — server-side locale resolver
26
+ │ └── routing.ts # defineRouting — supported locales and default
27
+ ├── messages/
28
+ │ ├── en.json # English translations
29
+ │ └── es.json # Spanish translations (example)
30
+ ```
31
+
32
+ ## Step 3: Define Routing Configuration
33
+
34
+ ```typescript
35
+ // src/i18n/routing.ts
36
+ import { defineRouting } from 'next-intl/routing';
37
+
38
+ export const routing = defineRouting({
39
+ locales: ['en', 'es', 'fr', 'de', 'ja', 'ar'],
40
+ defaultLocale: 'en',
41
+ });
42
+
43
+ export type Locale = (typeof routing.locales)[number];
44
+ ```
45
+
46
+ Adjust the `locales` array based on the languages your app needs.
47
+
48
+ ## Step 4: Request Configuration
49
+
50
+ ```typescript
51
+ // src/i18n/request.ts
52
+ import { getRequestConfig } from 'next-intl/server';
53
+ import { routing } from './routing';
54
+
55
+ export default getRequestConfig(async ({ requestLocale }) => {
56
+ let locale = await requestLocale;
57
+
58
+ if (!locale || !routing.locales.includes(locale as any)) {
59
+ locale = routing.defaultLocale;
60
+ }
61
+
62
+ return {
63
+ locale,
64
+ messages: (await import(`../messages/${locale}.json`)).default,
65
+ };
66
+ });
67
+ ```
68
+
69
+ ## Step 5: Translation Files
70
+
71
+ ### English (default)
72
+
73
+ ```json
74
+ // src/messages/en.json
75
+ {
76
+ "common": {
77
+ "save": "Save",
78
+ "cancel": "Cancel",
79
+ "delete": "Delete",
80
+ "edit": "Edit",
81
+ "loading": "Loading…",
82
+ "error": "Something went wrong",
83
+ "success": "Success",
84
+ "confirm": "Are you sure?",
85
+ "back": "Back",
86
+ "next": "Next",
87
+ "search": "Search",
88
+ "noResults": "No results found"
89
+ },
90
+ "auth": {
91
+ "login": "Log in",
92
+ "signup": "Sign up",
93
+ "logout": "Log out",
94
+ "forgotPassword": "Forgot password?",
95
+ "resetPassword": "Reset password",
96
+ "email": "Email",
97
+ "password": "Password",
98
+ "confirmPassword": "Confirm password",
99
+ "name": "Full name"
100
+ },
101
+ "nav": {
102
+ "home": "Home",
103
+ "dashboard": "Dashboard",
104
+ "settings": "Settings",
105
+ "admin": "Admin"
106
+ },
107
+ "settings": {
108
+ "title": "Settings",
109
+ "profile": "Profile",
110
+ "security": "Security",
111
+ "language": "Language",
112
+ "theme": "Theme"
113
+ },
114
+ "errors": {
115
+ "required": "This field is required",
116
+ "invalidEmail": "Please enter a valid email address",
117
+ "passwordTooShort": "Password must be at least 8 characters",
118
+ "passwordMismatch": "Passwords do not match",
119
+ "unauthorized": "You are not authorized to perform this action",
120
+ "notFound": "Page not found",
121
+ "serverError": "An unexpected error occurred. Please try again."
122
+ },
123
+ "metadata": {
124
+ "title": "My App",
125
+ "description": "Built with Mars"
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Spanish (example)
131
+
132
+ ```json
133
+ // src/messages/es.json
134
+ {
135
+ "common": {
136
+ "save": "Guardar",
137
+ "cancel": "Cancelar",
138
+ "delete": "Eliminar",
139
+ "edit": "Editar",
140
+ "loading": "Cargando…",
141
+ "error": "Algo salió mal",
142
+ "success": "Éxito",
143
+ "confirm": "¿Estás seguro?",
144
+ "back": "Atrás",
145
+ "next": "Siguiente",
146
+ "search": "Buscar",
147
+ "noResults": "No se encontraron resultados"
148
+ },
149
+ "auth": {
150
+ "login": "Iniciar sesión",
151
+ "signup": "Registrarse",
152
+ "logout": "Cerrar sesión",
153
+ "forgotPassword": "¿Olvidaste tu contraseña?",
154
+ "resetPassword": "Restablecer contraseña",
155
+ "email": "Correo electrónico",
156
+ "password": "Contraseña",
157
+ "confirmPassword": "Confirmar contraseña",
158
+ "name": "Nombre completo"
159
+ },
160
+ "nav": {
161
+ "home": "Inicio",
162
+ "dashboard": "Panel",
163
+ "settings": "Configuración",
164
+ "admin": "Administración"
165
+ },
166
+ "settings": {
167
+ "title": "Configuración",
168
+ "profile": "Perfil",
169
+ "security": "Seguridad",
170
+ "language": "Idioma",
171
+ "theme": "Tema"
172
+ },
173
+ "errors": {
174
+ "required": "Este campo es obligatorio",
175
+ "invalidEmail": "Introduce un correo electrónico válido",
176
+ "passwordTooShort": "La contraseña debe tener al menos 8 caracteres",
177
+ "passwordMismatch": "Las contraseñas no coinciden",
178
+ "unauthorized": "No tienes autorización para realizar esta acción",
179
+ "notFound": "Página no encontrada",
180
+ "serverError": "Ocurrió un error inesperado. Inténtalo de nuevo."
181
+ },
182
+ "metadata": {
183
+ "title": "Mi Aplicación",
184
+ "description": "Construido con Mars"
185
+ }
186
+ }
187
+ ```
188
+
189
+ ## Step 6: Middleware Integration
190
+
191
+ Integrate locale detection with the existing `proxy.ts` (MARS uses Next.js 16 proxy instead of `middleware.ts`):
192
+
193
+ ```typescript
194
+ // In src/proxy.ts — add locale middleware integration
195
+ import createIntlMiddleware from 'next-intl/middleware';
196
+ import { routing } from '@/i18n/routing';
197
+
198
+ const intlMiddleware = createIntlMiddleware(routing);
199
+
200
+ // In the proxy handler, before other route matching:
201
+ // 1. Run intlMiddleware to handle locale detection and redirects
202
+ // 2. It reads Accept-Language header, cookies, and URL prefix to determine locale
203
+ // 3. Falls back to defaultLocale when no match is found
204
+ ```
205
+
206
+ The middleware handles:
207
+ - **URL prefix detection**: `/es/dashboard` → Spanish locale
208
+ - **Accept-Language header**: Detects browser language preference
209
+ - **Cookie persistence**: Remembers user's locale choice via `NEXT_LOCALE` cookie
210
+ - **Default locale fallback**: Routes without a prefix use the default locale
211
+
212
+ ## Step 7: Layout Integration
213
+
214
+ ### Root Layout
215
+
216
+ ```typescript
217
+ // src/app/[locale]/layout.tsx
218
+ import { NextIntlClientProvider } from 'next-intl';
219
+ import { getMessages, getTranslations } from 'next-intl/server';
220
+ import { routing, type Locale } from '@/i18n/routing';
221
+ import { notFound } from 'next/navigation';
222
+
223
+ interface LayoutProps {
224
+ children: React.ReactNode;
225
+ params: { locale: string };
226
+ }
227
+
228
+ export function generateStaticParams() {
229
+ return routing.locales.map((locale) => ({ locale }));
230
+ }
231
+
232
+ export async function generateMetadata({ params }: { params: { locale: string } }) {
233
+ const t = await getTranslations({ locale: params.locale, namespace: 'metadata' });
234
+ return {
235
+ title: t('title'),
236
+ description: t('description'),
237
+ };
238
+ }
239
+
240
+ export default async function LocaleLayout({ children, params }: LayoutProps) {
241
+ const { locale } = params;
242
+
243
+ if (!routing.locales.includes(locale as Locale)) {
244
+ notFound();
245
+ }
246
+
247
+ const messages = await getMessages();
248
+ const isRTL = ['ar', 'he', 'fa'].includes(locale);
249
+
250
+ return (
251
+ <html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
252
+ <body>
253
+ <NextIntlClientProvider messages={messages}>
254
+ {children}
255
+ </NextIntlClientProvider>
256
+ </body>
257
+ </html>
258
+ );
259
+ }
260
+ ```
261
+
262
+ ## Step 8: Usage in Components
263
+
264
+ ### Server Components
265
+
266
+ ```typescript
267
+ import { getTranslations } from 'next-intl/server';
268
+
269
+ export default async function SettingsPage() {
270
+ const t = await getTranslations('settings');
271
+
272
+ return (
273
+ <div>
274
+ <h1>{t('title')}</h1>
275
+ <p>{t('profile')}</p>
276
+ </div>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ### Client Components
282
+
283
+ ```typescript
284
+ 'use client';
285
+
286
+ import { useTranslations } from 'next-intl';
287
+
288
+ export function SaveButton() {
289
+ const t = useTranslations('common');
290
+
291
+ return <button>{t('save')}</button>;
292
+ }
293
+ ```
294
+
295
+ ### With Interpolation
296
+
297
+ Translation file:
298
+ ```json
299
+ {
300
+ "greeting": "Hello, {name}!",
301
+ "itemCount": "You have {count, plural, =0 {no items} one {# item} other {# items}}"
302
+ }
303
+ ```
304
+
305
+ Component:
306
+ ```typescript
307
+ const t = useTranslations('dashboard');
308
+ t('greeting', { name: user.name });
309
+ t('itemCount', { count: items.length });
310
+ ```
311
+
312
+ ### Locale-Aware Links
313
+
314
+ ```typescript
315
+ import { Link } from '@/i18n/routing';
316
+
317
+ // Automatically prefixes the current locale
318
+ <Link href="/dashboard">Dashboard</Link>
319
+
320
+ // Switch locale explicitly
321
+ <Link href="/dashboard" locale="es">Dashboard (Spanish)</Link>
322
+ ```
323
+
324
+ Create the locale-aware link:
325
+
326
+ ```typescript
327
+ // src/i18n/routing.ts (add to existing file)
328
+ import { createNavigation } from 'next-intl/navigation';
329
+
330
+ export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
331
+ ```
332
+
333
+ ## Step 9: Locale Switcher Component
334
+
335
+ ```typescript
336
+ // src/features/i18n/components/locale-switcher.tsx
337
+ 'use client';
338
+
339
+ import { useLocale } from 'next-intl';
340
+ import { useRouter, usePathname } from '@/i18n/routing';
341
+ import type { Locale } from '@/i18n/routing';
342
+
343
+ const LOCALE_LABELS: Record<string, string> = {
344
+ en: 'English',
345
+ es: 'Español',
346
+ fr: 'Français',
347
+ de: 'Deutsch',
348
+ ja: '日本語',
349
+ ar: 'العربية',
350
+ };
351
+
352
+ export function LocaleSwitcher() {
353
+ const locale = useLocale();
354
+ const router = useRouter();
355
+ const pathname = usePathname();
356
+
357
+ function handleChange(newLocale: string) {
358
+ router.replace(pathname, { locale: newLocale as Locale });
359
+ }
360
+
361
+ return (
362
+ <select
363
+ value={locale}
364
+ onChange={(e) => handleChange(e.target.value)}
365
+ className="rounded-md border border-border-input bg-surface-input px-3 py-1.5 text-sm text-text-primary"
366
+ aria-label="Select language"
367
+ >
368
+ {Object.entries(LOCALE_LABELS).map(([code, label]) => (
369
+ <option key={code} value={code}>
370
+ {label}
371
+ </option>
372
+ ))}
373
+ </select>
374
+ );
375
+ }
376
+ ```
377
+
378
+ ## Step 10: Date and Number Formatting
379
+
380
+ ```typescript
381
+ 'use client';
382
+
383
+ import { useFormatter } from 'next-intl';
384
+
385
+ export function FormattedContent() {
386
+ const format = useFormatter();
387
+
388
+ return (
389
+ <div>
390
+ <p>{format.dateTime(new Date(), { dateStyle: 'long' })}</p>
391
+ <p>{format.number(1234.56, { style: 'currency', currency: 'USD' })}</p>
392
+ <p>{format.relativeTime(new Date(Date.now() - 3600_000))}</p>
393
+ <p>{format.list(['Alice', 'Bob', 'Charlie'], { type: 'conjunction' })}</p>
394
+ </div>
395
+ );
396
+ }
397
+ ```
398
+
399
+ ## Step 11: RTL Support
400
+
401
+ For right-to-left languages (Arabic, Hebrew, Farsi), use CSS logical properties:
402
+
403
+ ```css
404
+ /* Instead of: margin-left: 1rem */
405
+ margin-inline-start: 1rem;
406
+
407
+ /* Instead of: padding-right: 0.5rem */
408
+ padding-inline-end: 0.5rem;
409
+
410
+ /* Instead of: text-align: left */
411
+ text-align: start;
412
+
413
+ /* Instead of: border-left: 1px solid */
414
+ border-inline-start: 1px solid;
415
+ ```
416
+
417
+ Tailwind equivalents:
418
+ - `ml-4` → `ms-4` (margin-inline-start)
419
+ - `pr-2` → `pe-2` (padding-inline-end)
420
+ - `text-left` → `text-start`
421
+ - `pl-4` → `ps-4`
422
+ - `border-l` → `border-s`
423
+
424
+ The `dir` attribute on `<html>` is set automatically in the layout based on the locale.
425
+
426
+ ## Step 12: Adding a New Locale
427
+
428
+ 1. Add the locale code to `routing.locales` in `src/i18n/routing.ts`:
429
+ ```typescript
430
+ locales: ['en', 'es', 'fr', 'de', 'ja', 'ar', 'pt'],
431
+ ```
432
+
433
+ 2. Create the translation file `src/messages/pt.json` — copy `en.json` as a starting point:
434
+ ```bash
435
+ cp src/messages/en.json src/messages/pt.json
436
+ ```
437
+
438
+ 3. Translate all values in the new file (keys stay in English).
439
+
440
+ 4. Add the label to `LOCALE_LABELS` in the `LocaleSwitcher` component:
441
+ ```typescript
442
+ pt: 'Português',
443
+ ```
444
+
445
+ 5. If the locale is RTL, add it to the RTL check in the root layout:
446
+ ```typescript
447
+ const isRTL = ['ar', 'he', 'fa'].includes(locale);
448
+ ```
449
+
450
+ 6. Test the locale by visiting `/<locale>/` (e.g., `/pt/dashboard`).
451
+
452
+ ## Design Token Considerations
453
+
454
+ - Ensure text containers can handle longer translations (German text is often 30% longer than English).
455
+ - Use `min-width` / `min-height` instead of fixed dimensions for buttons and labels.
456
+ - Test RTL layout thoroughly — check that icons, arrows, and navigation flow correctly.
457
+ - Consider different numeral systems for Arabic (`useFormatter` handles this automatically).
458
+
459
+ ## Config Integration
460
+
461
+ Add to `app.config.ts`:
462
+
463
+ ```typescript
464
+ features: {
465
+ i18n: true,
466
+ },
467
+ services: {
468
+ i18n: {
469
+ defaultLocale: 'en',
470
+ locales: ['en', 'es'],
471
+ },
472
+ },
473
+ ```
474
+
475
+ ## Tests
476
+
477
+ ```typescript
478
+ import { describe, it, expect, vi } from 'vitest';
479
+ import { render, screen } from '@testing-library/react';
480
+ import { NextIntlClientProvider } from 'next-intl';
481
+
482
+ const messages = {
483
+ common: { save: 'Save', cancel: 'Cancel' },
484
+ };
485
+
486
+ function renderWithIntl(ui: React.ReactElement, locale = 'en') {
487
+ return render(
488
+ <NextIntlClientProvider locale={locale} messages={messages}>
489
+ {ui}
490
+ </NextIntlClientProvider>,
491
+ );
492
+ }
493
+
494
+ describe('Translated component', () => {
495
+ it('renders translated text', () => {
496
+ renderWithIntl(<SaveButton />);
497
+ expect(screen.getByText('Save')).toBeInTheDocument();
498
+ });
499
+ });
500
+ ```
501
+
502
+ ## Checklist
503
+
504
+ - [ ] `next-intl` installed
505
+ - [ ] `src/i18n/routing.ts` created with supported locales
506
+ - [ ] `src/i18n/request.ts` created with `getRequestConfig`
507
+ - [ ] Default translation file created (`src/messages/en.json`)
508
+ - [ ] At least one additional locale file created
509
+ - [ ] Proxy/middleware updated for locale detection and routing
510
+ - [ ] Root layout wrapped with `NextIntlClientProvider`
511
+ - [ ] `lang` and `dir` attributes set on `<html>` element
512
+ - [ ] `generateStaticParams` exports all locales
513
+ - [ ] Locale-aware `Link` and `useRouter` created via `createNavigation`
514
+ - [ ] `LocaleSwitcher` component added to settings or header
515
+ - [ ] Date/number formatting uses `useFormatter`
516
+ - [ ] RTL styles use CSS logical properties / Tailwind logical utilities
517
+ - [ ] Feature flag checked (`appConfig.features.i18n`)
518
+ - [ ] Tests use `NextIntlClientProvider` wrapper