locale-ai 0.6.7 → 0.6.8
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 +1 -1
- package/dist/locales.js +33 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Download 577 MB? (Y/n)
|
|
|
35
35
|
- **Incremental, and edit-aware**: only missing or changed keys are translated; untouched translations are kept, so re-runs are no-ops. A small `locale-ai.lock.json` (commit it, like a package lock) remembers a hash of each source string and of the output last written for it per target locale — so editing a source string gets it re-translated automatically (it isn't enough for the target to already hold *some* text), while editing a target string by hand is detected and reported for review instead of being silently overwritten on the next run. `--overwrite` bypasses the lock and re-translates everything.
|
|
36
36
|
- **Structure**: key order, numbers, booleans, arrays, indentation and trailing newline are preserved.
|
|
37
37
|
|
|
38
|
-
**Format notes**: for `.po`, `msgid` is the stable key (never changed) and `msgstr` is the translated value, as gettext expects — the English `.po` file's `msgstr` must hold the source text (most generators already fill it; an empty `msgstr` is treated as empty, not inherited from `msgid`). `msgid_plural`/`msgstr[n]` plural entries are translated too (the singular and plural source forms), then expanded to however many `msgstr[n]` the target language's plural rules need (6 for Arabic, 1 for Japanese, ...) — not full grammatical inflection for in-between categories (`few`/`many`/...), which get the plural-sourced translation verbatim, the same simplification `addPluralForms` makes for i18next. `Plural-Forms` in the header is computed for the target language, not copied from the source; an existing target file's header is otherwise left as you last edited it (a hand-fixed `Plural-Forms`, `Last-Translator`, ...) instead of being regenerated on every rerun. For `.arb`, `@key` metadata and `@@locale` are never translated; `@@locale` is set to the target locale on output.
|
|
38
|
+
**Format notes**: for `.po`, `msgid` is the stable key (never changed) and `msgstr` is the translated value, as gettext expects — the English `.po` file's `msgstr` must hold the source text (most generators already fill it; an empty `msgstr` is treated as empty, not inherited from `msgid`). `msgid_plural`/`msgstr[n]` plural entries are translated too (the singular and plural source forms), then expanded to however many `msgstr[n]` the target language's plural rules need (6 for Arabic, 1 for Japanese, ...) — not full grammatical inflection for in-between categories (`few`/`many`/...), which get the plural-sourced translation verbatim, the same simplification `addPluralForms` makes for i18next. `Plural-Forms` in the header is computed for the target language, not copied from the source; an existing target file's header is otherwise left as you last edited it (a hand-fixed `Plural-Forms`, `Last-Translator`, ...) instead of being regenerated on every rerun. For `.arb`, `@key` metadata and `@@locale` are never translated; `@@locale` is set to the target locale on output. For `.strings`, a file saved as UTF-16 (Xcode's traditional default) is read like a UTF-8 one, an unquoted key (`title = "Settings";`, without quotes around `title`) is read the same as a quoted one, and `\U00e9`-style escapes are decoded — output is always written as UTF-8 with quoted keys, regardless of how the source was saved.
|
|
39
39
|
|
|
40
40
|
## Why locale-ai?
|
|
41
41
|
|
package/dist/locales.js
CHANGED
|
@@ -151,19 +151,28 @@ function readYamlDoc(file, text, tag) {
|
|
|
151
151
|
}
|
|
152
152
|
/** Apple .strings: `"key" = "value";`, optionally preceded by a `/* ... *\/` comment. */
|
|
153
153
|
function unescapeC(s) {
|
|
154
|
-
return s.replace(/\\(.)/g, (_, c) =>
|
|
154
|
+
return s.replace(/\\(?:U([0-9a-fA-F]{4})|(.))/g, (_, hex, c) => {
|
|
155
|
+
// \Uxxxx is a UTF-16 code unit; two in a row combine into one character via
|
|
156
|
+
// ordinary JS string concatenation when they're a surrogate pair.
|
|
157
|
+
if (hex !== undefined)
|
|
158
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
159
|
+
return ({ n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" })[c] ?? c;
|
|
160
|
+
});
|
|
155
161
|
}
|
|
156
162
|
function escapeC(s) {
|
|
157
163
|
return s.replace(/[\\"\n\t\r]/g, (c) => ({ "\\": "\\\\", '"': '\\"', "\n": "\\n", "\t": "\\t", "\r": "\\r" })[c]);
|
|
158
164
|
}
|
|
159
165
|
function readStringsDoc(file, text) {
|
|
160
166
|
// A key's preceding comment is either a /* ... */ block, or one or more consecutive // lines.
|
|
161
|
-
|
|
167
|
+
// The old NeXT/Apple plist-string format allows a bare, unquoted key (no spaces,
|
|
168
|
+
// "=" or ";") as well as a quoted one — Xcode itself only ever writes quoted keys,
|
|
169
|
+
// but hand-edited or third-party-exported .strings files sometimes have both.
|
|
170
|
+
const PAIR = /(?:((?:\/\*[\s\S]*?\*\/)|(?:\/\/[^\r\n]*(?:\r?\n[ \t]*\/\/[^\r\n]*)*))[ \t]*\r?\n+)?[ \t]*(?:"((?:\\.|[^"\\])*)"|([^\s"=;]+))\s*=\s*"((?:\\.|[^"\\])*)"\s*;/g;
|
|
162
171
|
const data = newObj();
|
|
163
172
|
const entries = [];
|
|
164
173
|
for (const m of text.matchAll(PAIR)) {
|
|
165
|
-
const key = unescapeC(m[2]);
|
|
166
|
-
data[key] = unescapeC(m[
|
|
174
|
+
const key = m[2] !== undefined ? unescapeC(m[2]) : m[3];
|
|
175
|
+
data[key] = unescapeC(m[4]);
|
|
167
176
|
entries.push({ key, comment: m[1] });
|
|
168
177
|
}
|
|
169
178
|
if (entries.length === 0 && text.trim() !== "") {
|
|
@@ -431,9 +440,28 @@ function readPoDoc(file, text) {
|
|
|
431
440
|
};
|
|
432
441
|
return { data, serialize };
|
|
433
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Decodes a locale file's raw bytes to text, detecting a UTF-16 BOM first — Xcode's
|
|
445
|
+
* traditional default for `.strings` files, which `fs.readFileSync(file, "utf8")`
|
|
446
|
+
* would otherwise mangle into unreadable text (and then "no entries found").
|
|
447
|
+
*/
|
|
448
|
+
function readText(file) {
|
|
449
|
+
const buf = fs.readFileSync(file);
|
|
450
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe)
|
|
451
|
+
return buf.subarray(2).toString("utf16le");
|
|
452
|
+
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
453
|
+
const swapped = Buffer.alloc(buf.length - 2);
|
|
454
|
+
for (let i = 2; i + 1 < buf.length; i += 2) {
|
|
455
|
+
swapped[i - 2] = buf[i + 1];
|
|
456
|
+
swapped[i - 1] = buf[i];
|
|
457
|
+
}
|
|
458
|
+
return swapped.toString("utf16le");
|
|
459
|
+
}
|
|
460
|
+
return buf.toString("utf8").replace(/^/, "");
|
|
461
|
+
}
|
|
434
462
|
/** `tag` is the locale this file is for (the source tag, or an existing target's own tag). */
|
|
435
463
|
export function readLocaleDoc(file, tag) {
|
|
436
|
-
const text =
|
|
464
|
+
const text = readText(file);
|
|
437
465
|
const ext = path.extname(file).toLowerCase();
|
|
438
466
|
if (ext === ".yaml" || ext === ".yml")
|
|
439
467
|
return readYamlDoc(file, text, tag);
|