glotfile 0.5.3 → 0.6.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/dist/server/cli.js +1119 -229
- package/dist/server/server.js +1533 -713
- package/dist/ui/assets/{index-BjN06DHD.js → index-Vn-AfOXc.js} +13 -5
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +6 -1
- package/skill/references/cli-reference.md +6 -0
- package/skill/references/workflows.md +27 -1
package/dist/server/server.js
CHANGED
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
-
};
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// node_modules/dictionary-en/index.js
|
|
12
|
-
var dictionary_en_exports = {};
|
|
13
|
-
__export(dictionary_en_exports, {
|
|
14
|
-
default: () => dictionary_en_default
|
|
15
|
-
});
|
|
16
|
-
import fs from "fs/promises";
|
|
17
|
-
var aff, dic, dictionary, dictionary_en_default;
|
|
18
|
-
var init_dictionary_en = __esm({
|
|
19
|
-
async "node_modules/dictionary-en/index.js"() {
|
|
20
|
-
"use strict";
|
|
21
|
-
aff = await fs.readFile(new URL("index.aff", import.meta.url));
|
|
22
|
-
dic = await fs.readFile(new URL("index.dic", import.meta.url));
|
|
23
|
-
dictionary = { aff, dic };
|
|
24
|
-
dictionary_en_default = dictionary;
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
1
|
// src/server/server.ts
|
|
29
2
|
import { Hono as Hono2 } from "hono";
|
|
30
3
|
import { serve } from "@hono/node-server";
|
|
31
4
|
import { fileURLToPath } from "url";
|
|
32
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join15, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
33
6
|
import { readFile, stat } from "fs/promises";
|
|
34
7
|
import { createServer } from "net";
|
|
35
8
|
import open from "open";
|
|
@@ -148,6 +121,7 @@ function validate(raw) {
|
|
|
148
121
|
if (o.indent !== void 0 && typeof o.indent !== "number") fail("config.outputs[].indent must be a number");
|
|
149
122
|
if (o.finalNewline !== void 0 && typeof o.finalNewline !== "boolean") fail("config.outputs[].finalNewline must be a boolean");
|
|
150
123
|
if (o.includeLocale !== void 0 && typeof o.includeLocale !== "boolean") fail("config.outputs[].includeLocale must be a boolean");
|
|
124
|
+
if (o.skipSourceLocale !== void 0 && typeof o.skipSourceLocale !== "boolean") fail("config.outputs[].skipSourceLocale must be a boolean");
|
|
151
125
|
if (o.localeAliases !== void 0) {
|
|
152
126
|
if (!isObject(o.localeAliases)) fail("config.outputs[].localeAliases must be an object");
|
|
153
127
|
for (const [k, v] of Object.entries(o.localeAliases)) {
|
|
@@ -865,7 +839,7 @@ function findMissing(state) {
|
|
|
865
839
|
const entry = state.keys[key];
|
|
866
840
|
if (entry.skipTranslate) continue;
|
|
867
841
|
for (const locale of targets) {
|
|
868
|
-
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
842
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
|
|
869
843
|
if (!v) out.push({ key, locale });
|
|
870
844
|
}
|
|
871
845
|
}
|
|
@@ -1487,162 +1461,14 @@ function pluralFormPlaceholdersMatch(category, source, form) {
|
|
|
1487
1461
|
return COUNT_OPTIONAL.has(category) ? placeholdersSubset(source, form) : placeholdersMatch(source, form);
|
|
1488
1462
|
}
|
|
1489
1463
|
|
|
1490
|
-
// src/server/
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
// src/server/glob.ts
|
|
1495
|
-
function globToRegExp2(glob) {
|
|
1496
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1497
|
-
return new RegExp(`^${escaped}$`);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
// src/server/ai/batch.ts
|
|
1501
|
-
var MalformedReplyError = class extends Error {
|
|
1502
|
-
constructor(raw) {
|
|
1503
|
-
super("Model reply was not valid translation JSON.");
|
|
1504
|
-
this.raw = raw;
|
|
1505
|
-
this.name = "MalformedReplyError";
|
|
1506
|
-
}
|
|
1507
|
-
raw;
|
|
1508
|
-
};
|
|
1509
|
-
function parseReplyItems(text) {
|
|
1510
|
-
let parsed;
|
|
1511
|
-
try {
|
|
1512
|
-
parsed = JSON.parse(text);
|
|
1513
|
-
} catch {
|
|
1514
|
-
throw new MalformedReplyError(text);
|
|
1515
|
-
}
|
|
1516
|
-
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
1517
|
-
return parsed.items;
|
|
1518
|
-
}
|
|
1519
|
-
function chunk(items, size) {
|
|
1520
|
-
const out = [];
|
|
1521
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
1522
|
-
return out;
|
|
1523
|
-
}
|
|
1524
|
-
function validateTranslation(req, translation) {
|
|
1525
|
-
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
1526
|
-
if (!placeholdersMatch(req.source, translation)) {
|
|
1527
|
-
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
1528
|
-
}
|
|
1529
|
-
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
1530
|
-
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
1531
|
-
}
|
|
1532
|
-
return { id: req.id, translation };
|
|
1533
|
-
}
|
|
1534
|
-
function validatePlural(req, forms) {
|
|
1535
|
-
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
1536
|
-
const plural = req.plural;
|
|
1537
|
-
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
1538
|
-
const cats = plural.categories;
|
|
1539
|
-
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
1540
|
-
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
1541
|
-
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
1542
|
-
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
1543
|
-
if (req.maxLength !== void 0) {
|
|
1544
|
-
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
1545
|
-
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
1546
|
-
}
|
|
1547
|
-
const out = {};
|
|
1548
|
-
for (const c of cats) out[c] = forms[c];
|
|
1549
|
-
return { id: req.id, forms: out };
|
|
1550
|
-
}
|
|
1551
|
-
function validateReply(req, item) {
|
|
1552
|
-
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
1553
|
-
}
|
|
1554
|
-
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
1555
|
-
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
1556
|
-
async function resolveBatch(batch, isRetry = false) {
|
|
1557
|
-
let reply;
|
|
1558
|
-
try {
|
|
1559
|
-
reply = await callBatch(batch, signal);
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
if (!(err instanceof MalformedReplyError)) throw err;
|
|
1562
|
-
onMalformedReply?.(err.raw, batch.length);
|
|
1563
|
-
if (signal?.aborted) return failBatch(batch);
|
|
1564
|
-
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
1565
|
-
const mid = Math.ceil(batch.length / 2);
|
|
1566
|
-
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
1567
|
-
}
|
|
1568
|
-
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
1569
|
-
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
1570
|
-
}
|
|
1571
|
-
const results = [];
|
|
1572
|
-
const total = reqs.length;
|
|
1573
|
-
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
1574
|
-
if (signal?.aborted) break;
|
|
1575
|
-
const batchResults = await resolveBatch(batch);
|
|
1576
|
-
results.push(...batchResults);
|
|
1577
|
-
onBatchComplete?.(results.length, total, batchResults);
|
|
1578
|
-
}
|
|
1579
|
-
return results;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// src/server/ai/run.ts
|
|
1583
|
-
function selectRequests(state, opts) {
|
|
1584
|
-
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
1585
|
-
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
1586
|
-
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
1587
|
-
const reqs = [];
|
|
1588
|
-
let id = 0;
|
|
1589
|
-
for (const key of Object.keys(state.keys).sort()) {
|
|
1590
|
-
const entry = state.keys[key];
|
|
1591
|
-
if (entry.skipTranslate) continue;
|
|
1592
|
-
if (keyRe && !keyRe.test(key)) continue;
|
|
1593
|
-
if (keySet && !keySet.has(key)) continue;
|
|
1594
|
-
const sourceLv = entry.values[state.config.sourceLocale];
|
|
1595
|
-
if (entry.plural) {
|
|
1596
|
-
const sourceForms = sourceLv?.forms;
|
|
1597
|
-
const other = sourceForms?.other;
|
|
1598
|
-
if (!sourceForms || !other) continue;
|
|
1599
|
-
for (const locale of targets) {
|
|
1600
|
-
const have = entry.values[locale]?.forms ?? {};
|
|
1601
|
-
const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
|
|
1602
|
-
if (opts.onlyMissing && complete) continue;
|
|
1603
|
-
const glossary = relevantGlossary(other, locale, state.glossary);
|
|
1604
|
-
reqs.push({
|
|
1605
|
-
id: String(id++),
|
|
1606
|
-
key,
|
|
1607
|
-
source: other,
|
|
1608
|
-
sourceLocale: state.config.sourceLocale,
|
|
1609
|
-
context: entry.context,
|
|
1610
|
-
targetLocale: locale,
|
|
1611
|
-
maxLength: entry.maxLength,
|
|
1612
|
-
placeholders: extractPlaceholders(other),
|
|
1613
|
-
...glossary.length ? { glossary } : {},
|
|
1614
|
-
plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
continue;
|
|
1618
|
-
}
|
|
1619
|
-
const source = sourceLv?.value;
|
|
1620
|
-
if (!source) continue;
|
|
1621
|
-
for (const locale of targets) {
|
|
1622
|
-
const existing = entry.values[locale]?.value;
|
|
1623
|
-
if (opts.onlyMissing && existing) continue;
|
|
1624
|
-
const glossary = relevantGlossary(source, locale, state.glossary);
|
|
1625
|
-
reqs.push({
|
|
1626
|
-
id: String(id++),
|
|
1627
|
-
key,
|
|
1628
|
-
source,
|
|
1629
|
-
sourceLocale: state.config.sourceLocale,
|
|
1630
|
-
context: entry.context,
|
|
1631
|
-
targetLocale: locale,
|
|
1632
|
-
maxLength: entry.maxLength,
|
|
1633
|
-
placeholders: extractPlaceholders(source),
|
|
1634
|
-
...glossary.length ? { glossary } : {}
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
return reqs;
|
|
1464
|
+
// src/server/glossary.ts
|
|
1465
|
+
function contains(haystack, needle, caseSensitive) {
|
|
1466
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1639
1467
|
}
|
|
1640
1468
|
function relevantGlossary(source, targetLocale, glossary) {
|
|
1641
1469
|
const hints = [];
|
|
1642
1470
|
for (const entry of glossary) {
|
|
1643
|
-
|
|
1644
|
-
const needle = entry.caseSensitive ? entry.term : entry.term.toLowerCase();
|
|
1645
|
-
if (!haystack.includes(needle)) continue;
|
|
1471
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1646
1472
|
hints.push({
|
|
1647
1473
|
term: entry.term,
|
|
1648
1474
|
doNotTranslate: entry.doNotTranslate,
|
|
@@ -1652,170 +1478,84 @@ function relevantGlossary(source, targetLocale, glossary) {
|
|
|
1652
1478
|
}
|
|
1653
1479
|
return hints;
|
|
1654
1480
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
};
|
|
1662
|
-
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
1663
|
-
function attachScreenshots(reqs, state, projectRoot) {
|
|
1664
|
-
const cache2 = /* @__PURE__ */ new Map();
|
|
1665
|
-
for (const req of reqs) {
|
|
1666
|
-
const screenshot = state.keys[req.key]?.screenshot;
|
|
1667
|
-
if (!screenshot) continue;
|
|
1668
|
-
const mediaType = MEDIA_TYPES[extname2(screenshot).toLowerCase()];
|
|
1669
|
-
if (!mediaType) continue;
|
|
1670
|
-
if (!cache2.has(screenshot)) {
|
|
1671
|
-
const abs = resolve4(projectRoot, screenshot);
|
|
1672
|
-
if (!existsSync6(abs)) {
|
|
1673
|
-
cache2.set(screenshot, null);
|
|
1674
|
-
} else {
|
|
1675
|
-
const buf = readFileSync6(abs);
|
|
1676
|
-
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
1481
|
+
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
1482
|
+
const out = [];
|
|
1483
|
+
for (const entry of glossary) {
|
|
1484
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
1485
|
+
if (entry.doNotTranslate) {
|
|
1486
|
+
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
1487
|
+
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
1677
1488
|
}
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
const forced = entry.translations?.[targetLocale];
|
|
1492
|
+
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
1493
|
+
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
1678
1494
|
}
|
|
1679
|
-
const image = cache2.get(screenshot);
|
|
1680
|
-
if (image) req.image = image;
|
|
1681
1495
|
}
|
|
1496
|
+
return out;
|
|
1682
1497
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1498
|
+
|
|
1499
|
+
// src/server/spell.ts
|
|
1500
|
+
var instances = /* @__PURE__ */ new Map();
|
|
1501
|
+
var loading = /* @__PURE__ */ new Set();
|
|
1502
|
+
var unavailable = /* @__PURE__ */ new Set();
|
|
1503
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1504
|
+
var norm = (dictId) => dictId.toLowerCase();
|
|
1505
|
+
var ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
1506
|
+
var MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
1507
|
+
var WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
1508
|
+
function spellTokens(value) {
|
|
1509
|
+
return value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
|
|
1510
|
+
}
|
|
1511
|
+
function ignoreWordsFor(glossary, customWords = []) {
|
|
1512
|
+
const set = /* @__PURE__ */ new Set();
|
|
1513
|
+
const add = (text) => {
|
|
1514
|
+
for (const w of text.match(WORD) ?? []) set.add(w.toLowerCase());
|
|
1515
|
+
};
|
|
1516
|
+
for (const e of glossary) {
|
|
1517
|
+
add(e.term);
|
|
1518
|
+
for (const t of Object.values(e.translations ?? {})) add(t);
|
|
1687
1519
|
}
|
|
1688
|
-
const
|
|
1689
|
-
return
|
|
1520
|
+
for (const w of customWords) add(w);
|
|
1521
|
+
return set;
|
|
1690
1522
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1523
|
+
async function getSpeller(dictId) {
|
|
1524
|
+
const key = norm(dictId);
|
|
1525
|
+
const existing = instances.get(key);
|
|
1526
|
+
if (existing) return existing;
|
|
1527
|
+
if (unavailable.has(key)) return null;
|
|
1528
|
+
try {
|
|
1529
|
+
const nspellMod = await import("nspell");
|
|
1530
|
+
const nspell = nspellMod.default ?? nspellMod;
|
|
1531
|
+
const dictMod = await import(`dictionary-${key}`);
|
|
1532
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
1533
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
1534
|
+
const speller = nspell(dict);
|
|
1535
|
+
instances.set(key, speller);
|
|
1536
|
+
return speller;
|
|
1537
|
+
} catch {
|
|
1538
|
+
unavailable.add(key);
|
|
1539
|
+
return null;
|
|
1540
|
+
} finally {
|
|
1541
|
+
loading.delete(key);
|
|
1702
1542
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1543
|
+
}
|
|
1544
|
+
function spellValue(dictId, value, ignore) {
|
|
1545
|
+
const key = norm(dictId);
|
|
1546
|
+
if (unavailable.has(key)) return [];
|
|
1547
|
+
const spell = instances.get(key);
|
|
1548
|
+
if (!spell) {
|
|
1549
|
+
if (!loading.has(key)) {
|
|
1550
|
+
loading.add(key);
|
|
1551
|
+
void getSpeller(key);
|
|
1712
1552
|
}
|
|
1713
|
-
|
|
1714
|
-
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
1715
|
-
const started = /* @__PURE__ */ new Set();
|
|
1716
|
-
const total = reqs.length;
|
|
1717
|
-
let done = 0;
|
|
1718
|
-
const allResults = [];
|
|
1719
|
-
let next = 0;
|
|
1720
|
-
async function worker() {
|
|
1721
|
-
while (next < jobs.length) {
|
|
1722
|
-
if (signal?.aborted) break;
|
|
1723
|
-
const { locale, batch } = jobs[next++];
|
|
1724
|
-
if (!started.has(locale)) {
|
|
1725
|
-
started.add(locale);
|
|
1726
|
-
hooks.onLocaleStart?.(locale);
|
|
1727
|
-
}
|
|
1728
|
-
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
1729
|
-
done += results.length;
|
|
1730
|
-
hooks.onBatchComplete?.(done, total, results, locale);
|
|
1731
|
-
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
1732
|
-
allResults.push(...batchResults);
|
|
1733
|
-
const left = remaining.get(locale) - 1;
|
|
1734
|
-
remaining.set(locale, left);
|
|
1735
|
-
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
1739
|
-
await Promise.all(workers);
|
|
1740
|
-
return allResults;
|
|
1741
|
-
}
|
|
1742
|
-
function applyResults(state, reqs, results, clock = systemClock, force = false) {
|
|
1743
|
-
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
1744
|
-
let written = 0;
|
|
1745
|
-
const errors = [];
|
|
1746
|
-
for (const res of results) {
|
|
1747
|
-
const req = byId.get(res.id);
|
|
1748
|
-
if (!req) continue;
|
|
1749
|
-
if (req.plural) {
|
|
1750
|
-
if (res.error || res.forms === void 0) {
|
|
1751
|
-
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
1752
|
-
continue;
|
|
1753
|
-
}
|
|
1754
|
-
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (res.translation === void 0) {
|
|
1758
|
-
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
1759
|
-
continue;
|
|
1760
|
-
}
|
|
1761
|
-
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
1762
|
-
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
1763
|
-
}
|
|
1764
|
-
return { written, errors };
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// src/server/spell.ts
|
|
1768
|
-
import nspell from "nspell";
|
|
1769
|
-
var LOADERS = {
|
|
1770
|
-
en: () => init_dictionary_en().then(() => dictionary_en_exports),
|
|
1771
|
-
es: () => import("dictionary-es"),
|
|
1772
|
-
fr: () => import("dictionary-fr")
|
|
1773
|
-
};
|
|
1774
|
-
var instances = /* @__PURE__ */ new Map();
|
|
1775
|
-
var loading = /* @__PURE__ */ new Set();
|
|
1776
|
-
var unavailable = /* @__PURE__ */ new Set();
|
|
1777
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1778
|
-
var norm = (locale) => locale.toLowerCase();
|
|
1779
|
-
var ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
1780
|
-
var MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
1781
|
-
var WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
1782
|
-
async function loadDictionary(locale) {
|
|
1783
|
-
const key = norm(locale);
|
|
1784
|
-
if (instances.has(key) || unavailable.has(key)) return;
|
|
1785
|
-
const loader = LOADERS[key];
|
|
1786
|
-
if (!loader) {
|
|
1787
|
-
unavailable.add(key);
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
try {
|
|
1791
|
-
const { default: dict } = await loader();
|
|
1792
|
-
instances.set(key, nspell(dict));
|
|
1793
|
-
} catch {
|
|
1794
|
-
unavailable.add(key);
|
|
1795
|
-
} finally {
|
|
1796
|
-
loading.delete(key);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
function spellValue(locale, value, ignore) {
|
|
1800
|
-
const key = norm(locale);
|
|
1801
|
-
if (unavailable.has(key)) return [];
|
|
1802
|
-
const spell = instances.get(key);
|
|
1803
|
-
if (!spell) {
|
|
1804
|
-
if (!LOADERS[key]) {
|
|
1805
|
-
unavailable.add(key);
|
|
1806
|
-
return [];
|
|
1807
|
-
}
|
|
1808
|
-
if (!loading.has(key)) {
|
|
1809
|
-
loading.add(key);
|
|
1810
|
-
void loadDictionary(key);
|
|
1811
|
-
}
|
|
1812
|
-
return null;
|
|
1553
|
+
return null;
|
|
1813
1554
|
}
|
|
1814
1555
|
const cacheKey = key + " " + value;
|
|
1815
1556
|
let allBad = cache.get(cacheKey);
|
|
1816
1557
|
if (!allBad) {
|
|
1817
|
-
|
|
1818
|
-
allBad = words.filter((w) => !spell.correct(w));
|
|
1558
|
+
allBad = spellTokens(value).filter((w) => !spell.correct(w));
|
|
1819
1559
|
cache.set(cacheKey, allBad);
|
|
1820
1560
|
}
|
|
1821
1561
|
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
@@ -1830,39 +1570,21 @@ var CHECK_RULE = {
|
|
|
1830
1570
|
length: "max-length",
|
|
1831
1571
|
glossary: "glossary-violation"
|
|
1832
1572
|
};
|
|
1833
|
-
function contains(haystack, needle, caseSensitive) {
|
|
1834
|
-
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
1835
|
-
}
|
|
1836
1573
|
function runChecks(state, opts = {}) {
|
|
1837
1574
|
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
1838
1575
|
const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
|
|
1839
1576
|
const issues = [];
|
|
1840
1577
|
let spellPending = false;
|
|
1841
1578
|
const { sourceLocale } = state.config;
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
for (const t of Object.values(e.translations ?? {})) {
|
|
1847
|
-
for (const w of t.toLowerCase().split(/\s+/)) if (w) ignore.add(w);
|
|
1579
|
+
const ignore = ignoreWordsFor(state.glossary, state.config.spelling?.customWords);
|
|
1580
|
+
if (on("untranslated")) {
|
|
1581
|
+
for (const m of findMissing(state)) {
|
|
1582
|
+
issues.push({ key: m.key, locale: m.locale, check: "untranslated", message: "Not translated yet" });
|
|
1848
1583
|
}
|
|
1849
1584
|
}
|
|
1850
|
-
for (const word of state.config.spelling?.customWords ?? []) {
|
|
1851
|
-
const w = word.trim().toLowerCase();
|
|
1852
|
-
if (w) ignore.add(w);
|
|
1853
|
-
}
|
|
1854
|
-
const targetLocales = state.config.locales.filter((l) => l !== sourceLocale);
|
|
1855
1585
|
for (const key of Object.keys(state.keys).sort()) {
|
|
1856
1586
|
const entry = state.keys[key];
|
|
1857
1587
|
const source = entry.values[sourceLocale]?.value ?? "";
|
|
1858
|
-
if (on("untranslated") && !entry.skipTranslate) {
|
|
1859
|
-
for (const locale of targetLocales) {
|
|
1860
|
-
const translated = entry.plural ? (entry.values[locale]?.forms?.other ?? "").trim() !== "" : (entry.values[locale]?.value ?? "").trim() !== "";
|
|
1861
|
-
if (!translated) {
|
|
1862
|
-
issues.push({ key, locale, check: "untranslated", message: "Not translated yet" });
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
1588
|
if (entry.plural) {
|
|
1867
1589
|
if (on("placeholder")) {
|
|
1868
1590
|
const sourceForm = entry.values[sourceLocale]?.forms?.other ?? "";
|
|
@@ -1906,7 +1628,8 @@ function runChecks(state, opts = {}) {
|
|
|
1906
1628
|
});
|
|
1907
1629
|
}
|
|
1908
1630
|
if (on("spelling") && !blank) {
|
|
1909
|
-
const
|
|
1631
|
+
const dictId = state.config.lint?.spelling?.locales?.[locale] ?? locale;
|
|
1632
|
+
const bad = spellValue(dictId, value, ignore);
|
|
1910
1633
|
if (bad === null) spellPending = true;
|
|
1911
1634
|
else if (bad.length) {
|
|
1912
1635
|
issues.push({
|
|
@@ -1936,29 +1659,14 @@ function runChecks(state, opts = {}) {
|
|
|
1936
1659
|
});
|
|
1937
1660
|
}
|
|
1938
1661
|
if (on("glossary") && source) {
|
|
1939
|
-
for (const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
message: `Do-not-translate term "${hint.term}" is missing from the translation`,
|
|
1948
|
-
detail: [hint.term]
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
} else if (hint.forced) {
|
|
1952
|
-
if (!contains(value, hint.forced, cs)) {
|
|
1953
|
-
issues.push({
|
|
1954
|
-
key,
|
|
1955
|
-
locale,
|
|
1956
|
-
check: "glossary",
|
|
1957
|
-
message: `Should use "${hint.forced}" for "${hint.term}"`,
|
|
1958
|
-
detail: [hint.forced]
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1662
|
+
for (const viol of glossaryViolations(source, value, locale, state.glossary)) {
|
|
1663
|
+
issues.push({
|
|
1664
|
+
key,
|
|
1665
|
+
locale,
|
|
1666
|
+
check: "glossary",
|
|
1667
|
+
message: viol.kind === "do-not-translate" ? `Do-not-translate term "${viol.term}" is missing from the translation` : `Should use "${viol.expected}" for "${viol.term}"`,
|
|
1668
|
+
detail: [viol.expected]
|
|
1669
|
+
});
|
|
1962
1670
|
}
|
|
1963
1671
|
}
|
|
1964
1672
|
}
|
|
@@ -1970,19 +1678,13 @@ function runChecks(state, opts = {}) {
|
|
|
1970
1678
|
return { issues: visible, spellPending };
|
|
1971
1679
|
}
|
|
1972
1680
|
|
|
1973
|
-
// src/server/
|
|
1974
|
-
function
|
|
1975
|
-
|
|
1976
|
-
}
|
|
1977
|
-
function buildAllowWords(glossary, dictionary2 = []) {
|
|
1978
|
-
const set = /* @__PURE__ */ new Set();
|
|
1979
|
-
const add = (s) => {
|
|
1980
|
-
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
1981
|
-
};
|
|
1982
|
-
for (const g of glossary) add(g.term);
|
|
1983
|
-
for (const w of dictionary2) add(w);
|
|
1984
|
-
return set;
|
|
1681
|
+
// src/server/glob.ts
|
|
1682
|
+
function globToRegExp2(glob) {
|
|
1683
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1684
|
+
return new RegExp(`^${escaped}$`);
|
|
1985
1685
|
}
|
|
1686
|
+
|
|
1687
|
+
// src/server/lint/spelling.ts
|
|
1986
1688
|
var spellingRule = {
|
|
1987
1689
|
id: "spelling",
|
|
1988
1690
|
run(state, ctx) {
|
|
@@ -1994,10 +1696,8 @@ var spellingRule = {
|
|
|
1994
1696
|
if (!speller) continue;
|
|
1995
1697
|
const value = entry.values[locale]?.value;
|
|
1996
1698
|
if (!value) continue;
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
const lower = word.toLowerCase();
|
|
2000
|
-
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
1699
|
+
for (const word of spellTokens(value)) {
|
|
1700
|
+
if (ctx.allowWords.has(word.toLowerCase())) continue;
|
|
2001
1701
|
if (!speller.correct(word)) {
|
|
2002
1702
|
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
2003
1703
|
}
|
|
@@ -2007,18 +1707,7 @@ var spellingRule = {
|
|
|
2007
1707
|
return out;
|
|
2008
1708
|
}
|
|
2009
1709
|
};
|
|
2010
|
-
var defaultLoader =
|
|
2011
|
-
try {
|
|
2012
|
-
const nspellMod = await import("nspell");
|
|
2013
|
-
const nspell2 = nspellMod.default ?? nspellMod;
|
|
2014
|
-
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
2015
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
2016
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
2017
|
-
return nspell2(dict);
|
|
2018
|
-
} catch {
|
|
2019
|
-
return null;
|
|
2020
|
-
}
|
|
2021
|
-
};
|
|
1710
|
+
var defaultLoader = (dictId) => getSpeller(dictId);
|
|
2022
1711
|
|
|
2023
1712
|
// src/server/lint/rules.ts
|
|
2024
1713
|
var emptySourceRule = {
|
|
@@ -2035,17 +1724,14 @@ var emptySourceRule = {
|
|
|
2035
1724
|
};
|
|
2036
1725
|
var emptyTranslationRule = {
|
|
2037
1726
|
id: "empty-translation",
|
|
2038
|
-
|
|
1727
|
+
// findMissing is the shared "untranslated" walk (also behind the editor's
|
|
1728
|
+
// untranslated check and /scan/missing); a whitespace-only value counts as
|
|
1729
|
+
// missing there, so no separate whitespace pass is needed.
|
|
1730
|
+
run(state) {
|
|
2039
1731
|
const out = [];
|
|
2040
1732
|
for (const m of findMissing(state)) {
|
|
2041
1733
|
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
2042
1734
|
}
|
|
2043
|
-
for (const key of Object.keys(state.keys)) {
|
|
2044
|
-
for (const locale of ctx.targetLocales) {
|
|
2045
|
-
const v = state.keys[key].values[locale]?.value;
|
|
2046
|
-
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
1735
|
return out;
|
|
2050
1736
|
}
|
|
2051
1737
|
};
|
|
@@ -2173,13 +1859,13 @@ var glossaryViolationRule = {
|
|
|
2173
1859
|
for (const locale of ctx.targetLocales) {
|
|
2174
1860
|
const v = entry.values[locale]?.value;
|
|
2175
1861
|
if (!v) continue;
|
|
2176
|
-
for (const
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
}
|
|
1862
|
+
for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
|
|
1863
|
+
out.push({
|
|
1864
|
+
ruleId: "glossary-violation",
|
|
1865
|
+
key,
|
|
1866
|
+
locale,
|
|
1867
|
+
message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
|
|
1868
|
+
});
|
|
2183
1869
|
}
|
|
2184
1870
|
}
|
|
2185
1871
|
}
|
|
@@ -2238,7 +1924,7 @@ async function runLint(state, options = {}) {
|
|
|
2238
1924
|
const active = rules.filter(isActive);
|
|
2239
1925
|
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2240
1926
|
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2241
|
-
const allowWords = spellingOn ?
|
|
1927
|
+
const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
2242
1928
|
const ctx = {
|
|
2243
1929
|
config,
|
|
2244
1930
|
sourceLocale: state.config.sourceLocale,
|
|
@@ -2271,8 +1957,8 @@ async function runLint(state, options = {}) {
|
|
|
2271
1957
|
}
|
|
2272
1958
|
|
|
2273
1959
|
// src/server/lint/outputs.ts
|
|
2274
|
-
import { readFileSync as
|
|
2275
|
-
import { resolve as
|
|
1960
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
1961
|
+
import { resolve as resolve4 } from "path";
|
|
2276
1962
|
|
|
2277
1963
|
// src/server/adapters/options.ts
|
|
2278
1964
|
function applyCase(canonical, style) {
|
|
@@ -2850,27 +2536,41 @@ var vueI18nJson = {
|
|
|
2850
2536
|
function xmlEscape2(s) {
|
|
2851
2537
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2852
2538
|
}
|
|
2853
|
-
function
|
|
2539
|
+
function attrEscape(s) {
|
|
2540
|
+
return xmlEscape2(s).replace(/"/g, """);
|
|
2541
|
+
}
|
|
2542
|
+
function angularXMeta(placeholders, name) {
|
|
2543
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) ? placeholders?.[name] : void 0;
|
|
2544
|
+
}
|
|
2545
|
+
function renderInterpolations(text, ids, placeholders) {
|
|
2854
2546
|
let out = "";
|
|
2855
2547
|
let last = 0;
|
|
2856
2548
|
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
2857
2549
|
const name = m[1];
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2550
|
+
out += xmlEscape2(text.slice(last, m.index));
|
|
2551
|
+
const meta = angularXMeta(placeholders, name);
|
|
2552
|
+
if (meta) {
|
|
2553
|
+
const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
|
|
2554
|
+
const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
|
|
2555
|
+
out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
|
|
2556
|
+
} else {
|
|
2557
|
+
let id = ids.get(name);
|
|
2558
|
+
if (id === void 0) {
|
|
2559
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
2560
|
+
ids.set(name, id);
|
|
2561
|
+
}
|
|
2562
|
+
out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
2862
2563
|
}
|
|
2863
|
-
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
2864
2564
|
last = m.index + m[0].length;
|
|
2865
2565
|
}
|
|
2866
2566
|
return out + xmlEscape2(text.slice(last));
|
|
2867
2567
|
}
|
|
2868
|
-
function renderPluralIcu(forms, ids) {
|
|
2568
|
+
function renderPluralIcu(forms, ids, placeholders) {
|
|
2869
2569
|
const cats = [
|
|
2870
2570
|
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
2871
2571
|
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
2872
2572
|
];
|
|
2873
|
-
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
2573
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids, placeholders)}}`);
|
|
2874
2574
|
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
2875
2575
|
}
|
|
2876
2576
|
function renderEmbeddedIcu(value) {
|
|
@@ -2880,8 +2580,8 @@ function renderEmbeddedIcu(value) {
|
|
|
2880
2580
|
);
|
|
2881
2581
|
return xmlEscape2(renamed);
|
|
2882
2582
|
}
|
|
2883
|
-
function renderScalar(value, ids) {
|
|
2884
|
-
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
2583
|
+
function renderScalar(value, ids, placeholders) {
|
|
2584
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
|
|
2885
2585
|
}
|
|
2886
2586
|
var DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
|
|
2887
2587
|
var angularXliff = {
|
|
@@ -2904,6 +2604,7 @@ var angularXliff = {
|
|
|
2904
2604
|
const emptyAs = resolveEmptyAs(output, "source");
|
|
2905
2605
|
const keys = Object.keys(state.keys).sort();
|
|
2906
2606
|
for (const locale of state.config.locales) {
|
|
2607
|
+
if (output.skipSourceLocale && locale === sourceLocale) continue;
|
|
2907
2608
|
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
|
|
2908
2609
|
const units = [];
|
|
2909
2610
|
for (const key of keys) {
|
|
@@ -2914,17 +2615,18 @@ var angularXliff = {
|
|
|
2914
2615
|
if (entry.plural) {
|
|
2915
2616
|
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
2916
2617
|
if (targetForms === null) continue;
|
|
2917
|
-
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
2918
|
-
target = renderPluralIcu(targetForms, ids);
|
|
2618
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids, entry.placeholders);
|
|
2619
|
+
target = renderPluralIcu(targetForms, ids, entry.placeholders);
|
|
2919
2620
|
} else {
|
|
2920
2621
|
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
2921
2622
|
if (targetValue === null) continue;
|
|
2922
|
-
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
2923
|
-
target = renderScalar(targetValue, ids);
|
|
2623
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids, entry.placeholders);
|
|
2624
|
+
target = renderScalar(targetValue, ids, entry.placeholders);
|
|
2924
2625
|
}
|
|
2626
|
+
const translated = locale === sourceLocale || (entry.plural ? entry.values[locale]?.forms !== void 0 : !!entry.values[locale]?.value);
|
|
2925
2627
|
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
2926
2628
|
units.push(` <source>${source}</source>`);
|
|
2927
|
-
units.push(` <target>${target}</target>`);
|
|
2629
|
+
units.push(` <target${translated ? "" : ' state="new"'}>${target}</target>`);
|
|
2928
2630
|
if (entry.description) {
|
|
2929
2631
|
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
2930
2632
|
}
|
|
@@ -3072,8 +2774,8 @@ function checkOutputs(state, root) {
|
|
|
3072
2774
|
for (const output of state.config.outputs) {
|
|
3073
2775
|
const result = getAdapter(output.adapter).export(state, output);
|
|
3074
2776
|
for (const file of result.files) {
|
|
3075
|
-
const abs =
|
|
3076
|
-
const current =
|
|
2777
|
+
const abs = resolve4(root, file.path);
|
|
2778
|
+
const current = existsSync6(abs) ? readFileSync6(abs, "utf8") : null;
|
|
3077
2779
|
if (current === null) {
|
|
3078
2780
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
3079
2781
|
} else if (current !== file.contents) {
|
|
@@ -3085,7 +2787,7 @@ function checkOutputs(state, root) {
|
|
|
3085
2787
|
}
|
|
3086
2788
|
|
|
3087
2789
|
// src/server/api.ts
|
|
3088
|
-
import { readFileSync as
|
|
2790
|
+
import { readFileSync as readFileSync21, existsSync as existsSync11, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync4 } from "fs";
|
|
3089
2791
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
3090
2792
|
|
|
3091
2793
|
// src/server/ai/anthropic.ts
|
|
@@ -3187,6 +2889,88 @@ var BATCH_SCHEMA = {
|
|
|
3187
2889
|
additionalProperties: false
|
|
3188
2890
|
};
|
|
3189
2891
|
|
|
2892
|
+
// src/server/ai/batch.ts
|
|
2893
|
+
var MalformedReplyError = class extends Error {
|
|
2894
|
+
constructor(raw) {
|
|
2895
|
+
super("Model reply was not valid translation JSON.");
|
|
2896
|
+
this.raw = raw;
|
|
2897
|
+
this.name = "MalformedReplyError";
|
|
2898
|
+
}
|
|
2899
|
+
raw;
|
|
2900
|
+
};
|
|
2901
|
+
function parseReplyItems(text) {
|
|
2902
|
+
let parsed;
|
|
2903
|
+
try {
|
|
2904
|
+
parsed = JSON.parse(text);
|
|
2905
|
+
} catch {
|
|
2906
|
+
throw new MalformedReplyError(text);
|
|
2907
|
+
}
|
|
2908
|
+
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2909
|
+
return parsed.items;
|
|
2910
|
+
}
|
|
2911
|
+
function chunk(items, size) {
|
|
2912
|
+
const out = [];
|
|
2913
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2914
|
+
return out;
|
|
2915
|
+
}
|
|
2916
|
+
function validateTranslation(req, translation) {
|
|
2917
|
+
if (translation === void 0) return { id: req.id, error: "No translation returned." };
|
|
2918
|
+
if (!placeholdersMatch(req.source, translation)) {
|
|
2919
|
+
return { id: req.id, error: "Placeholder mismatch between source and translation." };
|
|
2920
|
+
}
|
|
2921
|
+
if (req.maxLength !== void 0 && translation.length > req.maxLength) {
|
|
2922
|
+
return { id: req.id, translation, error: `Exceeds maxLength (${translation.length} > ${req.maxLength}).` };
|
|
2923
|
+
}
|
|
2924
|
+
return { id: req.id, translation };
|
|
2925
|
+
}
|
|
2926
|
+
function validatePlural(req, forms) {
|
|
2927
|
+
if (!forms) return { id: req.id, error: "No translation returned." };
|
|
2928
|
+
const plural = req.plural;
|
|
2929
|
+
if (!plural) return { id: req.id, error: "validatePlural called on a non-plural request." };
|
|
2930
|
+
const cats = plural.categories;
|
|
2931
|
+
const missing = cats.filter((c) => typeof forms[c] !== "string");
|
|
2932
|
+
if (missing.length) return { id: req.id, error: `Missing plural categories: ${missing.join(", ")}.` };
|
|
2933
|
+
const badPh = cats.find((c) => !pluralFormPlaceholdersMatch(c, req.source, forms[c]));
|
|
2934
|
+
if (badPh) return { id: req.id, error: `Placeholder mismatch in plural form "${badPh}".` };
|
|
2935
|
+
if (req.maxLength !== void 0) {
|
|
2936
|
+
const over = cats.find((c) => forms[c].length > req.maxLength);
|
|
2937
|
+
if (over) return { id: req.id, error: `Plural form "${over}" exceeds maxLength (${forms[over].length} > ${req.maxLength}).` };
|
|
2938
|
+
}
|
|
2939
|
+
const out = {};
|
|
2940
|
+
for (const c of cats) out[c] = forms[c];
|
|
2941
|
+
return { id: req.id, forms: out };
|
|
2942
|
+
}
|
|
2943
|
+
function validateReply(req, item) {
|
|
2944
|
+
return req.plural ? validatePlural(req, item?.forms) : validateTranslation(req, item?.translation);
|
|
2945
|
+
}
|
|
2946
|
+
async function runBatched(reqs, batchSize, callBatch, onBatchComplete, signal, onMalformedReply) {
|
|
2947
|
+
const failBatch = (batch) => batch.map((req) => ({ id: req.id, error: "Model returned malformed JSON for this string." }));
|
|
2948
|
+
async function resolveBatch(batch, isRetry = false) {
|
|
2949
|
+
let reply;
|
|
2950
|
+
try {
|
|
2951
|
+
reply = await callBatch(batch, signal);
|
|
2952
|
+
} catch (err) {
|
|
2953
|
+
if (!(err instanceof MalformedReplyError)) throw err;
|
|
2954
|
+
onMalformedReply?.(err.raw, batch.length);
|
|
2955
|
+
if (signal?.aborted) return failBatch(batch);
|
|
2956
|
+
if (batch.length === 1) return isRetry ? failBatch(batch) : resolveBatch(batch, true);
|
|
2957
|
+
const mid = Math.ceil(batch.length / 2);
|
|
2958
|
+
return [...await resolveBatch(batch.slice(0, mid)), ...await resolveBatch(batch.slice(mid))];
|
|
2959
|
+
}
|
|
2960
|
+
const byId = new Map(reply.map((r) => [r.id, r]));
|
|
2961
|
+
return batch.map((req) => validateReply(req, byId.get(req.id)));
|
|
2962
|
+
}
|
|
2963
|
+
const results = [];
|
|
2964
|
+
const total = reqs.length;
|
|
2965
|
+
for (const batch of chunk(reqs, Math.max(1, batchSize))) {
|
|
2966
|
+
if (signal?.aborted) break;
|
|
2967
|
+
const batchResults = await resolveBatch(batch);
|
|
2968
|
+
results.push(...batchResults);
|
|
2969
|
+
onBatchComplete?.(results.length, total, batchResults);
|
|
2970
|
+
}
|
|
2971
|
+
return results;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
3190
2974
|
// src/server/ai/anthropic.ts
|
|
3191
2975
|
var AnthropicProvider = class {
|
|
3192
2976
|
constructor(config, client) {
|
|
@@ -3618,61 +3402,233 @@ function makeProvider(ai) {
|
|
|
3618
3402
|
}
|
|
3619
3403
|
}
|
|
3620
3404
|
|
|
3621
|
-
// src/server/ai/
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
];
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3405
|
+
// src/server/ai/run.ts
|
|
3406
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3407
|
+
import { resolve as resolve5, extname as extname2 } from "path";
|
|
3408
|
+
function selectRequests(state, opts) {
|
|
3409
|
+
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3410
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
3411
|
+
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3412
|
+
const reqs = [];
|
|
3413
|
+
let id = 0;
|
|
3414
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
3415
|
+
const entry = state.keys[key];
|
|
3416
|
+
if (entry.skipTranslate) continue;
|
|
3417
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
3418
|
+
if (keySet && !keySet.has(key)) continue;
|
|
3419
|
+
const sourceLv = entry.values[state.config.sourceLocale];
|
|
3420
|
+
if (entry.plural) {
|
|
3421
|
+
const sourceForms = sourceLv?.forms;
|
|
3422
|
+
const other = sourceForms?.other;
|
|
3423
|
+
if (!sourceForms || !other) continue;
|
|
3424
|
+
for (const locale of targets) {
|
|
3425
|
+
const have = entry.values[locale]?.forms ?? {};
|
|
3426
|
+
const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
|
|
3427
|
+
if (opts.onlyMissing && complete) continue;
|
|
3428
|
+
const glossary = relevantGlossary(other, locale, state.glossary);
|
|
3429
|
+
reqs.push({
|
|
3430
|
+
id: String(id++),
|
|
3431
|
+
key,
|
|
3432
|
+
source: other,
|
|
3433
|
+
sourceLocale: state.config.sourceLocale,
|
|
3434
|
+
context: entry.context,
|
|
3435
|
+
targetLocale: locale,
|
|
3436
|
+
maxLength: entry.maxLength,
|
|
3437
|
+
placeholders: extractPlaceholders(other),
|
|
3438
|
+
...glossary.length ? { glossary } : {},
|
|
3439
|
+
plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
const source = sourceLv?.value;
|
|
3445
|
+
if (!source) continue;
|
|
3446
|
+
for (const locale of targets) {
|
|
3447
|
+
const existing = entry.values[locale]?.value;
|
|
3448
|
+
if (opts.onlyMissing && existing) continue;
|
|
3449
|
+
const glossary = relevantGlossary(source, locale, state.glossary);
|
|
3450
|
+
reqs.push({
|
|
3451
|
+
id: String(id++),
|
|
3452
|
+
key,
|
|
3453
|
+
source,
|
|
3454
|
+
sourceLocale: state.config.sourceLocale,
|
|
3455
|
+
context: entry.context,
|
|
3456
|
+
targetLocale: locale,
|
|
3457
|
+
maxLength: entry.maxLength,
|
|
3458
|
+
placeholders: extractPlaceholders(source),
|
|
3459
|
+
...glossary.length ? { glossary } : {}
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
return reqs;
|
|
3464
|
+
}
|
|
3465
|
+
var MEDIA_TYPES = {
|
|
3466
|
+
".png": "image/png",
|
|
3467
|
+
".jpg": "image/jpeg",
|
|
3468
|
+
".jpeg": "image/jpeg",
|
|
3469
|
+
".webp": "image/webp",
|
|
3470
|
+
".gif": "image/gif"
|
|
3471
|
+
};
|
|
3472
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
3473
|
+
function attachScreenshots(reqs, state, projectRoot) {
|
|
3474
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
3475
|
+
for (const req of reqs) {
|
|
3476
|
+
const screenshot = state.keys[req.key]?.screenshot;
|
|
3477
|
+
if (!screenshot) continue;
|
|
3478
|
+
const mediaType = MEDIA_TYPES[extname2(screenshot).toLowerCase()];
|
|
3479
|
+
if (!mediaType) continue;
|
|
3480
|
+
if (!cache2.has(screenshot)) {
|
|
3481
|
+
const abs = resolve5(projectRoot, screenshot);
|
|
3482
|
+
if (!existsSync7(abs)) {
|
|
3483
|
+
cache2.set(screenshot, null);
|
|
3484
|
+
} else {
|
|
3485
|
+
const buf = readFileSync7(abs);
|
|
3486
|
+
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const image = cache2.get(screenshot);
|
|
3490
|
+
if (image) req.image = image;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision) {
|
|
3494
|
+
if (supportsVision) {
|
|
3495
|
+
attachScreenshots(reqs, state, projectRoot);
|
|
3496
|
+
return { skipped: 0 };
|
|
3497
|
+
}
|
|
3498
|
+
const keys = new Set(reqs.filter((r) => state.keys[r.key]?.screenshot).map((r) => r.key));
|
|
3499
|
+
return { skipped: keys.size };
|
|
3500
|
+
}
|
|
3501
|
+
var DEFAULT_LOCALE_CONCURRENCY = 3;
|
|
3502
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity) {
|
|
3503
|
+
if (!reqs.length) return [];
|
|
3504
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3505
|
+
for (const req of reqs) {
|
|
3506
|
+
let group = byLocale.get(req.targetLocale);
|
|
3507
|
+
if (!group) {
|
|
3508
|
+
group = [];
|
|
3509
|
+
byLocale.set(req.targetLocale, group);
|
|
3510
|
+
}
|
|
3511
|
+
group.push(req);
|
|
3512
|
+
}
|
|
3513
|
+
const localeBatches = [...byLocale.entries()].map(([locale, group]) => ({
|
|
3514
|
+
locale,
|
|
3515
|
+
batches: chunk(group, Math.max(1, batchSize))
|
|
3516
|
+
}));
|
|
3517
|
+
const jobs = [];
|
|
3518
|
+
const maxBatches = Math.max(...localeBatches.map((g) => g.batches.length));
|
|
3519
|
+
for (let i = 0; i < maxBatches; i++) {
|
|
3520
|
+
for (const g of localeBatches) {
|
|
3521
|
+
if (i < g.batches.length) jobs.push({ locale: g.locale, batch: g.batches[i] });
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const remaining = new Map(localeBatches.map((g) => [g.locale, g.batches.length]));
|
|
3525
|
+
const started = /* @__PURE__ */ new Set();
|
|
3526
|
+
const total = reqs.length;
|
|
3527
|
+
let done = 0;
|
|
3528
|
+
const allResults = [];
|
|
3529
|
+
let next = 0;
|
|
3530
|
+
async function worker() {
|
|
3531
|
+
while (next < jobs.length) {
|
|
3532
|
+
if (signal?.aborted) break;
|
|
3533
|
+
const { locale, batch } = jobs[next++];
|
|
3534
|
+
if (!started.has(locale)) {
|
|
3535
|
+
started.add(locale);
|
|
3536
|
+
hooks.onLocaleStart?.(locale);
|
|
3537
|
+
}
|
|
3538
|
+
const batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
3539
|
+
done += results.length;
|
|
3540
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
3541
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
3542
|
+
allResults.push(...batchResults);
|
|
3543
|
+
const left = remaining.get(locale) - 1;
|
|
3544
|
+
remaining.set(locale, left);
|
|
3545
|
+
if (left === 0 && !signal?.aborted) hooks.onLocaleDone?.(locale);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
const workers = Array.from({ length: Math.min(concurrency, jobs.length) }, worker);
|
|
3549
|
+
await Promise.all(workers);
|
|
3550
|
+
return allResults;
|
|
3551
|
+
}
|
|
3552
|
+
function applyResults(state, reqs, results, clock = systemClock, force = false) {
|
|
3553
|
+
const byId = new Map(reqs.map((r) => [r.id, r]));
|
|
3554
|
+
let written = 0;
|
|
3555
|
+
const errors = [];
|
|
3556
|
+
for (const res of results) {
|
|
3557
|
+
const req = byId.get(res.id);
|
|
3558
|
+
if (!req) continue;
|
|
3559
|
+
if (req.plural) {
|
|
3560
|
+
if (res.error || res.forms === void 0) {
|
|
3561
|
+
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
if (applyMachineTranslationForms(state, req.key, req.targetLocale, res.forms, clock, force)) written++;
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
if (res.translation === void 0) {
|
|
3568
|
+
errors.push({ key: req.key, locale: req.targetLocale, error: res.error ?? "no translation" });
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3571
|
+
if (res.error) errors.push({ key: req.key, locale: req.targetLocale, error: res.error });
|
|
3572
|
+
if (applyMachineTranslation(state, req.key, req.targetLocale, res.translation, clock, force)) written++;
|
|
3573
|
+
}
|
|
3574
|
+
return { written, errors };
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// src/server/ai/pricing.ts
|
|
3578
|
+
var PRICE_TABLE = [
|
|
3579
|
+
["claude-fable-5", 10, 50],
|
|
3580
|
+
["claude-mythos-5", 10, 50],
|
|
3581
|
+
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
3582
|
+
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
3583
|
+
["claude-opus-4-1", 15, 75],
|
|
3584
|
+
["claude-opus-4-0", 15, 75],
|
|
3585
|
+
["claude-opus-4-2025", 15, 75],
|
|
3586
|
+
["claude-opus-4", 5, 25],
|
|
3587
|
+
["claude-sonnet-4", 3, 15],
|
|
3588
|
+
["claude-haiku-4", 1, 5],
|
|
3589
|
+
["claude-3-5-haiku", 0.8, 4],
|
|
3590
|
+
["gpt-5.5-pro", 30, 180],
|
|
3591
|
+
["gpt-5.5", 5, 30],
|
|
3592
|
+
["gpt-5.4-pro", 30, 180],
|
|
3593
|
+
["gpt-5.4-mini", 0.75, 4.5],
|
|
3594
|
+
["gpt-5.4-nano", 0.2, 1.25],
|
|
3595
|
+
["gpt-5.4", 2.5, 15],
|
|
3596
|
+
["gpt-5.3-codex", 1.75, 14]
|
|
3597
|
+
];
|
|
3598
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
3599
|
+
function bareModelId(model) {
|
|
3600
|
+
let id = model.trim().toLowerCase();
|
|
3601
|
+
const slash = id.lastIndexOf("/");
|
|
3602
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
3603
|
+
const anth = id.lastIndexOf("anthropic.");
|
|
3604
|
+
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3605
|
+
return id;
|
|
3606
|
+
}
|
|
3607
|
+
function resolvePricing(ai) {
|
|
3608
|
+
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3609
|
+
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3610
|
+
}
|
|
3611
|
+
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3612
|
+
const id = bareModelId(ai.model);
|
|
3613
|
+
let best;
|
|
3614
|
+
for (const row of PRICE_TABLE) {
|
|
3615
|
+
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
3616
|
+
}
|
|
3617
|
+
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/server/ai/estimate.ts
|
|
3621
|
+
var CJK_RE = /[ -鿿가-豈-]/g;
|
|
3622
|
+
function estimateTokens(text) {
|
|
3623
|
+
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
3624
|
+
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
3625
|
+
}
|
|
3626
|
+
var EXPANSION = 1.2;
|
|
3627
|
+
var ITEM_REPLY_OVERHEAD = 16;
|
|
3628
|
+
var FORM_REPLY_OVERHEAD = 8;
|
|
3629
|
+
function estimateOutputTokens(req) {
|
|
3630
|
+
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
3631
|
+
if (req.plural) {
|
|
3676
3632
|
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
3677
3633
|
}
|
|
3678
3634
|
return ITEM_REPLY_OVERHEAD + translated;
|
|
@@ -3736,7 +3692,7 @@ function readLog(projectRoot, limit = 100) {
|
|
|
3736
3692
|
import { relative as relative3 } from "path";
|
|
3737
3693
|
|
|
3738
3694
|
// src/server/import/detect.ts
|
|
3739
|
-
import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
3695
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
|
|
3740
3696
|
import { join as join4 } from "path";
|
|
3741
3697
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3742
3698
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
@@ -3825,41 +3781,163 @@ function detectApple(root) {
|
|
|
3825
3781
|
}
|
|
3826
3782
|
return best;
|
|
3827
3783
|
}
|
|
3828
|
-
var
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
if (
|
|
3784
|
+
var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
3785
|
+
function detectAngularXliff(root) {
|
|
3786
|
+
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
3787
|
+
const localeRoot = rel === "." ? root : join4(root, rel);
|
|
3788
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
3789
|
+
const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
3790
|
+
if (files.length === 0) continue;
|
|
3791
|
+
const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
3792
|
+
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
3793
|
+
let sourceLocale;
|
|
3794
|
+
try {
|
|
3795
|
+
sourceLocale = readFileSync9(join4(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
3796
|
+
} catch {
|
|
3797
|
+
}
|
|
3798
|
+
if (!sourceLocale && locales.length === 0) continue;
|
|
3799
|
+
sourceLocale ??= pickSource(locales, () => 0);
|
|
3800
|
+
if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
|
|
3801
|
+
return { format: "angular-xliff", localeRoot, locales, sourceLocale };
|
|
3845
3802
|
}
|
|
3846
3803
|
return null;
|
|
3847
3804
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3805
|
+
function detectRails(root) {
|
|
3806
|
+
const localeRoot = join4(root, "config", "locales");
|
|
3807
|
+
if (!safeIsDir(localeRoot)) return null;
|
|
3808
|
+
const locales = [];
|
|
3809
|
+
for (const file of readdirSync3(localeRoot).sort()) {
|
|
3810
|
+
if (!/\.ya?ml$/.test(file)) continue;
|
|
3811
|
+
let text;
|
|
3812
|
+
try {
|
|
3813
|
+
text = readFileSync9(join4(localeRoot, file), "utf8");
|
|
3814
|
+
} catch {
|
|
3815
|
+
continue;
|
|
3816
|
+
}
|
|
3817
|
+
for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
|
|
3818
|
+
const token = m[2];
|
|
3819
|
+
if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
if (locales.length === 0) return null;
|
|
3823
|
+
return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3824
|
+
}
|
|
3825
|
+
var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
3826
|
+
function detectI18next(root) {
|
|
3827
|
+
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
3828
|
+
const localeRoot = join4(root, rel);
|
|
3829
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
3830
|
+
const locales = listDirs(localeRoot).filter(
|
|
3831
|
+
(d) => LOCALE_RE.test(d) && readdirSync3(join4(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
3832
|
+
);
|
|
3833
|
+
if (locales.length === 0) continue;
|
|
3834
|
+
const sourceLocale = pickSource(locales, (loc) => {
|
|
3835
|
+
try {
|
|
3836
|
+
return readdirSync3(join4(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join4(localeRoot, loc, f)).size, 0);
|
|
3837
|
+
} catch {
|
|
3838
|
+
return 0;
|
|
3839
|
+
}
|
|
3840
|
+
});
|
|
3841
|
+
return { format: "i18next-json", localeRoot, locales, sourceLocale };
|
|
3842
|
+
}
|
|
3843
|
+
return null;
|
|
3844
|
+
}
|
|
3845
|
+
function gettextLocales(dir) {
|
|
3846
|
+
const locales = [];
|
|
3847
|
+
for (const entry of readdirSync3(dir).sort()) {
|
|
3848
|
+
const flat = entry.match(/^(.+)\.po$/)?.[1];
|
|
3849
|
+
if (flat && LOCALE_RE.test(flat)) {
|
|
3850
|
+
if (!locales.includes(flat)) locales.push(flat);
|
|
3851
|
+
continue;
|
|
3852
|
+
}
|
|
3853
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join4(dir, entry))) continue;
|
|
3854
|
+
const sub = join4(dir, entry);
|
|
3855
|
+
const hasPo = (d) => {
|
|
3856
|
+
try {
|
|
3857
|
+
return readdirSync3(d).some((f) => f.endsWith(".po"));
|
|
3858
|
+
} catch {
|
|
3859
|
+
return false;
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
if (hasPo(join4(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
3863
|
+
if (!locales.includes(entry)) locales.push(entry);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return locales;
|
|
3867
|
+
}
|
|
3868
|
+
var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
3869
|
+
function detectGettext(root) {
|
|
3870
|
+
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
3871
|
+
const localeRoot = join4(root, rel);
|
|
3872
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
3873
|
+
const locales = gettextLocales(localeRoot);
|
|
3874
|
+
if (locales.length === 0) continue;
|
|
3875
|
+
return { format: "gettext-po", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3876
|
+
}
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
function detectAppleStringsdict(root) {
|
|
3880
|
+
const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
|
|
3881
|
+
let best = null;
|
|
3882
|
+
for (const dir of candidates) {
|
|
3883
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync9(join4(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
3884
|
+
if (locales.length === 0) continue;
|
|
3885
|
+
if (!best || locales.length > best.locales.length) {
|
|
3886
|
+
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
return best;
|
|
3890
|
+
}
|
|
3891
|
+
var DETECTORS = [
|
|
3892
|
+
detectLaravel,
|
|
3893
|
+
detectVue,
|
|
3894
|
+
detectArb,
|
|
3895
|
+
detectApple,
|
|
3896
|
+
detectAngularXliff,
|
|
3897
|
+
detectRails,
|
|
3898
|
+
detectI18next,
|
|
3899
|
+
detectGettext,
|
|
3900
|
+
detectAppleStringsdict
|
|
3901
|
+
];
|
|
3902
|
+
var BY_FORMAT = {
|
|
3903
|
+
"laravel-php": detectLaravel,
|
|
3904
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3905
|
+
"flutter-arb": detectArb,
|
|
3906
|
+
"apple-strings": detectApple,
|
|
3907
|
+
"angular-xliff": detectAngularXliff,
|
|
3908
|
+
"rails-yaml": detectRails,
|
|
3909
|
+
"i18next-json": detectI18next,
|
|
3910
|
+
"gettext-po": detectGettext,
|
|
3911
|
+
"apple-stringsdict": detectAppleStringsdict
|
|
3912
|
+
};
|
|
3913
|
+
function detect(root, formatOverride) {
|
|
3914
|
+
if (!existsSync9(root)) return null;
|
|
3915
|
+
if (formatOverride) {
|
|
3916
|
+
const fn = BY_FORMAT[formatOverride];
|
|
3917
|
+
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
3918
|
+
return fn(root);
|
|
3919
|
+
}
|
|
3920
|
+
for (const fn of DETECTORS) {
|
|
3921
|
+
const d = fn(root);
|
|
3922
|
+
if (d) return d;
|
|
3923
|
+
}
|
|
3924
|
+
return null;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
// src/server/import/parsers/vue-i18n-json.ts
|
|
3928
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
|
|
3929
|
+
import { join as join5 } from "path";
|
|
3930
|
+
|
|
3931
|
+
// src/server/import/flatten.ts
|
|
3932
|
+
function flattenObject(value, prefix, warnings) {
|
|
3933
|
+
const out = {};
|
|
3934
|
+
const walk = (node, path) => {
|
|
3935
|
+
if (typeof node === "string") {
|
|
3936
|
+
out[path] = node;
|
|
3937
|
+
} else if (typeof node === "number" || typeof node === "boolean") {
|
|
3938
|
+
out[path] = String(node);
|
|
3939
|
+
} else if (Array.isArray(node)) {
|
|
3940
|
+
node.forEach((el, i) => walk(el, path ? `${path}.${i}` : String(i)));
|
|
3863
3941
|
} else if (node && typeof node === "object") {
|
|
3864
3942
|
for (const [k, v] of Object.entries(node)) {
|
|
3865
3943
|
walk(v, path ? `${path}.${k}` : k);
|
|
@@ -3887,7 +3965,7 @@ var vueI18nJson2 = {
|
|
|
3887
3965
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3888
3966
|
let data;
|
|
3889
3967
|
try {
|
|
3890
|
-
data = JSON.parse(
|
|
3968
|
+
data = JSON.parse(readFileSync10(join5(localeRoot, file), "utf8"));
|
|
3891
3969
|
} catch (e) {
|
|
3892
3970
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
3893
3971
|
continue;
|
|
@@ -3927,240 +4005,970 @@ function listPhpFiles(dir) {
|
|
|
3927
4005
|
walk(dir);
|
|
3928
4006
|
return out.sort();
|
|
3929
4007
|
}
|
|
3930
|
-
var PHP_READ_ALL = '$fs=array_filter(array_map("trim",explode("\\n",stream_get_contents(STDIN))),"strlen");$o=[];foreach($fs as $f){try{$o[$f]=require $f;}catch(\\Throwable $e){}}echo json_encode($o);';
|
|
3931
|
-
function readPhpArrays(files) {
|
|
3932
|
-
if (files.length === 0) return {};
|
|
3933
|
-
let stdout;
|
|
4008
|
+
var PHP_READ_ALL = '$fs=array_filter(array_map("trim",explode("\\n",stream_get_contents(STDIN))),"strlen");$o=[];foreach($fs as $f){try{$o[$f]=require $f;}catch(\\Throwable $e){}}echo json_encode($o);';
|
|
4009
|
+
function readPhpArrays(files) {
|
|
4010
|
+
if (files.length === 0) return {};
|
|
4011
|
+
let stdout;
|
|
4012
|
+
try {
|
|
4013
|
+
stdout = execFileSync("php", ["-r", PHP_READ_ALL], {
|
|
4014
|
+
input: files.join("\n"),
|
|
4015
|
+
encoding: "utf8",
|
|
4016
|
+
maxBuffer: 256 * 1024 * 1024
|
|
4017
|
+
});
|
|
4018
|
+
} catch (e) {
|
|
4019
|
+
const err = e;
|
|
4020
|
+
if (err.code === "ENOENT") {
|
|
4021
|
+
throw new Error("php is required to import Laravel PHP files but was not found on PATH");
|
|
4022
|
+
}
|
|
4023
|
+
throw new Error(`php failed to evaluate Laravel lang files: ${err.message}`);
|
|
4024
|
+
}
|
|
4025
|
+
return JSON.parse(stdout);
|
|
4026
|
+
}
|
|
4027
|
+
var laravelPhp2 = {
|
|
4028
|
+
name: "laravel-php",
|
|
4029
|
+
parse(localeRoot, opts) {
|
|
4030
|
+
const warnings = [];
|
|
4031
|
+
const keys = {};
|
|
4032
|
+
const locales = [];
|
|
4033
|
+
const entries = [];
|
|
4034
|
+
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4035
|
+
if (locale === "vendor") continue;
|
|
4036
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4037
|
+
const localeDir = join6(localeRoot, locale);
|
|
4038
|
+
locales.push(locale);
|
|
4039
|
+
for (const file of listPhpFiles(localeDir)) {
|
|
4040
|
+
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
4041
|
+
entries.push({ locale, group, file });
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
const data = readPhpArrays(entries.map((e) => e.file));
|
|
4045
|
+
for (const { locale, group, file } of entries) {
|
|
4046
|
+
if (!(file in data)) {
|
|
4047
|
+
warnings.push(`laravel-php: failed to read ${file}`);
|
|
4048
|
+
continue;
|
|
4049
|
+
}
|
|
4050
|
+
for (const [inner, value] of Object.entries(flattenObject(data[file], "", warnings))) {
|
|
4051
|
+
const key = `${group}.${inner}`;
|
|
4052
|
+
(keys[key] ??= { values: {} }).values[locale] = laravelToCanonical(value);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
return { locales, keys, warnings };
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
|
|
4059
|
+
// src/server/import/parsers/flutter-arb.ts
|
|
4060
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
|
|
4061
|
+
import { join as join7 } from "path";
|
|
4062
|
+
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4063
|
+
function localeFromArbName(file) {
|
|
4064
|
+
const m = file.match(/^(.+)\.arb$/);
|
|
4065
|
+
if (!m) return null;
|
|
4066
|
+
let locale = m[1];
|
|
4067
|
+
if (locale.startsWith("app_")) locale = locale.slice(4);
|
|
4068
|
+
return LOCALE_RE3.test(locale) ? locale : null;
|
|
4069
|
+
}
|
|
4070
|
+
function placeholderMeta(raw) {
|
|
4071
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
4072
|
+
const out = {};
|
|
4073
|
+
for (const [name, def] of Object.entries(raw)) {
|
|
4074
|
+
if (!def || typeof def !== "object") continue;
|
|
4075
|
+
const o = def;
|
|
4076
|
+
const d = {};
|
|
4077
|
+
if (typeof o.type === "string") d.type = o.type;
|
|
4078
|
+
if (typeof o.format === "string") d.format = o.format;
|
|
4079
|
+
if (typeof o.example === "string") d.example = o.example;
|
|
4080
|
+
if (Object.keys(d).length) out[name] = d;
|
|
4081
|
+
}
|
|
4082
|
+
return Object.keys(out).length ? out : void 0;
|
|
4083
|
+
}
|
|
4084
|
+
var flutterArb2 = {
|
|
4085
|
+
name: "flutter-arb",
|
|
4086
|
+
parse(localeRoot, opts) {
|
|
4087
|
+
const warnings = [];
|
|
4088
|
+
const keys = {};
|
|
4089
|
+
const locales = [];
|
|
4090
|
+
for (const file of readdirSync6(localeRoot).sort()) {
|
|
4091
|
+
if (!file.endsWith(".arb")) continue;
|
|
4092
|
+
const locale = localeFromArbName(file);
|
|
4093
|
+
if (!locale) continue;
|
|
4094
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4095
|
+
let data;
|
|
4096
|
+
try {
|
|
4097
|
+
data = JSON.parse(readFileSync11(join7(localeRoot, file), "utf8"));
|
|
4098
|
+
} catch (e) {
|
|
4099
|
+
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
4102
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
4103
|
+
for (const [key, value] of Object.entries(data)) {
|
|
4104
|
+
if (key.startsWith("@@")) continue;
|
|
4105
|
+
if (key.startsWith("@")) {
|
|
4106
|
+
const meta = placeholderMeta(value?.placeholders);
|
|
4107
|
+
if (meta) (keys[key.slice(1)] ??= { values: {} }).placeholders = meta;
|
|
4108
|
+
continue;
|
|
4109
|
+
}
|
|
4110
|
+
if (typeof value !== "string") {
|
|
4111
|
+
warnings.push(`flutter-arb: skipped non-string ${file}:${key}`);
|
|
4112
|
+
continue;
|
|
4113
|
+
}
|
|
4114
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
return { locales, keys, warnings };
|
|
4118
|
+
}
|
|
4119
|
+
};
|
|
4120
|
+
|
|
4121
|
+
// src/server/import/parsers/apple-strings.ts
|
|
4122
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
4123
|
+
import { join as join8 } from "path";
|
|
4124
|
+
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4125
|
+
var TABLE = "Localizable.strings";
|
|
4126
|
+
function localeFromLproj(dir) {
|
|
4127
|
+
const m = dir.match(/^(.+)\.lproj$/);
|
|
4128
|
+
if (!m) return null;
|
|
4129
|
+
return LOCALE_RE4.test(m[1]) ? m[1] : null;
|
|
4130
|
+
}
|
|
4131
|
+
function unescape(body) {
|
|
4132
|
+
return body.replace(/\\(U[0-9a-fA-F]{4}|u[0-9a-fA-F]{4}|.)/g, (_m, esc) => {
|
|
4133
|
+
const c = esc[0];
|
|
4134
|
+
if (c === "U" || c === "u") return String.fromCharCode(parseInt(esc.slice(1), 16));
|
|
4135
|
+
if (c === "n") return "\n";
|
|
4136
|
+
if (c === "t") return " ";
|
|
4137
|
+
if (c === "r") return "\r";
|
|
4138
|
+
return esc;
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
4141
|
+
function parseStrings(text, file, warnings) {
|
|
4142
|
+
const pairs = [];
|
|
4143
|
+
let i = 0;
|
|
4144
|
+
const n = text.length;
|
|
4145
|
+
const skipTrivia = () => {
|
|
4146
|
+
while (i < n) {
|
|
4147
|
+
const c = text[i];
|
|
4148
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
4149
|
+
i++;
|
|
4150
|
+
continue;
|
|
4151
|
+
}
|
|
4152
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
4153
|
+
i += 2;
|
|
4154
|
+
while (i < n && text[i] !== "\n") i++;
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
4158
|
+
i += 2;
|
|
4159
|
+
while (i < n && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
4160
|
+
i += 2;
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
break;
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
const readToken = () => {
|
|
4167
|
+
if (i >= n) return null;
|
|
4168
|
+
if (text[i] === '"') {
|
|
4169
|
+
i++;
|
|
4170
|
+
let raw2 = "";
|
|
4171
|
+
while (i < n) {
|
|
4172
|
+
const c = text[i];
|
|
4173
|
+
if (c === "\\") {
|
|
4174
|
+
raw2 += c + (text[i + 1] ?? "");
|
|
4175
|
+
i += 2;
|
|
4176
|
+
continue;
|
|
4177
|
+
}
|
|
4178
|
+
if (c === '"') {
|
|
4179
|
+
i++;
|
|
4180
|
+
return unescape(raw2);
|
|
4181
|
+
}
|
|
4182
|
+
raw2 += c;
|
|
4183
|
+
i++;
|
|
4184
|
+
}
|
|
4185
|
+
return null;
|
|
4186
|
+
}
|
|
4187
|
+
let raw = "";
|
|
4188
|
+
while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
|
|
4189
|
+
return raw.length ? raw : null;
|
|
4190
|
+
};
|
|
4191
|
+
while (true) {
|
|
4192
|
+
skipTrivia();
|
|
4193
|
+
if (i >= n) break;
|
|
4194
|
+
const key = readToken();
|
|
4195
|
+
if (key === null) {
|
|
4196
|
+
warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
|
|
4197
|
+
break;
|
|
4198
|
+
}
|
|
4199
|
+
skipTrivia();
|
|
4200
|
+
if (text[i] !== "=") {
|
|
4201
|
+
warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
|
|
4202
|
+
break;
|
|
4203
|
+
}
|
|
4204
|
+
i++;
|
|
4205
|
+
skipTrivia();
|
|
4206
|
+
const value = readToken();
|
|
4207
|
+
if (value === null) {
|
|
4208
|
+
warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
|
|
4209
|
+
break;
|
|
4210
|
+
}
|
|
4211
|
+
skipTrivia();
|
|
4212
|
+
if (text[i] === ";") i++;
|
|
4213
|
+
pairs.push({ key, value });
|
|
4214
|
+
}
|
|
4215
|
+
return pairs;
|
|
4216
|
+
}
|
|
4217
|
+
var appleStrings2 = {
|
|
4218
|
+
name: "apple-strings",
|
|
4219
|
+
parse(localeRoot, opts) {
|
|
4220
|
+
const warnings = [];
|
|
4221
|
+
const keys = {};
|
|
4222
|
+
const locales = [];
|
|
4223
|
+
for (const dir of readdirSync7(localeRoot).sort()) {
|
|
4224
|
+
const locale = localeFromLproj(dir);
|
|
4225
|
+
if (!locale) continue;
|
|
4226
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4227
|
+
const file = join8(localeRoot, dir, TABLE);
|
|
4228
|
+
let text;
|
|
4229
|
+
try {
|
|
4230
|
+
if (!statSync4(file).isFile()) continue;
|
|
4231
|
+
text = readFileSync12(file, "utf8");
|
|
4232
|
+
} catch {
|
|
4233
|
+
continue;
|
|
4234
|
+
}
|
|
4235
|
+
locales.push(locale);
|
|
4236
|
+
const others = readdirSync7(join8(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
4237
|
+
if (others.length) {
|
|
4238
|
+
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
4239
|
+
}
|
|
4240
|
+
for (const { key, value } of parseStrings(text, file, warnings)) {
|
|
4241
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
return { locales, keys, warnings };
|
|
4245
|
+
}
|
|
4246
|
+
};
|
|
4247
|
+
|
|
4248
|
+
// src/server/import/parsers/angular-xliff.ts
|
|
4249
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync13 } from "fs";
|
|
4250
|
+
import { join as join9 } from "path";
|
|
4251
|
+
var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4252
|
+
var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4253
|
+
function decodeEntities(s) {
|
|
4254
|
+
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
4255
|
+
}
|
|
4256
|
+
function parseAttrs(s) {
|
|
4257
|
+
const out = {};
|
|
4258
|
+
for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
|
|
4259
|
+
return out;
|
|
4260
|
+
}
|
|
4261
|
+
function decodeInline(raw, addMeta) {
|
|
4262
|
+
let out = "";
|
|
4263
|
+
let last = 0;
|
|
4264
|
+
for (const m of raw.matchAll(/<x\b([^>]*?)\/>/g)) {
|
|
4265
|
+
out += decodeEntities(raw.slice(last, m.index));
|
|
4266
|
+
const attrs = parseAttrs(m[1]);
|
|
4267
|
+
const id = attrs["id"] ?? "X";
|
|
4268
|
+
const equiv = attrs["equiv-text"];
|
|
4269
|
+
const simple = equiv?.match(/^\{\{\s*(\w+)\s*\}\}$/);
|
|
4270
|
+
if (simple) {
|
|
4271
|
+
out += `{${simple[1]}}`;
|
|
4272
|
+
} else {
|
|
4273
|
+
out += `{${id}}`;
|
|
4274
|
+
const meta = {};
|
|
4275
|
+
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4276
|
+
if (equiv !== void 0) meta.example = equiv;
|
|
4277
|
+
addMeta(id, meta);
|
|
4278
|
+
}
|
|
4279
|
+
last = m.index + m[0].length;
|
|
4280
|
+
}
|
|
4281
|
+
return out + decodeEntities(raw.slice(last));
|
|
4282
|
+
}
|
|
4283
|
+
var angularXliff2 = {
|
|
4284
|
+
name: "angular-xliff",
|
|
4285
|
+
parse(localeRoot, opts) {
|
|
4286
|
+
const warnings = [];
|
|
4287
|
+
const keys = {};
|
|
4288
|
+
const locales = [];
|
|
4289
|
+
const seen = (loc) => {
|
|
4290
|
+
if (!locales.includes(loc)) locales.push(loc);
|
|
4291
|
+
};
|
|
4292
|
+
const files = readdirSync8(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
|
|
4293
|
+
for (const file of files) {
|
|
4294
|
+
const fnameLocale = file.match(FILE_RE)[1];
|
|
4295
|
+
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4296
|
+
let xml;
|
|
4297
|
+
try {
|
|
4298
|
+
xml = readFileSync13(join9(localeRoot, file), "utf8");
|
|
4299
|
+
} catch (e) {
|
|
4300
|
+
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4301
|
+
continue;
|
|
4302
|
+
}
|
|
4303
|
+
const sourceLocale = xml.match(/source-language="([^"]+)"/)?.[1];
|
|
4304
|
+
if (!sourceLocale) {
|
|
4305
|
+
warnings.push(`angular-xliff: ${file} has no source-language attribute; skipped`);
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
const targetLocale = xml.match(/target-language="([^"]+)"/)?.[1] ?? fnameLocale;
|
|
4309
|
+
if (opts?.locales && !opts.locales.includes(targetLocale ?? sourceLocale)) continue;
|
|
4310
|
+
for (const unit of xml.matchAll(/<trans-unit\b([^>]*)>([\s\S]*?)<\/trans-unit>/g)) {
|
|
4311
|
+
const id = parseAttrs(unit[1])["id"];
|
|
4312
|
+
if (!id) {
|
|
4313
|
+
warnings.push(`angular-xliff: ${file} has a trans-unit without an id; skipped`);
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
const body = unit[2];
|
|
4317
|
+
const src = body.match(/<source\b[^>]*>([\s\S]*?)<\/source>/);
|
|
4318
|
+
let tgt = body.match(/<target\b([^>]*)>([\s\S]*?)<\/target>/);
|
|
4319
|
+
if (tgt && /\bstate="new"/.test(tgt[1])) tgt = null;
|
|
4320
|
+
const entry = keys[id] ??= { values: {} };
|
|
4321
|
+
const addMeta = (name, meta) => {
|
|
4322
|
+
(entry.placeholders ??= {})[name] ??= meta;
|
|
4323
|
+
};
|
|
4324
|
+
if (src && entry.values[sourceLocale] === void 0) {
|
|
4325
|
+
entry.values[sourceLocale] = decodeInline(src[1], addMeta);
|
|
4326
|
+
seen(sourceLocale);
|
|
4327
|
+
}
|
|
4328
|
+
if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
|
|
4329
|
+
entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
|
|
4330
|
+
seen(targetLocale);
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
return { locales, keys, warnings };
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
|
|
4338
|
+
// src/server/import/parsers/gettext-po.ts
|
|
4339
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync14 } from "fs";
|
|
4340
|
+
import { join as join10 } from "path";
|
|
4341
|
+
var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4342
|
+
var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4343
|
+
var CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
4344
|
+
function unescapePo(s) {
|
|
4345
|
+
return s.replace(
|
|
4346
|
+
/\\([\\"ntr])/g,
|
|
4347
|
+
(_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
|
|
4348
|
+
);
|
|
4349
|
+
}
|
|
4350
|
+
function parseEntries(text) {
|
|
4351
|
+
const entries = [];
|
|
4352
|
+
let cur = null;
|
|
4353
|
+
let append = null;
|
|
4354
|
+
const flush = () => {
|
|
4355
|
+
if (cur && cur.msgid !== void 0) entries.push(cur);
|
|
4356
|
+
cur = null;
|
|
4357
|
+
append = null;
|
|
4358
|
+
};
|
|
4359
|
+
for (const line of text.split("\n")) {
|
|
4360
|
+
if (line.trim() === "") {
|
|
4361
|
+
flush();
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
if (line.startsWith("#")) continue;
|
|
4365
|
+
const m = line.match(DIRECTIVE_RE);
|
|
4366
|
+
if (m) {
|
|
4367
|
+
const kw = m[1];
|
|
4368
|
+
const idx = m[2];
|
|
4369
|
+
const body = unescapePo(m[3]);
|
|
4370
|
+
if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
|
|
4371
|
+
cur ??= { plurals: /* @__PURE__ */ new Map() };
|
|
4372
|
+
const entry = cur;
|
|
4373
|
+
if (kw === "msgctxt") {
|
|
4374
|
+
entry.msgctxt = body;
|
|
4375
|
+
append = (c) => {
|
|
4376
|
+
entry.msgctxt = (entry.msgctxt ?? "") + c;
|
|
4377
|
+
};
|
|
4378
|
+
} else if (kw === "msgid") {
|
|
4379
|
+
entry.msgid = body;
|
|
4380
|
+
append = (c) => {
|
|
4381
|
+
entry.msgid = (entry.msgid ?? "") + c;
|
|
4382
|
+
};
|
|
4383
|
+
} else if (kw === "msgid_plural") {
|
|
4384
|
+
entry.msgidPlural = body;
|
|
4385
|
+
append = (c) => {
|
|
4386
|
+
entry.msgidPlural = (entry.msgidPlural ?? "") + c;
|
|
4387
|
+
};
|
|
4388
|
+
} else if (idx !== void 0) {
|
|
4389
|
+
const i = Number(idx);
|
|
4390
|
+
entry.plurals.set(i, body);
|
|
4391
|
+
append = (c) => {
|
|
4392
|
+
entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
|
|
4393
|
+
};
|
|
4394
|
+
} else {
|
|
4395
|
+
entry.msgstr = body;
|
|
4396
|
+
append = (c) => {
|
|
4397
|
+
entry.msgstr = (entry.msgstr ?? "") + c;
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
const cont = line.match(CONT_RE);
|
|
4403
|
+
if (cont && append) append(unescapePo(cont[1]));
|
|
4404
|
+
}
|
|
4405
|
+
flush();
|
|
4406
|
+
return entries;
|
|
4407
|
+
}
|
|
4408
|
+
function discoverPoFiles(root) {
|
|
4409
|
+
const found = [];
|
|
4410
|
+
const entries = readdirSync9(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
4411
|
+
for (const e of entries) {
|
|
4412
|
+
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4413
|
+
const base = e.name.slice(0, -3);
|
|
4414
|
+
found.push({ path: join10(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4415
|
+
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4416
|
+
for (const sub of [join10(e.name, "LC_MESSAGES"), e.name]) {
|
|
4417
|
+
let names;
|
|
4418
|
+
try {
|
|
4419
|
+
names = readdirSync9(join10(root, sub)).sort();
|
|
4420
|
+
} catch {
|
|
4421
|
+
continue;
|
|
4422
|
+
}
|
|
4423
|
+
for (const f of names) {
|
|
4424
|
+
if (f.endsWith(".po")) found.push({ path: join10(root, sub, f), rel: join10(sub, f), locale: e.name });
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
return found;
|
|
4430
|
+
}
|
|
4431
|
+
var gettextPo2 = {
|
|
4432
|
+
name: "gettext-po",
|
|
4433
|
+
parse(localeRoot, opts) {
|
|
4434
|
+
const warnings = [];
|
|
4435
|
+
const keys = {};
|
|
4436
|
+
const locales = [];
|
|
4437
|
+
for (const file of discoverPoFiles(localeRoot)) {
|
|
4438
|
+
let entries;
|
|
4439
|
+
try {
|
|
4440
|
+
entries = parseEntries(readFileSync14(file.path, "utf8"));
|
|
4441
|
+
} catch (e) {
|
|
4442
|
+
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
|
|
4446
|
+
const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
|
|
4447
|
+
const locale = file.locale ?? headerLang;
|
|
4448
|
+
if (!locale) {
|
|
4449
|
+
warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
|
|
4450
|
+
continue;
|
|
4451
|
+
}
|
|
4452
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4453
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
4454
|
+
const cats = categoriesFor(locale);
|
|
4455
|
+
for (const entry of entries) {
|
|
4456
|
+
if (entry === header) continue;
|
|
4457
|
+
const key = entry.msgctxt ?? entry.msgid;
|
|
4458
|
+
if (!key) continue;
|
|
4459
|
+
if (entry.msgidPlural !== void 0) {
|
|
4460
|
+
const forms = {};
|
|
4461
|
+
for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
|
|
4462
|
+
if (body === "") continue;
|
|
4463
|
+
const cat = cats[i];
|
|
4464
|
+
if (!cat) {
|
|
4465
|
+
warnings.push(
|
|
4466
|
+
`gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
|
|
4467
|
+
);
|
|
4468
|
+
continue;
|
|
4469
|
+
}
|
|
4470
|
+
forms[cat] = body.split("%d").join("{count}");
|
|
4471
|
+
}
|
|
4472
|
+
if (!forms.other) continue;
|
|
4473
|
+
(keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
|
|
4474
|
+
} else {
|
|
4475
|
+
if (!entry.msgstr) continue;
|
|
4476
|
+
(keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
return { locales, keys, warnings };
|
|
4481
|
+
}
|
|
4482
|
+
};
|
|
4483
|
+
|
|
4484
|
+
// src/server/import/parsers/i18next-json.ts
|
|
4485
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
|
|
4486
|
+
import { join as join11 } from "path";
|
|
4487
|
+
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4488
|
+
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
4489
|
+
var PLURAL_ARG = "count";
|
|
4490
|
+
var DEFAULT_NAMESPACE = "translation";
|
|
4491
|
+
function safeIsDir2(p) {
|
|
4492
|
+
try {
|
|
4493
|
+
return statSync5(p).isDirectory();
|
|
4494
|
+
} catch {
|
|
4495
|
+
return false;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
function fromI18next(value) {
|
|
4499
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
4500
|
+
return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
|
|
4501
|
+
}
|
|
4502
|
+
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
4503
|
+
let data;
|
|
3934
4504
|
try {
|
|
3935
|
-
|
|
3936
|
-
input: files.join("\n"),
|
|
3937
|
-
encoding: "utf8",
|
|
3938
|
-
maxBuffer: 256 * 1024 * 1024
|
|
3939
|
-
});
|
|
4505
|
+
data = JSON.parse(readFileSync15(path, "utf8"));
|
|
3940
4506
|
} catch (e) {
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4507
|
+
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
4508
|
+
return false;
|
|
4509
|
+
}
|
|
4510
|
+
const fileWarnings = [];
|
|
4511
|
+
const flat = flattenObject(data, "", fileWarnings);
|
|
4512
|
+
for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
|
|
4513
|
+
const families = /* @__PURE__ */ new Set();
|
|
4514
|
+
for (const [k, v] of Object.entries(flat)) {
|
|
4515
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
4516
|
+
if (m && m[2] === "other" && v !== "") families.add(m[1]);
|
|
4517
|
+
}
|
|
4518
|
+
const pluralForms = {};
|
|
4519
|
+
for (const [k, raw] of Object.entries(flat)) {
|
|
4520
|
+
if (raw === "") continue;
|
|
4521
|
+
const value = fromI18next(raw);
|
|
4522
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
4523
|
+
if (m && families.has(m[1])) {
|
|
4524
|
+
(pluralForms[m[1]] ??= {})[m[2]] = value;
|
|
4525
|
+
continue;
|
|
3944
4526
|
}
|
|
3945
|
-
|
|
4527
|
+
if (families.has(k)) {
|
|
4528
|
+
warnings.push(
|
|
4529
|
+
`i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
|
|
4530
|
+
);
|
|
4531
|
+
continue;
|
|
4532
|
+
}
|
|
4533
|
+
(keys[prefix + k] ??= { values: {} }).values[locale] = value;
|
|
3946
4534
|
}
|
|
3947
|
-
|
|
4535
|
+
for (const [base, forms] of Object.entries(pluralForms)) {
|
|
4536
|
+
(keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
|
|
4537
|
+
}
|
|
4538
|
+
return true;
|
|
3948
4539
|
}
|
|
3949
|
-
var
|
|
3950
|
-
name: "
|
|
4540
|
+
var i18nextJson2 = {
|
|
4541
|
+
name: "i18next-json",
|
|
3951
4542
|
parse(localeRoot, opts) {
|
|
3952
4543
|
const warnings = [];
|
|
3953
4544
|
const keys = {};
|
|
3954
4545
|
const locales = [];
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
if (
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
4546
|
+
for (const entry of readdirSync10(localeRoot).sort()) {
|
|
4547
|
+
const full = join11(localeRoot, entry);
|
|
4548
|
+
if (safeIsDir2(full)) {
|
|
4549
|
+
if (!LOCALE_RE7.test(entry)) continue;
|
|
4550
|
+
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
4551
|
+
let any = false;
|
|
4552
|
+
for (const file of readdirSync10(full).sort()) {
|
|
4553
|
+
if (!file.endsWith(".json")) continue;
|
|
4554
|
+
const ns = file.slice(0, -".json".length);
|
|
4555
|
+
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
4556
|
+
if (ingestFile(join11(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
4557
|
+
}
|
|
4558
|
+
if (any && !locales.includes(entry)) locales.push(entry);
|
|
4559
|
+
} else if (entry.endsWith(".json")) {
|
|
4560
|
+
const locale = entry.slice(0, -".json".length);
|
|
4561
|
+
if (!LOCALE_RE7.test(locale)) continue;
|
|
4562
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4563
|
+
if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
|
|
4564
|
+
locales.push(locale);
|
|
4565
|
+
}
|
|
3964
4566
|
}
|
|
3965
4567
|
}
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
4568
|
+
return { locales, keys, warnings };
|
|
4569
|
+
}
|
|
4570
|
+
};
|
|
4571
|
+
|
|
4572
|
+
// src/server/import/parsers/rails-yaml.ts
|
|
4573
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync16 } from "fs";
|
|
4574
|
+
import { join as join12 } from "path";
|
|
4575
|
+
var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
4576
|
+
var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
4577
|
+
function fromRuby(value) {
|
|
4578
|
+
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
4579
|
+
}
|
|
4580
|
+
function makeNode() {
|
|
4581
|
+
return /* @__PURE__ */ Object.create(null);
|
|
4582
|
+
}
|
|
4583
|
+
function decodeDouble(body) {
|
|
4584
|
+
let out = "";
|
|
4585
|
+
for (let i = 0; i < body.length; i++) {
|
|
4586
|
+
const c = body[i];
|
|
4587
|
+
if (c !== "\\") {
|
|
4588
|
+
out += c;
|
|
4589
|
+
continue;
|
|
4590
|
+
}
|
|
4591
|
+
const n = body[++i];
|
|
4592
|
+
if (n === void 0) break;
|
|
4593
|
+
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
4594
|
+
}
|
|
4595
|
+
return out;
|
|
4596
|
+
}
|
|
4597
|
+
function scanQuoted(s, start) {
|
|
4598
|
+
const q = s[start];
|
|
4599
|
+
if (q === '"') {
|
|
4600
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
4601
|
+
if (s[i] === "\\") i++;
|
|
4602
|
+
else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
|
|
4603
|
+
}
|
|
4604
|
+
return null;
|
|
4605
|
+
}
|
|
4606
|
+
let out = "";
|
|
4607
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
4608
|
+
if (s[i] === "'") {
|
|
4609
|
+
if (s[i + 1] === "'") {
|
|
4610
|
+
out += "'";
|
|
4611
|
+
i++;
|
|
4612
|
+
} else {
|
|
4613
|
+
return { text: out, end: i + 1 };
|
|
4614
|
+
}
|
|
4615
|
+
} else {
|
|
4616
|
+
out += s[i];
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
return null;
|
|
4620
|
+
}
|
|
4621
|
+
function stripPlainComment(s) {
|
|
4622
|
+
const m = /(^|\s)#/.exec(s);
|
|
4623
|
+
return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
|
|
4624
|
+
}
|
|
4625
|
+
function onlyTrailing(s) {
|
|
4626
|
+
return /^\s*(#.*)?$/.test(s);
|
|
4627
|
+
}
|
|
4628
|
+
function parseYamlSubset(text, file, warnings) {
|
|
4629
|
+
const roots = {};
|
|
4630
|
+
const lines = text.split(/\r?\n/);
|
|
4631
|
+
let stack = [];
|
|
4632
|
+
let skipDeeperThan = null;
|
|
4633
|
+
let lastLeafIndent = null;
|
|
4634
|
+
for (let n = 0; n < lines.length; n++) {
|
|
4635
|
+
const raw = lines[n];
|
|
4636
|
+
const lineNo = n + 1;
|
|
4637
|
+
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
4638
|
+
if (raw.trim() === "---") continue;
|
|
4639
|
+
const indentMatch = /^[ \t]*/.exec(raw)[0];
|
|
4640
|
+
if (indentMatch.includes(" ")) {
|
|
4641
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
const indent = indentMatch.length;
|
|
4645
|
+
if (skipDeeperThan !== null) {
|
|
4646
|
+
if (indent > skipDeeperThan) continue;
|
|
4647
|
+
skipDeeperThan = null;
|
|
4648
|
+
}
|
|
4649
|
+
const content = raw.slice(indent);
|
|
4650
|
+
if (content.startsWith("- ") || content === "-") {
|
|
4651
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
|
|
4652
|
+
skipDeeperThan = indent;
|
|
4653
|
+
continue;
|
|
4654
|
+
}
|
|
4655
|
+
let key;
|
|
4656
|
+
let rest;
|
|
4657
|
+
if (content[0] === '"' || content[0] === "'") {
|
|
4658
|
+
const k = scanQuoted(content, 0);
|
|
4659
|
+
if (!k || content[k.end] !== ":") {
|
|
4660
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
|
|
4661
|
+
skipDeeperThan = indent;
|
|
3970
4662
|
continue;
|
|
3971
4663
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
4664
|
+
key = k.text;
|
|
4665
|
+
rest = content.slice(k.end + 1);
|
|
4666
|
+
} else {
|
|
4667
|
+
const m = /^(.*?):(?=\s|$)/.exec(content);
|
|
4668
|
+
if (!m) {
|
|
4669
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
|
|
4670
|
+
skipDeeperThan = indent;
|
|
4671
|
+
continue;
|
|
3975
4672
|
}
|
|
4673
|
+
key = m[1].trim();
|
|
4674
|
+
rest = content.slice(m[0].length);
|
|
4675
|
+
}
|
|
4676
|
+
if (lastLeafIndent !== null && indent > lastLeafIndent) {
|
|
4677
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
|
|
4678
|
+
skipDeeperThan = indent - 1;
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
4682
|
+
const trimmed = rest.trim();
|
|
4683
|
+
let value;
|
|
4684
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
4685
|
+
value = null;
|
|
4686
|
+
} else if (trimmed[0] === "&" || trimmed[0] === "*") {
|
|
4687
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
|
|
4688
|
+
skipDeeperThan = indent;
|
|
4689
|
+
continue;
|
|
4690
|
+
} else if (trimmed[0] === "|" || trimmed[0] === ">") {
|
|
4691
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
|
|
4692
|
+
skipDeeperThan = indent;
|
|
4693
|
+
continue;
|
|
4694
|
+
} else if (trimmed[0] === "[" || trimmed[0] === "{") {
|
|
4695
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
|
|
4696
|
+
skipDeeperThan = indent;
|
|
4697
|
+
continue;
|
|
4698
|
+
} else if (trimmed[0] === '"' || trimmed[0] === "'") {
|
|
4699
|
+
const v = scanQuoted(trimmed, 0);
|
|
4700
|
+
if (!v || !onlyTrailing(trimmed.slice(v.end))) {
|
|
4701
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
value = v.text;
|
|
4705
|
+
} else {
|
|
4706
|
+
value = stripPlainComment(trimmed);
|
|
4707
|
+
}
|
|
4708
|
+
if (stack.length === 0) {
|
|
4709
|
+
if (value !== null) {
|
|
4710
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
|
|
4711
|
+
lastLeafIndent = indent;
|
|
4712
|
+
continue;
|
|
4713
|
+
}
|
|
4714
|
+
const root = roots[key] ??= makeNode();
|
|
4715
|
+
stack = [{ indent, node: root }];
|
|
4716
|
+
lastLeafIndent = null;
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4719
|
+
const parent = stack[stack.length - 1].node;
|
|
4720
|
+
if (key in parent) {
|
|
4721
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
|
|
4722
|
+
}
|
|
4723
|
+
if (value === null) {
|
|
4724
|
+
const child = makeNode();
|
|
4725
|
+
parent[key] = child;
|
|
4726
|
+
stack.push({ indent, node: child });
|
|
4727
|
+
lastLeafIndent = null;
|
|
4728
|
+
} else {
|
|
4729
|
+
parent[key] = value;
|
|
4730
|
+
lastLeafIndent = indent;
|
|
3976
4731
|
}
|
|
3977
|
-
return { locales, keys, warnings };
|
|
3978
4732
|
}
|
|
3979
|
-
};
|
|
3980
|
-
|
|
3981
|
-
// src/server/import/parsers/flutter-arb.ts
|
|
3982
|
-
import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
|
|
3983
|
-
import { join as join7 } from "path";
|
|
3984
|
-
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3985
|
-
function localeFromArbName(file) {
|
|
3986
|
-
const m = file.match(/^(.+)\.arb$/);
|
|
3987
|
-
if (!m) return null;
|
|
3988
|
-
let locale = m[1];
|
|
3989
|
-
if (locale.startsWith("app_")) locale = locale.slice(4);
|
|
3990
|
-
return LOCALE_RE3.test(locale) ? locale : null;
|
|
4733
|
+
return { roots };
|
|
3991
4734
|
}
|
|
3992
|
-
function
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
if (typeof o.type === "string") d.type = o.type;
|
|
4000
|
-
if (typeof o.format === "string") d.format = o.format;
|
|
4001
|
-
if (typeof o.example === "string") d.example = o.example;
|
|
4002
|
-
if (Object.keys(d).length) out[name] = d;
|
|
4735
|
+
function asPluralForms(node) {
|
|
4736
|
+
const entries = Object.entries(node);
|
|
4737
|
+
if (entries.length === 0) return null;
|
|
4738
|
+
const forms = {};
|
|
4739
|
+
for (const [k, v] of entries) {
|
|
4740
|
+
if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
|
|
4741
|
+
if (v !== "") forms[k] = v;
|
|
4003
4742
|
}
|
|
4004
|
-
|
|
4743
|
+
if (!("other" in forms)) return null;
|
|
4744
|
+
return forms;
|
|
4005
4745
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
4746
|
+
function synthesizeIcu(forms, file, key, warnings) {
|
|
4747
|
+
const parts = [];
|
|
4748
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
4749
|
+
const body = forms[cat];
|
|
4750
|
+
if (body === void 0) continue;
|
|
4751
|
+
if (body.includes("#")) {
|
|
4752
|
+
warnings.push(
|
|
4753
|
+
`rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
|
|
4754
|
+
);
|
|
4755
|
+
}
|
|
4756
|
+
parts.push(`${cat} {${fromRuby(body)}}`);
|
|
4757
|
+
}
|
|
4758
|
+
return `{count, plural, ${parts.join(" ")}}`;
|
|
4759
|
+
}
|
|
4760
|
+
var railsYaml2 = {
|
|
4761
|
+
name: "rails-yaml",
|
|
4008
4762
|
parse(localeRoot, opts) {
|
|
4009
4763
|
const warnings = [];
|
|
4010
4764
|
const keys = {};
|
|
4011
4765
|
const locales = [];
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4766
|
+
const wanted = opts?.locales?.map((l) => l.toLowerCase());
|
|
4767
|
+
const addValue = (key, locale, value) => {
|
|
4768
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
4769
|
+
};
|
|
4770
|
+
const flatten = (node, prefix, locale, file) => {
|
|
4771
|
+
for (const [k, v] of Object.entries(node)) {
|
|
4772
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
4773
|
+
if (typeof v === "string") {
|
|
4774
|
+
if (v !== "") addValue(key, locale, fromRuby(v));
|
|
4775
|
+
continue;
|
|
4776
|
+
}
|
|
4777
|
+
const forms = asPluralForms(v);
|
|
4778
|
+
if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
|
|
4779
|
+
else flatten(v, key, locale, file);
|
|
4780
|
+
}
|
|
4781
|
+
};
|
|
4782
|
+
for (const file of readdirSync11(localeRoot).sort()) {
|
|
4783
|
+
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
4784
|
+
let text;
|
|
4018
4785
|
try {
|
|
4019
|
-
|
|
4786
|
+
text = readFileSync16(join12(localeRoot, file), "utf8");
|
|
4020
4787
|
} catch (e) {
|
|
4021
|
-
warnings.push(`
|
|
4788
|
+
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
4022
4789
|
continue;
|
|
4023
4790
|
}
|
|
4024
|
-
|
|
4025
|
-
for (const
|
|
4026
|
-
if (
|
|
4027
|
-
|
|
4028
|
-
const meta = placeholderMeta(value?.placeholders);
|
|
4029
|
-
if (meta) (keys[key.slice(1)] ??= { values: {} }).placeholders = meta;
|
|
4030
|
-
continue;
|
|
4031
|
-
}
|
|
4032
|
-
if (typeof value !== "string") {
|
|
4033
|
-
warnings.push(`flutter-arb: skipped non-string ${file}:${key}`);
|
|
4791
|
+
const { roots } = parseYamlSubset(text, file, warnings);
|
|
4792
|
+
for (const token of Object.keys(roots).sort()) {
|
|
4793
|
+
if (!LOCALE_RE8.test(token)) {
|
|
4794
|
+
warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
|
|
4034
4795
|
continue;
|
|
4035
4796
|
}
|
|
4036
|
-
(
|
|
4797
|
+
if (wanted && !wanted.includes(token.toLowerCase())) continue;
|
|
4798
|
+
if (!locales.includes(token)) locales.push(token);
|
|
4799
|
+
flatten(roots[token], "", token, file);
|
|
4037
4800
|
}
|
|
4038
4801
|
}
|
|
4039
4802
|
return { locales, keys, warnings };
|
|
4040
4803
|
}
|
|
4041
4804
|
};
|
|
4042
4805
|
|
|
4043
|
-
// src/server/import/parsers/apple-
|
|
4044
|
-
import { readdirSync as
|
|
4045
|
-
import { join as
|
|
4046
|
-
var
|
|
4047
|
-
var
|
|
4048
|
-
function
|
|
4806
|
+
// src/server/import/parsers/apple-stringsdict.ts
|
|
4807
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
4808
|
+
import { join as join13 } from "path";
|
|
4809
|
+
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4810
|
+
var TABLE2 = "Localizable.stringsdict";
|
|
4811
|
+
function localeFromLproj2(dir) {
|
|
4049
4812
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4050
4813
|
if (!m) return null;
|
|
4051
|
-
return
|
|
4814
|
+
return LOCALE_RE9.test(m[1]) ? m[1] : null;
|
|
4052
4815
|
}
|
|
4053
|
-
function
|
|
4054
|
-
return
|
|
4055
|
-
const c = esc[0];
|
|
4056
|
-
if (c === "U" || c === "u") return String.fromCharCode(parseInt(esc.slice(1), 16));
|
|
4057
|
-
if (c === "n") return "\n";
|
|
4058
|
-
if (c === "t") return " ";
|
|
4059
|
-
if (c === "r") return "\r";
|
|
4060
|
-
return esc;
|
|
4061
|
-
});
|
|
4816
|
+
function decodeEntities2(s) {
|
|
4817
|
+
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
4062
4818
|
}
|
|
4063
|
-
function
|
|
4064
|
-
const pairs = [];
|
|
4819
|
+
function parsePlistDict(xml) {
|
|
4065
4820
|
let i = 0;
|
|
4066
|
-
const n =
|
|
4821
|
+
const n = xml.length;
|
|
4067
4822
|
const skipTrivia = () => {
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
if (
|
|
4071
|
-
i
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
if (c === "/" && text[i + 1] === "/") {
|
|
4075
|
-
i += 2;
|
|
4076
|
-
while (i < n && text[i] !== "\n") i++;
|
|
4823
|
+
for (; ; ) {
|
|
4824
|
+
while (i < n && /\s/.test(xml[i])) i++;
|
|
4825
|
+
if (xml.startsWith("<!--", i)) {
|
|
4826
|
+
const end = xml.indexOf("-->", i + 4);
|
|
4827
|
+
if (end === -1) throw new Error("unterminated comment");
|
|
4828
|
+
i = end + 3;
|
|
4077
4829
|
continue;
|
|
4078
4830
|
}
|
|
4079
|
-
if (
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
i
|
|
4831
|
+
if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
|
|
4832
|
+
const end = xml.indexOf(">", i);
|
|
4833
|
+
if (end === -1) throw new Error("unterminated declaration");
|
|
4834
|
+
i = end + 1;
|
|
4083
4835
|
continue;
|
|
4084
4836
|
}
|
|
4085
4837
|
break;
|
|
4086
4838
|
}
|
|
4087
4839
|
};
|
|
4088
|
-
const
|
|
4089
|
-
if (i
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
i++;
|
|
4102
|
-
return unescape(raw2);
|
|
4103
|
-
}
|
|
4104
|
-
raw2 += c;
|
|
4105
|
-
i++;
|
|
4106
|
-
}
|
|
4107
|
-
return null;
|
|
4108
|
-
}
|
|
4109
|
-
let raw = "";
|
|
4110
|
-
while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
|
|
4111
|
-
return raw.length ? raw : null;
|
|
4840
|
+
const readTag = () => {
|
|
4841
|
+
if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
|
|
4842
|
+
const end = xml.indexOf(">", i);
|
|
4843
|
+
if (end === -1) throw new Error("unterminated tag");
|
|
4844
|
+
let body = xml.slice(i + 1, end).trim();
|
|
4845
|
+
i = end + 1;
|
|
4846
|
+
const closing = body.startsWith("/");
|
|
4847
|
+
if (closing) body = body.slice(1).trim();
|
|
4848
|
+
const selfClosing = body.endsWith("/");
|
|
4849
|
+
if (selfClosing) body = body.slice(0, -1).trim();
|
|
4850
|
+
const name = body.split(/\s/)[0];
|
|
4851
|
+
if (!name) throw new Error(`empty tag at offset ${end}`);
|
|
4852
|
+
return { name, closing, selfClosing };
|
|
4112
4853
|
};
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
const
|
|
4117
|
-
if (
|
|
4118
|
-
|
|
4119
|
-
|
|
4854
|
+
const readElementText = (name) => {
|
|
4855
|
+
const re = new RegExp(`</${name}\\s*>`, "g");
|
|
4856
|
+
re.lastIndex = i;
|
|
4857
|
+
const m = re.exec(xml);
|
|
4858
|
+
if (!m) throw new Error(`unterminated <${name}>`);
|
|
4859
|
+
const text = xml.slice(i, m.index);
|
|
4860
|
+
i = m.index + m[0].length;
|
|
4861
|
+
return decodeEntities2(text);
|
|
4862
|
+
};
|
|
4863
|
+
const readValue = (tag2) => {
|
|
4864
|
+
if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
|
|
4865
|
+
if (tag2.name === "true" || tag2.name === "false") {
|
|
4866
|
+
if (!tag2.selfClosing) readElementText(tag2.name);
|
|
4867
|
+
return tag2.name;
|
|
4120
4868
|
}
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
|
|
4124
|
-
break;
|
|
4869
|
+
if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
|
|
4870
|
+
return tag2.selfClosing ? "" : readElementText(tag2.name);
|
|
4125
4871
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4872
|
+
throw new Error(`unsupported plist element <${tag2.name}>`);
|
|
4873
|
+
};
|
|
4874
|
+
const readDict = () => {
|
|
4875
|
+
const out = {};
|
|
4876
|
+
for (; ; ) {
|
|
4877
|
+
skipTrivia();
|
|
4878
|
+
const tag2 = readTag();
|
|
4879
|
+
if (tag2.closing) {
|
|
4880
|
+
if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
|
|
4881
|
+
return out;
|
|
4882
|
+
}
|
|
4883
|
+
if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
|
|
4884
|
+
const key = readElementText("key");
|
|
4885
|
+
skipTrivia();
|
|
4886
|
+
const vt = readTag();
|
|
4887
|
+
if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
|
|
4888
|
+
out[key] = readValue(vt);
|
|
4132
4889
|
}
|
|
4890
|
+
};
|
|
4891
|
+
skipTrivia();
|
|
4892
|
+
let tag = readTag();
|
|
4893
|
+
if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
|
|
4133
4894
|
skipTrivia();
|
|
4134
|
-
|
|
4135
|
-
pairs.push({ key, value });
|
|
4895
|
+
tag = readTag();
|
|
4136
4896
|
}
|
|
4137
|
-
|
|
4897
|
+
if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
|
|
4898
|
+
return tag.selfClosing ? {} : readDict();
|
|
4138
4899
|
}
|
|
4139
|
-
var
|
|
4140
|
-
|
|
4900
|
+
var VAR_RE = /%#@([^@]*)@/g;
|
|
4901
|
+
function entryToIcu(key, entry, file, warnings) {
|
|
4902
|
+
const warn = (msg) => {
|
|
4903
|
+
warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
|
|
4904
|
+
return null;
|
|
4905
|
+
};
|
|
4906
|
+
if (typeof entry !== "object") return warn("value is not a dict; skipped");
|
|
4907
|
+
const fmt = entry["NSStringLocalizedFormatKey"];
|
|
4908
|
+
if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
|
|
4909
|
+
const vars = [...fmt.matchAll(VAR_RE)];
|
|
4910
|
+
if (vars.length !== 1) {
|
|
4911
|
+
return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
|
|
4912
|
+
}
|
|
4913
|
+
const arg = vars[0][1];
|
|
4914
|
+
if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
|
|
4915
|
+
const prefix = fmt.slice(0, vars[0].index);
|
|
4916
|
+
const suffix = fmt.slice(vars[0].index + vars[0][0].length);
|
|
4917
|
+
const varDict = entry[arg];
|
|
4918
|
+
if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
|
|
4919
|
+
const specType = varDict["NSStringFormatSpecTypeKey"];
|
|
4920
|
+
if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
|
|
4921
|
+
return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
|
|
4922
|
+
}
|
|
4923
|
+
const valueType = varDict["NSStringFormatValueTypeKey"];
|
|
4924
|
+
const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
|
|
4925
|
+
const forms = {};
|
|
4926
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
4927
|
+
const body = varDict[cat];
|
|
4928
|
+
if (typeof body !== "string") continue;
|
|
4929
|
+
forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
|
|
4930
|
+
}
|
|
4931
|
+
if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
|
|
4932
|
+
return formsToIcu(arg, forms);
|
|
4933
|
+
}
|
|
4934
|
+
var appleStringsdict2 = {
|
|
4935
|
+
name: "apple-stringsdict",
|
|
4141
4936
|
parse(localeRoot, opts) {
|
|
4142
4937
|
const warnings = [];
|
|
4143
4938
|
const keys = {};
|
|
4144
4939
|
const locales = [];
|
|
4145
|
-
for (const dir of
|
|
4146
|
-
const locale =
|
|
4940
|
+
for (const dir of readdirSync12(localeRoot).sort()) {
|
|
4941
|
+
const locale = localeFromLproj2(dir);
|
|
4147
4942
|
if (!locale) continue;
|
|
4148
4943
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4149
|
-
const file =
|
|
4944
|
+
const file = join13(localeRoot, dir, TABLE2);
|
|
4150
4945
|
let text;
|
|
4151
4946
|
try {
|
|
4152
|
-
if (!
|
|
4153
|
-
text =
|
|
4947
|
+
if (!statSync6(file).isFile()) continue;
|
|
4948
|
+
text = readFileSync17(file, "utf8");
|
|
4154
4949
|
} catch {
|
|
4155
4950
|
continue;
|
|
4156
4951
|
}
|
|
4157
4952
|
locales.push(locale);
|
|
4158
|
-
const others =
|
|
4953
|
+
const others = readdirSync12(join13(localeRoot, dir)).filter(
|
|
4954
|
+
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
4955
|
+
);
|
|
4159
4956
|
if (others.length) {
|
|
4160
|
-
warnings.push(
|
|
4957
|
+
warnings.push(
|
|
4958
|
+
`apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
|
|
4959
|
+
);
|
|
4161
4960
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4961
|
+
let root;
|
|
4962
|
+
try {
|
|
4963
|
+
root = parsePlistDict(text);
|
|
4964
|
+
} catch (e) {
|
|
4965
|
+
warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
for (const key of Object.keys(root).sort()) {
|
|
4969
|
+
const icu = entryToIcu(key, root[key], file, warnings);
|
|
4970
|
+
if (icu === null) continue;
|
|
4971
|
+
(keys[key] ??= { values: {} }).values[locale] = icu;
|
|
4164
4972
|
}
|
|
4165
4973
|
}
|
|
4166
4974
|
return { locales, keys, warnings };
|
|
@@ -4172,7 +4980,12 @@ var REGISTRY = {
|
|
|
4172
4980
|
[vueI18nJson2.name]: vueI18nJson2,
|
|
4173
4981
|
[laravelPhp2.name]: laravelPhp2,
|
|
4174
4982
|
[flutterArb2.name]: flutterArb2,
|
|
4175
|
-
[appleStrings2.name]: appleStrings2
|
|
4983
|
+
[appleStrings2.name]: appleStrings2,
|
|
4984
|
+
[angularXliff2.name]: angularXliff2,
|
|
4985
|
+
[gettextPo2.name]: gettextPo2,
|
|
4986
|
+
[i18nextJson2.name]: i18nextJson2,
|
|
4987
|
+
[railsYaml2.name]: railsYaml2,
|
|
4988
|
+
[appleStringsdict2.name]: appleStringsdict2
|
|
4176
4989
|
};
|
|
4177
4990
|
function getParser(name) {
|
|
4178
4991
|
const p = REGISTRY[name];
|
|
@@ -4185,7 +4998,14 @@ var OUTPUT_BY_FORMAT = {
|
|
|
4185
4998
|
"laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
|
|
4186
4999
|
"vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
|
|
4187
5000
|
"flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
|
|
4188
|
-
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
|
|
5001
|
+
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
|
|
5002
|
+
// skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
|
|
5003
|
+
// only writes the translation files back next to it.
|
|
5004
|
+
"angular-xliff": { adapter: "angular-xliff", path: "messages.{locale}.xlf", rootRelative: true, skipSourceLocale: true },
|
|
5005
|
+
"gettext-po": { adapter: "gettext-po", path: "{locale}.po", rootRelative: true },
|
|
5006
|
+
"i18next-json": { adapter: "i18next-json", path: "{locale}/translation.json", rootRelative: true },
|
|
5007
|
+
"rails-yaml": { adapter: "rails-yaml", path: "config/locales/{locale}.yml" },
|
|
5008
|
+
"apple-stringsdict": { adapter: "apple-stringsdict", path: "{locale}.lproj/Localizable.stringsdict", rootRelative: true }
|
|
4189
5009
|
};
|
|
4190
5010
|
function assemble2(parsed, opts) {
|
|
4191
5011
|
const warnings = [...parsed.warnings];
|
|
@@ -4303,7 +5123,7 @@ function runImport(opts) {
|
|
|
4303
5123
|
}
|
|
4304
5124
|
|
|
4305
5125
|
// src/server/export-run.ts
|
|
4306
|
-
import { existsSync as existsSync10, readFileSync as
|
|
5126
|
+
import { existsSync as existsSync10, readFileSync as readFileSync18, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
|
|
4307
5127
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
4308
5128
|
function effectiveLocales(config) {
|
|
4309
5129
|
const limit = config.exportLocales;
|
|
@@ -4346,7 +5166,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
4346
5166
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
4347
5167
|
const next = resolve7(dir, segment);
|
|
4348
5168
|
if (isLast) {
|
|
4349
|
-
if (stale(locale) && existsSync10(next) &&
|
|
5169
|
+
if (stale(locale) && existsSync10(next) && statSync7(next).isFile()) {
|
|
4350
5170
|
unlinkSync(next);
|
|
4351
5171
|
deleted++;
|
|
4352
5172
|
removeEmptyDirs(dir, root);
|
|
@@ -4359,7 +5179,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
4359
5179
|
const re = segmentRegExp(segment);
|
|
4360
5180
|
let entries;
|
|
4361
5181
|
try {
|
|
4362
|
-
entries =
|
|
5182
|
+
entries = readdirSync13(dir, { withFileTypes: true });
|
|
4363
5183
|
} catch {
|
|
4364
5184
|
return;
|
|
4365
5185
|
}
|
|
@@ -4402,7 +5222,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
4402
5222
|
writtenPaths.add(abs);
|
|
4403
5223
|
let current = null;
|
|
4404
5224
|
try {
|
|
4405
|
-
current =
|
|
5225
|
+
current = readFileSync18(abs, "utf8");
|
|
4406
5226
|
} catch {
|
|
4407
5227
|
}
|
|
4408
5228
|
if (current === f.contents) {
|
|
@@ -4419,17 +5239,17 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
4419
5239
|
}
|
|
4420
5240
|
|
|
4421
5241
|
// src/server/ui-prefs.ts
|
|
4422
|
-
import { readFileSync as
|
|
5242
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
4423
5243
|
import { homedir } from "os";
|
|
4424
|
-
import { join as
|
|
5244
|
+
import { join as join14 } from "path";
|
|
4425
5245
|
var THEMES = ["system", "light", "dark"];
|
|
4426
5246
|
var isThemeMode = (v) => THEMES.includes(v);
|
|
4427
5247
|
var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
4428
|
-
var defaultUiPrefsPath = () =>
|
|
5248
|
+
var defaultUiPrefsPath = () => join14(homedir(), ".glotfile", "ui.json");
|
|
4429
5249
|
var DEFAULTS = { theme: "system" };
|
|
4430
5250
|
function readJson(path) {
|
|
4431
5251
|
try {
|
|
4432
|
-
const parsed = JSON.parse(
|
|
5252
|
+
const parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
4433
5253
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4434
5254
|
} catch {
|
|
4435
5255
|
return {};
|
|
@@ -4448,7 +5268,7 @@ function saveUiPrefs(path, prefs) {
|
|
|
4448
5268
|
}
|
|
4449
5269
|
|
|
4450
5270
|
// src/server/local-settings.ts
|
|
4451
|
-
import { readFileSync as
|
|
5271
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
4452
5272
|
import { resolve as resolve8 } from "path";
|
|
4453
5273
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
4454
5274
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
@@ -4463,7 +5283,7 @@ var DEFAULT_EDITOR = "vscode";
|
|
|
4463
5283
|
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
4464
5284
|
function readJson2(path) {
|
|
4465
5285
|
try {
|
|
4466
|
-
const parsed = JSON.parse(
|
|
5286
|
+
const parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
4467
5287
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4468
5288
|
} catch {
|
|
4469
5289
|
return {};
|
|
@@ -4536,7 +5356,7 @@ function projectName(root) {
|
|
|
4536
5356
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
4537
5357
|
if (existsSync11(nameFile)) {
|
|
4538
5358
|
try {
|
|
4539
|
-
const name =
|
|
5359
|
+
const name = readFileSync21(nameFile, "utf8").trim();
|
|
4540
5360
|
if (name) return name;
|
|
4541
5361
|
} catch {
|
|
4542
5362
|
}
|
|
@@ -4661,7 +5481,7 @@ function createApi(deps) {
|
|
|
4661
5481
|
if (depth > 4) return;
|
|
4662
5482
|
let entries = [];
|
|
4663
5483
|
try {
|
|
4664
|
-
entries =
|
|
5484
|
+
entries = readdirSync14(dir);
|
|
4665
5485
|
} catch {
|
|
4666
5486
|
return;
|
|
4667
5487
|
}
|
|
@@ -4675,7 +5495,7 @@ function createApi(deps) {
|
|
|
4675
5495
|
filePath = abs;
|
|
4676
5496
|
} else {
|
|
4677
5497
|
try {
|
|
4678
|
-
if (
|
|
5498
|
+
if (statSync8(abs).isDirectory()) walk(abs, depth + 1);
|
|
4679
5499
|
} catch {
|
|
4680
5500
|
}
|
|
4681
5501
|
continue;
|
|
@@ -5456,7 +6276,7 @@ function createApi(deps) {
|
|
|
5456
6276
|
|
|
5457
6277
|
// src/server/server.ts
|
|
5458
6278
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
5459
|
-
var DEFAULT_UI_DIR =
|
|
6279
|
+
var DEFAULT_UI_DIR = join15(here, "..", "ui");
|
|
5460
6280
|
var MIME = {
|
|
5461
6281
|
".html": "text/html; charset=utf-8",
|
|
5462
6282
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -5510,7 +6330,7 @@ function buildApp(opts) {
|
|
|
5510
6330
|
const file = await readFileResponse(target);
|
|
5511
6331
|
if (file) return file;
|
|
5512
6332
|
}
|
|
5513
|
-
const index = await readFileResponse(
|
|
6333
|
+
const index = await readFileResponse(join15(root, "index.html"));
|
|
5514
6334
|
if (index) return index;
|
|
5515
6335
|
return c.notFound();
|
|
5516
6336
|
});
|