@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.
- package/dist/index.js +137 -12
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/template/.cursor/rules/composition-patterns.mdc +186 -0
- package/template/.cursor/rules/data-access.mdc +29 -0
- package/template/.cursor/rules/project-structure.mdc +34 -0
- package/template/.cursor/rules/security.mdc +25 -0
- package/template/.cursor/rules/testing.mdc +24 -0
- package/template/.cursor/rules/ui-conventions.mdc +29 -0
- package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
- package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
- package/template/.cursor/skills/add-blog/SKILL.md +447 -0
- package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
- package/template/.cursor/skills/add-component/SKILL.md +158 -0
- package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
- package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
- package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
- package/template/.cursor/skills/add-feature/SKILL.md +174 -0
- package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
- package/template/.cursor/skills/add-page/SKILL.md +151 -0
- package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
- package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
- package/template/.cursor/skills/add-role/SKILL.md +156 -0
- package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
- package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
- package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
- package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
- package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
- package/template/.cursor/skills/build-form/SKILL.md +231 -0
- package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
- package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
- package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
- package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
- package/template/.cursor/skills/configure-email/SKILL.md +170 -0
- package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
- package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
- package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
- package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
- package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
- package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
- package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
- package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
- package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
- package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
- package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
- package/template/.cursor/skills/configure-search/SKILL.md +581 -0
- package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
- package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
- package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
- package/template/.cursor/skills/create-seed/SKILL.md +191 -0
- package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
- package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
- package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
- package/template/.cursor/skills/setup-project/SKILL.md +104 -0
- package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
- package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
- package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
- package/template/AGENTS.md +104 -0
- package/template/ARCHITECTURE.md +102 -0
- package/template/docs/QUALITY_SCORE.md +20 -0
- package/template/docs/design-docs/conversation-as-system-record.md +70 -0
- package/template/docs/design-docs/core-beliefs.md +43 -0
- package/template/docs/design-docs/index.md +8 -0
- package/template/docs/exec-plans/active/.gitkeep +0 -0
- package/template/docs/exec-plans/completed/.gitkeep +0 -0
- package/template/docs/exec-plans/tech-debt.md +7 -0
- package/template/docs/generated/.gitkeep +0 -0
- package/template/docs/product-specs/index.md +7 -0
- package/template/docs/references/index.md +18 -0
- package/template/e2e/api.spec.ts +20 -0
- package/template/e2e/auth.spec.ts +24 -0
- package/template/e2e/public.spec.ts +25 -0
- package/template/eslint.config.mjs +24 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.ts +45 -0
- package/template/package.json +80 -0
- package/template/playwright.config.ts +31 -0
- package/template/postcss.config.mjs +8 -0
- package/template/prisma/generated/prisma/browser.ts +49 -0
- package/template/prisma/generated/prisma/client.ts +73 -0
- package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
- package/template/prisma/generated/prisma/enums.ts +15 -0
- package/template/prisma/generated/prisma/internal/class.ts +254 -0
- package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
- package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
- package/template/prisma/generated/prisma/models/Account.ts +1543 -0
- package/template/prisma/generated/prisma/models/File.ts +1529 -0
- package/template/prisma/generated/prisma/models/Session.ts +1415 -0
- package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
- package/template/prisma/generated/prisma/models/User.ts +2235 -0
- package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
- package/template/prisma/generated/prisma/models.ts +17 -0
- package/template/prisma/schema/auth.prisma +69 -0
- package/template/prisma/schema/base.prisma +8 -0
- package/template/prisma/schema/file.prisma +15 -0
- package/template/prisma/schema/subscription.prisma +17 -0
- package/template/prisma.config.ts +13 -0
- package/template/scripts/check-architecture.ts +221 -0
- package/template/scripts/check-doc-freshness.ts +242 -0
- package/template/scripts/ensure-db.mjs +291 -0
- package/template/scripts/generate-docs.ts +143 -0
- package/template/scripts/generate-env-example.ts +89 -0
- package/template/scripts/seed.ts +56 -0
- package/template/scripts/update-quality-score.ts +263 -0
- package/template/src/__tests__/architecture.test.ts +114 -0
- package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
- package/template/src/app/(auth)/layout.tsx +11 -0
- package/template/src/app/(auth)/register/page.tsx +162 -0
- package/template/src/app/(auth)/reset-password/page.tsx +109 -0
- package/template/src/app/(auth)/sign-in/page.tsx +122 -0
- package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
- package/template/src/app/(auth)/verify/page.tsx +56 -0
- package/template/src/app/(protected)/admin/page.tsx +108 -0
- package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
- package/template/src/app/(protected)/dashboard/page.tsx +22 -0
- package/template/src/app/(protected)/layout.tsx +262 -0
- package/template/src/app/(protected)/settings/page.tsx +370 -0
- package/template/src/app/api/auth/forgot/route.ts +63 -0
- package/template/src/app/api/auth/login/route.ts +121 -0
- package/template/src/app/api/auth/logout/route.ts +19 -0
- package/template/src/app/api/auth/me/route.ts +30 -0
- package/template/src/app/api/auth/reset/route.ts +45 -0
- package/template/src/app/api/auth/signup/route.ts +85 -0
- package/template/src/app/api/auth/verify/route.ts +46 -0
- package/template/src/app/api/csrf/route.ts +12 -0
- package/template/src/app/api/health/route.ts +10 -0
- package/template/src/app/api/protected/admin/users/route.ts +24 -0
- package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
- package/template/src/app/api/protected/billing/portal/route.ts +39 -0
- package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
- package/template/src/app/api/protected/files/upload/route.ts +64 -0
- package/template/src/app/api/protected/user/password/route.ts +63 -0
- package/template/src/app/api/protected/user/profile/route.ts +35 -0
- package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
- package/template/src/app/api/protected/user/sessions/route.ts +22 -0
- package/template/src/app/api/readiness/route.ts +15 -0
- package/template/src/app/api/webhooks/stripe/route.ts +166 -0
- package/template/src/app/error.tsx +33 -0
- package/template/src/app/layout.tsx +29 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +136 -0
- package/template/src/app/privacy/page.tsx +178 -0
- package/template/src/app/providers.tsx +8 -0
- package/template/src/app/terms/page.tsx +139 -0
- package/template/src/config/app.config.ts +70 -0
- package/template/src/config/routes.ts +17 -0
- package/template/src/features/admin/index.ts +11 -0
- package/template/src/features/admin/permissions.ts +64 -0
- package/template/src/features/auth/context/AuthContext.tsx +96 -0
- package/template/src/features/auth/context/index.ts +2 -0
- package/template/src/features/auth/index.ts +3 -0
- package/template/src/features/auth/server/consent.ts +66 -0
- package/template/src/features/auth/server/session-revocation.ts +20 -0
- package/template/src/features/auth/server/sessions.ts +66 -0
- package/template/src/features/auth/server/user.ts +166 -0
- package/template/src/features/auth/types.ts +19 -0
- package/template/src/features/auth/validators.ts +29 -0
- package/template/src/features/billing/server/index.ts +66 -0
- package/template/src/features/billing/types.ts +43 -0
- package/template/src/features/uploads/server/index.ts +49 -0
- package/template/src/features/uploads/types.ts +26 -0
- package/template/src/lib/core/email/templates/base-layout.ts +122 -0
- package/template/src/lib/core/email/templates/index.ts +4 -0
- package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
- package/template/src/lib/core/email/templates/verification-email.ts +41 -0
- package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
- package/template/src/lib/mars.ts +56 -0
- package/template/src/lib/prisma.ts +19 -0
- package/template/src/proxy.ts +92 -0
- package/template/src/styles/brand.css +15 -0
- package/template/src/styles/globals.css +7 -0
- package/template/tsconfig.json +59 -0
- package/template/vitest.config.ts +41 -0
- 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
|