react-autolocale 0.4.0 → 0.5.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/README.md +36 -6
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +13 -1
- package/dist/index.js +47 -4
- package/dist/{project-4C7cUxzd.js → project-DcYRIvym.js} +952 -92
- package/dist/vite.js +34 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -321,16 +321,45 @@ const { language, languages, setLanguage, basePath } = useLanguage();
|
|
|
321
321
|
|
|
322
322
|
## What gets translated
|
|
323
323
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
You write normal code and nothing in your files changes. The scanner finds text wherever people usually put it, not only in JSX:
|
|
325
|
+
|
|
326
|
+
| Where the text is written | Example |
|
|
327
|
+
| --- | --- |
|
|
328
|
+
| JSX text | `<h1>Welcome</h1>` |
|
|
329
|
+
| Sentences with variables, links or bold words | `<p>Hello {user.name}</p>`, `<p>Read the <a href="/terms">terms</a> now</p>` (one message: `Read the {1} now`, so word order can change per language) |
|
|
330
|
+
| Attributes | `placeholder`, `title`, `alt`, `aria-label` (and `aria-description`, `aria-placeholder`, ...), `<input type="submit" value="Send">`, `<meta name="description" content="...">`, `data-tooltip` |
|
|
331
|
+
| Props on your own components | `<Hero title="Welcome" subtitle="Build it" />`, `<Chart text="VEHICLE · VAN" />` (not code-like props such as `variant`, `size`, `className`, `to`, `href`, `icon`) |
|
|
332
|
+
| Expressions in JSX | `{open ? "Close menu" : "Open menu"}`, `{error && "Something went wrong"}`, `{name \|\| "Guest"}`, `` {`Hello, ${name}!`} ``, `{"Page " + n + " of " + total}` |
|
|
333
|
+
| Data in arrays and objects, in the same file or a shared content file | `const nav = ["Home", "Pricing"]`, `{ title: "Fast", body: "Ships in seconds." }`, `[["Weight", "1.56 kg"]]`, `export const specs = {...}` in `site.js` |
|
|
334
|
+
| JSON files your code imports | `import faq from "./faq.json"` (keys such as `id`, `url`, `type`, `href` are skipped) |
|
|
335
|
+
| Text inside logic | `useState("Loading...")`, `setError("Invalid email")`, `alert("Saved!")`, `toast.success("Done")`, `document.title = "Dashboard"`, default props, functions that return labels |
|
|
336
|
+
| Hand-written elements | `React.createElement("h1", null, "Hello")` |
|
|
337
|
+
|
|
338
|
+
Numbers are kept exactly as written: `translate` checks that every digit (prices, phone numbers, hours, versions) survives translation, re-translates with the number locked if not, and otherwise keeps the original text. Use `useFormat()` (below) for numbers you compute.
|
|
339
|
+
|
|
340
|
+
### How it tells text from code
|
|
341
|
+
|
|
342
|
+
A string is translated when it reads like text a person sees. URLs, file names, class names (`"flex items-center"`), CSS values, colors, SVG paths, ids, lowercase code words (`"primary"`, `"submit"`), date formats and similar are left alone. Whole categories of code are never touched: object keys, `import` paths, TypeScript types and enums, `className`/`style`/`href`/`id` and other code attributes, selectors, storage keys, `fetch` and `console` calls, analytics events, class helpers (`clsx`, `cn`), and `throw new Error(...)`.
|
|
343
|
+
|
|
344
|
+
**A string your code depends on is never translated, anywhere in the project.** If the code compares it (`status === "Active"`, `case "Open":`), looks it up (`icons["Home"]`, `{ Home: ... }`), checks it (`.includes("Pending")`) or names it in a type (`type S = "Open" | "Closed"`), that exact text is left as written everywhere, so nothing breaks.
|
|
345
|
+
|
|
346
|
+
### When it gets one wrong
|
|
347
|
+
|
|
348
|
+
- `// i18n-ignore` on or above a string (or its statement) leaves it alone; `translate="no"` on an element leaves it and everything inside it alone; `exclude` lists phrases such as brand names that are never translated.
|
|
349
|
+
- `msg("text")` forces a string to be translated wherever the automatic rules can't see it, such as a lowercase word: `import { msg } from "react-autolocale"` then `msg("open")`, `msg("Hello {name}", { name })`. It returns the text, so it is safe without the plugin.
|
|
328
350
|
|
|
329
351
|
```tsx
|
|
330
352
|
<p translate="no">Acme Cloud</p> // never translated
|
|
331
|
-
|
|
353
|
+
const DEBUG_LABEL = "Internal build"; // i18n-ignore
|
|
332
354
|
```
|
|
333
355
|
|
|
356
|
+
### Good to know
|
|
357
|
+
|
|
358
|
+
- **Switching language reloads the page** when your app has text outside React components (data in a `site.js`, `useState` starting values, JSON): that text is translated when the page loads, so the new language needs a fresh page. Apps that only use text in components switch in place without a reload. Either way each language has its own URL.
|
|
359
|
+
- **Compare ids, not labels.** A tab labelled `"All"` that your code checks with `=== "All"` stays in English (the code needs that exact text). To translate the label, compare something else: `{ id: "all", label: "All" }` and `filter === "all"`.
|
|
360
|
+
- Text passed through your own function (`t(...)`-style helpers) and data that arrives at runtime (API responses, user-written content) is not translated: it does not exist at build time. Text you write in code around those values is.
|
|
361
|
+
- Not translated yet: Markdown/MDX files, HTML inside strings (`dangerouslySetInnerHTML`), and CSS `content:`.
|
|
362
|
+
|
|
334
363
|
## Supported languages
|
|
335
364
|
|
|
336
365
|
Use these codes in `languages={[...]}`. The source (`original`) language can be any of them.
|
|
@@ -457,7 +486,8 @@ With `seo`, switching language navigates to the real page (so its head tags are
|
|
|
457
486
|
## Limits
|
|
458
487
|
|
|
459
488
|
- Data that arrives at runtime (API responses, user-written content) is not translated: it does not exist at build time. Text you write in JSX around those values is.
|
|
460
|
-
-
|
|
489
|
+
- No plural rules: write `{count} message(s)` or branch in your code (`count === 1 ? "1 message" : `${count} messages``; both branches are translated).
|
|
490
|
+
- Strings your code compares, uses as keys or names in types are left as written (see "How it tells text from code"). Lowercase single words in data (`["red", "green"]`) look like code and are skipped; wrap them with `msg()`.
|
|
461
491
|
- Dynamic routes (`/users/:id`) only get pages you list in `routes`, or that are linked.
|
|
462
492
|
- `<AutoScale>` props must be literals so the plugin can read them at build time.
|
|
463
493
|
- The CLI reads `auto-locale.config.ts`, not `vite.config`. Set `include` there if you change the plugin's `include`.
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { F as
|
|
2
|
+
import { B as getLanguage, F as writeJson, I as TranslationError, L as createLocalTranslator, N as ensureBanner, R as ollama, S as scanProject, a as findSite, b as manifestFile, c as hasRootSrc, d as OVERRIDES_FILE, f as exportTranslations, g as importLegacyCache, h as catalogFile, i as defaultInclude, l as resolveOutput, m as readOverrides, n as loadConfigFile, o as META_DIR, r as loadProject, s as defaultOutput, t as findConfigFile, u as toProjectPath, v as removeFromCatalogs, x as readManifest, y as translateMissing, z as toTranslator } from "./project-DcYRIvym.js";
|
|
3
3
|
import { n as DEFAULT_MODELS, t as DEFAULT_KEY_ENV } from "./config-ChdN65Rg.js";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,16 @@ declare global {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Marks any string for translation, wherever the automatic rules cannot see it (a lowercase word, text built in an
|
|
15
|
+
* unusual way): `msg("Open")`, `msg("Hello {name}", { name })`. The build looks the text up in the catalogs; without
|
|
16
|
+
* the plugin it simply returns the text.
|
|
17
|
+
*/
|
|
18
|
+
export declare function msg(text: string, vars?: Record<string, unknown>): string;
|
|
19
|
+
/** @internal Used by the Vite plugin for a string outside any component. */
|
|
20
|
+
export declare function __rlText(i: string, s: string): string;
|
|
21
|
+
/** @internal Same, for a string with variables: `Hello, ${name}` becomes ("Hello, {name}", { name }). */
|
|
22
|
+
export declare function __rlFmt(i: string, s: string, vars: Record<string, unknown>): string;
|
|
13
23
|
export interface AutoScaleProps {
|
|
14
24
|
/** Language the app is written in, e.g. "en". Must be a literal so the build can read it. */
|
|
15
25
|
original: string;
|
|
@@ -37,7 +47,9 @@ export declare function __T({ i, s }: {
|
|
|
37
47
|
s: string;
|
|
38
48
|
}): ReactElement;
|
|
39
49
|
/** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
|
|
40
|
-
export declare function __useT(): (i: string, s: string) => string
|
|
50
|
+
export declare function __useT(): ((i: string, s: string) => string) & {
|
|
51
|
+
f: (i: string, s: string, vars: Record<string, unknown>) => string;
|
|
52
|
+
};
|
|
41
53
|
/** @internal Inserted by the Vite plugin for text with variables, e.g. `Hello {name}`. */
|
|
42
54
|
export declare function __TD({ i, s, v }: {
|
|
43
55
|
i: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { r as redirectTarget, t as STORAGE_KEY } from "./redirect-pDFblLdm.js";
|
|
2
2
|
import { n as DISMISSED_KEY, r as suggestLanguage, t as BANNER_EN } from "./suggestion-CkZQqMvZ.js";
|
|
3
3
|
import { Fragment, createContext, createElement, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { banner, languages, loaders, original, originalNative, seo } from "virtual:react-autolocale";
|
|
4
|
+
import { banner, eager, languages, loaders, original, originalNative, seo } from "virtual:react-autolocale";
|
|
5
5
|
//#region src/runtime/index.ts
|
|
6
6
|
const hasDom = typeof window !== "undefined" && typeof document !== "undefined";
|
|
7
7
|
const codes = /* @__PURE__ */ new Set([original, ...languages.map((l) => l.code)]);
|
|
@@ -22,6 +22,43 @@ function load(code) {
|
|
|
22
22
|
if (!loader) return Promise.resolve({});
|
|
23
23
|
return loader().then((m) => m.default).catch(() => ({})).then((dict) => (cache.set(code, dict), dict));
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Strings written outside React components (data in a content file, helpers, class components) are looked up
|
|
27
|
+
* here, at the moment the code runs. The dictionary is the one for the language in the URL, taken from the page
|
|
28
|
+
* itself (production, prerendering) or from the catalogs the dev server inlines.
|
|
29
|
+
*/
|
|
30
|
+
let currentDict = null;
|
|
31
|
+
/** Set once such a string has been used: it cannot follow an in-place language change, so switching reloads. */
|
|
32
|
+
let staticUsed = false;
|
|
33
|
+
function dictNow() {
|
|
34
|
+
if (currentDict) return currentDict;
|
|
35
|
+
const boot = hasDom ? window.__AUTOSCALE__ : void 0;
|
|
36
|
+
const lang = (hasDom ? prefixOf(window.location.pathname) : null) ?? boot?.lang ?? original;
|
|
37
|
+
if (boot && boot.lang === lang) currentDict = boot.dict;
|
|
38
|
+
else currentDict = lang === original ? {} : eager[lang] ?? cache.get(lang) ?? {};
|
|
39
|
+
return currentDict;
|
|
40
|
+
}
|
|
41
|
+
function fill(template, vars) {
|
|
42
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => name in vars ? String(vars[name] ?? "") : match);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Marks any string for translation, wherever the automatic rules cannot see it (a lowercase word, text built in an
|
|
46
|
+
* unusual way): `msg("Open")`, `msg("Hello {name}", { name })`. The build looks the text up in the catalogs; without
|
|
47
|
+
* the plugin it simply returns the text.
|
|
48
|
+
*/
|
|
49
|
+
function msg(text, vars) {
|
|
50
|
+
return vars ? fill(text, vars) : text;
|
|
51
|
+
}
|
|
52
|
+
/** @internal Used by the Vite plugin for a string outside any component. */
|
|
53
|
+
function __rlText(i, s) {
|
|
54
|
+
staticUsed = true;
|
|
55
|
+
return dictNow()[i] || s;
|
|
56
|
+
}
|
|
57
|
+
/** @internal Same, for a string with variables: `Hello, ${name}` becomes ("Hello, {name}", { name }). */
|
|
58
|
+
function __rlFmt(i, s, vars) {
|
|
59
|
+
staticUsed = true;
|
|
60
|
+
return fill(dictNow()[i] || s, vars);
|
|
61
|
+
}
|
|
25
62
|
const Context = createContext({
|
|
26
63
|
language: original,
|
|
27
64
|
dict: {},
|
|
@@ -34,6 +71,7 @@ function initialState() {
|
|
|
34
71
|
};
|
|
35
72
|
const boot = window.__AUTOSCALE__;
|
|
36
73
|
if (boot) cache.set(boot.lang, boot.dict);
|
|
74
|
+
for (const [code, dict] of Object.entries(eager)) if (!cache.has(code)) cache.set(code, dict);
|
|
37
75
|
const { pathname, search, hash } = window.location;
|
|
38
76
|
const fromUrl = prefixOf(pathname);
|
|
39
77
|
if (fromUrl === null) {
|
|
@@ -65,13 +103,14 @@ function initialState() {
|
|
|
65
103
|
*/
|
|
66
104
|
function AutoScale({ children }) {
|
|
67
105
|
const [state, setState] = useState(initialState);
|
|
106
|
+
currentDict = state.dict;
|
|
68
107
|
const apply = useCallback(async (code, push) => {
|
|
69
108
|
if (!codes.has(code)) return;
|
|
70
109
|
if (push && hasDom) {
|
|
71
110
|
try {
|
|
72
111
|
window.localStorage.setItem(STORAGE_KEY, code);
|
|
73
112
|
} catch {}
|
|
74
|
-
if (seo) {
|
|
113
|
+
if (seo || staticUsed) {
|
|
75
114
|
window.location.assign(pathFor(code));
|
|
76
115
|
return;
|
|
77
116
|
}
|
|
@@ -108,7 +147,11 @@ function __T({ i, s }) {
|
|
|
108
147
|
/** @internal Inserted by the Vite plugin for translated attributes (placeholder, alt, ...). */
|
|
109
148
|
function __useT() {
|
|
110
149
|
const { dict } = useContext(Context);
|
|
111
|
-
return
|
|
150
|
+
return useMemo(() => {
|
|
151
|
+
const lookup = ((i, s) => dict[i] || s);
|
|
152
|
+
lookup.f = (i, s, vars) => fill(dict[i] || s, vars);
|
|
153
|
+
return lookup;
|
|
154
|
+
}, [dict]);
|
|
112
155
|
}
|
|
113
156
|
/** @internal Inserted by the Vite plugin for text with variables, e.g. `Hello {name}`. */
|
|
114
157
|
function __TD({ i, s, v }) {
|
|
@@ -263,4 +306,4 @@ function LanguageSwitcher({ className, style, label = "Language" }) {
|
|
|
263
306
|
}, l.native)));
|
|
264
307
|
}
|
|
265
308
|
//#endregion
|
|
266
|
-
export { AutoScale, ForLanguages, LanguageBanner, LanguageSwitcher, Localized, __T, __TD, __useT, useFormat, useLanguage, useLanguageSuggestion, useLocalized };
|
|
309
|
+
export { AutoScale, ForLanguages, LanguageBanner, LanguageSwitcher, Localized, __T, __TD, __rlFmt, __rlText, __useT, msg, useFormat, useLanguage, useLanguageSuggestion, useLocalized };
|