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 CHANGED
@@ -321,16 +321,45 @@ const { language, languages, setLanguage, basePath } = useLanguage();
321
321
 
322
322
  ## What gets translated
323
323
 
324
- - Static JSX text, plus `placeholder`, `title`, `alt`, `aria-label`, `aria-placeholder` and `aria-description` on HTML elements. Other attributes (class names, styles, URLs, ids, event handlers) and props on your own components are never touched. JSX whitespace is collapsed the way React renders it.
325
- - **Text with variables**: `<p>Welcome, {user.name}. You have {count} new messages</p>` is translated as one sentence (`Welcome, {name}. You have {count} new messages`) and your values are filled in at runtime. Variables must be plain names or property paths (`name`, `user.name`, `props.count`). Text mixed with function calls, conditions or nested elements is left as written. There is no plural handling, so write `{count} message(s)` or branch in your code.
326
- - **Not translated**: anything with `translate="no"` (the standard HTML attribute, inherited by children), `exclude` phrases, `<code>` / `<pre>` / `<script>` / `<style>`, text made only of numbers and symbols, and data that arrives at runtime (API responses, user content).
327
- - **Numbers** are kept exactly as written. `translate` checks that every digit (prices, phone numbers, hours, versions) survives translation and re-translates with the number locked if not. If that still fails, the original text is kept instead of shipping a wrong number. Use `useFormat()` (below) for numbers you compute.
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
- <section translate="no"><Terminal /></section> // nothing inside is translated
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
- - Variables in text must be plain names or property paths; no plural rules.
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 toTranslator, I as getLanguage, M as TranslationError, N as createLocalTranslator, P 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, j as writeJson, k as ensureBanner, 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 } from "./project-4C7cUxzd.js";
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 useCallback((i, s) => dict[i] || s, [dict]);
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 };