@agility/create-next-app 1.0.0-beta.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/.claude/settings.json +7 -0
- package/.claude/settings.local.json +24 -0
- package/FEATURE_ROADMAP.md +343 -0
- package/README.md +205 -0
- package/TESTING.md +131 -0
- package/bin/create-agility-app.js +48 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/generateApiKeys.js +99 -0
- package/dist/agility/api-keys/generateApiKeys.js.map +1 -0
- package/dist/agility/api-keys/getApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/getApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/getApiKeys.js +14 -0
- package/dist/agility/api-keys/getApiKeys.js.map +1 -0
- package/dist/agility/index.d.ts +3 -0
- package/dist/agility/index.d.ts.map +1 -0
- package/dist/agility/index.js +8 -0
- package/dist/agility/index.js.map +1 -0
- package/dist/agility/instance/createNewInstance.d.ts +8 -0
- package/dist/agility/instance/createNewInstance.d.ts.map +1 -0
- package/dist/agility/instance/createNewInstance.js +65 -0
- package/dist/agility/instance/createNewInstance.js.map +1 -0
- package/dist/agility/instance/getAvailableInstances.d.ts +8 -0
- package/dist/agility/instance/getAvailableInstances.d.ts.map +1 -0
- package/dist/agility/instance/getAvailableInstances.js +43 -0
- package/dist/agility/instance/getAvailableInstances.js.map +1 -0
- package/dist/agility/instance/manageInstance.d.ts +9 -0
- package/dist/agility/instance/manageInstance.d.ts.map +1 -0
- package/dist/agility/instance/manageInstance.js +82 -0
- package/dist/agility/instance/manageInstance.js.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts +20 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.js +61 -0
- package/dist/agility/utils/getMgmtAPIUrl.js.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts +6 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.js +28 -0
- package/dist/auth/api-key/authenticateWithApiKey.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth/authenticate.d.ts +6 -0
- package/dist/auth/oauth/authenticate.d.ts.map +1 -0
- package/dist/auth/oauth/authenticate.js +162 -0
- package/dist/auth/oauth/authenticate.js.map +1 -0
- package/dist/auth/oauth/constants.d.ts +5 -0
- package/dist/auth/oauth/constants.d.ts.map +1 -0
- package/dist/auth/oauth/constants.js +9 -0
- package/dist/auth/oauth/constants.js.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts +7 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.js +39 -0
- package/dist/auth/oauth/exchangeCodeForToken.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +290 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/promptForMissingOptions.d.ts +8 -0
- package/dist/cli/promptForMissingOptions.d.ts.map +1 -0
- package/dist/cli/promptForMissingOptions.js +92 -0
- package/dist/cli/promptForMissingOptions.js.map +1 -0
- package/dist/config/env/createEnvFile.d.ts +6 -0
- package/dist/config/env/createEnvFile.d.ts.map +1 -0
- package/dist/config/env/createEnvFile.js +31 -0
- package/dist/config/env/createEnvFile.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mcp/createMcpConfig.d.ts +5 -0
- package/dist/config/mcp/createMcpConfig.d.ts.map +1 -0
- package/dist/config/mcp/createMcpConfig.js +32 -0
- package/dist/config/mcp/createMcpConfig.js.map +1 -0
- package/dist/config/packages/installAgilityPackages.d.ts +6 -0
- package/dist/config/packages/installAgilityPackages.d.ts.map +1 -0
- package/dist/config/packages/installAgilityPackages.js +61 -0
- package/dist/config/packages/installAgilityPackages.js.map +1 -0
- package/dist/config/setupProject.d.ts +8 -0
- package/dist/config/setupProject.d.ts.map +1 -0
- package/dist/config/setupProject.js +32 -0
- package/dist/config/setupProject.js.map +1 -0
- package/dist/create-next-app/createNextApp.d.ts +9 -0
- package/dist/create-next-app/createNextApp.d.ts.map +1 -0
- package/dist/create-next-app/createNextApp.js +83 -0
- package/dist/create-next-app/createNextApp.js.map +1 -0
- package/dist/create-next-app/index.d.ts +3 -0
- package/dist/create-next-app/index.d.ts.map +1 -0
- package/dist/create-next-app/index.js +8 -0
- package/dist/create-next-app/index.js.map +1 -0
- package/dist/scaffold/components/createPageComponents.d.ts +6 -0
- package/dist/scaffold/components/createPageComponents.d.ts.map +1 -0
- package/dist/scaffold/components/createPageComponents.js +62 -0
- package/dist/scaffold/components/createPageComponents.js.map +1 -0
- package/dist/scaffold/containers/createContainers.d.ts +6 -0
- package/dist/scaffold/containers/createContainers.d.ts.map +1 -0
- package/dist/scaffold/containers/createContainers.js +48 -0
- package/dist/scaffold/containers/createContainers.js.map +1 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +6 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts +8 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.js +51 -0
- package/dist/scaffold/instance/createBlankInstance.js.map +1 -0
- package/dist/scaffold/models/createContentModels.d.ts +6 -0
- package/dist/scaffold/models/createContentModels.d.ts.map +1 -0
- package/dist/scaffold/models/createContentModels.js +70 -0
- package/dist/scaffold/models/createContentModels.js.map +1 -0
- package/dist/templates/copyDirectory.d.ts +5 -0
- package/dist/templates/copyDirectory.d.ts.map +1 -0
- package/dist/templates/copyDirectory.js +28 -0
- package/dist/templates/copyDirectory.js.map +1 -0
- package/dist/templates/copyTemplates.d.ts +8 -0
- package/dist/templates/copyTemplates.d.ts.map +1 -0
- package/dist/templates/copyTemplates.js +58 -0
- package/dist/templates/copyTemplates.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +6 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +71 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/validation.d.ts +45 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +180 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/agility/api-keys/generateApiKeys.ts +100 -0
- package/src/agility/api-keys/getApiKeys.ts +13 -0
- package/src/agility/index.ts +3 -0
- package/src/agility/instance/createNewInstance.ts +67 -0
- package/src/agility/instance/getAvailableInstances.ts +49 -0
- package/src/agility/instance/manageInstance.ts +90 -0
- package/src/agility/utils/getMgmtAPIUrl.ts +68 -0
- package/src/auth/api-key/authenticateWithApiKey.ts +24 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oauth/authenticate.ts +165 -0
- package/src/auth/oauth/constants.ts +6 -0
- package/src/auth/oauth/exchangeCodeForToken.ts +43 -0
- package/src/cli/index.ts +281 -0
- package/src/cli/promptForMissingOptions.ts +104 -0
- package/src/config/env/createEnvFile.ts +30 -0
- package/src/config/index.ts +2 -0
- package/src/config/mcp/createMcpConfig.ts +30 -0
- package/src/config/packages/installAgilityPackages.ts +63 -0
- package/src/config/setupProject.ts +31 -0
- package/src/create-next-app/createNextApp.ts +75 -0
- package/src/create-next-app/index.ts +3 -0
- package/src/scaffold/components/createPageComponents.ts +74 -0
- package/src/scaffold/containers/createContainers.ts +55 -0
- package/src/scaffold/index.ts +2 -0
- package/src/scaffold/instance/createBlankInstance.ts +55 -0
- package/src/scaffold/models/createContentModels.ts +83 -0
- package/src/templates/copyDirectory.ts +24 -0
- package/src/templates/copyTemplates.ts +57 -0
- package/src/templates/index.ts +2 -0
- package/src/types/index.ts +55 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/validation.ts +184 -0
- package/templates/.claude/QUICK-START.md +230 -0
- package/templates/.claude/README.md +32 -0
- package/templates/.claude/settings.json +8 -0
- package/templates/BLANK-INSTANCE-SETUP.md +375 -0
- package/templates/DEVELOPMENT.md +160 -0
- package/templates/EXAMPLE-PROMPTS.md +643 -0
- package/templates/PROMPTS.md +410 -0
- package/templates/README.md +281 -0
- package/templates/agents.md +429 -0
- package/templates/app/[locale]/[...slug]/error.tsx +17 -0
- package/templates/app/[locale]/[...slug]/not-found.tsx +9 -0
- package/templates/app/[locale]/[...slug]/page.tsx +102 -0
- package/templates/app/[locale]/layout.tsx +22 -0
- package/templates/app/[locale]/page.tsx +12 -0
- package/templates/app/api/dynamic-redirect/route.ts +24 -0
- package/templates/app/api/preview/exit/route.ts +34 -0
- package/templates/app/api/preview/route.ts +63 -0
- package/templates/app/api/revalidate/route.ts +118 -0
- package/templates/components/agility-components/RichTextArea.tsx +66 -0
- package/templates/components/agility-components/index.ts +30 -0
- package/templates/components/agility-pages/MainTemplate.tsx +36 -0
- package/templates/components/agility-pages/index.ts +11 -0
- package/templates/docs/01-agility-cms-overview.md +139 -0
- package/templates/docs/02-page-routing.md +251 -0
- package/templates/docs/03-creating-components.md +462 -0
- package/templates/docs/04-data-fetching.md +484 -0
- package/templates/docs/05-containers-and-lists.md +596 -0
- package/templates/docs/06-localization.md +561 -0
- package/templates/docs/07-caching-strategies.md +410 -0
- package/templates/docs/08-common-components.md +756 -0
- package/templates/docs/09-whats-included.md +279 -0
- package/templates/docs/10-mcp-server-setup.md +153 -0
- package/templates/docs/11-linked-nested-content.md +611 -0
- package/templates/docs/README.md +164 -0
- package/templates/lib/cms/getAgilityContext.ts +28 -0
- package/templates/lib/cms/getAgilityPage.ts +51 -0
- package/templates/lib/cms/getAgilitySDK.ts +22 -0
- package/templates/lib/cms/getContentItem.ts +20 -0
- package/templates/lib/cms/getContentList.ts +19 -0
- package/templates/lib/cms/getRedirections.ts +85 -0
- package/templates/lib/cms/getSitemapFlat.ts +19 -0
- package/templates/lib/cms/getSitemapNested.ts +19 -0
- package/templates/lib/env.ts +99 -0
- package/templates/lib/i18n/config.ts +28 -0
- package/templates/proxy.ts +101 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# Localization (i18n)
|
|
2
|
+
|
|
3
|
+
This document explains how to implement multi-language support in your Agility CMS Next.js application.
|
|
4
|
+
|
|
5
|
+
## Locale Configuration
|
|
6
|
+
|
|
7
|
+
Locales are configured in `src/lib/i18n/config.ts`:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
export const defaultLocale = "en-us"
|
|
11
|
+
export const locales = ["en-us", "fr", "es"] as const
|
|
12
|
+
|
|
13
|
+
export type Locale = (typeof locales)[number]
|
|
14
|
+
|
|
15
|
+
export function isValidLocale(locale: string, locales: readonly string[]): boolean {
|
|
16
|
+
return locales.includes(locale)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getLocaleFromPathname(
|
|
20
|
+
pathname: string,
|
|
21
|
+
locales: readonly string[]
|
|
22
|
+
): string | null {
|
|
23
|
+
const segments = pathname.split("/").filter(Boolean)
|
|
24
|
+
const firstSegment = segments[0]
|
|
25
|
+
return firstSegment && locales.includes(firstSegment) ? firstSegment : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function removeLocaleFromPathname(
|
|
29
|
+
pathname: string,
|
|
30
|
+
locale: string
|
|
31
|
+
): string {
|
|
32
|
+
if (pathname.startsWith(`/${locale}`)) {
|
|
33
|
+
return pathname.slice(`/${locale}`.length) || "/"
|
|
34
|
+
}
|
|
35
|
+
return pathname
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## URL Structure
|
|
40
|
+
|
|
41
|
+
### Default Locale (Clean URLs)
|
|
42
|
+
The default locale doesn't appear in URLs:
|
|
43
|
+
```
|
|
44
|
+
/ → en-us homepage
|
|
45
|
+
/about → en-us about page
|
|
46
|
+
/blog/my-post → en-us blog post
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Other Locales (Prefixed)
|
|
50
|
+
Other locales have a prefix:
|
|
51
|
+
```
|
|
52
|
+
/fr → French homepage
|
|
53
|
+
/fr/a-propos → French about page
|
|
54
|
+
/es/blog/mi-post → Spanish blog post
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Environment Configuration
|
|
58
|
+
|
|
59
|
+
Configure locales in `.env.local`:
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
AGILITY_LOCALES=en-us,fr,es
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Adding a New Locale
|
|
66
|
+
|
|
67
|
+
### Step 1: Update Configuration
|
|
68
|
+
|
|
69
|
+
1. Add to environment variable:
|
|
70
|
+
```env
|
|
71
|
+
AGILITY_LOCALES=en-us,fr,es,de
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
2. Update config file:
|
|
75
|
+
```typescript
|
|
76
|
+
// src/lib/i18n/config.ts
|
|
77
|
+
export const locales = ["en-us", "fr", "es", "de"] as const
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Step 2: Add Content in Agility CMS
|
|
81
|
+
|
|
82
|
+
1. Go to **Settings > Locales**
|
|
83
|
+
2. Add the new locale (e.g., "de" - German)
|
|
84
|
+
3. Add content for all pages and modules in the new locale
|
|
85
|
+
|
|
86
|
+
### Step 3: Rebuild
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run build
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
All pages will be generated for the new locale.
|
|
93
|
+
|
|
94
|
+
## Locale Detection and Routing
|
|
95
|
+
|
|
96
|
+
The proxy handles locale routing:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// proxy.ts
|
|
100
|
+
|
|
101
|
+
export async function proxy(request: NextRequest) {
|
|
102
|
+
const pathname = request.nextUrl.pathname
|
|
103
|
+
const hasLocalePrefix = locales.some(
|
|
104
|
+
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// If no locale prefix, rewrite to default locale
|
|
108
|
+
if (!hasLocalePrefix) {
|
|
109
|
+
const localeBasedUrl = new URL(`/${defaultLocale}${pathname}`, request.url)
|
|
110
|
+
return NextResponse.rewrite(localeBasedUrl)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Lang Query Parameter
|
|
116
|
+
|
|
117
|
+
You can switch locales using `?lang=` parameter:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
/about?lang=fr → Redirects to /fr/about
|
|
121
|
+
/fr/about?lang=es → Redirects to /es/about
|
|
122
|
+
/about?lang=en-us → Redirects to /about (default locale)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Fetching Localized Content
|
|
126
|
+
|
|
127
|
+
### In Page Components
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// app/[locale]/[...slug]/page.tsx
|
|
131
|
+
|
|
132
|
+
export default async function Page({ params }: PageProps) {
|
|
133
|
+
const { locale } = await params;
|
|
134
|
+
|
|
135
|
+
const agilityData = await getAgilityPage({ params });
|
|
136
|
+
|
|
137
|
+
// Data is automatically fetched for the current locale
|
|
138
|
+
return <div>{/* ... */}</div>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### In Module Components
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// components/agility-components/PostListing.tsx
|
|
146
|
+
|
|
147
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
148
|
+
|
|
149
|
+
export default async function PostListing({ module, locale }: any) {
|
|
150
|
+
// Always pass locale to CMS functions
|
|
151
|
+
const posts = await getContentList({
|
|
152
|
+
referenceName: "posts",
|
|
153
|
+
locale, // Current locale from props
|
|
154
|
+
take: 10,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return <div>{/* ... */}</div>;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Manual Locale Selection
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { getContentItem } from "@/lib/cms/getContentItem";
|
|
165
|
+
|
|
166
|
+
// Fetch content in specific locale
|
|
167
|
+
const frenchPost = await getContentItem({
|
|
168
|
+
contentID: 123,
|
|
169
|
+
locale: "fr",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const spanishPost = await getContentItem({
|
|
173
|
+
contentID: 123,
|
|
174
|
+
locale: "es",
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Locale Switcher Component
|
|
179
|
+
|
|
180
|
+
### Simple Locale Switcher
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// components/LocaleSwitcher.tsx
|
|
184
|
+
|
|
185
|
+
import { locales, defaultLocale } from "@/lib/i18n/config";
|
|
186
|
+
|
|
187
|
+
interface LocaleSwitcherProps {
|
|
188
|
+
currentLocale: string;
|
|
189
|
+
currentPath: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default function LocaleSwitcher({
|
|
193
|
+
currentLocale,
|
|
194
|
+
currentPath,
|
|
195
|
+
}: LocaleSwitcherProps) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex gap-2">
|
|
198
|
+
{locales.map((locale) => {
|
|
199
|
+
// Build the locale-specific URL
|
|
200
|
+
let href = currentPath;
|
|
201
|
+
if (locale === defaultLocale) {
|
|
202
|
+
// Remove locale prefix for default locale
|
|
203
|
+
href = currentPath.replace(/^\/[a-z]{2}(-[a-z]{2})?/, "") || "/";
|
|
204
|
+
} else {
|
|
205
|
+
// Add or replace locale prefix
|
|
206
|
+
href = currentPath.replace(/^\/[a-z]{2}(-[a-z]{2})?/, "");
|
|
207
|
+
href = `/${locale}${href}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<a
|
|
212
|
+
key={locale}
|
|
213
|
+
href={href}
|
|
214
|
+
className={locale === currentLocale ? "font-bold" : ""}
|
|
215
|
+
>
|
|
216
|
+
{locale.toUpperCase()}
|
|
217
|
+
</a>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Using in Layout
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// app/[locale]/layout.tsx
|
|
229
|
+
|
|
230
|
+
import LocaleSwitcher from "@/components/LocaleSwitcher";
|
|
231
|
+
|
|
232
|
+
export default async function LocaleLayout({
|
|
233
|
+
children,
|
|
234
|
+
params,
|
|
235
|
+
}: {
|
|
236
|
+
children: React.ReactNode;
|
|
237
|
+
params: Promise<{ locale: string }>;
|
|
238
|
+
}) {
|
|
239
|
+
const { locale } = await params;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<html lang={locale}>
|
|
243
|
+
<body>
|
|
244
|
+
<header>
|
|
245
|
+
<LocaleSwitcher currentLocale={locale} currentPath="/" />
|
|
246
|
+
</header>
|
|
247
|
+
{children}
|
|
248
|
+
</body>
|
|
249
|
+
</html>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Translation Helpers
|
|
255
|
+
|
|
256
|
+
### Creating Translation Files
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// lib/i18n/translations.ts
|
|
260
|
+
|
|
261
|
+
export const translations = {
|
|
262
|
+
"en-us": {
|
|
263
|
+
nav: {
|
|
264
|
+
home: "Home",
|
|
265
|
+
about: "About",
|
|
266
|
+
contact: "Contact",
|
|
267
|
+
},
|
|
268
|
+
common: {
|
|
269
|
+
readMore: "Read More",
|
|
270
|
+
loadMore: "Load More",
|
|
271
|
+
backToHome: "Back to Home",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
fr: {
|
|
275
|
+
nav: {
|
|
276
|
+
home: "Accueil",
|
|
277
|
+
about: "À propos",
|
|
278
|
+
contact: "Contact",
|
|
279
|
+
},
|
|
280
|
+
common: {
|
|
281
|
+
readMore: "Lire la suite",
|
|
282
|
+
loadMore: "Charger plus",
|
|
283
|
+
backToHome: "Retour à l'accueil",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
es: {
|
|
287
|
+
nav: {
|
|
288
|
+
home: "Inicio",
|
|
289
|
+
about: "Acerca de",
|
|
290
|
+
contact: "Contacto",
|
|
291
|
+
},
|
|
292
|
+
common: {
|
|
293
|
+
readMore: "Leer más",
|
|
294
|
+
loadMore: "Cargar más",
|
|
295
|
+
backToHome: "Volver al inicio",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
} as const;
|
|
299
|
+
|
|
300
|
+
export function t(locale: string, key: string) {
|
|
301
|
+
const keys = key.split(".");
|
|
302
|
+
let value: any = translations[locale as keyof typeof translations];
|
|
303
|
+
|
|
304
|
+
for (const k of keys) {
|
|
305
|
+
value = value?.[k];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return value || key;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Using Translations
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// components/Header.tsx
|
|
316
|
+
|
|
317
|
+
import { t } from "@/lib/i18n/translations";
|
|
318
|
+
|
|
319
|
+
export default function Header({ locale }: { locale: string }) {
|
|
320
|
+
return (
|
|
321
|
+
<nav>
|
|
322
|
+
<a href="/">{t(locale, "nav.home")}</a>
|
|
323
|
+
<a href="/about">{t(locale, "nav.about")}</a>
|
|
324
|
+
<a href="/contact">{t(locale, "nav.contact")}</a>
|
|
325
|
+
</nav>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Date and Number Formatting
|
|
331
|
+
|
|
332
|
+
### Date Formatting
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
// lib/i18n/formatters.ts
|
|
336
|
+
|
|
337
|
+
export function formatDate(date: string, locale: string): string {
|
|
338
|
+
return new Date(date).toLocaleDateString(locale, {
|
|
339
|
+
year: "numeric",
|
|
340
|
+
month: "long",
|
|
341
|
+
day: "numeric",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Usage:
|
|
347
|
+
```tsx
|
|
348
|
+
import { formatDate } from "@/lib/i18n/formatters";
|
|
349
|
+
|
|
350
|
+
export default function BlogPost({ post, locale }: any) {
|
|
351
|
+
return (
|
|
352
|
+
<article>
|
|
353
|
+
<h1>{post.fields.title}</h1>
|
|
354
|
+
<time>{formatDate(post.fields.date, locale)}</time>
|
|
355
|
+
<div dangerouslySetInnerHTML={{ __html: post.fields.content }} />
|
|
356
|
+
</article>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Number Formatting
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
export function formatNumber(num: number, locale: string): string {
|
|
365
|
+
return new Intl.NumberFormat(locale).format(num);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function formatCurrency(
|
|
369
|
+
amount: number,
|
|
370
|
+
locale: string,
|
|
371
|
+
currency: string = "USD"
|
|
372
|
+
): string {
|
|
373
|
+
return new Intl.NumberFormat(locale, {
|
|
374
|
+
style: "currency",
|
|
375
|
+
currency,
|
|
376
|
+
}).format(amount);
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Locale-Specific Sitemap
|
|
381
|
+
|
|
382
|
+
Each locale has its own sitemap in Agility CMS:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Generate pages for each locale
|
|
386
|
+
export async function generateStaticParams() {
|
|
387
|
+
const allPaths: { locale: string; slug: string[] }[] = [];
|
|
388
|
+
|
|
389
|
+
for (const locale of locales) {
|
|
390
|
+
const sitemap = await agilityClient.getSitemapFlat({
|
|
391
|
+
channelName: "website",
|
|
392
|
+
languageCode: locale,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const localePaths = Object.values(sitemap).map((node: any) => ({
|
|
396
|
+
locale,
|
|
397
|
+
slug: node.path.split("/").slice(1),
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
allPaths.push(...localePaths);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return allPaths;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## SEO for Multi-Locale
|
|
408
|
+
|
|
409
|
+
### Alternate Language Links
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// app/[locale]/[...slug]/page.tsx
|
|
413
|
+
|
|
414
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
415
|
+
const { locale, slug } = await params;
|
|
416
|
+
const agilityData = await getAgilityPage({ params });
|
|
417
|
+
|
|
418
|
+
const alternates: Record<string, string> = {};
|
|
419
|
+
|
|
420
|
+
for (const loc of locales) {
|
|
421
|
+
if (loc === defaultLocale) {
|
|
422
|
+
alternates[loc] = `https://yourdomain.com${agilityData.sitemapNode.path}`;
|
|
423
|
+
} else {
|
|
424
|
+
alternates[loc] = `https://yourdomain.com/${loc}${agilityData.sitemapNode.path}`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
title: agilityData.page?.seo?.metaTitle,
|
|
430
|
+
description: agilityData.page?.seo?.metaDescription,
|
|
431
|
+
alternates: {
|
|
432
|
+
languages: alternates,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Sitemap.xml with Locales
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
// app/sitemap.ts
|
|
442
|
+
|
|
443
|
+
import { locales, defaultLocale } from "@/lib/i18n/config";
|
|
444
|
+
import { getSitemapFlat } from "@/lib/cms/getSitemapFlat";
|
|
445
|
+
|
|
446
|
+
export default async function sitemap() {
|
|
447
|
+
const urls = [];
|
|
448
|
+
|
|
449
|
+
for (const locale of locales) {
|
|
450
|
+
const sitemap = await getSitemapFlat({ locale });
|
|
451
|
+
|
|
452
|
+
for (const path in sitemap) {
|
|
453
|
+
const url =
|
|
454
|
+
locale === defaultLocale
|
|
455
|
+
? `https://yourdomain.com${path}`
|
|
456
|
+
: `https://yourdomain.com/${locale}${path}`;
|
|
457
|
+
|
|
458
|
+
urls.push({
|
|
459
|
+
url,
|
|
460
|
+
lastModified: new Date(),
|
|
461
|
+
alternates: {
|
|
462
|
+
languages: locales.reduce((acc, loc) => {
|
|
463
|
+
acc[loc] =
|
|
464
|
+
loc === defaultLocale
|
|
465
|
+
? `https://yourdomain.com${path}`
|
|
466
|
+
: `https://yourdomain.com/${loc}${path}`;
|
|
467
|
+
return acc;
|
|
468
|
+
}, {} as Record<string, string>),
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return urls;
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Right-to-Left (RTL) Support
|
|
479
|
+
|
|
480
|
+
For RTL languages like Arabic:
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
// app/[locale]/layout.tsx
|
|
484
|
+
|
|
485
|
+
const rtlLocales = ["ar", "he"];
|
|
486
|
+
|
|
487
|
+
export default function LocaleLayout({ children, params }: any) {
|
|
488
|
+
const { locale } = params;
|
|
489
|
+
const dir = rtlLocales.includes(locale) ? "rtl" : "ltr";
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<html lang={locale} dir={dir}>
|
|
493
|
+
<body>{children}</body>
|
|
494
|
+
</html>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Best Practices
|
|
500
|
+
|
|
501
|
+
1. **Always Pass Locale**: Pass locale to all CMS functions
|
|
502
|
+
2. **Use Translation Keys**: Don't hardcode strings in components
|
|
503
|
+
3. **Format Dates/Numbers**: Use locale-specific formatting
|
|
504
|
+
4. **SEO Optimization**: Add alternate language links
|
|
505
|
+
5. **Test All Locales**: Ensure content exists for all locales
|
|
506
|
+
6. **Fallback Content**: Handle missing translations gracefully
|
|
507
|
+
7. **URL Structure**: Keep URLs SEO-friendly in each locale
|
|
508
|
+
|
|
509
|
+
## Common Patterns
|
|
510
|
+
|
|
511
|
+
### Pattern 1: Locale-Aware Navigation
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
import { getSitemapNested } from "@/lib/cms/getSitemapNested";
|
|
515
|
+
|
|
516
|
+
export default async function Navigation({ locale }: { locale: string }) {
|
|
517
|
+
const sitemap = await getSitemapNested({ locale });
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<nav>
|
|
521
|
+
{sitemap[0].children.map((item: any) => (
|
|
522
|
+
<a key={item.pageID} href={item.path}>
|
|
523
|
+
{item.menuText || item.title}
|
|
524
|
+
</a>
|
|
525
|
+
))}
|
|
526
|
+
</nav>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Pattern 2: Locale-Specific Footer
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
import { getContentItem } from "@/lib/cms/getContentItem";
|
|
535
|
+
|
|
536
|
+
export default async function Footer({ locale }: { locale: string }) {
|
|
537
|
+
const footer = await getContentItem({
|
|
538
|
+
referenceName: "footerSettings",
|
|
539
|
+
locale,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<footer>
|
|
544
|
+
<p>{footer.fields.copyrightText}</p>
|
|
545
|
+
<nav>
|
|
546
|
+
{footer.fields.links.map((link: any) => (
|
|
547
|
+
<a key={link.href} href={link.href}>
|
|
548
|
+
{link.text}
|
|
549
|
+
</a>
|
|
550
|
+
))}
|
|
551
|
+
</nav>
|
|
552
|
+
</footer>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Next Steps
|
|
558
|
+
|
|
559
|
+
- Read [02-page-routing.md](./02-page-routing.md) for locale routing details
|
|
560
|
+
- Read [04-data-fetching.md](./04-data-fetching.md) for fetching localized content
|
|
561
|
+
- Read [07-caching-strategies.md](./07-caching-strategies.md) for locale-specific caching
|