livelocale 0.0.0-stage โ 0.8.0
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/LICENSE +21 -0
- package/README.md +532 -2
- package/dist/client/index.cjs +853 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +109 -0
- package/dist/client/index.d.ts +109 -0
- package/dist/client/index.js +818 -0
- package/dist/client/index.js.map +1 -0
- package/dist/codemod/index.cjs +2060 -0
- package/dist/codemod/index.cjs.map +1 -0
- package/dist/codemod/index.d.cts +1 -0
- package/dist/codemod/index.d.ts +1 -0
- package/dist/codemod/index.js +2038 -0
- package/dist/codemod/index.js.map +1 -0
- package/dist/react/index.cjs +911 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +10 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +893 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.cjs +2434 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +362 -0
- package/dist/server/index.d.ts +362 -0
- package/dist/server/index.js +2381 -0
- package/dist/server/index.js.map +1 -0
- package/dist/studio/assets/index-D7ifnqy5.js +10 -0
- package/dist/studio/assets/index-Zh0KtFUH.css +2 -0
- package/dist/studio/index.html +17 -0
- package/dist/vite/index.cjs +1696 -0
- package/dist/vite/index.cjs.map +1 -0
- package/dist/vite/index.d.cts +28 -0
- package/dist/vite/index.d.ts +28 -0
- package/dist/vite/index.js +1661 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/wrap-CsOtHp8r.d.cts +98 -0
- package/dist/wrap-CsOtHp8r.d.ts +98 -0
- package/package.json +136 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,533 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LiveLocale
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
|
+
<p><strong>Click any text in your app. Fix the translation. Ship it as a pull request.</strong></p>
|
|
5
|
+
<p>Keep next-intl or react-i18next. LiveLocale adds in-context editing for Next.js and React + Vite: no account, no server, no browser extension. Your translations stay as JSON in git.</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://github.com/Araby99/livelocale/actions/workflows/ci.yml"><img src="https://github.com/Araby99/livelocale/actions/workflows/ci.yml/badge.svg" alt="CI Status" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/livelocale"><img src="https://img.shields.io/npm/v/livelocale.svg?style=flat-square" alt="NPM Version" /></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/livelocale"><img src="https://img.shields.io/npm/dm/livelocale.svg?style=flat-square" alt="NPM Downloads" /></a>
|
|
11
|
+
<a href="https://github.com/Araby99/livelocale/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/livelocale.svg?style=flat-square" alt="License" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## ๐ก How It Works: Zero-Width Unicode Watermarking
|
|
18
|
+
|
|
19
|
+
`livelocale` bridges your running application UI with your local JSON translation files using **Zero-Width Unicode Watermarking**.
|
|
20
|
+
|
|
21
|
+
1. **Watermark Encoding:** In development, when a translation key is looked up, the library prepends an invisible Unicode watermark (using combinations of `\u200D` (ZWJ), `\u200B` (ZWS), and `\u200C` (ZWNJ)) to the text.
|
|
22
|
+
2. **DOM Scanning:** A lightweight dev-only browser listener scans the active DOM, decodes the watermarks, and dynamically hooks visual highlights onto translated elements.
|
|
23
|
+
3. **Input & Clipboard Sanitization:** To ensure the watermarks never pollute your database, forms, or clipboard, the library automatically intercepts `'copy'`/`'paste'` events and patches controlled input elements at runtime.
|
|
24
|
+
4. **Zero Production Footprint:** In production mode, all interceptors, listeners, and wrapper logic are bypassed completely, returning plain strings without overhead.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## โจ Features
|
|
29
|
+
|
|
30
|
+
- ๐ฏ **In-Context Visual Editing:** Hover over any translated text to highlight it; click to instantly edit in the visual panel.
|
|
31
|
+
- ๐ด **Missing Key Indicators:** Identifies translation keys that are missing in the active locale but exist in the codebase or other translation files, highlighting them with red indicators.
|
|
32
|
+
- ๐ **Smart Fallback Placeholders:** Automatically renders translation placeholders from alternative languages for missing keys in development, keeping them visible and fully click-to-edit inline.
|
|
33
|
+
- ๐ **Namespaced Folder Support:** Handles nested multi-file folder layouts (e.g., `locales/en/auth.json`) natively, merging files during load and separating them during mutation.
|
|
34
|
+
- ๐งผ **Input & Form Sanitization:** Patches controlled inputs and intercepts clipboard actions to strip watermarks in development automatically.
|
|
35
|
+
- โก **Atomic File Operations:** Prevents Next.js hot module replacement (HMR) reading corrupt half-written files via atomic temp-to-final writes.
|
|
36
|
+
- ๐ **Secure Local Boundaries:** Restricts directory traversal, verifies origins, and supports deep schemas up to 30 levels of recursion.
|
|
37
|
+
- ๐ค **CLI Migration Tool:** Automatically parses and wraps standard `react-i18next` hooks with a single command.
|
|
38
|
+
- ๐ข **Plural forms editor:** ICU `plural` / `select` messages are edited one branch at a time, with the categories your locale needs (Arabic gets all six) pre-filled from CLDR rules.
|
|
39
|
+
- ๐งญ **Namespace-aware key scanning:** `useTranslations('home')`, `getTranslations`, `useTranslation('auth')`, and the wrap helpers are understood, so the Locale Directory shows `home.title`, not a false "missing" `title`.
|
|
40
|
+
- ๐งฉ **Placeholder safety:** Saves that drop or invent a `{placeholder}`, `{{interpolation}}`, `%s`, or `<tag>` are held back with a clear warning, and broken ICU plural/select syntax is rejected, unless you choose Save anyway.
|
|
41
|
+
- ๐ **Preview mode for non-developers:** Run a token-protected Studio against a preview deployment; every save becomes a commit on a GitHub branch and one click opens the pull request.
|
|
42
|
+
- ๐งพ **Review, commit, undo:** The Studio's Changes panel shows git status and a diff of your locale files, commits them with a message, discards them, and undoes any edit from the session.
|
|
43
|
+
- ๐ฉบ **Doctor:** `npx livelocale doctor` finds the usual integration mistakes before you open the Studio.
|
|
44
|
+
- โ
**Translation check for CI:** `npx livelocale check` fails the build on missing keys, placeholder drift between locales, and broken ICU syntax, and lists keys nothing uses.
|
|
45
|
+
- ๐ **Safety switches:** `LIVELOCALE=off` kill switch, and automatic shut-off on hosting platforms so a staging build can never leak watermarks.
|
|
46
|
+
- ๐งญ **App Router and Pages Router:** One handler works with both `route.ts` and `pages/api` signatures.
|
|
47
|
+
- ๐ **Works beyond localhost:3000:** Point the Studio at any port or hostname with `--app` or `?app=`, and allow Codespaces or custom origins with one env var.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## โก Quick Setup
|
|
52
|
+
|
|
53
|
+
### 1. Install Dependency
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install livelocale
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### 2. Add the Client Listener
|
|
62
|
+
|
|
63
|
+
Mount the `LiveLocaleProvider` inside your root layout. This launches the dev-mode DOM scanners and intercepts clipboard copy/paste actions.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// app/layout.tsx
|
|
67
|
+
import { LiveLocaleProvider } from 'livelocale/react';
|
|
68
|
+
|
|
69
|
+
export default function RootLayout({ children }) {
|
|
70
|
+
return (
|
|
71
|
+
<html lang="en">
|
|
72
|
+
<body>
|
|
73
|
+
<LiveLocaleProvider />
|
|
74
|
+
{children}
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 3. Initialize Server Mutation Handler
|
|
84
|
+
|
|
85
|
+
Initialize the server-side API endpoints automatically by running:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx livelocale init
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
_The CLI will check for TypeScript/JavaScript and write the appropriate mutation file (`/app/api/livelocale/route.ts` or `/pages/api/livelocale.ts`) directly into your project._
|
|
92
|
+
|
|
93
|
+
> [!NOTE]
|
|
94
|
+
> For manual setup, create `app/api/livelocale/route.ts` with:
|
|
95
|
+
>
|
|
96
|
+
> ```typescript
|
|
97
|
+
> import { createLiveLocaleHandler } from 'livelocale/server';
|
|
98
|
+
> import * as path from 'path';
|
|
99
|
+
>
|
|
100
|
+
> const handler = createLiveLocaleHandler({
|
|
101
|
+
> localesPath: path.resolve('./locales'),
|
|
102
|
+
> });
|
|
103
|
+
>
|
|
104
|
+
> export const GET = handler;
|
|
105
|
+
> export const POST = handler;
|
|
106
|
+
> export const OPTIONS = handler;
|
|
107
|
+
> ```
|
|
108
|
+
>
|
|
109
|
+
> For the **Pages Router**, create `pages/api/livelocale.ts` instead and default-export the
|
|
110
|
+
> same handler. It accepts the `(req, res)` signature automatically:
|
|
111
|
+
>
|
|
112
|
+
> ```typescript
|
|
113
|
+
> import { createLiveLocaleHandler } from 'livelocale/server';
|
|
114
|
+
> import * as path from 'path';
|
|
115
|
+
>
|
|
116
|
+
> export default createLiveLocaleHandler({ localesPath: path.resolve('./locales') });
|
|
117
|
+
> ```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 4. Enable Key Watermarking
|
|
122
|
+
|
|
123
|
+
Wrap translation lookups to inject dev-only watermarks.
|
|
124
|
+
|
|
125
|
+
#### Option A: Server Components (Direct Loader)
|
|
126
|
+
|
|
127
|
+
Use `createTranslations` to load flat JSON files or directory-based namespaces:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { createTranslations } from 'livelocale/server';
|
|
131
|
+
|
|
132
|
+
export default async function Page({ searchParams }) {
|
|
133
|
+
const { locale = 'en' } = await searchParams;
|
|
134
|
+
const t = createTranslations(locale, {
|
|
135
|
+
supportedLocales: ['en', 'ar', 'es'],
|
|
136
|
+
localesDir: './locales',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return <h1>{t.home?.welcome}</h1>;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Option B: Client-side Hooks (react-i18next or next-intl)
|
|
144
|
+
|
|
145
|
+
Wrap hook return values with `wrapTranslationEngine`:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
'use client';
|
|
149
|
+
import { useTranslation } from 'react-i18next';
|
|
150
|
+
import { wrapTranslationEngine } from 'livelocale/client';
|
|
151
|
+
|
|
152
|
+
export default function Page() {
|
|
153
|
+
const { t: rawT } = useTranslation('auth');
|
|
154
|
+
const t = wrapTranslationEngine(rawT, { keyPrefix: 'auth' });
|
|
155
|
+
|
|
156
|
+
return <h1>{t('login.title')}</h1>;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 5. Launch the Visual Studio
|
|
163
|
+
|
|
164
|
+
Boot the editing studio next to your local development server:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx livelocale studio --port 3010 --app http://localhost:3000
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Open `http://localhost:3010` to view the Studio, loaded with your app. `--app` defaults to
|
|
171
|
+
`http://localhost:3000`; you can also switch apps without restarting by opening
|
|
172
|
+
`http://localhost:3010/?app=http://localhost:3001`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### 6. Check your setup
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx livelocale doctor --app http://localhost:3000
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The doctor verifies the route handler, the provider mount, middleware matchers, frame-blocking
|
|
183
|
+
headers, and locale JSON, then probes the running app. It exits non-zero on blocking problems, so
|
|
184
|
+
it can also run in CI.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## ๐ ๏ธ CLI Reference
|
|
189
|
+
|
|
190
|
+
### `init`
|
|
191
|
+
|
|
192
|
+
Generates API route handlers based on your router structure (App vs Pages Router) and configuration language (TypeScript vs JavaScript).
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx livelocale init [--dir <path>]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `studio`
|
|
199
|
+
|
|
200
|
+
Spins up a lightweight, CORS-enabled HTTP server serving the Visual Studio assets statically.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx livelocale studio [--port <number>] [--app <origin>]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `--port` โ port for the Studio itself (default `3010`).
|
|
207
|
+
- `--app` โ origin of the app being edited (default `http://localhost:3000`).
|
|
208
|
+
|
|
209
|
+
### `doctor`
|
|
210
|
+
|
|
211
|
+
Inspects the project for the integration problems the Studio cannot recover from, and optionally
|
|
212
|
+
probes the running app.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npx livelocale doctor [--dir <path>] [--locales <path>] [--app <origin>] [--token <token>]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- `--locales` โ locales directory relative to the project (default `locales`).
|
|
219
|
+
- `--app` โ also check that the mutation API answers and that pages are embeddable.
|
|
220
|
+
- `--token` โ access token for a preview deployment (defaults to `LIVELOCALE_TOKEN`); with it the
|
|
221
|
+
doctor also reports the server mode and repository access problems.
|
|
222
|
+
|
|
223
|
+
### `check`
|
|
224
|
+
|
|
225
|
+
A read-only consistency check of your translations, built for CI. It runs in any `NODE_ENV`.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx livelocale check [--dir <path>] [--locales <path>] [--source <locale>] [--no-unused] [--strict] [--json]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Finding | Level |
|
|
232
|
+
| ------------------------------------------------------------------------------- | ------- |
|
|
233
|
+
| Locale file is not valid JSON | problem |
|
|
234
|
+
| Broken ICU syntax (unbalanced braces, `plural` without `other`) | problem |
|
|
235
|
+
| Key used in code but missing from a locale | problem |
|
|
236
|
+
| Key in the source locale but missing from another locale | problem |
|
|
237
|
+
| Placeholders differ from the source locale (`{name}`, `{{count}}`, `%s`, `<b>`) | problem |
|
|
238
|
+
| Empty value | warning |
|
|
239
|
+
| Key never referenced in code | warning |
|
|
240
|
+
|
|
241
|
+
- `--source` โ locale the others are compared against (default `en`, else the first locale).
|
|
242
|
+
- `--no-unused` โ skip the unused-key report. Keys built at run time (``t(`status.${s}`)``) cannot be
|
|
243
|
+
detected, so review this list before deleting anything.
|
|
244
|
+
- `--strict` โ treat warnings as problems.
|
|
245
|
+
- `--json` โ print the full report for other tools.
|
|
246
|
+
|
|
247
|
+
The command exits with code 1 when there are problems. Code references resolve namespaces the same
|
|
248
|
+
way the Studio does, an object access such as `t.home` covers every key under `home`, and i18next
|
|
249
|
+
plural families (`items_one`, `items_other`, โฆ) count as one key, so Arabic's six forms against
|
|
250
|
+
English's two are not reported.
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
# .github/workflows/i18n.yml
|
|
254
|
+
- run: npx livelocale check
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `migrate`
|
|
258
|
+
|
|
259
|
+
Performs static analysis to scan and automatically wrap existing `react-i18next` hooks with `wrapTranslationEngine`.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npx livelocale migrate [--dir <path>] [--exclude <dirs>] [--dry-run]
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`migrate` needs the `typescript` package to parse your source. It is an optional dependency so
|
|
266
|
+
JavaScript-only projects are not forced to install it; `init` and `studio` never need it.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## โ๏ธ Non-Next.js Integration (React + Vite SPA)
|
|
271
|
+
|
|
272
|
+
If you are using a standard React Client-Side SPA (like Vite), you don't have Next.js API endpoints. You can use the built-in Vite plugin inside `vite.config.ts` to automatically capture the mutation endpoints:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { defineConfig } from 'vite';
|
|
276
|
+
import react from '@vitejs/plugin-react';
|
|
277
|
+
import { liveLocaleVite } from 'livelocale/vite';
|
|
278
|
+
|
|
279
|
+
export default defineConfig({
|
|
280
|
+
plugins: [
|
|
281
|
+
react(),
|
|
282
|
+
liveLocaleVite({
|
|
283
|
+
localesPath: './locales', // path to your locales directory
|
|
284
|
+
}),
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## ๐งพ Reviewing and committing from the Studio
|
|
292
|
+
|
|
293
|
+
Every save writes to disk immediately. The **Changes** button in the Studio header opens a panel
|
|
294
|
+
that keeps those writes reviewable:
|
|
295
|
+
|
|
296
|
+
- **Locale files** lists what git sees as modified, added, or deleted under your locales directory,
|
|
297
|
+
with the current branch and an on-demand diff.
|
|
298
|
+
- **Commit** stages and commits _only_ the locales directory with the message you type. Other
|
|
299
|
+
work in your tree is never touched. Push when you are ready.
|
|
300
|
+
- **Discard** reverts the locales directory to the last commit (tracked files restored, new locale
|
|
301
|
+
files removed) after a confirmation.
|
|
302
|
+
- **This session** lists each edit you saved with its previous and new value. **Undo** writes the
|
|
303
|
+
previous value back through the normal save path.
|
|
304
|
+
|
|
305
|
+
Git integration needs the `git` command on PATH and the locales directory inside a repository. It
|
|
306
|
+
is on by default and can be turned off per handler:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
createLiveLocaleHandler({ localesPath: path.resolve('./locales'), git: false });
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Rewrites keep your files diff-friendly: the existing indentation unit (tabs, 2 or 4 spaces) and
|
|
313
|
+
trailing newline are preserved.
|
|
314
|
+
|
|
315
|
+
## ๐ข Editing plural forms
|
|
316
|
+
|
|
317
|
+
When a message is a single ICU `plural`, `select`, or `selectordinal` argument, optionally with text
|
|
318
|
+
around it, the editor replaces the textarea with one field per branch:
|
|
319
|
+
|
|
320
|
+
- Fields are the union of the branches already in the message (including explicit cases such as
|
|
321
|
+
`=0`) and the CLDR categories of the active locale, from `Intl.PluralRules`. English shows
|
|
322
|
+
`one` and `other`; Arabic shows `zero`, `one`, `two`, `few`, `many`, `other`; Japanese shows
|
|
323
|
+
`other` only. Required categories that are still empty are flagged.
|
|
324
|
+
- `#` stands for the number. Text before and after the argument is editable separately.
|
|
325
|
+
- Empty branches are dropped from the saved message, except `other`, which ICU requires.
|
|
326
|
+
- **Edit raw** switches back to the plain textarea at any time.
|
|
327
|
+
|
|
328
|
+
## ๐งฉ Placeholder safety
|
|
329
|
+
|
|
330
|
+
Most production incidents caused by copy edits come from a placeholder that went missing. The
|
|
331
|
+
Studio and the API guard against that:
|
|
332
|
+
|
|
333
|
+
- The editor shows the placeholders found in the reference text as chips. A chip turns red when
|
|
334
|
+
your translation no longer contains it and amber when you introduce one the reference lacks.
|
|
335
|
+
- On save, the server compares the placeholder sets of the reference and the new value. A
|
|
336
|
+
mismatch answers `422 PLACEHOLDER_MISMATCH` and the Studio shows the reason with **Save
|
|
337
|
+
anyway** and **Keep editing**.
|
|
338
|
+
- Structurally broken ICU messages (unbalanced braces, a `plural`/`select` without an `other`
|
|
339
|
+
branch) answer `422 MESSAGE_SYNTAX` with the same override.
|
|
340
|
+
|
|
341
|
+
Recognised syntaxes: ICU/next-intl/react-intl `{name}`, `{count, plural, โฆ}` (with apostrophe
|
|
342
|
+
quoting), i18next `{{name}}` and `$t(key)`, printf `%s` / `%1$s`, and rich-text tags such as
|
|
343
|
+
`<b>` or `<0>`. The reference is the source-language value for other locales, and the previous
|
|
344
|
+
value when you edit the source language itself.
|
|
345
|
+
|
|
346
|
+
For API clients: send `reference` with the POST to enable the check, and `force: true` to bypass
|
|
347
|
+
it. Without `reference` only the syntax check runs.
|
|
348
|
+
|
|
349
|
+
## ๐ Preview mode: editing without a checkout
|
|
350
|
+
|
|
351
|
+
Development mode needs a developer's machine. Preview mode lets a product manager, translator, or
|
|
352
|
+
reviewer edit copy against a **deployed preview** of the app, with changes going to a git branch
|
|
353
|
+
and a pull request instead of a local file. Nothing is written to the server's filesystem.
|
|
354
|
+
|
|
355
|
+
### How it fits together
|
|
356
|
+
|
|
357
|
+
1. The app is deployed from a branch (say `i18n/preview-edits`) with preview mode on.
|
|
358
|
+
2. The Studio, hosted anywhere or run locally with `--app <preview-url>`, embeds that deployment.
|
|
359
|
+
3. Each save calls the app's mutation API, which commits the change to `i18n/preview-edits`
|
|
360
|
+
through the GitHub API. The live preview in the iframe updates immediately; the deployment
|
|
361
|
+
itself updates when your platform rebuilds the branch.
|
|
362
|
+
4. **Open pull request** in the Changes panel proposes the branch to `main`. Reviewers merge.
|
|
363
|
+
|
|
364
|
+
### Server setup
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// app/api/livelocale/route.ts
|
|
368
|
+
import { createLiveLocaleHandler } from 'livelocale/server';
|
|
369
|
+
|
|
370
|
+
const handler = createLiveLocaleHandler({
|
|
371
|
+
localesPath: 'locales', // repository-relative in GitHub mode
|
|
372
|
+
allowedOrigins: ['https://i18n-studio.example.com'], // where the Studio is served from
|
|
373
|
+
github: {
|
|
374
|
+
repo: 'acme/site',
|
|
375
|
+
branch: 'i18n/preview-edits',
|
|
376
|
+
baseBranch: 'main',
|
|
377
|
+
token: process.env.GITHUB_TOKEN!, // contents: write, pull requests: write
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
export const GET = handler;
|
|
382
|
+
export const POST = handler;
|
|
383
|
+
export const OPTIONS = handler;
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Environment on the preview deployment:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
LIVELOCALE=preview # server: watermark and serve the API in a production build
|
|
390
|
+
NEXT_PUBLIC_LIVELOCALE=preview # client bundle: run the scanner and clipboard guards
|
|
391
|
+
LIVELOCALE_TOKEN=<long random> # required; the Studio asks for it once per browser session
|
|
392
|
+
GITHUB_TOKEN=<fine-grained PAT> # Contents and Pull requests: "Read and write" on the repository
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
The app must also let the Studio embed it. Next.js evaluates `headers()` in `next.config` at
|
|
396
|
+
**build** time, so a `frame-ancestors` value that lists the Studio origin has to be present when the
|
|
397
|
+
preview is built, not only at run time.
|
|
398
|
+
|
|
399
|
+
For a Vite app, the Vite plugin mirrors the server's `LIVELOCALE` flag into the page, so
|
|
400
|
+
`LIVELOCALE=preview` alone is enough; no `NEXT_PUBLIC_` variable is needed there. The plugin runs
|
|
401
|
+
on the Vite dev server only: a static `vite build` served by Apache, Nginx or a CDN has no server for
|
|
402
|
+
the Studio to save through, so preview mode does not work there yet.
|
|
403
|
+
|
|
404
|
+
Then open the Studio with `?app=https://<preview-url>` (or `studio --app`). It shows a
|
|
405
|
+
**PREVIEW** badge, asks for the access token, and the Changes panel switches to branch and
|
|
406
|
+
pull-request actions. Code-reference scanning is off in this mode (there is no checkout), so the
|
|
407
|
+
Locale Directory lists keys from the locale files only.
|
|
408
|
+
|
|
409
|
+
Check a preview deployment before handing it to anyone:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
npx livelocale doctor --app https://<preview-url> --token <LIVELOCALE_TOKEN>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The server verifies its GitHub token when the Studio connects: a token that cannot write contents
|
|
416
|
+
or open pull requests, a missing base branch, or an unknown repository is reported in a banner
|
|
417
|
+
in the Studio and by `doctor`, before anyone loses an edit to a failed save.
|
|
418
|
+
|
|
419
|
+
Behaviour worth knowing:
|
|
420
|
+
|
|
421
|
+
- **Edits stay visible.** The deployment keeps serving the text it was built with until the
|
|
422
|
+
branch is rebuilt, so the Studio re-applies this session's saved values on reload and navigation.
|
|
423
|
+
- **Concurrent saves are safe.** Saves to one file are queued, and a save that loses a race with
|
|
424
|
+
another server instance or another Studio re-reads the file and retries.
|
|
425
|
+
- **Discard closes the pull request.** Resetting the branch to the base leaves the pull request
|
|
426
|
+
with no changes, and GitHub closes it.
|
|
427
|
+
- **Every save is a commit, and most hosts build every commit.** To save build minutes, skip builds
|
|
428
|
+
for commits that only touch locale files; the Studio already shows saved edits. On Vercel, set
|
|
429
|
+
_Project Settings โ Git โ Ignored Build Step_ to
|
|
430
|
+
`git diff --quiet HEAD^ HEAD -- . ':(exclude)locales'`.
|
|
431
|
+
- **Hosting login walls block the Studio.** Vercel's Deployment Protection (on by default for
|
|
432
|
+
previews) sends the iframe and the API calls to a Vercel login page. Turn off Vercel
|
|
433
|
+
Authentication for the preview deployments you edit; `doctor --app` reports it.
|
|
434
|
+
|
|
435
|
+
### What preview mode does not do
|
|
436
|
+
|
|
437
|
+
- It never writes to the deployment's filesystem; the GitHub branch is the only store.
|
|
438
|
+
- It does not replace authentication for the app itself. The token only guards the mutation API.
|
|
439
|
+
- It is not a production feature. Do not set `LIVELOCALE=preview` on a production deployment.
|
|
440
|
+
|
|
441
|
+
## ๐ Safety switches
|
|
442
|
+
|
|
443
|
+
Everything runs only when `NODE_ENV=development`, or when preview mode is switched on explicitly.
|
|
444
|
+
Two more guards exist for teams:
|
|
445
|
+
|
|
446
|
+
- **Kill switch.** `LIVELOCALE=off` disables watermarking, the mutation API, and the client scanner.
|
|
447
|
+
Client bundles read `NEXT_PUBLIC_LIVELOCALE=off` instead, because Next.js only inlines public
|
|
448
|
+
variables.
|
|
449
|
+
- **Deployed-platform guard.** If the server detects Vercel (preview or production), Netlify,
|
|
450
|
+
Kubernetes, Render, Fly.io, Railway, or Heroku while `NODE_ENV` is still `development`, the tool
|
|
451
|
+
switches itself off and logs one warning. Local `vercel dev` and `netlify dev` are unaffected.
|
|
452
|
+
Set `LIVELOCALE=force` to override on purpose.
|
|
453
|
+
|
|
454
|
+
## ๐ช When the watermark bites
|
|
455
|
+
|
|
456
|
+
In development every translated string starts with invisible characters. Most code never notices,
|
|
457
|
+
but anything that treats the string as data will:
|
|
458
|
+
|
|
459
|
+
| Symptom | Cause | Fix |
|
|
460
|
+
| --------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------- |
|
|
461
|
+
| `t('yes') === 'Yes'` is false | equality on a watermarked string | `unwrap(t('yes')) === 'Yes'` |
|
|
462
|
+
| `maxLength` or `.length` checks are off by a few | invisible prefix counts as characters | `unwrap(value).length` |
|
|
463
|
+
| `<title>`, `aria-label`, `alt`, `placeholder` show odd spacing in dev tools | attributes do not strip the prefix | pass `unwrap(...)` to attributes, or leave it: it is dev-only |
|
|
464
|
+
| Screenshot or snapshot tests differ between dev and prod | watermarks only exist in development | run those tests with `LIVELOCALE=off` |
|
|
465
|
+
| A value saved from a form contains zero-width characters | input sanitizers were bypassed (custom components) | call `sanitizeZeroWidth` before submitting |
|
|
466
|
+
|
|
467
|
+
`unwrap` and `sanitizeZeroWidth` are exported from both `livelocale/client` and
|
|
468
|
+
`livelocale/server`. `unwrap` is deep: it cleans strings inside arrays and objects and never
|
|
469
|
+
mutates its input.
|
|
470
|
+
|
|
471
|
+
## ๐ข Running in team and enterprise setups
|
|
472
|
+
|
|
473
|
+
The defaults assume `localhost:3000` for the app and `localhost:3010` for the Studio. Anything else
|
|
474
|
+
needs one of the knobs below.
|
|
475
|
+
|
|
476
|
+
**App on another port or hostname.** Start the Studio with `--app https://app.dev.local:3000`, or
|
|
477
|
+
open the Studio with `?app=<origin>`.
|
|
478
|
+
|
|
479
|
+
**Studio not on localhost (GitHub Codespaces, Gitpod, a shared dev box).** The mutation API only
|
|
480
|
+
accepts writes from localhost origins unless told otherwise. Either pass `allowedOrigins` to
|
|
481
|
+
`createLiveLocaleHandler`, or set an environment variable in the app's dev process:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
LIVELOCALE_ALLOWED_ORIGINS="https://abc-3010.app.github.dev,https://studio.dev.local"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Content-Security-Policy blocks the iframe.** If your app sends `frame-ancestors 'none'` or
|
|
488
|
+
`X-Frame-Options: DENY` in development, the Studio cannot embed it. Relax the header only when
|
|
489
|
+
`NODE_ENV === 'development'`, for example by allowing `frame-ancestors 'self' http://localhost:3010`.
|
|
490
|
+
|
|
491
|
+
**Auth middleware redirects the API.** Exclude `/api/livelocale` from your middleware matcher so the
|
|
492
|
+
Studio can reach the mutation route without a session:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
export const config = { matcher: ['/((?!api/livelocale).*)'] };
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Not sure which of these applies?** Run `npx livelocale doctor --app <origin>`; it names
|
|
499
|
+
the exact file and header involved.
|
|
500
|
+
|
|
501
|
+
**Namespaced hooks.** The key scanner reads the namespace from `useTranslations('ns')`,
|
|
502
|
+
`getTranslations('ns')`, `useTranslation('ns')`, `wrapNextIntl(โฆ, 'ns')`, and
|
|
503
|
+
`wrapTranslationEngine(โฆ, { keyPrefix: 'ns' })` in each file, and prefixes the keys it finds. It
|
|
504
|
+
does not follow translator functions passed between files or chosen at run time.
|
|
505
|
+
|
|
506
|
+
**Monorepos.** `localesPath` is resolved relative to the process working directory of the app, so
|
|
507
|
+
point it at the package that owns the locale files, for example `path.resolve('./src/locales')`.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## ๐ Migrating from next-i18n-lens
|
|
512
|
+
|
|
513
|
+
LiveLocale was published as `next-i18n-lens` up to 0.7.0. Version 0.8.0 renames everything, with no
|
|
514
|
+
aliases for the old names:
|
|
515
|
+
|
|
516
|
+
1. `npm uninstall next-i18n-lens && npm install livelocale`, and import from `livelocale/client`,
|
|
517
|
+
`livelocale/server`, `livelocale/react` and `livelocale/vite`.
|
|
518
|
+
2. `I18nLensProvider` โ `LiveLocaleProvider`, `createI18nLensHandler` โ `createLiveLocaleHandler`,
|
|
519
|
+
`i18nLensVite` โ `liveLocaleVite`, `getLensMode` / `isLensEnabled` โ `getLiveLocaleMode` /
|
|
520
|
+
`isLiveLocaleEnabled`; types `I18nLensHandler` โ `LiveLocaleHandler` and `LensMode` โ
|
|
521
|
+
`LiveLocaleMode` (`ClientInitializer` is gone).
|
|
522
|
+
3. Move the route file: `app/api/i18n-lens/mutate/route.ts` โ `app/api/livelocale/route.ts`
|
|
523
|
+
(Pages Router: `pages/api/i18n-lens/mutate.ts` โ `pages/api/livelocale.ts`), or run
|
|
524
|
+
`npx livelocale init` again.
|
|
525
|
+
4. Rename environment variables: `I18N_LENS` โ `LIVELOCALE`, `NEXT_PUBLIC_I18N_LENS` โ
|
|
526
|
+
`NEXT_PUBLIC_LIVELOCALE`, `I18N_LENS_TOKEN` โ `LIVELOCALE_TOKEN`, `I18N_LENS_ALLOWED_ORIGINS` โ
|
|
527
|
+
`LIVELOCALE_ALLOWED_ORIGINS`. Old names are ignored, so a forgotten kill switch stops working:
|
|
528
|
+
check your hosting settings.
|
|
529
|
+
5. The CLI is `npx livelocale` (`doctor`, `check`, `studio`, `init`, `migrate`).
|
|
530
|
+
|
|
531
|
+
## โ๏ธ License
|
|
532
|
+
|
|
533
|
+
MIT License. See [LICENSE](LICENSE) for more details.
|