@translation-cms/sync
v1.2.41
Published
Scan translation keys in your codebase and sync them to the Translations CMS
Readme
@translation-cms/sync
Automatically scan translation keys from your codebase, sync with the Translations CMS, and fetch translations as local JSON files. Built for Next.js + i18next.
Integration Methods
Choose the approach that fits your workflow:
- Next.js Plugin (Recommended) — Automatic sync on
next devandnext build - CLI (Manual) —
pnpm sync-translationsfor on-demand control - Programmatic API — Call sync/pull directly in your code or scripts
Method 1: Next.js Plugin (Automatic)
Edit next.config.ts:
import withTranslationsCMS from '@translation-cms/sync/next';
export default withTranslationsCMS(
{
// your Next.js config
},
{
pullOnBuild: true, // auto pull on `next build`
pullOnDev: true, // auto pull on `next dev` startup
watchInterval: 10000, // poll CMS for updates every 10s in dev (0 to disable)
}
);Your translations sync automatically — and in dev mode, the JSON files are re-pulled automatically whenever you publish new translations in the CMS. No extra commands needed.
Method 2: CLI (Manual)
# Scan code + upload to CMS
pnpm sync-translations sync
# Download translations
pnpm sync-translations pull
# Watch mode — auto-sync on file changes
pnpm sync-translations watch
# Interactive setup
pnpm sync-translations initMethod 3: Programmatic API
Use in scripts, build tools, or a custom next.config.ts:
import type { NextConfig } from 'next';
import {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_BUILD,
} from 'next/constants';
import {
syncTranslations,
pullTranslations,
watchTranslations,
} from '@translation-cms/sync/api';
const pathMappings = { '@/*': ['src/*'] }; // mirror your tsconfig paths
export default async function config(phase: string): Promise<NextConfig> {
if (phase === PHASE_DEVELOPMENT_SERVER) {
// Scan keys and upload, then pull translations
await syncTranslations({
projectRoot: '.',
pathMappings,
verbose: true,
});
await pullTranslations({ projectRoot: '.', verbose: true });
// Poll the CMS in the background — re-pulls whenever you publish
watchTranslations({ projectRoot: '.' });
}
if (phase === PHASE_PRODUCTION_BUILD) {
await syncTranslations({ projectRoot: '.', pathMappings });
await pullTranslations({ projectRoot: '.' });
}
return {
// ... your Next.js config
};
}Or use syncAndPull in scripts / CI:
import { syncAndPull } from '@translation-cms/sync/api';
await syncAndPull({ projectRoot: './my-app' });Choose Your Method
| Method | When to Use | Setup Time | | -------------------- | --------------------------------- | ---------- | | Next.js Plugin | Team projects, automatic workflow | 5 min | | CLI | Manual control, debugging, CI/CD | 10 min | | Programmatic API | Custom workflows, scripts | 5 min |
Recommended: Start with the Next.js Plugin for automatic updates during development, then add CLI or API commands as needed.
What is this?
Hybrid translation management system with three integration methods:
- Scans your code for translation keys (React-i18next pattern)
- Syncs new keys to the Translations CMS
- Fetches translations as JSON files
- Automatic or manual — choose what works for your team
- Programmatic API — bring your own CI/CD or tooling
Your Code → scan → CMS → pull → JSON files → i18next → Your AppProgrammatic API Reference
Use these functions when you need fine-grained control or integration with custom workflows. Perfect for CI/CD pipelines, build tools, or custom scripts.
syncTranslations(options?)
Scans your codebase for translation keys and uploads them to the CMS.
import { syncTranslations } from '@translation-cms/sync/api';
const result = await syncTranslations({
projectRoot: './apps/web', // auto-detect if omitted
dryRun: false, // preview without uploading
force: false, // ignore cache
reportPath: './sync-report.json', // optional report output
verbose: true, // detailed logging
});
console.log(`Added ${result.keysAdded} keys`);pullTranslations(options?)
Fetches translations from the CMS and writes them as JSON files.
import { pullTranslations } from '@translation-cms/sync/api';
const result = await pullTranslations({
projectRoot: './apps/web', // auto-detect if omitted
outputDir: './src/i18n/locales', // custom output location
force: false, // ignore cache
ttl: 300000, // cache duration in ms
environment: 'production', // pull from specific env
verbose: true, // detailed logging
});watchTranslations(options?)
Starts a polling loop that detects CMS publishes and re-pulls automatically.
Non-blocking — call it after pullTranslations and it runs in the background.
import { watchTranslations } from '@translation-cms/sync/api';
watchTranslations({
projectRoot: './apps/web', // auto-detect if omitted
outputDir: './src/i18n/locales', // must match pull outputDir
interval: 10000, // poll every 10s (default)
});When you publish translations in the CMS, the JSON files are updated within
interval milliseconds — no manual pull needed.
syncAndPull(options?)
Convenience function: sync keys then pull translations in one call.
import { syncAndPull } from '@translation-cms/sync/api';
const { synced, pulled } = await syncAndPull({
projectRoot: './apps/web',
verbose: true,
});Setup Guide — From Zero to Working
Step 1: Prerequisites
Check what you need:
- Next.js project (v14+)
- pnpm (recommended) or npm
- Translations CMS account + project created
- CMS project credentials (URL, project ID, API key)
Step 2: Installation
pnpm add @translation-cms/syncPlus the peer dependencies:
pnpm add i18next react-i18next i18next-resources-to-backendStep 3: Set Up CMS Credentials
Create .env.local in your project root:
NEXT_PUBLIC_CMS_URL=https://cms.example.com
NEXT_PUBLIC_CMS_PROJECT_ID=your-project-id
CMS_SYNC_API_KEY=your-jwt-api-keyThe CMS_SYNC_API_KEY is a JWT token. You can find it in the CMS under
Project Settings → Environments → your environment → API Key. If the key
there still looks like a UUID, use "Regenerate" to generate a JWT.
Note: use
CMS_SYNC_API_KEY(withoutNEXT_PUBLIC_). This prevents the JWT from accidentally ending up in the browser. The old nameNEXT_PUBLIC_CMS_ANON_KEYstill works as a fallback, but is discouraged.
Step 4: Automatic Setup (Recommended)
Run the interactive setup wizard:
pnpm sync-translations initThis automatically creates:
src/lib/i18n/
├── settings.ts # i18next configuration
├── types.ts # TypeScript types for translations
├── client.ts # 'use client' hook + static imports
├── server.ts # Server component support
├── provider.tsx # Provider wrapper
└── dictionaries/
├── en.json # English (empty, filled by pull)
└── nl.json # Dutch (empty, filled by pull)Also .translationsrc.json is generated:
{
"outputDir": "./src/lib/i18n/dictionaries",
"pullTtlMs": 300000,
"excludedDirs": ["e2e", "node_modules", "fixtures"],
"routeParams": {}
}Note: routeParams can stay empty! The scanner automatically generates
sensible defaults for all dynamic routes.
Step 5: First Sync — Upload Keys
Scan your code and upload detected keys to the CMS:
pnpm sync-translations syncThis will:
- Scan your code for translation keys (
t('namespace:key')pattern) - Detect routes where each key is used
- Upload new keys to CMS
- Show report (which keys added/changed)
Check in the CMS if your keys appeared.
On first sync: you might get an error that no translations can be downloaded
— this is normal! The keys were just uploaded but don't have translations yet.
Go to your CMS, fill in translations, then run pull again.
Step 5b: Add Translations in CMS
- Open your Translations CMS project
- Your keys are now listed (without translations)
- Fill in translations per locale (English, Dutch, etc.)
- Click Save
Step 6: Fetch Translations
If you've done the first sync AND filled in translations in the CMS:
pnpm sync-translations pullThis fetches translations from the CMS and writes them to local JSON:
dictionaries/en.json→ English translationsdictionaries/nl.json→ Dutch translations- etc. (one per locale)
Step 7: Integration in Your Project
A. Root Layout Setup
Add the provider to your src/app/layout.tsx:
import { TranslationProvider } from '@/lib/i18n/provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<TranslationProvider>{children}</TranslationProvider>
</body>
</html>
);
}B. Server Components (Recommended)
import { getTranslation } from '@/lib/i18n/server';
export default async function Page() {
const { t } = await getTranslation('common');
return (
<div>
<h1>{t('common:app.title')}</h1>
<p>{t('common:app.description')}</p>
</div>
);
}C. Client Components
'use client';
import { useTranslation } from '@/lib/i18n/client';
export function MyButton() {
const { t } = useTranslation('common');
return <button>{t('common:button.click')}</button>;
}D. Multiple Namespaces (With Type-Checking)
const { t } = useTranslation(['common', 'auth']);
// This works
t('common:nav.home');
t('auth:login.email');
// This gives a type error
t('payments:amount'); // namespace not addedStep 8: Automatic Sync in Your Workflow
Add this to package.json:
{
"scripts": {
"dev": "sync-translations pull && next dev",
"build": "sync-translations pull && next build"
}
}Now you automatically pull the latest translations on startup.
Step 9 (Optional): Set Up Preview Mode
Enable live in-context preview — editors can live see how their text looks in your app.
Add this to your root layout:
'use client';
import { useEffect } from 'react';
import { initPreviewListener } from '@translation-cms/sync';
export function CMSPreview() {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
initPreviewListener({
onLocaleSwitch: locale => {
// change language if CMS selects a different locale
window.location.href = `/${locale}`;
},
});
}
}, []);
return null;
}And add to layout:
<TranslationProvider>
<CMSPreview />
{children}
</TranslationProvider>Add data-cms-key to elements for precise highlighting:
<h1 data-cms-key="common:page.title">{t('common:page.title')}</h1>CLI Commands Reference
Basic Commands
# Scan code + upload to CMS
pnpm sync-translations sync
# Download translations to local JSON
pnpm sync-translations pull
# See what would change (without upload)
pnpm sync-translations sync --dry-run
# See difference from previous sync
pnpm sync-translations status
# Watch mode — auto-sync on file changes
pnpm sync-translations watch
# Interactive setup
pnpm sync-translations initAdvanced Flags
| Flag | Description |
| ----------------- | ---------------------------------------- |
| --dry-run | Show changes, don't execute |
| --force | Ignore cache, force refresh |
| --output <dir> | Custom output directory for JSON |
| --env <name> | Pull from staging/production environment |
| --ttl <ms> | Override cache TTL (ms) |
| --project-id | Override project ID |
| --api-key | Override API key |
| --cms-url | Override CMS URL |
| --report <file> | Write sync report to JSON file |
Examples
# Force refresh, ignore cache
pnpm sync-translations pull --force
# Custom output directory
pnpm sync-translations pull --output ./locales
# Pull from staging environment
pnpm sync-translations pull --env staging
# Write report of changes
pnpm sync-translations sync --report ./sync-report.jsonKey Scanner — How It Works
The tool recognizes these patterns in your code:
// React-i18next hook
const { t } = useTranslation('blog');
t('blog:post.title');
// Server-side helper
const { t } = await getTranslation('blog');
t('blog:post.title');
// CMS client helper
const t = await client.getTranslations(locale, 'blog');
t('blog:post.title');
// Trans component
<Trans i18nKey="blog:post.title" />
// I18nKey-typed config (only if the file imports `I18nKey`)
import type { I18nKey } from '@/lib/i18n/types';
const label: I18nKey = 'sidebar:settings';
// Object properties ending in "Key"
const config = { titleKey: 'blog:post.title' };Format: namespace:key
All keys must follow this format. Otherwise you'll get a warning:
// Good
t('common:button.save');
// Wrong — namespace missing
t('save');Dynamic Routes — Auto-Generated routeParams
How it works:
At each pnpm sync-translations sync:
- Scanner automatically detects all routes with dynamic parameters (e.g.
/[locale]/blog/[slug]) - For each
[param]a sensible default is automatically generated:locale/lang→"en"id,postId,productId→"123"slug→"demo"username→"demo-user"email→"[email protected]"
- This is saved in
.cms-sync-cache-meta.json(you can ignore it) - The CMS uses this to generate working preview URLs
You don't have to do anything! — everything works out-of-the-box.
Manual override (optional):
If you want to use different test values, fill in .translationsrc.json:
{
"routeParams": {
"/[locale]/products/[id]": { "id": "prod-999" },
"/[locale]/blog/[slug]": { "slug": "my-custom-post" }
}
}This overrides the auto-generated values. Everything you add here takes priority.
Cache Files
At each sync two cache files are created:
.cms-sync-cache.json— Uploaded keys and their routes (for diff on next sync).cms-sync-cache-meta.json— Auto-generated route params (you can add to .gitignore)
Scanner Options
Customize scan behavior in .translationsrc.json:
{
"excludedDirs": ["e2e", "node_modules", "fixtures"],
"sourceExtensions": [".ts", ".tsx", ".js", ".jsx"],
"reservedCssNamespaces": ["after", "before"]
}Advanced Features
Preview Mode — Live Highlighting
Editors can click "Show in app" in the CMS and your app opens in an iframe with live highlighting of the element.
Setup
'use client';
import { initPreviewListener } from '@translation-cms/sync';
useEffect(() => {
initPreviewListener({
highlightStyles: {
outline: '3px solid #3b82f6',
outlineOffset: '2px',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
});
}, []);Element Targeting
Use data-cms-key for precise targeting:
<h1 data-cms-key="blog:title">{t('blog:title')}</h1>
<p data-cms-key="blog:excerpt">{t('blog:excerpt')}</p>JSON Output Format
After sync-translations pull these kinds of files are generated:
{
"common": {
"app.title": "My App",
"button.save": "Save",
"nav.home": "Home"
},
"auth": {
"login.email": "Email",
"login.password": "Password"
}
}Structure:
- Top-level keys = namespaces
- Nested keys = translation strings
- 1 file per language (
en.json,nl.json, etc.)
Troubleshooting
No keys found
# Check scanner configuration
pnpm sync-translations sync --dry-runMake sure your keys use namespace:key format.
Environment variables not found
Check .env.local:
grep -E 'CMS_URL|CMS_PROJECT_ID|CMS_SYNC_API_KEY' .env.localMust contain NEXT_PUBLIC_CMS_URL, NEXT_PUBLIC_CMS_PROJECT_ID and
CMS_SYNC_API_KEY. The value of CMS_SYNC_API_KEY is a JWT token — get it from
the CMS Project Settings.
Pull doesn't work
# Force refresh, ignore cache
pnpm sync-translations pull --force
# Check configuration
cat .translationsrc.jsonType errors in TypeScript
Make sure src/lib/i18n/types.ts is generated correctly and that your
namespaces are in .translationsrc.json.
Best Practices
- Use server components where possible — better for performance
- Add
data-cms-keyon visually important elements - Commit
.translationsrc.jsonto git — not.env.local - Ignore cache files: add to
.gitignore:.cms-sync-cache.json .cms-sync-cache-meta.json .last-pulled - Run
syncin CI/CD on main branch — catch missing translations - Add
pullto build script — always fresh translations
License
MIT
