glotfile 0.8.5 → 0.8.7
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 +7 -7
- package/dist/server/cli.js +167 -43
- package/dist/server/server.js +161 -41
- package/dist/ui/assets/{index-Bx7davi7.js → index-CVA535xu.js} +92 -19
- package/dist/ui/index.html +1 -1
- package/package.json +25 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/cli-reference.md +25 -8
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ web UI, and you export to whatever locale formats your apps consume — no SaaS,
|
|
|
6
6
|
no hosted database, nothing leaves your machine except the AI calls you choose
|
|
7
7
|
to make.
|
|
8
8
|
|
|
9
|
+
**[glotfile.dev](https://glotfile.dev)** · [Docs](https://glotfile.dev/docs/) · [npm](https://www.npmjs.com/package/glotfile)
|
|
10
|
+
|
|
9
11
|
- **One source of truth** — every string and translation lives in `glotfile.json`, committed alongside your code. Versioning, review, and rollback come from git. For large catalogs, run `glotfile split` to store the catalog as a `glotfile/` directory with one file per locale — so a one-locale change is a one-file `git diff` instead of a multi-megabyte one. The in-app experience is identical.
|
|
10
12
|
- **Local web UI** — run one command, edit in the browser, changes save straight back to the file.
|
|
11
13
|
- **AI translation** — fill in missing languages with Anthropic, OpenAI, AWS Bedrock, OpenRouter, or a local Ollama model, using per-key context, a glossary, and screenshots.
|
|
@@ -19,21 +21,19 @@ to make.
|
|
|
19
21
|
|
|
20
22
|
## Getting started
|
|
21
23
|
|
|
22
|
-
Glotfile
|
|
23
|
-
not yet published to npm, so for now run it from a checkout of this repo:
|
|
24
|
+
Glotfile runs with no install via `npx`. It's pre-1.0, so expect rough edges:
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
|
|
27
|
-
npm run build # builds the UI + server into dist/
|
|
28
|
-
node bin/glotfile.js # same as the `glotfile` command
|
|
27
|
+
npx glotfile
|
|
29
28
|
```
|
|
30
29
|
|
|
31
30
|
That starts a local server bound to `127.0.0.1`, opens your browser, and — if
|
|
32
31
|
there's no `glotfile.json` in the current directory yet — starts from sensible
|
|
33
32
|
defaults and writes the file as soon as you make your first edit.
|
|
34
33
|
|
|
35
|
-
> Working on Glotfile itself?
|
|
36
|
-
> the
|
|
34
|
+
> Working on Glotfile itself? Clone the repo, then `npm install` and `npm run dev`
|
|
35
|
+
> to run the Vite UI with hot-reload alongside the server. `npm run build` followed
|
|
36
|
+
> by `node bin/glotfile.js` runs the built CLI exactly like the published `glotfile`.
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
package/dist/server/cli.js
CHANGED
|
@@ -959,17 +959,57 @@ function extractPlaceholders(value) {
|
|
|
959
959
|
function isIcuPluralOrSelect(value) {
|
|
960
960
|
return ICU_PLURAL_SELECT.test(value);
|
|
961
961
|
}
|
|
962
|
+
function withLiterals(value, convertGap, emitLiteral) {
|
|
963
|
+
let out = "";
|
|
964
|
+
let gap = "";
|
|
965
|
+
const flushGap = () => {
|
|
966
|
+
if (gap) {
|
|
967
|
+
out += convertGap(gap);
|
|
968
|
+
gap = "";
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
for (let i = 0; i < value.length; ) {
|
|
972
|
+
if (value[i] === "'") {
|
|
973
|
+
const next = value[i + 1];
|
|
974
|
+
if (next === "'") {
|
|
975
|
+
gap += "'";
|
|
976
|
+
i += 2;
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (next === "{" || next === "}" || next === "#" || next === "|") {
|
|
980
|
+
flushGap();
|
|
981
|
+
let j = i + 1;
|
|
982
|
+
while (j < value.length && value[j] !== "'") j++;
|
|
983
|
+
out += emitLiteral(value.slice(i + 1, j));
|
|
984
|
+
i = j < value.length ? j + 1 : j;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
gap += value[i];
|
|
989
|
+
i++;
|
|
990
|
+
}
|
|
991
|
+
flushGap();
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
function extractLiterals(value) {
|
|
995
|
+
const out = [];
|
|
996
|
+
withLiterals(value, () => "", (lit) => {
|
|
997
|
+
out.push(lit);
|
|
998
|
+
return "";
|
|
999
|
+
});
|
|
1000
|
+
return out;
|
|
1001
|
+
}
|
|
962
1002
|
function toLaravel(value) {
|
|
963
1003
|
if (isIcuPluralOrSelect(value)) return value;
|
|
964
|
-
return value.replace(/\{(\w+)\}/g, ":$1");
|
|
1004
|
+
return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
|
|
965
1005
|
}
|
|
966
1006
|
function toI18next(value) {
|
|
967
1007
|
if (isIcuPluralOrSelect(value)) return value;
|
|
968
|
-
return value.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}");
|
|
1008
|
+
return withLiterals(value, (gap) => gap.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}"), (lit) => lit);
|
|
969
1009
|
}
|
|
970
1010
|
function toRuby(value) {
|
|
971
1011
|
if (isIcuPluralOrSelect(value)) return value;
|
|
972
|
-
return value.replace(/(?<!%)\{(\w+)\}/g, "%{$1}");
|
|
1012
|
+
return withLiterals(value, (gap) => gap.replace(/(?<!%)\{(\w+)\}/g, "%{$1}"), (lit) => lit);
|
|
973
1013
|
}
|
|
974
1014
|
function placeholdersMatch(source, translation) {
|
|
975
1015
|
const a = extractPlaceholders(source).sort();
|
|
@@ -1146,7 +1186,7 @@ var init_laravel_php = __esm({
|
|
|
1146
1186
|
init_options();
|
|
1147
1187
|
init_placeholders();
|
|
1148
1188
|
init_schema();
|
|
1149
|
-
DEFAULT_LOCALE_CASE2 = "
|
|
1189
|
+
DEFAULT_LOCALE_CASE2 = "bcp47-underscore";
|
|
1150
1190
|
laravelPhp = {
|
|
1151
1191
|
name: "laravel-php",
|
|
1152
1192
|
capabilities: {
|
|
@@ -1186,6 +1226,20 @@ var init_laravel_php = __esm({
|
|
|
1186
1226
|
message: "laravel-php cannot represent ICU plural/select; written unconverted"
|
|
1187
1227
|
});
|
|
1188
1228
|
}
|
|
1229
|
+
if (raw) {
|
|
1230
|
+
const names = new Set(extractPlaceholders(raw));
|
|
1231
|
+
for (const m of raw.matchAll(/:([a-zA-Z][a-zA-Z0-9_]*)/g)) {
|
|
1232
|
+
if (names.has(m[1])) {
|
|
1233
|
+
warnings.push({
|
|
1234
|
+
code: "lossy-literal",
|
|
1235
|
+
key,
|
|
1236
|
+
locale,
|
|
1237
|
+
message: `literal ":${m[1]}" collides with the :${m[1]} placeholder; Laravel will interpolate both`
|
|
1238
|
+
});
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1189
1243
|
(tree[locale][namespace] ??= {})[inner] = toLaravel(raw);
|
|
1190
1244
|
}
|
|
1191
1245
|
}
|
|
@@ -1277,6 +1331,9 @@ var init_i18next_json = __esm({
|
|
|
1277
1331
|
if (raw && isIcuPluralOrSelect(raw)) {
|
|
1278
1332
|
warnings.push({ code: "lossy-plural", key, locale, message: "i18next-json does not yet convert ICU plural/select; written unconverted" });
|
|
1279
1333
|
}
|
|
1334
|
+
if (raw && extractLiterals(raw).some((lit) => /\{\{\w+\}\}/.test(lit))) {
|
|
1335
|
+
warnings.push({ code: "lossy-literal", key, locale, message: "i18next will interpolate a literal containing {{\u2026}}; i18next has no escape for it" });
|
|
1336
|
+
}
|
|
1280
1337
|
if (setNested(obj, segments, toI18next(raw))) collided.add(key);
|
|
1281
1338
|
}
|
|
1282
1339
|
files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE3)), contents: serializeJson(obj, fmt) });
|
|
@@ -1295,7 +1352,9 @@ function poString(s) {
|
|
|
1295
1352
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r") + '"';
|
|
1296
1353
|
}
|
|
1297
1354
|
function toGettext(body, arg) {
|
|
1298
|
-
|
|
1355
|
+
const gap = (text) => text.replace(/%/g, "%%").split(`{${arg}}`).join("%d");
|
|
1356
|
+
if (isIcuPluralOrSelect(body)) return gap(body);
|
|
1357
|
+
return withLiterals(body, gap, (lit) => lit.replace(/%/g, "%%"));
|
|
1299
1358
|
}
|
|
1300
1359
|
var DEFAULT_LOCALE_CASE4, gettextPo;
|
|
1301
1360
|
var init_gettext_po = __esm({
|
|
@@ -1304,6 +1363,7 @@ var init_gettext_po = __esm({
|
|
|
1304
1363
|
init_adapters();
|
|
1305
1364
|
init_options();
|
|
1306
1365
|
init_plurals();
|
|
1366
|
+
init_placeholders();
|
|
1307
1367
|
DEFAULT_LOCALE_CASE4 = "lower-hyphen";
|
|
1308
1368
|
gettextPo = {
|
|
1309
1369
|
name: "gettext-po",
|
|
@@ -1362,8 +1422,10 @@ var init_gettext_po = __esm({
|
|
|
1362
1422
|
blocks.push(
|
|
1363
1423
|
[
|
|
1364
1424
|
`msgctxt ${poString(key)}`,
|
|
1365
|
-
|
|
1366
|
-
|
|
1425
|
+
// Scalar keys carry no count arg; toGettext still strips literal
|
|
1426
|
+
// spans and escapes a literal % to %%.
|
|
1427
|
+
`msgid ${poString(toGettext(src?.value ?? "", ""))}`,
|
|
1428
|
+
`msgstr ${poString(toGettext(lv?.value ?? "", ""))}`
|
|
1367
1429
|
].join("\n")
|
|
1368
1430
|
);
|
|
1369
1431
|
}
|
|
@@ -1389,7 +1451,9 @@ function xmlEscape(s) {
|
|
|
1389
1451
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1390
1452
|
}
|
|
1391
1453
|
function toApple(body, arg) {
|
|
1392
|
-
|
|
1454
|
+
const gap = (text) => text.replace(/%/g, "%%").split(`{${arg}}`).join("%d");
|
|
1455
|
+
if (isIcuPluralOrSelect(body)) return gap(body);
|
|
1456
|
+
return withLiterals(body, gap, (lit) => lit.replace(/%/g, "%%"));
|
|
1393
1457
|
}
|
|
1394
1458
|
var DEFAULT_LOCALE_CASE5, appleStringsdict;
|
|
1395
1459
|
var init_apple_stringsdict = __esm({
|
|
@@ -1398,6 +1462,7 @@ var init_apple_stringsdict = __esm({
|
|
|
1398
1462
|
init_adapters();
|
|
1399
1463
|
init_options();
|
|
1400
1464
|
init_schema();
|
|
1465
|
+
init_placeholders();
|
|
1401
1466
|
DEFAULT_LOCALE_CASE5 = "lower-hyphen";
|
|
1402
1467
|
appleStringsdict = {
|
|
1403
1468
|
name: "apple-stringsdict",
|
|
@@ -1461,12 +1526,18 @@ var init_apple_stringsdict = __esm({
|
|
|
1461
1526
|
function escape(s) {
|
|
1462
1527
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
|
|
1463
1528
|
}
|
|
1529
|
+
function toApple2(value) {
|
|
1530
|
+
const gap = (text) => text.replace(/%/g, "%%");
|
|
1531
|
+
if (isIcuPluralOrSelect(value)) return gap(value);
|
|
1532
|
+
return withLiterals(value, gap, (lit) => lit.replace(/%/g, "%%"));
|
|
1533
|
+
}
|
|
1464
1534
|
var DEFAULT_LOCALE_CASE6, appleStrings;
|
|
1465
1535
|
var init_apple_strings = __esm({
|
|
1466
1536
|
"src/server/adapters/apple-strings.ts"() {
|
|
1467
1537
|
"use strict";
|
|
1468
1538
|
init_adapters();
|
|
1469
1539
|
init_options();
|
|
1540
|
+
init_placeholders();
|
|
1470
1541
|
DEFAULT_LOCALE_CASE6 = "bcp47-hyphen";
|
|
1471
1542
|
appleStrings = {
|
|
1472
1543
|
name: "apple-strings",
|
|
@@ -1493,7 +1564,7 @@ var init_apple_strings = __esm({
|
|
|
1493
1564
|
if (entry.plural) continue;
|
|
1494
1565
|
const value = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1495
1566
|
if (value === null) continue;
|
|
1496
|
-
lines.push(`"${escape(key)}" = "${escape(value)}";`);
|
|
1567
|
+
lines.push(`"${escape(key)}" = "${escape(toApple2(value))}";`);
|
|
1497
1568
|
}
|
|
1498
1569
|
const contents = lines.length ? lines.join("\n") + "\n" : "";
|
|
1499
1570
|
files.push({
|
|
@@ -1508,6 +1579,10 @@ var init_apple_strings = __esm({
|
|
|
1508
1579
|
});
|
|
1509
1580
|
|
|
1510
1581
|
// src/server/adapters/vue-i18n-json.ts
|
|
1582
|
+
function toVueI18n(value) {
|
|
1583
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
1584
|
+
return withLiterals(value, (gap) => gap, (content) => `{'${content}'}`);
|
|
1585
|
+
}
|
|
1511
1586
|
var DEFAULT_LOCALE_CASE7, vueI18nJson;
|
|
1512
1587
|
var init_vue_i18n_json = __esm({
|
|
1513
1588
|
"src/server/adapters/vue-i18n-json.ts"() {
|
|
@@ -1544,7 +1619,7 @@ var init_vue_i18n_json = __esm({
|
|
|
1544
1619
|
if (entry.plural) {
|
|
1545
1620
|
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1546
1621
|
if (!forms) continue;
|
|
1547
|
-
const parts = PLURAL_CATEGORIES.map((c) => forms[c]).filter((v) => v !== void 0);
|
|
1622
|
+
const parts = PLURAL_CATEGORIES.map((c) => forms[c]).filter((v) => v !== void 0).map(toVueI18n);
|
|
1548
1623
|
flat[key] = parts.join(" | ");
|
|
1549
1624
|
} else {
|
|
1550
1625
|
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
@@ -1557,7 +1632,7 @@ var init_vue_i18n_json = __esm({
|
|
|
1557
1632
|
message: "vue-i18n-json does not yet convert ICU plural/select; written unconverted"
|
|
1558
1633
|
});
|
|
1559
1634
|
}
|
|
1560
|
-
flat[key] = raw;
|
|
1635
|
+
flat[key] = toVueI18n(raw);
|
|
1561
1636
|
}
|
|
1562
1637
|
}
|
|
1563
1638
|
let payload = flat;
|
|
@@ -1594,27 +1669,30 @@ function angularXMeta(placeholders, name) {
|
|
|
1594
1669
|
return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
|
|
1595
1670
|
}
|
|
1596
1671
|
function renderInterpolations(text, ids, placeholders) {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
id
|
|
1611
|
-
|
|
1672
|
+
const convertGap = (gap) => {
|
|
1673
|
+
let out = "";
|
|
1674
|
+
let last = 0;
|
|
1675
|
+
for (const m of gap.matchAll(/\{(\w+)\}/g)) {
|
|
1676
|
+
const name = m[1];
|
|
1677
|
+
out += xmlEscape2(gap.slice(last, m.index));
|
|
1678
|
+
const meta = angularXMeta(placeholders, name);
|
|
1679
|
+
if (meta) {
|
|
1680
|
+
const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
|
|
1681
|
+
const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
|
|
1682
|
+
out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
|
|
1683
|
+
} else {
|
|
1684
|
+
let id = ids.get(name);
|
|
1685
|
+
if (id === void 0) {
|
|
1686
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
1687
|
+
ids.set(name, id);
|
|
1688
|
+
}
|
|
1689
|
+
out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
1612
1690
|
}
|
|
1613
|
-
|
|
1691
|
+
last = m.index + m[0].length;
|
|
1614
1692
|
}
|
|
1615
|
-
|
|
1616
|
-
}
|
|
1617
|
-
return
|
|
1693
|
+
return out + xmlEscape2(gap.slice(last));
|
|
1694
|
+
};
|
|
1695
|
+
return withLiterals(text, convertGap, (lit) => xmlEscape2(`'${lit}'`));
|
|
1618
1696
|
}
|
|
1619
1697
|
function renderPluralIcu(forms, ids, placeholders) {
|
|
1620
1698
|
const cats = [
|
|
@@ -4065,7 +4143,7 @@ function extractPrefixes(content, scanner) {
|
|
|
4065
4143
|
result.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
4066
4144
|
return result;
|
|
4067
4145
|
}
|
|
4068
|
-
function
|
|
4146
|
+
function extractLiterals2(content) {
|
|
4069
4147
|
const starts = lineStartOffsets(content);
|
|
4070
4148
|
const result = [];
|
|
4071
4149
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4166,7 +4244,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4166
4244
|
size,
|
|
4167
4245
|
refs: extractRefs(content, scanner, opts),
|
|
4168
4246
|
prefixes: extractPrefixes(content, scanner),
|
|
4169
|
-
literals:
|
|
4247
|
+
literals: extractLiterals2(content)
|
|
4170
4248
|
};
|
|
4171
4249
|
}
|
|
4172
4250
|
saveUsageCache(projectRoot, cache2);
|
|
@@ -4554,6 +4632,9 @@ var init_flatten = __esm({
|
|
|
4554
4632
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4555
4633
|
import { readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
|
|
4556
4634
|
import { join as join7 } from "path";
|
|
4635
|
+
function fromVueI18n(value) {
|
|
4636
|
+
return value.replace(/\{'([^']*)'\}/g, "'$1'");
|
|
4637
|
+
}
|
|
4557
4638
|
var LOCALE_RE2, vueI18nJson2;
|
|
4558
4639
|
var init_vue_i18n_json2 = __esm({
|
|
4559
4640
|
"src/server/import/parsers/vue-i18n-json.ts"() {
|
|
@@ -4580,7 +4661,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4580
4661
|
}
|
|
4581
4662
|
if (!locales.includes(locale)) locales.push(locale);
|
|
4582
4663
|
for (const [key, value] of Object.entries(flattenObject(data, "", warnings))) {
|
|
4583
|
-
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
4664
|
+
(keys[key] ??= { values: {} }).values[locale] = fromVueI18n(value);
|
|
4584
4665
|
}
|
|
4585
4666
|
}
|
|
4586
4667
|
return { locales, keys, warnings };
|
|
@@ -4590,8 +4671,14 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4590
4671
|
});
|
|
4591
4672
|
|
|
4592
4673
|
// src/server/import/placeholders.ts
|
|
4674
|
+
function markBareBracesLiteral(value) {
|
|
4675
|
+
return value.replace(/(?<!%)\{(\w+)\}/g, "'{$1}'");
|
|
4676
|
+
}
|
|
4593
4677
|
function laravelToCanonical(value) {
|
|
4594
|
-
return value.replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, "{$1}");
|
|
4678
|
+
return markBareBracesLiteral(value).replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, "{$1}");
|
|
4679
|
+
}
|
|
4680
|
+
function railsToCanonical(value) {
|
|
4681
|
+
return markBareBracesLiteral(value).replace(/%\{(\w+)\}/g, "{$1}");
|
|
4595
4682
|
}
|
|
4596
4683
|
var init_placeholders2 = __esm({
|
|
4597
4684
|
"src/server/import/placeholders.ts"() {
|
|
@@ -4753,6 +4840,9 @@ function localeFromLproj(dir) {
|
|
|
4753
4840
|
if (!m) return null;
|
|
4754
4841
|
return LOCALE_RE4.test(m[1]) ? m[1] : null;
|
|
4755
4842
|
}
|
|
4843
|
+
function printfToCanonical(s) {
|
|
4844
|
+
return s.replace(/%%/g, "%");
|
|
4845
|
+
}
|
|
4756
4846
|
function unescape(body) {
|
|
4757
4847
|
return body.replace(/\\(U[0-9a-fA-F]{4}|u[0-9a-fA-F]{4}|.)/g, (_m, esc) => {
|
|
4758
4848
|
const c = esc[0];
|
|
@@ -4878,7 +4968,7 @@ var init_apple_strings2 = __esm({
|
|
|
4878
4968
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4879
4969
|
}
|
|
4880
4970
|
for (const { key, value } of parseStrings(text, file, warnings)) {
|
|
4881
|
-
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
4971
|
+
(keys[key] ??= { values: {} }).values[locale] = printfToCanonical(value);
|
|
4882
4972
|
}
|
|
4883
4973
|
}
|
|
4884
4974
|
return { locales, keys, warnings };
|
|
@@ -5015,6 +5105,24 @@ function unescapePo(s) {
|
|
|
5015
5105
|
(_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
|
|
5016
5106
|
);
|
|
5017
5107
|
}
|
|
5108
|
+
function printfToCanonical2(s, arg) {
|
|
5109
|
+
let out = "";
|
|
5110
|
+
for (let i = 0; i < s.length; ) {
|
|
5111
|
+
if (s[i] === "%" && s[i + 1] === "%") {
|
|
5112
|
+
out += "%";
|
|
5113
|
+
i += 2;
|
|
5114
|
+
continue;
|
|
5115
|
+
}
|
|
5116
|
+
if (arg && s[i] === "%" && s[i + 1] === "d") {
|
|
5117
|
+
out += `{${arg}}`;
|
|
5118
|
+
i += 2;
|
|
5119
|
+
continue;
|
|
5120
|
+
}
|
|
5121
|
+
out += s[i];
|
|
5122
|
+
i++;
|
|
5123
|
+
}
|
|
5124
|
+
return out;
|
|
5125
|
+
}
|
|
5018
5126
|
function parseEntries(text) {
|
|
5019
5127
|
const entries = [];
|
|
5020
5128
|
let cur = null;
|
|
@@ -5143,13 +5251,13 @@ var init_gettext_po2 = __esm({
|
|
|
5143
5251
|
);
|
|
5144
5252
|
continue;
|
|
5145
5253
|
}
|
|
5146
|
-
forms[cat] = body
|
|
5254
|
+
forms[cat] = printfToCanonical2(body, "count");
|
|
5147
5255
|
}
|
|
5148
5256
|
if (!forms.other) continue;
|
|
5149
5257
|
(keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
|
|
5150
5258
|
} else {
|
|
5151
5259
|
if (!entry.msgstr) continue;
|
|
5152
|
-
(keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
|
|
5260
|
+
(keys[key] ??= { values: {} }).values[locale] = printfToCanonical2(entry.msgstr, "");
|
|
5153
5261
|
}
|
|
5154
5262
|
}
|
|
5155
5263
|
}
|
|
@@ -5259,9 +5367,6 @@ var init_i18next_json2 = __esm({
|
|
|
5259
5367
|
// src/server/import/parsers/rails-yaml.ts
|
|
5260
5368
|
import { readdirSync as readdirSync12, readFileSync as readFileSync19 } from "fs";
|
|
5261
5369
|
import { join as join14 } from "path";
|
|
5262
|
-
function fromRuby(value) {
|
|
5263
|
-
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
5264
|
-
}
|
|
5265
5370
|
function makeNode() {
|
|
5266
5371
|
return /* @__PURE__ */ Object.create(null);
|
|
5267
5372
|
}
|
|
@@ -5447,7 +5552,7 @@ function synthesizeIcu(forms, file, key, warnings) {
|
|
|
5447
5552
|
`rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
|
|
5448
5553
|
);
|
|
5449
5554
|
}
|
|
5450
|
-
parts.push(`${cat} {${
|
|
5555
|
+
parts.push(`${cat} {${railsToCanonical(body)}}`);
|
|
5451
5556
|
}
|
|
5452
5557
|
return `{count, plural, ${parts.join(" ")}}`;
|
|
5453
5558
|
}
|
|
@@ -5456,6 +5561,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5456
5561
|
"src/server/import/parsers/rails-yaml.ts"() {
|
|
5457
5562
|
"use strict";
|
|
5458
5563
|
init_schema();
|
|
5564
|
+
init_placeholders2();
|
|
5459
5565
|
LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5460
5566
|
CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5461
5567
|
railsYaml2 = {
|
|
@@ -5472,7 +5578,7 @@ var init_rails_yaml2 = __esm({
|
|
|
5472
5578
|
for (const [k, v] of Object.entries(node)) {
|
|
5473
5579
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
5474
5580
|
if (typeof v === "string") {
|
|
5475
|
-
if (v !== "") addValue(key, locale,
|
|
5581
|
+
if (v !== "") addValue(key, locale, railsToCanonical(v));
|
|
5476
5582
|
continue;
|
|
5477
5583
|
}
|
|
5478
5584
|
const forms = asPluralForms(v);
|
|
@@ -5598,6 +5704,24 @@ function parsePlistDict(xml) {
|
|
|
5598
5704
|
if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
|
|
5599
5705
|
return tag.selfClosing ? {} : readDict();
|
|
5600
5706
|
}
|
|
5707
|
+
function printfToCanonical3(body, token, arg) {
|
|
5708
|
+
let out = "";
|
|
5709
|
+
for (let i = 0; i < body.length; ) {
|
|
5710
|
+
if (body[i] === "%" && body[i + 1] === "%") {
|
|
5711
|
+
out += "%";
|
|
5712
|
+
i += 2;
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5715
|
+
if (body.startsWith(token, i)) {
|
|
5716
|
+
out += `{${arg}}`;
|
|
5717
|
+
i += token.length;
|
|
5718
|
+
continue;
|
|
5719
|
+
}
|
|
5720
|
+
out += body[i];
|
|
5721
|
+
i++;
|
|
5722
|
+
}
|
|
5723
|
+
return out;
|
|
5724
|
+
}
|
|
5601
5725
|
function entryToIcu(key, entry, file, warnings) {
|
|
5602
5726
|
const warn = (msg) => {
|
|
5603
5727
|
warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
|
|
@@ -5626,7 +5750,7 @@ function entryToIcu(key, entry, file, warnings) {
|
|
|
5626
5750
|
for (const cat of PLURAL_CATEGORIES) {
|
|
5627
5751
|
const body = varDict[cat];
|
|
5628
5752
|
if (typeof body !== "string") continue;
|
|
5629
|
-
forms[cat] = prefix + body
|
|
5753
|
+
forms[cat] = prefix + printfToCanonical3(body, token, arg) + suffix;
|
|
5630
5754
|
}
|
|
5631
5755
|
if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
|
|
5632
5756
|
return formsToIcu(arg, forms);
|