@translation-cms/sync 1.1.69 → 1.1.71
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/README.md +131 -545
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +268 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.d.ts +15 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -263
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +26 -25
- package/dist/client.js.map +1 -1
- package/dist/i18next.d.ts +62 -0
- package/dist/i18next.d.ts.map +1 -0
- package/dist/i18next.js +127 -0
- package/dist/i18next.js.map +1 -0
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -15
- package/dist/index.js.map +1 -1
- package/dist/{config.d.ts → load-config.d.ts} +1 -1
- package/dist/load-config.d.ts.map +1 -0
- package/dist/{config.js → load-config.js} +1 -1
- package/dist/load-config.js.map +1 -0
- package/dist/{preview-listener.d.ts → preview.d.ts} +1 -1
- package/dist/preview.d.ts.map +1 -0
- package/dist/{preview-listener.js → preview.js} +1 -1
- package/dist/preview.js.map +1 -0
- package/dist/{cli-config.d.ts → resolve-config.d.ts} +2 -2
- package/dist/resolve-config.d.ts.map +1 -0
- package/dist/{cli-config.js → resolve-config.js} +1 -1
- package/dist/resolve-config.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +27 -13
- package/dist/sync.js.map +1 -1
- package/package.json +9 -17
- package/dist/cli-config.d.ts.map +0 -1
- package/dist/cli-config.js.map +0 -1
- package/dist/cli-utils.d.ts +0 -16
- package/dist/cli-utils.d.ts.map +0 -1
- package/dist/cli-utils.js +0 -16
- package/dist/cli-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/preview-listener.d.ts.map +0 -1
- package/dist/preview-listener.js.map +0 -1
- package/dist/trans.d.ts +0 -2
- package/dist/trans.d.ts.map +0 -1
- package/dist/trans.js +0 -3
- package/dist/trans.js.map +0 -1
- package/dist/translation-context.d.ts +0 -10
- package/dist/translation-context.d.ts.map +0 -1
- package/dist/translation-context.js +0 -4
- package/dist/translation-context.js.map +0 -1
- package/dist/translation-provider.d.ts +0 -15
- package/dist/translation-provider.d.ts.map +0 -1
- package/dist/translation-provider.js +0 -97
- package/dist/translation-provider.js.map +0 -1
- package/dist/use-translation.d.ts +0 -35
- package/dist/use-translation.d.ts.map +0 -1
- package/dist/use-translation.js +0 -110
- package/dist/use-translation.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @translation-cms/sync
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Scan translation keys in your codebase, sync them to the Translations CMS, and
|
|
4
|
+
pull translations back as local JSON files. Built for Next.js + i18next.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import { TranslationProvider } from '@translation-cms/sync';
|
|
8
|
-
|
|
9
|
-
export default function RootLayout({
|
|
10
|
-
children,
|
|
11
|
-
}: {
|
|
12
|
-
children: React.ReactNode;
|
|
13
|
-
}) {
|
|
14
|
-
return <TranslationProvider locale="nl">{children}</TranslationProvider>;
|
|
15
|
-
}
|
|
16
|
-
```
|
|
6
|
+
## How it works
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
```
|
|
9
|
+
1. sync-translations sync → scans your code, pushes new keys to CMS
|
|
10
|
+
2. sync-translations pull → fetches translations, writes public/locales/{locale}/{ns}.json
|
|
11
|
+
3. i18next loads the files → useTranslation('auth') works as normal
|
|
12
|
+
```
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
Translations CMS. Features type-safe translation functions, server-side
|
|
25
|
-
stale-while-revalidate caching, and a live in-context preview.
|
|
14
|
+
---
|
|
26
15
|
|
|
27
16
|
## Installation
|
|
28
17
|
|
|
@@ -30,121 +19,69 @@ stale-while-revalidate caching, and a live in-context preview.
|
|
|
30
19
|
pnpm add @translation-cms/sync
|
|
31
20
|
```
|
|
32
21
|
|
|
22
|
+
---
|
|
23
|
+
|
|
33
24
|
## Configuration
|
|
34
25
|
|
|
35
|
-
###
|
|
26
|
+
### `.env.local`
|
|
36
27
|
|
|
37
28
|
```bash
|
|
38
|
-
# CMS location
|
|
39
29
|
NEXT_PUBLIC_CMS_URL=https://cms.example.com
|
|
40
|
-
|
|
41
|
-
# Your project ID from the CMS dashboard
|
|
42
30
|
NEXT_PUBLIC_CMS_PROJECT_ID=your-project-id
|
|
43
|
-
|
|
44
|
-
# API key (project settings → environments → api_key)
|
|
45
|
-
# Used for both CLI syncing and client-side translation fetching
|
|
46
31
|
NEXT_PUBLIC_CMS_ANON_KEY=your-api-key
|
|
47
32
|
```
|
|
48
33
|
|
|
49
|
-
###
|
|
34
|
+
### `.translationsrc.json` (optional)
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
Run `sync-translations init` to generate it interactively, or create it
|
|
37
|
+
manually:
|
|
53
38
|
|
|
54
39
|
```json
|
|
55
40
|
{
|
|
56
41
|
"outputDir": "./public/locales",
|
|
57
42
|
"pullTtlMs": 300000,
|
|
58
43
|
"excludedDirs": ["e2e", "fixtures"],
|
|
59
|
-
"reservedCssNamespaces": ["after", "before"],
|
|
60
44
|
"previewRoutes": {
|
|
61
|
-
"
|
|
62
|
-
{
|
|
63
|
-
"route": "/[locale]/blog/[slug]",
|
|
64
|
-
"params": { "slug": "first-post" }
|
|
65
|
-
}
|
|
45
|
+
"blog:post.title": [
|
|
46
|
+
{ "route": "/blog/[slug]", "params": { "slug": "first-post" } }
|
|
66
47
|
]
|
|
67
48
|
}
|
|
68
49
|
}
|
|
69
50
|
```
|
|
70
51
|
|
|
71
|
-
|
|
72
|
-
entry maps a `namespace:key` to one or more routes. For dynamic routes, use an
|
|
73
|
-
object with a `params` field so the CMS can construct the correct preview URL:
|
|
74
|
-
|
|
75
|
-
```json
|
|
76
|
-
"previewRoutes": {
|
|
77
|
-
"blog:meta.author": [
|
|
78
|
-
{ "route": "/[locale]/blog/[slug]", "params": { "slug": "first-post" } }
|
|
79
|
-
],
|
|
80
|
-
"common:button.back": [
|
|
81
|
-
{ "route": "/[locale]/blog/[slug]", "params": { "slug": "first-post" } },
|
|
82
|
-
{ "route": "/[locale]/products/[slug]", "params": { "slug": "product-a" } }
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Plain strings are also accepted for static routes or when segment values are
|
|
88
|
-
resolved via the project's global preview params:
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
"previewRoutes": {
|
|
92
|
-
"home:hero.title": ["/"],
|
|
93
|
-
"about:intro": ["/about"]
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
> `previewRoutes` in `.translationsrc.json` is a fallback for keys that lack
|
|
98
|
-
> `@preview` annotations in source code. Per-key `@preview` annotations always
|
|
99
|
-
> take priority.
|
|
100
|
-
|
|
101
|
-
> Keep API keys in `.env.local`, not in the config file. Add
|
|
102
|
-
> `.translationsrc.json` to `.gitignore` if it contains the `apiKey` field.
|
|
103
|
-
|
|
104
|
-
Run `sync-translations init` to generate the file interactively.
|
|
105
|
-
|
|
106
|
-
### Environment priority
|
|
107
|
-
|
|
108
|
-
`CLI args` → `.env.local` → `apps/web/.env.local` → `.translationsrc.json`
|
|
52
|
+
**Priority:** CLI args → `.env.local` → `.translationsrc.json`
|
|
109
53
|
|
|
110
54
|
---
|
|
111
55
|
|
|
112
|
-
## CLI
|
|
113
|
-
|
|
114
|
-
### Subcommands
|
|
56
|
+
## CLI
|
|
115
57
|
|
|
116
58
|
```bash
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# Same, explicit
|
|
121
|
-
pnpm sync-translations sync
|
|
122
|
-
|
|
123
|
-
# Preview what would change — no network calls
|
|
124
|
-
pnpm sync-translations sync --dry-run
|
|
59
|
+
# Scan keys + push to CMS + pull translations
|
|
60
|
+
sync-translations
|
|
125
61
|
|
|
126
|
-
#
|
|
127
|
-
|
|
62
|
+
# Only pull (respects TTL cache)
|
|
63
|
+
sync-translations pull
|
|
64
|
+
sync-translations pull --force # ignore TTL
|
|
65
|
+
sync-translations pull --output ./locales # custom output dir
|
|
66
|
+
sync-translations pull --env staging # pull staging environment
|
|
67
|
+
sync-translations pull --ttl 600000 # custom TTL in ms
|
|
128
68
|
|
|
129
|
-
#
|
|
130
|
-
|
|
69
|
+
# Preview changes without touching the CMS
|
|
70
|
+
sync-translations sync --dry-run
|
|
131
71
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
pnpm sync-translations pull --output ./locales # custom output dir
|
|
135
|
-
pnpm sync-translations pull --env staging # pull staging environment
|
|
136
|
-
pnpm sync-translations pull --ttl 600000 # custom TTL in ms
|
|
72
|
+
# Show diff vs last sync (no network)
|
|
73
|
+
sync-translations status
|
|
137
74
|
|
|
138
|
-
#
|
|
139
|
-
|
|
75
|
+
# Watch + auto-sync on file save
|
|
76
|
+
sync-translations watch
|
|
140
77
|
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Interactive setup — creates .translationsrc.json
|
|
145
|
-
pnpm sync-translations init
|
|
78
|
+
# Interactive setup
|
|
79
|
+
sync-translations init
|
|
146
80
|
```
|
|
147
81
|
|
|
82
|
+
**Output:** `public/locales/{locale}/{namespace}.json` — one file per locale per
|
|
83
|
+
namespace, flat key/value pairs. Compatible with `i18next-http-backend`.
|
|
84
|
+
|
|
148
85
|
### Flags
|
|
149
86
|
|
|
150
87
|
| Flag | Applies to | Description |
|
|
@@ -159,428 +96,77 @@ pnpm sync-translations init
|
|
|
159
96
|
| `--api-key` | all | Override API key |
|
|
160
97
|
| `--cms-url` | all | Override CMS URL |
|
|
161
98
|
|
|
162
|
-
### Example output
|
|
163
|
-
|
|
164
|
-
```
|
|
165
|
-
Scanning project: /path/to/project
|
|
166
|
-
Scanned 127 files.
|
|
167
|
-
Found 45 key(s) across 3 namespace(s): common, post, nav
|
|
168
|
-
Posting to https://cms.example.com/api/sync/abc123...
|
|
169
|
-
Synced: 3 created, 1 routes updated, 41 existing
|
|
170
|
-
|
|
171
|
-
[CMS] Fetching from: https://cms.example.com/api/translations/abc123
|
|
172
|
-
[CMS] ✓ Refresh complete: wrote 4 JSON files, updated .last-pulled
|
|
173
|
-
```
|
|
174
|
-
|
|
175
99
|
---
|
|
176
100
|
|
|
177
|
-
##
|
|
178
|
-
|
|
179
|
-
Fetch translations with the same API as your app's own i18n library.
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import { getCMSClient } from '@translation-cms/sync';
|
|
183
|
-
|
|
184
|
-
// Reads NEXT_PUBLIC_CMS_URL + NEXT_PUBLIC_CMS_PROJECT_ID from env
|
|
185
|
-
const client = getCMSClient();
|
|
186
|
-
|
|
187
|
-
// Or configure explicitly
|
|
188
|
-
import { CMSClient } from '@translation-cms/sync';
|
|
189
|
-
|
|
190
|
-
const client = new CMSClient({
|
|
191
|
-
cmsUrl: 'https://cms.example.com',
|
|
192
|
-
projectId: 'your-project-id',
|
|
193
|
-
anonKey: 'optional-api-key',
|
|
194
|
-
defaultLocale: 'nl',
|
|
195
|
-
fallbackLocale: 'en',
|
|
196
|
-
});
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### `getTranslation()` — Server Components & async contexts
|
|
200
|
-
|
|
201
|
-
Mirrors `getTranslation(ns)` from your app's `@/lib/i18n/server`.
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
// Single namespace — uses defaultLocale
|
|
205
|
-
const { t, tDynamic } = await client.getTranslation('post');
|
|
206
|
-
t('post:first_post.title'); // ✓ type-safe
|
|
207
|
-
t('common:submit'); // ✗ Type error — wrong namespace
|
|
208
|
-
|
|
209
|
-
// Explicit locale
|
|
210
|
-
const { t } = await client.getTranslation('post', 'en');
|
|
211
|
-
|
|
212
|
-
// Multiple namespaces
|
|
213
|
-
const { t } = await client.getTranslation(['post', 'common'] as const);
|
|
214
|
-
t('post:first_post.title'); // ✓
|
|
215
|
-
t('common:submit'); // ✓
|
|
216
|
-
t('nav:home'); // ✗ Type error
|
|
217
|
-
|
|
218
|
-
// tDynamic: for keys from variables or data structures
|
|
219
|
-
const post = { titleKey: 'post:title' };
|
|
220
|
-
tDynamic(post.titleKey); // ✓ accepts any string
|
|
221
|
-
t(post.titleKey); // ✗ Type error — not a literal
|
|
222
|
-
|
|
223
|
-
// Interpolation and plurals — same as real t()
|
|
224
|
-
t('common:greeting', { name: 'Rick' }); // "Hello Rick"
|
|
225
|
-
t('common:items', { count: 3 }); // "3 items"
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### `useTranslation()` — React Client Components
|
|
229
|
-
|
|
230
|
-
Mirrors `useTranslation(ns)` from your app's `@/lib/i18n/client`. Import from
|
|
231
|
-
the `/react` subpath to keep React out of the server bundle.
|
|
232
|
-
|
|
233
|
-
```tsx
|
|
234
|
-
'use client';
|
|
235
|
-
import { useTranslation } from '@translation-cms/sync/react';
|
|
236
|
-
|
|
237
|
-
export function MyComponent() {
|
|
238
|
-
const { t, tDynamic, isLoading } = useTranslation('about');
|
|
239
|
-
|
|
240
|
-
return <h1>{t('about:hero.title')}</h1>;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Multiple namespaces
|
|
244
|
-
const { t } = useTranslation(['about', 'common'] as const);
|
|
245
|
-
t('about:hero.title'); // ✓
|
|
246
|
-
t('common:submit'); // ✓
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Translations load asynchronously. The `isLoading` flag is `true` until the first
|
|
250
|
-
fetch completes — keys return their raw key string in the meantime.
|
|
251
|
-
|
|
252
|
-
### `getAllNamespaces()` — all namespaces for a locale
|
|
253
|
-
|
|
254
|
-
Useful in build scripts or getStaticProps to prefetch everything at once.
|
|
101
|
+
## i18next integration
|
|
255
102
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// { common: { 'button.save': 'Opslaan' }, nav: { ... }, ... }
|
|
103
|
+
The package ships a native i18next backend plugin as an alternative to
|
|
104
|
+
`i18next-http-backend`. It reads `NEXT_PUBLIC_*` env vars automatically.
|
|
259
105
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
106
|
+
```ts
|
|
107
|
+
// lib/i18n.ts
|
|
108
|
+
import i18next from 'i18next';
|
|
109
|
+
import { initReactI18next } from 'react-i18next';
|
|
110
|
+
import { CMSBackend } from '@translation-cms/sync';
|
|
265
111
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
localDir: './public/locales', // required for auto-refresh
|
|
276
|
-
autoRefresh: true,
|
|
277
|
-
autoRefreshTtlMs: 5 * 60 * 1000, // 5 minutes (default)
|
|
278
|
-
});
|
|
112
|
+
i18next
|
|
113
|
+
.use(CMSBackend)
|
|
114
|
+
.use(initReactI18next)
|
|
115
|
+
.init({
|
|
116
|
+
lng: 'nl',
|
|
117
|
+
fallbackLng: 'en',
|
|
118
|
+
ns: ['common', 'auth'],
|
|
119
|
+
defaultNS: 'common',
|
|
120
|
+
});
|
|
279
121
|
```
|
|
280
122
|
|
|
281
|
-
|
|
123
|
+
Or use `i18next-http-backend` directly — both work with the files `sync pull`
|
|
124
|
+
writes:
|
|
282
125
|
|
|
283
|
-
|
|
126
|
+
```ts
|
|
127
|
+
import HttpBackend from 'i18next-http-backend';
|
|
284
128
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
host or port:
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
const client = new CMSClient({
|
|
291
|
-
// ...
|
|
292
|
-
serverBaseUrl: 'http://localhost:3001',
|
|
129
|
+
i18next.use(HttpBackend).init({
|
|
130
|
+
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
|
|
293
131
|
});
|
|
294
132
|
```
|
|
295
133
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
## Scanner Patterns
|
|
299
|
-
|
|
300
|
-
The CLI scanner recognises four translation key patterns.
|
|
301
|
-
|
|
302
|
-
### Pattern 1: CMS client style
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
const { t } = await client.getTranslation('post');
|
|
306
|
-
const { t } = useTranslation('post');
|
|
307
|
-
t('post:key'); // ✓
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
### Pattern 2: react-i18next style
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
const { t } = useTranslation('namespace');
|
|
314
|
-
t('namespace:key'); // ✓ explicit namespace prefix required
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Pattern 3: `<Trans>` component
|
|
134
|
+
Then in any component:
|
|
318
135
|
|
|
319
136
|
```tsx
|
|
320
|
-
|
|
321
|
-
<Trans i18nKey="blog:post.title" />
|
|
322
|
-
<Trans i18nKey={'blog:post.title'} />
|
|
323
|
-
```
|
|
137
|
+
import { useTranslation } from 'react-i18next';
|
|
324
138
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
const posts = [
|
|
329
|
-
{ titleKey: 'post:first_post.title' }, // ✓
|
|
330
|
-
{ bodyKey: 'post:1.body' }, // ✓
|
|
331
|
-
];
|
|
139
|
+
const { t } = useTranslation('auth');
|
|
140
|
+
t('signIn');
|
|
332
141
|
```
|
|
333
142
|
|
|
334
|
-
**All keys must use `namespace:key` format.** Bare keys (`t('save')`) and
|
|
335
|
-
dot-notation standalones (`"post.1.title"`) are rejected with a warning.
|
|
336
|
-
|
|
337
|
-
### Pattern 5: Per-route preview parameters with `@preview`
|
|
338
|
-
|
|
339
|
-
When a translation key appears on multiple routes or with dynamic parameters,
|
|
340
|
-
use the `@preview` annotation to specify which route(s) and parameter values
|
|
341
|
-
should be used in the live preview.
|
|
342
|
-
|
|
343
|
-
```typescript
|
|
344
|
-
// Single route with parameters
|
|
345
|
-
// @preview /blog/[slug] { "slug": "first-post" }
|
|
346
|
-
t('blog:post.1.title');
|
|
347
|
-
|
|
348
|
-
// Multiple routes (same parameters applied to both)
|
|
349
|
-
// @preview ["/blog/[slug]", "/posts/[slug]"] { "slug": "my-post" }
|
|
350
|
-
t('blog:title');
|
|
351
|
-
|
|
352
|
-
// Route without parameters
|
|
353
|
-
// @preview /about
|
|
354
|
-
t('common:section.title');
|
|
355
|
-
|
|
356
|
-
// Multiple parameters
|
|
357
|
-
// @preview /[locale]/category/[id]/item/[itemId] { "locale": "en", "id": "tech", "itemId": "42" }
|
|
358
|
-
t('catalog:item.name');
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Annotation placement:** Place `@preview` on the line immediately before or on
|
|
362
|
-
the same line as the translation call. Multiple annotations are allowed for the
|
|
363
|
-
same key:
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
// @preview /blog/[slug] { "slug": "post-1" }
|
|
367
|
-
const title1 = t('blog:post.title');
|
|
368
|
-
|
|
369
|
-
// @preview /blog/[slug] { "slug": "post-2" }
|
|
370
|
-
const title2 = t('blog:post.title'); // Same key, different route params
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**Parameter values:** Parameters must be valid JSON and match the dynamic
|
|
374
|
-
segments in your route. They're used by the CMS preview to construct the correct
|
|
375
|
-
URL and pass to your app.
|
|
376
|
-
|
|
377
|
-
**Priority:** Route-specific parameters > global project parameters > fallback.
|
|
378
|
-
Global parameters are configured in the CMS project settings (Preview URL
|
|
379
|
-
section).
|
|
380
|
-
|
|
381
143
|
---
|
|
382
144
|
|
|
383
|
-
##
|
|
384
|
-
|
|
385
|
-
### Why you need this
|
|
386
|
-
|
|
387
|
-
When a translation key is used on **multiple pages with different URL
|
|
388
|
-
patterns**, the preview system needs to know which preview URL to load. Without
|
|
389
|
-
parameters, CMS editors cannot properly preview the content in context.
|
|
390
|
-
|
|
391
|
-
### Example: Blog posts
|
|
392
|
-
|
|
393
|
-
Imagine you have the same blog post translations used across multiple places:
|
|
394
|
-
|
|
395
|
-
```typescript
|
|
396
|
-
// src/app/blog/[slug]/page.tsx
|
|
397
|
-
const { t } = useTranslation('blog');
|
|
398
|
-
|
|
399
|
-
export default function BlogPost({ params: { slug } }) {
|
|
400
|
-
return (
|
|
401
|
-
<article>
|
|
402
|
-
{/* @preview /blog/[slug] { "slug": "my-first-post" } */}
|
|
403
|
-
<h1>{t('blog:post.title')}</h1>
|
|
404
|
-
{/* @preview /blog/[slug] { "slug": "my-first-post" } */}
|
|
405
|
-
<p>{t('blog:post.excerpt')}</p>
|
|
406
|
-
</article>
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### How annotations work
|
|
412
|
-
|
|
413
|
-
The `@preview` annotation tells the sync CLI **which routes and parameters** to
|
|
414
|
-
associate with a translation key. When synced to the CMS, editors can preview
|
|
415
|
-
each key on the specified routes.
|
|
416
|
-
|
|
417
|
-
### Syntax
|
|
418
|
-
|
|
419
|
-
```
|
|
420
|
-
// @preview <route> [{ <json-params> }]
|
|
421
|
-
// @preview [<routes>] [{ <json-params> }]
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
**Single route:**
|
|
425
|
-
|
|
426
|
-
```typescript
|
|
427
|
-
// @preview /contact
|
|
428
|
-
t('contact:form.submit');
|
|
429
|
-
|
|
430
|
-
// @preview /blog/[slug] { "slug": "hello-world" }
|
|
431
|
-
t('blog:post.title');
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
**Multiple routes (optional):**
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
// @preview ["/blog/[slug]", "/news/[slug]"] { "slug": "my-post" }
|
|
438
|
-
t('common:share.text');
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
**No parameters needed:**
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
// @preview /about
|
|
445
|
-
t('about:hero.title');
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### Best practices
|
|
449
|
-
|
|
450
|
-
1. **Place the annotation above or on the same line** as the call that uses the
|
|
451
|
-
key.
|
|
452
|
-
|
|
453
|
-
✓ Good:
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
// @preview /blog/[slug] { "slug": "post-1" }
|
|
457
|
-
t('blog:title');
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
✗ Avoid:
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
t('blog:title');
|
|
464
|
-
// @preview /blog/[slug] { "slug": "post-1" } // Too far away
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
2. **Use representative values** in parameters — these become the default
|
|
468
|
-
preview values editors see.
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
// Use actual example values that make sense for testing
|
|
472
|
-
// @preview /blog/[slug] { "slug": "integration-guide" }
|
|
473
|
-
t('blog:post.title');
|
|
474
|
-
|
|
475
|
-
// Avoid placeholder-like values
|
|
476
|
-
// ✗ @preview /blog/[slug] { "slug": "xyz" }
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
3. **One annotation per key usage** when the same key is used on different
|
|
480
|
-
routes with different parameters.
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// src/components/blog-card.tsx
|
|
484
|
-
// @preview /blog/[slug] { "slug": "post-1" }
|
|
485
|
-
export function BlogCard({ post }) {
|
|
486
|
-
return <h3>{t('blog:post.title')}</h3>
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// src/components/featured-post.tsx
|
|
490
|
-
// @preview /featured/[id] { "id": "top-story" }
|
|
491
|
-
export function FeaturedPost() {
|
|
492
|
-
return <h2>{t('blog:post.title')}</h2>
|
|
493
|
-
}
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
4. **Global project parameters** are a fallback — use `@preview` for exact
|
|
497
|
-
control. You can configure default parameters in the CMS project settings,
|
|
498
|
-
but route-specific `@preview` annotations override them.
|
|
499
|
-
|
|
500
|
-
### Common patterns
|
|
501
|
-
|
|
502
|
-
**Category + item pages:**
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
// @preview /shop/[category]/[itemId] { "category": "electronics", "itemId": "laptop-pro" }
|
|
506
|
-
t('shop:item.price');
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
**Locale in the route:**
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
// @preview /[locale]/blog/[slug] { "locale": "en", "slug": "getting-started" }
|
|
513
|
-
t('blog:meta.description');
|
|
514
|
-
|
|
515
|
-
// Locale placeholders are auto-replaced
|
|
516
|
-
// The system understands [locale], [lang], [language] as special identifiers
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
**No-parameter routes (CMS defaults to `/`):**
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// @preview /
|
|
523
|
-
t('home:hero.headline');
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### Syncing to the CMS
|
|
527
|
-
|
|
528
|
-
After adding annotations, run sync:
|
|
145
|
+
## Server Components (RSC)
|
|
529
146
|
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
The CLI will:
|
|
535
|
-
|
|
536
|
-
1. Scan your code and extract routes + parameters from `@preview` annotations
|
|
537
|
-
2. Show a summary of what changed:
|
|
538
|
-
```
|
|
539
|
-
Found 12 key(s) across 2 namespace(s): blog, common
|
|
540
|
-
Synced: 2 created, 1 routes updated, 9 existing
|
|
541
|
-
```
|
|
542
|
-
3. Upload to the CMS, making routes available for preview
|
|
147
|
+
```ts
|
|
148
|
+
import { getTranslation, getTranslations } from '@translation-cms/sync';
|
|
543
149
|
|
|
544
|
-
|
|
150
|
+
// Type-safe t() for a single namespace
|
|
151
|
+
const { t } = await getTranslation('common', 'nl');
|
|
152
|
+
t('common:nav.home');
|
|
545
153
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
1. Select a translation key
|
|
549
|
-
2. Open the **Preview** panel
|
|
550
|
-
3. You'll see a dropdown of available routes (from your `@preview` annotations)
|
|
551
|
-
4. Click a route to load a live preview iframe
|
|
552
|
-
5. Edit the translation and see changes live
|
|
553
|
-
|
|
554
|
-
---
|
|
555
|
-
|
|
556
|
-
### Configurable scan options
|
|
557
|
-
|
|
558
|
-
Via `.translationsrc.json`:
|
|
559
|
-
|
|
560
|
-
```json
|
|
561
|
-
{
|
|
562
|
-
"excludedDirs": ["e2e", "test-fixtures"],
|
|
563
|
-
"sourceExtensions": [".ts", ".tsx", ".js", ".jsx", ".vue"],
|
|
564
|
-
"reservedCssNamespaces": ["after", "before", "placeholder"]
|
|
565
|
-
}
|
|
154
|
+
// Raw translations for passing as props
|
|
155
|
+
const translations = await getTranslations(['common', 'auth'], 'nl');
|
|
566
156
|
```
|
|
567
157
|
|
|
568
158
|
---
|
|
569
159
|
|
|
570
160
|
## Preview Listener
|
|
571
161
|
|
|
572
|
-
Enable live in-context preview
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
— links, buttons, and forms are disabled so the editor can focus on editing
|
|
576
|
-
content without accidentally navigating away.
|
|
577
|
-
|
|
578
|
-
### Setup in Next.js
|
|
162
|
+
Enable live in-context preview. When an editor selects a key in the CMS, the
|
|
163
|
+
matching element in your app (loaded in an iframe) is highlighted. Navigation
|
|
164
|
+
and interactions are blocked during preview.
|
|
579
165
|
|
|
580
166
|
```tsx
|
|
581
167
|
'use client';
|
|
582
168
|
import { useEffect } from 'react';
|
|
583
|
-
import { initPreviewListener } from '@translation-cms/sync
|
|
169
|
+
import { initPreviewListener } from '@translation-cms/sync';
|
|
584
170
|
|
|
585
171
|
export function CMSPreview() {
|
|
586
172
|
useEffect(() => {
|
|
@@ -594,91 +180,91 @@ export function CMSPreview() {
|
|
|
594
180
|
|
|
595
181
|
Add `<CMSPreview />` to your root layout.
|
|
596
182
|
|
|
597
|
-
###
|
|
183
|
+
### `data-cms-key`
|
|
598
184
|
|
|
599
|
-
|
|
600
|
-
precise highlighting — especially useful with interpolated values or split
|
|
601
|
-
siblings — add a `data-cms-key` attribute to the element that renders the
|
|
602
|
-
translation:
|
|
185
|
+
For precise element targeting — useful with interpolated values:
|
|
603
186
|
|
|
604
187
|
```tsx
|
|
605
|
-
<h1 data-cms-key="blog:post.title">
|
|
606
|
-
|
|
607
|
-
</h1>
|
|
608
|
-
|
|
609
|
-
<p data-cms-key="common:welcome">
|
|
610
|
-
{t('common:welcome', { name })}
|
|
611
|
-
</p>
|
|
188
|
+
<h1 data-cms-key="blog:post.title">{t('blog:post.title')}</h1>
|
|
189
|
+
<p data-cms-key="common:welcome">{t('common:welcome', { name })}</p>
|
|
612
190
|
```
|
|
613
191
|
|
|
614
|
-
|
|
192
|
+
### Locale switching
|
|
615
193
|
|
|
616
|
-
|
|
194
|
+
```ts
|
|
195
|
+
initPreviewListener({
|
|
196
|
+
onLocaleSwitch: locale => i18n.changeLanguage(locale),
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Custom highlight styles
|
|
617
201
|
|
|
618
|
-
```
|
|
202
|
+
```ts
|
|
619
203
|
initPreviewListener({
|
|
620
204
|
highlightStyles: {
|
|
621
205
|
outline: '3px solid #3b82f6',
|
|
622
206
|
outlineOffset: '4px',
|
|
623
|
-
borderRadius: '8px',
|
|
624
207
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
625
208
|
},
|
|
626
209
|
});
|
|
627
210
|
```
|
|
628
211
|
|
|
629
|
-
###
|
|
212
|
+
### Cleanup
|
|
630
213
|
|
|
631
|
-
|
|
632
|
-
|
|
214
|
+
```ts
|
|
215
|
+
import { cleanupPreviewListener } from '@translation-cms/sync';
|
|
216
|
+
cleanupPreviewListener();
|
|
217
|
+
```
|
|
633
218
|
|
|
634
|
-
|
|
635
|
-
// react-i18next
|
|
636
|
-
initPreviewListener({
|
|
637
|
-
onLocaleSwitch: locale => i18n.changeLanguage(locale),
|
|
638
|
-
});
|
|
219
|
+
---
|
|
639
220
|
|
|
640
|
-
|
|
641
|
-
window.__cmsSetLocale = locale => i18n.changeLanguage(locale);
|
|
642
|
-
```
|
|
221
|
+
## Scanner
|
|
643
222
|
|
|
644
|
-
|
|
223
|
+
The CLI scans your source files for translation keys. Recognised patterns:
|
|
645
224
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
4. Element is highlighted and scrolled into view
|
|
651
|
-
5. Edits in the CMS input are reflected live in the preview (no reload)
|
|
652
|
-
6. A transparent overlay and capture-phase event listeners block all clicks,
|
|
653
|
-
link navigation, and form submits for the duration of the preview session
|
|
225
|
+
```ts
|
|
226
|
+
// react-i18next
|
|
227
|
+
const { t } = useTranslation('blog');
|
|
228
|
+
t('blog:post.title');
|
|
654
229
|
|
|
655
|
-
|
|
656
|
-
|
|
230
|
+
// <Trans> component
|
|
231
|
+
<Trans i18nKey="blog:post.title" />
|
|
657
232
|
|
|
658
|
-
|
|
233
|
+
// CMS client (server-side)
|
|
234
|
+
const { t } = await client.getTranslation('blog');
|
|
235
|
+
t('blog:post.title');
|
|
659
236
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
cleanupPreviewListener(); // removes overlay, unblocks interactions, clears highlights
|
|
237
|
+
// Standalone string literals
|
|
238
|
+
const key = 'blog:post.title';
|
|
663
239
|
```
|
|
664
240
|
|
|
665
|
-
|
|
241
|
+
All keys must use `namespace:key` format. Bare keys (`t('save')`) produce a
|
|
242
|
+
warning.
|
|
666
243
|
|
|
667
|
-
|
|
244
|
+
### `@preview` annotations
|
|
668
245
|
|
|
669
|
-
|
|
246
|
+
Associate a key with a specific route for CMS live preview:
|
|
670
247
|
|
|
671
|
-
```
|
|
672
|
-
[
|
|
673
|
-
|
|
674
|
-
|
|
248
|
+
```ts
|
|
249
|
+
// @preview /blog/[slug] { "slug": "first-post" }
|
|
250
|
+
t('blog:post.title');
|
|
251
|
+
|
|
252
|
+
// Multiple routes
|
|
253
|
+
// @preview ["/blog/[slug]", "/news/[slug]"] { "slug": "my-post" }
|
|
254
|
+
t('common:share.text');
|
|
675
255
|
```
|
|
676
256
|
|
|
677
|
-
|
|
257
|
+
Place the annotation on the line immediately before the call. Parameters must be
|
|
258
|
+
valid JSON matching the dynamic segments in the route.
|
|
678
259
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
260
|
+
### Scan options (`.translationsrc.json`)
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"excludedDirs": ["e2e", "fixtures"],
|
|
265
|
+
"sourceExtensions": [".ts", ".tsx", ".js", ".jsx", ".vue"],
|
|
266
|
+
"reservedCssNamespaces": ["after", "before"]
|
|
267
|
+
}
|
|
682
268
|
```
|
|
683
269
|
|
|
684
270
|
---
|