locale-ai 0.6.1 → 0.6.2

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
@@ -31,7 +31,7 @@ Download 577 MB? (Y/n)
31
31
  - **Layouts**: `locales/en/*.json` (nested folders too) → `locales/fr/...`, or `messages/en.json` → `messages/fr.json`. `.json`, `.yaml`, `.yml`, `.po` (gettext), `.arb` (Flutter) and `.strings` (Apple) are all supported, mixed freely in the same project. Common folders (`locales`, `src/locales`, `public/locales`, `src/i18n`, `messages`, `lang`, ...) are found automatically, or pass `--input`.
32
32
  - **Placeholders are never translated or lost**: `{name}`, `{{count}}`, `%s`, `%1$s`, `%(name)s`, `%{name}`, `${name}`, `:attribute`, `$t(key)`, `@:key`, `<b>…</b>`, `<0>…</0>`, `&nbsp;`, URLs, e-mails.
33
33
  - **ICU MessageFormat**: `{count, plural, one {# file} other {# files}}`, `select`, nesting. Only the text is translated.
34
- - **i18next plurals**: adds the forms the target language needs (`_few`, `_many` for Arabic, Russian, Polish...).
34
+ - **i18next plurals**: adds the forms the target language needs (`_few`, `_many` for Arabic, Russian, Polish...), so the app doesn't fall back to the English string — each added form copies the `_other` translation verbatim rather than inflecting it, so review and adjust the wording of `_zero`/`_two`/`_few`/`_many` by hand for languages where those forms use a different noun/adjective form.
35
35
  - **Incremental**: only missing keys are translated, existing translations are kept, re-runs are no-ops. `--overwrite` re-translates everything.
36
36
  - **Structure**: key order, numbers, booleans, arrays, indentation and trailing newline are preserved.
37
37
 
@@ -157,3 +157,4 @@ npm run build # → dist/
157
157
  ## Next steps
158
158
 
159
159
  1. **More languages**: ja, ko have no usable OPUS-MT model yet; a different model family would be needed.
160
+ 2. **Rare batch-dependent mistranslation**: translating a short string (e.g. "© 2026 Acme Inc. All rights reserved.") can occasionally produce an unrelated/garbled result depending on which *other* strings share its translation batch — the same string translates correctly alone or in a different batch. Root cause: the built-in models are dynamically int8-quantized per-tensor, so the quantization scale for a batched activation tensor is computed from the whole batch, not per row; most of the time this is just quantization noise, but occasionally it's enough to tip beam search into a bad output with no warning. A fix would mean re-quantizing the models with static per-channel scales (or giving up CPU-batching speed) — out of scope for a quick patch; flagging here so it's a known, understood limitation rather than a silent one.
package/dist/engine.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { extractIcu } from "./icu.js";
2
+ import { extractPipePlural } from "./vue-plural.js";
2
3
  import { fixTagSpacing, hasTranslatableText, mapOutsideTokens, mask, numericStyleSafe, splitOnTokens, stripMarkers, unmask, } from "./placeholders.js";
3
4
  import { splitSentences } from "./segment.js";
4
5
  const letters = (s) => stripMarkers(s).replace(/[^\p{L}]/gu, "");
@@ -207,8 +208,8 @@ export async function translateStrings(strings, hops, opts) {
207
208
  remaining.push(s);
208
209
  }
209
210
  const plans = remaining.map((s) => {
210
- const icu = extractIcu(s);
211
- return { source: s, icu, units: (icu?.units ?? [s]).map((text) => ({ text, owner: s })) };
211
+ const split = extractIcu(s) ?? extractPipePlural(s);
212
+ return { source: s, split, units: (split?.units ?? [s]).map((text) => ({ text, owner: s })) };
212
213
  });
213
214
  const units = plans.flatMap((p) => p.units);
214
215
  let pending = units;
@@ -220,7 +221,7 @@ export async function translateStrings(strings, hops, opts) {
220
221
  await splitFallback(pending, hops, opts, progress, issues);
221
222
  for (const p of plans) {
222
223
  const parts = p.units.map((u) => u.result ?? u.text);
223
- translations.set(p.source, p.icu ? p.icu.rebuild(parts) : parts[0]);
224
+ translations.set(p.source, p.split ? p.split.rebuild(parts) : parts[0]);
224
225
  }
225
226
  return { translations, issues };
226
227
  }
package/dist/locales.js CHANGED
@@ -51,7 +51,7 @@ export function detectLayout(cwd, from, input) {
51
51
  return { kind: "file", root: path.dirname(abs), ext: directExt };
52
52
  const l = layoutAt(abs, from);
53
53
  if (!l)
54
- throw new Error(`No "${from}" locale found in ${input} (looked for ${from}/*.json and ${from}.json/.yaml/.yml).`);
54
+ throw new Error(`No "${from}" locale found in ${input} (looked for ${from}/* and ${from}${EXTENSIONS.join("/")}).`);
55
55
  return l;
56
56
  }
57
57
  for (const c of CANDIDATES) {
@@ -59,7 +59,7 @@ export function detectLayout(cwd, from, input) {
59
59
  if (l)
60
60
  return l;
61
61
  }
62
- throw new Error(`Could not find "${from}" locale files. Use --input <dir> to point at the folder containing ${from}/ or ${from}.json/.yaml/.yml.`);
62
+ throw new Error(`Could not find "${from}" locale files. Use --input <dir> to point at the folder containing ${from}/ or ${from}${EXTENSIONS.join("/")}.`);
63
63
  }
64
64
  function listLocaleFiles(dir) {
65
65
  const out = [];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * vue-i18n's plural syntax: forms separated by a literal `|`, chosen by count at
3
+ * runtime (`"no items | one item | {count} items"`). The `|` carries no meaning to
4
+ * a translation model — it reads as stray punctuation and gets dropped or garbles
5
+ * the sentence around it — so each form is translated on its own and the `|`
6
+ * separators are kept untouched, exactly where they were.
7
+ */
8
+ /** Returns undefined when the string has no top-level `|` (ignoring one inside `{...}`). */
9
+ export function extractPipePlural(s) {
10
+ const segments = [];
11
+ let depth = 0;
12
+ let start = 0;
13
+ const push = (end) => {
14
+ const seg = s.slice(start, end);
15
+ const m = /^(\s*)([\s\S]*?)(\s*)$/.exec(seg);
16
+ segments.push({ lead: m[1], core: m[2], trail: m[3] });
17
+ };
18
+ for (let i = 0; i < s.length; i++) {
19
+ const c = s[i];
20
+ if (c === "{")
21
+ depth++;
22
+ else if (c === "}")
23
+ depth = Math.max(0, depth - 1);
24
+ else if (c === "|" && depth === 0) {
25
+ push(i);
26
+ start = i + 1;
27
+ }
28
+ }
29
+ if (start === 0)
30
+ return undefined; // no top-level '|'
31
+ push(s.length);
32
+ if (segments.some((p) => !p.core))
33
+ return undefined; // "a||b" or a leading/trailing '|': not a plural list
34
+ return {
35
+ units: segments.map((p) => p.core),
36
+ rebuild: (translations) => segments.map((p, i) => p.lead + translations[i] + p.trail).join("|"),
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locale-ai",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Translate your i18n locale files locally on CPU. Free, offline, no API keys.",
5
5
  "author": "Taha KHABOUSS",
6
6
  "repository": {