@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.
Files changed (63) hide show
  1. package/README.md +131 -545
  2. package/dist/cli-entry.d.ts +3 -0
  3. package/dist/cli-entry.d.ts.map +1 -0
  4. package/dist/cli-entry.js +268 -0
  5. package/dist/cli-entry.js.map +1 -0
  6. package/dist/cli.d.ts +15 -2
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +11 -263
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +26 -25
  12. package/dist/client.js.map +1 -1
  13. package/dist/i18next.d.ts +62 -0
  14. package/dist/i18next.d.ts.map +1 -0
  15. package/dist/i18next.js +127 -0
  16. package/dist/i18next.js.map +1 -0
  17. package/dist/index.d.ts +20 -6
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +30 -15
  20. package/dist/index.js.map +1 -1
  21. package/dist/{config.d.ts → load-config.d.ts} +1 -1
  22. package/dist/load-config.d.ts.map +1 -0
  23. package/dist/{config.js → load-config.js} +1 -1
  24. package/dist/load-config.js.map +1 -0
  25. package/dist/{preview-listener.d.ts → preview.d.ts} +1 -1
  26. package/dist/preview.d.ts.map +1 -0
  27. package/dist/{preview-listener.js → preview.js} +1 -1
  28. package/dist/preview.js.map +1 -0
  29. package/dist/{cli-config.d.ts → resolve-config.d.ts} +2 -2
  30. package/dist/resolve-config.d.ts.map +1 -0
  31. package/dist/{cli-config.js → resolve-config.js} +1 -1
  32. package/dist/resolve-config.js.map +1 -0
  33. package/dist/sync.d.ts +1 -1
  34. package/dist/sync.d.ts.map +1 -1
  35. package/dist/sync.js +27 -13
  36. package/dist/sync.js.map +1 -1
  37. package/package.json +9 -17
  38. package/dist/cli-config.d.ts.map +0 -1
  39. package/dist/cli-config.js.map +0 -1
  40. package/dist/cli-utils.d.ts +0 -16
  41. package/dist/cli-utils.d.ts.map +0 -1
  42. package/dist/cli-utils.js +0 -16
  43. package/dist/cli-utils.js.map +0 -1
  44. package/dist/config.d.ts.map +0 -1
  45. package/dist/config.js.map +0 -1
  46. package/dist/preview-listener.d.ts.map +0 -1
  47. package/dist/preview-listener.js.map +0 -1
  48. package/dist/trans.d.ts +0 -2
  49. package/dist/trans.d.ts.map +0 -1
  50. package/dist/trans.js +0 -3
  51. package/dist/trans.js.map +0 -1
  52. package/dist/translation-context.d.ts +0 -10
  53. package/dist/translation-context.d.ts.map +0 -1
  54. package/dist/translation-context.js +0 -4
  55. package/dist/translation-context.js.map +0 -1
  56. package/dist/translation-provider.d.ts +0 -15
  57. package/dist/translation-provider.d.ts.map +0 -1
  58. package/dist/translation-provider.js +0 -97
  59. package/dist/translation-provider.js.map +0 -1
  60. package/dist/use-translation.d.ts +0 -35
  61. package/dist/use-translation.d.ts.map +0 -1
  62. package/dist/use-translation.js +0 -110
  63. package/dist/use-translation.js.map +0 -1
package/README.md CHANGED
@@ -1,28 +1,17 @@
1
- # React TranslationProvider
1
+ # @translation-cms/sync
2
2
 
3
- Wrap je app of componenten met de TranslationProvider om i18n context te
4
- leveren:
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
- ```tsx
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
- De provider initialiseert i18n en zorgt dat de context beschikbaar is voor alle
19
- onderliggende componenten.
20
-
21
- # @translation-cms/sync
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
- Automatically scan translation keys in your codebase and sync them to the
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
- ### Option A — `.env.local`
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
- ### Option B — `.translationsrc.json`
34
+ ### `.translationsrc.json` (optional)
50
35
 
51
- Create a `translations.config.json` (or `.translationsrc.json`) in your project
52
- root. Values here are merged with env vars — CLI args take highest priority.
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
- "namespace:key": [
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
- Use `previewRoutes` to define default preview routes per translation key. Each
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 Usage
113
-
114
- ### Subcommands
56
+ ## CLI
115
57
 
116
58
  ```bash
117
- # Full sync: scan keys + push to CMS + pull translations locally
118
- pnpm sync-translations
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
- # Write a JSON report of all discovered keys
127
- pnpm sync-translations sync --report ./scan-report.json
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
- # Only pull translations (respects TTL cache)
130
- pnpm sync-translations pull
69
+ # Preview changes without touching the CMS
70
+ sync-translations sync --dry-run
131
71
 
132
- # Pull with options
133
- pnpm sync-translations pull --force # ignore TTL
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
- # Show diff vs last sync without touching the CMS
139
- pnpm sync-translations status
75
+ # Watch + auto-sync on file save
76
+ sync-translations watch
140
77
 
141
- # Watch for file changes and auto-sync on save
142
- pnpm sync-translations watch
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
- ## CMS Client
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
- ```typescript
257
- const all = await client.getAllNamespaces('nl');
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
- // Uses defaultLocale when omitted
261
- const all = await client.getAllNamespaces();
262
- ```
263
-
264
- ### Auto-refresh caching (server-side)
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
- When `autoRefresh` is enabled the client uses a **stale-while-revalidate**
267
- strategy: if local files exist but are older than the TTL, they are returned
268
- immediately while a background refresh updates them for the next request. Only
269
- the very first request (no local files yet) blocks.
270
-
271
- ```typescript
272
- const client = new CMSClient({
273
- cmsUrl: 'https://cms.example.com',
274
- projectId: 'abc123',
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
- Only runs server-side (Node.js). Browser requests skip auto-refresh entirely.
123
+ Or use `i18next-http-backend` directly both work with the files `sync pull`
124
+ writes:
282
125
 
283
- ### `serverBaseUrl` — custom SSR base URL
126
+ ```ts
127
+ import HttpBackend from 'i18next-http-backend';
284
128
 
285
- When building local file URLs server-side the client defaults to
286
- `http://localhost:${PORT}`. Override this if your dev server runs on a different
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
- // Both JSX attribute formats are supported
321
- <Trans i18nKey="blog:post.title" />
322
- <Trans i18nKey={'blog:post.title'} />
323
- ```
137
+ import { useTranslation } from 'react-i18next';
324
138
 
325
- ### Pattern 4: Standalone `namespace:key` string literals
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
- ## Using Preview Routes with Parameters
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
- ```bash
531
- pnpm sync-translations sync
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
- ### Verifying in the CMS
150
+ // Type-safe t() for a single namespace
151
+ const { t } = await getTranslation('common', 'nl');
152
+ t('common:nav.home');
545
153
 
546
- In the CMS editor:
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 of translations. When an editor selects a key in
573
- the CMS, the listener highlights the matching element in your app (loaded in an
574
- iframe). While in preview mode, **all navigation and interactions are blocked**
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/preview';
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
- ### Precise highlighting with `data-cms-key`
183
+ ### `data-cms-key`
598
184
 
599
- By default the listener finds elements by matching text content. For more
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
- {t('blog:post.title')}
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
- When the attribute is present, text-search heuristics are skipped entirely.
192
+ ### Locale switching
615
193
 
616
- ### Custom styling
194
+ ```ts
195
+ initPreviewListener({
196
+ onLocaleSwitch: locale => i18n.changeLanguage(locale),
197
+ });
198
+ ```
199
+
200
+ ### Custom highlight styles
617
201
 
618
- ```typescript
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
- ### Locale switching
212
+ ### Cleanup
630
213
 
631
- The CMS can request a locale change without a page reload. Wire it up with your
632
- i18n library:
214
+ ```ts
215
+ import { cleanupPreviewListener } from '@translation-cms/sync';
216
+ cleanupPreviewListener();
217
+ ```
633
218
 
634
- ```typescript
635
- // react-i18next
636
- initPreviewListener({
637
- onLocaleSwitch: locale => i18n.changeLanguage(locale),
638
- });
219
+ ---
639
220
 
640
- // Or set a global handler (useful when initPreviewListener is called elsewhere)
641
- window.__cmsSetLocale = locale => i18n.changeLanguage(locale);
642
- ```
221
+ ## Scanner
643
222
 
644
- ### How it works
223
+ The CLI scans your source files for translation keys. Recognised patterns:
645
224
 
646
- 1. Editor opens the preview panel in the CMS for a translation key
647
- 2. CMS sends a `postMessage` to your app (opened in an iframe)
648
- 3. The listener finds the element — via `data-cms-key` attribute first, falling
649
- back to text-content search
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
- The listener only activates when `hostname` is `localhost` / `127.0.0.1` or the
656
- URL contains `?preview=true`. Production is never affected.
230
+ // <Trans> component
231
+ <Trans i18nKey="blog:post.title" />
657
232
 
658
- ### Cleanup
233
+ // CMS client (server-side)
234
+ const { t } = await client.getTranslation('blog');
235
+ t('blog:post.title');
659
236
 
660
- ```typescript
661
- import { cleanupPreviewListener } from '@translation-cms/sync/preview';
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
- ## Logging
244
+ ### `@preview` annotations
668
245
 
669
- All operations log with a `[CMS]` prefix:
246
+ Associate a key with a specific route for CMS live preview:
670
247
 
671
- ```
672
- [CMS] Translations fresh (age: 45s < TTL: 300s) — using local files
673
- [CMS] ⟳ Stale — serving cached, refreshing in background...
674
- [CMS] ✓ Refresh complete: wrote 4 JSON files, updated .last-pulled
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
- Scanner warnings:
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
- [WARN] Bare key in src/page.tsx: t('save') — use t('namespace:save') instead
681
- [WARN] Standalone key in src/data.ts: "post.1.title" — use "namespace:key" format
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
  ---