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/cli.js
CHANGED
|
@@ -122,6 +122,7 @@ function validate(raw) {
|
|
|
122
122
|
if (o.indent !== void 0 && typeof o.indent !== "number") fail("config.outputs[].indent must be a number");
|
|
123
123
|
if (o.finalNewline !== void 0 && typeof o.finalNewline !== "boolean") fail("config.outputs[].finalNewline must be a boolean");
|
|
124
124
|
if (o.includeLocale !== void 0 && typeof o.includeLocale !== "boolean") fail("config.outputs[].includeLocale must be a boolean");
|
|
125
|
+
if (o.skipSourceLocale !== void 0 && typeof o.skipSourceLocale !== "boolean") fail("config.outputs[].skipSourceLocale must be a boolean");
|
|
125
126
|
if (o.localeAliases !== void 0) {
|
|
126
127
|
if (!isObject(o.localeAliases)) fail("config.outputs[].localeAliases must be an object");
|
|
127
128
|
for (const [k, v] of Object.entries(o.localeAliases)) {
|
|
@@ -1574,27 +1575,41 @@ var init_vue_i18n_json = __esm({
|
|
|
1574
1575
|
function xmlEscape2(s) {
|
|
1575
1576
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1576
1577
|
}
|
|
1577
|
-
function
|
|
1578
|
+
function attrEscape(s) {
|
|
1579
|
+
return xmlEscape2(s).replace(/"/g, """);
|
|
1580
|
+
}
|
|
1581
|
+
function angularXMeta(placeholders, name) {
|
|
1582
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) ? placeholders?.[name] : void 0;
|
|
1583
|
+
}
|
|
1584
|
+
function renderInterpolations(text, ids, placeholders) {
|
|
1578
1585
|
let out = "";
|
|
1579
1586
|
let last = 0;
|
|
1580
1587
|
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
1581
1588
|
const name = m[1];
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1589
|
+
out += xmlEscape2(text.slice(last, m.index));
|
|
1590
|
+
const meta = angularXMeta(placeholders, name);
|
|
1591
|
+
if (meta) {
|
|
1592
|
+
const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
|
|
1593
|
+
const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
|
|
1594
|
+
out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
|
|
1595
|
+
} else {
|
|
1596
|
+
let id = ids.get(name);
|
|
1597
|
+
if (id === void 0) {
|
|
1598
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
1599
|
+
ids.set(name, id);
|
|
1600
|
+
}
|
|
1601
|
+
out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
1586
1602
|
}
|
|
1587
|
-
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
1588
1603
|
last = m.index + m[0].length;
|
|
1589
1604
|
}
|
|
1590
1605
|
return out + xmlEscape2(text.slice(last));
|
|
1591
1606
|
}
|
|
1592
|
-
function renderPluralIcu(forms, ids) {
|
|
1607
|
+
function renderPluralIcu(forms, ids, placeholders) {
|
|
1593
1608
|
const cats = [
|
|
1594
1609
|
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
1595
1610
|
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
1596
1611
|
];
|
|
1597
|
-
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
1612
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids, placeholders)}}`);
|
|
1598
1613
|
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
1599
1614
|
}
|
|
1600
1615
|
function renderEmbeddedIcu(value) {
|
|
@@ -1604,8 +1619,8 @@ function renderEmbeddedIcu(value) {
|
|
|
1604
1619
|
);
|
|
1605
1620
|
return xmlEscape2(renamed);
|
|
1606
1621
|
}
|
|
1607
|
-
function renderScalar(value, ids) {
|
|
1608
|
-
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
1622
|
+
function renderScalar(value, ids, placeholders) {
|
|
1623
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
|
|
1609
1624
|
}
|
|
1610
1625
|
var DEFAULT_LOCALE_CASE8, angularXliff;
|
|
1611
1626
|
var init_angular_xliff = __esm({
|
|
@@ -1636,6 +1651,7 @@ var init_angular_xliff = __esm({
|
|
|
1636
1651
|
const emptyAs = resolveEmptyAs(output, "source");
|
|
1637
1652
|
const keys = Object.keys(state.keys).sort();
|
|
1638
1653
|
for (const locale of state.config.locales) {
|
|
1654
|
+
if (output.skipSourceLocale && locale === sourceLocale) continue;
|
|
1639
1655
|
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
|
|
1640
1656
|
const units = [];
|
|
1641
1657
|
for (const key of keys) {
|
|
@@ -1646,17 +1662,18 @@ var init_angular_xliff = __esm({
|
|
|
1646
1662
|
if (entry.plural) {
|
|
1647
1663
|
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
1648
1664
|
if (targetForms === null) continue;
|
|
1649
|
-
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
1650
|
-
target = renderPluralIcu(targetForms, ids);
|
|
1665
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids, entry.placeholders);
|
|
1666
|
+
target = renderPluralIcu(targetForms, ids, entry.placeholders);
|
|
1651
1667
|
} else {
|
|
1652
1668
|
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
1653
1669
|
if (targetValue === null) continue;
|
|
1654
|
-
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
1655
|
-
target = renderScalar(targetValue, ids);
|
|
1670
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids, entry.placeholders);
|
|
1671
|
+
target = renderScalar(targetValue, ids, entry.placeholders);
|
|
1656
1672
|
}
|
|
1673
|
+
const translated = locale === sourceLocale || (entry.plural ? entry.values[locale]?.forms !== void 0 : !!entry.values[locale]?.value);
|
|
1657
1674
|
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
1658
1675
|
units.push(` <source>${source}</source>`);
|
|
1659
|
-
units.push(` <target>${target}</target>`);
|
|
1676
|
+
units.push(` <target${translated ? "" : ' state="new"'}>${target}</target>`);
|
|
1660
1677
|
if (entry.description) {
|
|
1661
1678
|
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
1662
1679
|
}
|
|
@@ -2744,6 +2761,46 @@ var init_local_settings = __esm({
|
|
|
2744
2761
|
}
|
|
2745
2762
|
});
|
|
2746
2763
|
|
|
2764
|
+
// src/server/glossary.ts
|
|
2765
|
+
function contains(haystack, needle, caseSensitive) {
|
|
2766
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
2767
|
+
}
|
|
2768
|
+
function relevantGlossary(source, targetLocale, glossary) {
|
|
2769
|
+
const hints = [];
|
|
2770
|
+
for (const entry of glossary) {
|
|
2771
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
2772
|
+
hints.push({
|
|
2773
|
+
term: entry.term,
|
|
2774
|
+
doNotTranslate: entry.doNotTranslate,
|
|
2775
|
+
forced: entry.translations?.[targetLocale],
|
|
2776
|
+
notes: entry.notes
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
return hints;
|
|
2780
|
+
}
|
|
2781
|
+
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
2782
|
+
const out = [];
|
|
2783
|
+
for (const entry of glossary) {
|
|
2784
|
+
if (!contains(source, entry.term, entry.caseSensitive)) continue;
|
|
2785
|
+
if (entry.doNotTranslate) {
|
|
2786
|
+
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
2787
|
+
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
2788
|
+
}
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
const forced = entry.translations?.[targetLocale];
|
|
2792
|
+
if (forced && !contains(value, forced, entry.caseSensitive)) {
|
|
2793
|
+
out.push({ term: entry.term, expected: forced, kind: "forced" });
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return out;
|
|
2797
|
+
}
|
|
2798
|
+
var init_glossary = __esm({
|
|
2799
|
+
"src/server/glossary.ts"() {
|
|
2800
|
+
"use strict";
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
|
|
2747
2804
|
// src/server/glob.ts
|
|
2748
2805
|
function globToRegExp(glob) {
|
|
2749
2806
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -2815,21 +2872,6 @@ function selectRequests(state, opts) {
|
|
|
2815
2872
|
}
|
|
2816
2873
|
return reqs;
|
|
2817
2874
|
}
|
|
2818
|
-
function relevantGlossary(source, targetLocale, glossary) {
|
|
2819
|
-
const hints = [];
|
|
2820
|
-
for (const entry of glossary) {
|
|
2821
|
-
const haystack = entry.caseSensitive ? source : source.toLowerCase();
|
|
2822
|
-
const needle = entry.caseSensitive ? entry.term : entry.term.toLowerCase();
|
|
2823
|
-
if (!haystack.includes(needle)) continue;
|
|
2824
|
-
hints.push({
|
|
2825
|
-
term: entry.term,
|
|
2826
|
-
doNotTranslate: entry.doNotTranslate,
|
|
2827
|
-
forced: entry.translations?.[targetLocale],
|
|
2828
|
-
notes: entry.notes
|
|
2829
|
-
});
|
|
2830
|
-
}
|
|
2831
|
-
return hints;
|
|
2832
|
-
}
|
|
2833
2875
|
function attachScreenshots(reqs, state, projectRoot) {
|
|
2834
2876
|
const cache2 = /* @__PURE__ */ new Map();
|
|
2835
2877
|
for (const req of reqs) {
|
|
@@ -2936,6 +2978,7 @@ var MEDIA_TYPES, MAX_IMAGE_BYTES, DEFAULT_LOCALE_CONCURRENCY;
|
|
|
2936
2978
|
var init_run = __esm({
|
|
2937
2979
|
"src/server/ai/run.ts"() {
|
|
2938
2980
|
"use strict";
|
|
2981
|
+
init_glossary();
|
|
2939
2982
|
init_placeholders();
|
|
2940
2983
|
init_plurals();
|
|
2941
2984
|
init_state();
|
|
@@ -3113,7 +3156,7 @@ function findMissing(state) {
|
|
|
3113
3156
|
const entry = state.keys[key];
|
|
3114
3157
|
if (entry.skipTranslate) continue;
|
|
3115
3158
|
for (const locale of targets) {
|
|
3116
|
-
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
3159
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
|
|
3117
3160
|
if (!v) out.push({ key, locale });
|
|
3118
3161
|
}
|
|
3119
3162
|
}
|
|
@@ -3581,24 +3624,83 @@ var init_context = __esm({
|
|
|
3581
3624
|
}
|
|
3582
3625
|
});
|
|
3583
3626
|
|
|
3584
|
-
// src/server/
|
|
3585
|
-
function
|
|
3586
|
-
return
|
|
3627
|
+
// src/server/spell.ts
|
|
3628
|
+
function spellTokens(value) {
|
|
3629
|
+
return value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
|
|
3587
3630
|
}
|
|
3588
|
-
function
|
|
3631
|
+
function ignoreWordsFor(glossary, customWords = []) {
|
|
3589
3632
|
const set = /* @__PURE__ */ new Set();
|
|
3590
|
-
const add = (
|
|
3591
|
-
for (const w of
|
|
3633
|
+
const add = (text) => {
|
|
3634
|
+
for (const w of text.match(WORD) ?? []) set.add(w.toLowerCase());
|
|
3592
3635
|
};
|
|
3593
|
-
for (const
|
|
3594
|
-
|
|
3636
|
+
for (const e of glossary) {
|
|
3637
|
+
add(e.term);
|
|
3638
|
+
for (const t of Object.values(e.translations ?? {})) add(t);
|
|
3639
|
+
}
|
|
3640
|
+
for (const w of customWords) add(w);
|
|
3595
3641
|
return set;
|
|
3596
3642
|
}
|
|
3643
|
+
async function getSpeller(dictId) {
|
|
3644
|
+
const key = norm(dictId);
|
|
3645
|
+
const existing = instances.get(key);
|
|
3646
|
+
if (existing) return existing;
|
|
3647
|
+
if (unavailable.has(key)) return null;
|
|
3648
|
+
try {
|
|
3649
|
+
const nspellMod = await import("nspell");
|
|
3650
|
+
const nspell = nspellMod.default ?? nspellMod;
|
|
3651
|
+
const dictMod = await import(`dictionary-${key}`);
|
|
3652
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
3653
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
3654
|
+
const speller = nspell(dict);
|
|
3655
|
+
instances.set(key, speller);
|
|
3656
|
+
return speller;
|
|
3657
|
+
} catch {
|
|
3658
|
+
unavailable.add(key);
|
|
3659
|
+
return null;
|
|
3660
|
+
} finally {
|
|
3661
|
+
loading.delete(key);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
function spellValue(dictId, value, ignore) {
|
|
3665
|
+
const key = norm(dictId);
|
|
3666
|
+
if (unavailable.has(key)) return [];
|
|
3667
|
+
const spell = instances.get(key);
|
|
3668
|
+
if (!spell) {
|
|
3669
|
+
if (!loading.has(key)) {
|
|
3670
|
+
loading.add(key);
|
|
3671
|
+
void getSpeller(key);
|
|
3672
|
+
}
|
|
3673
|
+
return null;
|
|
3674
|
+
}
|
|
3675
|
+
const cacheKey = key + " " + value;
|
|
3676
|
+
let allBad = cache.get(cacheKey);
|
|
3677
|
+
if (!allBad) {
|
|
3678
|
+
allBad = spellTokens(value).filter((w) => !spell.correct(w));
|
|
3679
|
+
cache.set(cacheKey, allBad);
|
|
3680
|
+
}
|
|
3681
|
+
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
3682
|
+
}
|
|
3683
|
+
var instances, loading, unavailable, cache, norm, ICU_BLOCK, MASK, WORD;
|
|
3684
|
+
var init_spell = __esm({
|
|
3685
|
+
"src/server/spell.ts"() {
|
|
3686
|
+
"use strict";
|
|
3687
|
+
instances = /* @__PURE__ */ new Map();
|
|
3688
|
+
loading = /* @__PURE__ */ new Set();
|
|
3689
|
+
unavailable = /* @__PURE__ */ new Set();
|
|
3690
|
+
cache = /* @__PURE__ */ new Map();
|
|
3691
|
+
norm = (dictId) => dictId.toLowerCase();
|
|
3692
|
+
ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
3693
|
+
MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
3694
|
+
WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
|
|
3698
|
+
// src/server/lint/spelling.ts
|
|
3597
3699
|
var spellingRule, defaultLoader;
|
|
3598
3700
|
var init_spelling = __esm({
|
|
3599
3701
|
"src/server/lint/spelling.ts"() {
|
|
3600
3702
|
"use strict";
|
|
3601
|
-
|
|
3703
|
+
init_spell();
|
|
3602
3704
|
spellingRule = {
|
|
3603
3705
|
id: "spelling",
|
|
3604
3706
|
run(state, ctx) {
|
|
@@ -3610,10 +3712,8 @@ var init_spelling = __esm({
|
|
|
3610
3712
|
if (!speller) continue;
|
|
3611
3713
|
const value = entry.values[locale]?.value;
|
|
3612
3714
|
if (!value) continue;
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
const lower = word.toLowerCase();
|
|
3616
|
-
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
3715
|
+
for (const word of spellTokens(value)) {
|
|
3716
|
+
if (ctx.allowWords.has(word.toLowerCase())) continue;
|
|
3617
3717
|
if (!speller.correct(word)) {
|
|
3618
3718
|
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
3619
3719
|
}
|
|
@@ -3623,18 +3723,7 @@ var init_spelling = __esm({
|
|
|
3623
3723
|
return out;
|
|
3624
3724
|
}
|
|
3625
3725
|
};
|
|
3626
|
-
defaultLoader =
|
|
3627
|
-
try {
|
|
3628
|
-
const nspellMod = await import("nspell");
|
|
3629
|
-
const nspell2 = nspellMod.default ?? nspellMod;
|
|
3630
|
-
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
3631
|
-
const dictExport = dictMod.default ?? dictMod;
|
|
3632
|
-
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
3633
|
-
return nspell2(dict);
|
|
3634
|
-
} catch {
|
|
3635
|
-
return null;
|
|
3636
|
-
}
|
|
3637
|
-
};
|
|
3726
|
+
defaultLoader = (dictId) => getSpeller(dictId);
|
|
3638
3727
|
}
|
|
3639
3728
|
});
|
|
3640
3729
|
|
|
@@ -3645,7 +3734,7 @@ var init_rules = __esm({
|
|
|
3645
3734
|
"use strict";
|
|
3646
3735
|
init_scan();
|
|
3647
3736
|
init_placeholders();
|
|
3648
|
-
|
|
3737
|
+
init_glossary();
|
|
3649
3738
|
init_spelling();
|
|
3650
3739
|
emptySourceRule = {
|
|
3651
3740
|
id: "empty-source",
|
|
@@ -3661,17 +3750,14 @@ var init_rules = __esm({
|
|
|
3661
3750
|
};
|
|
3662
3751
|
emptyTranslationRule = {
|
|
3663
3752
|
id: "empty-translation",
|
|
3664
|
-
|
|
3753
|
+
// findMissing is the shared "untranslated" walk (also behind the editor's
|
|
3754
|
+
// untranslated check and /scan/missing); a whitespace-only value counts as
|
|
3755
|
+
// missing there, so no separate whitespace pass is needed.
|
|
3756
|
+
run(state) {
|
|
3665
3757
|
const out = [];
|
|
3666
3758
|
for (const m of findMissing(state)) {
|
|
3667
3759
|
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
3668
3760
|
}
|
|
3669
|
-
for (const key of Object.keys(state.keys)) {
|
|
3670
|
-
for (const locale of ctx.targetLocales) {
|
|
3671
|
-
const v = state.keys[key].values[locale]?.value;
|
|
3672
|
-
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
3761
|
return out;
|
|
3676
3762
|
}
|
|
3677
3763
|
};
|
|
@@ -3799,13 +3885,13 @@ var init_rules = __esm({
|
|
|
3799
3885
|
for (const locale of ctx.targetLocales) {
|
|
3800
3886
|
const v = entry.values[locale]?.value;
|
|
3801
3887
|
if (!v) continue;
|
|
3802
|
-
for (const
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
}
|
|
3888
|
+
for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
|
|
3889
|
+
out.push({
|
|
3890
|
+
ruleId: "glossary-violation",
|
|
3891
|
+
key,
|
|
3892
|
+
locale,
|
|
3893
|
+
message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
|
|
3894
|
+
});
|
|
3809
3895
|
}
|
|
3810
3896
|
}
|
|
3811
3897
|
}
|
|
@@ -3866,7 +3952,7 @@ async function runLint(state, options = {}) {
|
|
|
3866
3952
|
const active = rules.filter(isActive);
|
|
3867
3953
|
const spellingOn = active.some((r) => r.id === "spelling");
|
|
3868
3954
|
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
3869
|
-
const allowWords = spellingOn ?
|
|
3955
|
+
const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
|
|
3870
3956
|
const ctx = {
|
|
3871
3957
|
config,
|
|
3872
3958
|
sourceLocale: state.config.sourceLocale,
|
|
@@ -3904,6 +3990,7 @@ var init_run2 = __esm({
|
|
|
3904
3990
|
init_registry();
|
|
3905
3991
|
init_rules();
|
|
3906
3992
|
init_spelling();
|
|
3993
|
+
init_spell();
|
|
3907
3994
|
init_suppress();
|
|
3908
3995
|
}
|
|
3909
3996
|
});
|
|
@@ -3962,7 +4049,7 @@ var init_accept = __esm({
|
|
|
3962
4049
|
});
|
|
3963
4050
|
|
|
3964
4051
|
// src/server/import/detect.ts
|
|
3965
|
-
import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
4052
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
|
|
3966
4053
|
import { join as join4 } from "path";
|
|
3967
4054
|
function safeIsDir(p) {
|
|
3968
4055
|
try {
|
|
@@ -4049,6 +4136,110 @@ function detectApple(root) {
|
|
|
4049
4136
|
}
|
|
4050
4137
|
return best;
|
|
4051
4138
|
}
|
|
4139
|
+
function detectAngularXliff(root) {
|
|
4140
|
+
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4141
|
+
const localeRoot = rel === "." ? root : join4(root, rel);
|
|
4142
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4143
|
+
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4144
|
+
if (files.length === 0) continue;
|
|
4145
|
+
const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
4146
|
+
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4147
|
+
let sourceLocale;
|
|
4148
|
+
try {
|
|
4149
|
+
sourceLocale = readFileSync11(join4(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4150
|
+
} catch {
|
|
4151
|
+
}
|
|
4152
|
+
if (!sourceLocale && locales.length === 0) continue;
|
|
4153
|
+
sourceLocale ??= pickSource(locales, () => 0);
|
|
4154
|
+
if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
|
|
4155
|
+
return { format: "angular-xliff", localeRoot, locales, sourceLocale };
|
|
4156
|
+
}
|
|
4157
|
+
return null;
|
|
4158
|
+
}
|
|
4159
|
+
function detectRails(root) {
|
|
4160
|
+
const localeRoot = join4(root, "config", "locales");
|
|
4161
|
+
if (!safeIsDir(localeRoot)) return null;
|
|
4162
|
+
const locales = [];
|
|
4163
|
+
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4164
|
+
if (!/\.ya?ml$/.test(file)) continue;
|
|
4165
|
+
let text;
|
|
4166
|
+
try {
|
|
4167
|
+
text = readFileSync11(join4(localeRoot, file), "utf8");
|
|
4168
|
+
} catch {
|
|
4169
|
+
continue;
|
|
4170
|
+
}
|
|
4171
|
+
for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
|
|
4172
|
+
const token = m[2];
|
|
4173
|
+
if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
if (locales.length === 0) return null;
|
|
4177
|
+
return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4178
|
+
}
|
|
4179
|
+
function detectI18next(root) {
|
|
4180
|
+
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4181
|
+
const localeRoot = join4(root, rel);
|
|
4182
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4183
|
+
const locales = listDirs(localeRoot).filter(
|
|
4184
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join4(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4185
|
+
);
|
|
4186
|
+
if (locales.length === 0) continue;
|
|
4187
|
+
const sourceLocale = pickSource(locales, (loc) => {
|
|
4188
|
+
try {
|
|
4189
|
+
return readdirSync4(join4(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join4(localeRoot, loc, f)).size, 0);
|
|
4190
|
+
} catch {
|
|
4191
|
+
return 0;
|
|
4192
|
+
}
|
|
4193
|
+
});
|
|
4194
|
+
return { format: "i18next-json", localeRoot, locales, sourceLocale };
|
|
4195
|
+
}
|
|
4196
|
+
return null;
|
|
4197
|
+
}
|
|
4198
|
+
function gettextLocales(dir) {
|
|
4199
|
+
const locales = [];
|
|
4200
|
+
for (const entry of readdirSync4(dir).sort()) {
|
|
4201
|
+
const flat = entry.match(/^(.+)\.po$/)?.[1];
|
|
4202
|
+
if (flat && LOCALE_RE.test(flat)) {
|
|
4203
|
+
if (!locales.includes(flat)) locales.push(flat);
|
|
4204
|
+
continue;
|
|
4205
|
+
}
|
|
4206
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join4(dir, entry))) continue;
|
|
4207
|
+
const sub = join4(dir, entry);
|
|
4208
|
+
const hasPo = (d) => {
|
|
4209
|
+
try {
|
|
4210
|
+
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
4211
|
+
} catch {
|
|
4212
|
+
return false;
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
if (hasPo(join4(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4216
|
+
if (!locales.includes(entry)) locales.push(entry);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
return locales;
|
|
4220
|
+
}
|
|
4221
|
+
function detectGettext(root) {
|
|
4222
|
+
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4223
|
+
const localeRoot = join4(root, rel);
|
|
4224
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
4225
|
+
const locales = gettextLocales(localeRoot);
|
|
4226
|
+
if (locales.length === 0) continue;
|
|
4227
|
+
return { format: "gettext-po", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4228
|
+
}
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
function detectAppleStringsdict(root) {
|
|
4232
|
+
const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
|
|
4233
|
+
let best = null;
|
|
4234
|
+
for (const dir of candidates) {
|
|
4235
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join4(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4236
|
+
if (locales.length === 0) continue;
|
|
4237
|
+
if (!best || locales.length > best.locales.length) {
|
|
4238
|
+
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
return best;
|
|
4242
|
+
}
|
|
4052
4243
|
function detect(root, formatOverride) {
|
|
4053
4244
|
if (!existsSync10(root)) return null;
|
|
4054
4245
|
if (formatOverride) {
|
|
@@ -4062,18 +4253,36 @@ function detect(root, formatOverride) {
|
|
|
4062
4253
|
}
|
|
4063
4254
|
return null;
|
|
4064
4255
|
}
|
|
4065
|
-
var LOCALE_RE, VUE_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
|
|
4256
|
+
var LOCALE_RE, VUE_DIR_CANDIDATES, ANGULAR_DIR_CANDIDATES, I18NEXT_DIR_CANDIDATES, GETTEXT_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
|
|
4066
4257
|
var init_detect = __esm({
|
|
4067
4258
|
"src/server/import/detect.ts"() {
|
|
4068
4259
|
"use strict";
|
|
4069
4260
|
LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4070
4261
|
VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4071
|
-
|
|
4262
|
+
ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4263
|
+
I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4264
|
+
GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4265
|
+
DETECTORS = [
|
|
4266
|
+
detectLaravel,
|
|
4267
|
+
detectVue,
|
|
4268
|
+
detectArb,
|
|
4269
|
+
detectApple,
|
|
4270
|
+
detectAngularXliff,
|
|
4271
|
+
detectRails,
|
|
4272
|
+
detectI18next,
|
|
4273
|
+
detectGettext,
|
|
4274
|
+
detectAppleStringsdict
|
|
4275
|
+
];
|
|
4072
4276
|
BY_FORMAT = {
|
|
4073
4277
|
"laravel-php": detectLaravel,
|
|
4074
4278
|
"vue-i18n-json": (root) => detectVue(root, true),
|
|
4075
4279
|
"flutter-arb": detectArb,
|
|
4076
|
-
"apple-strings": detectApple
|
|
4280
|
+
"apple-strings": detectApple,
|
|
4281
|
+
"angular-xliff": detectAngularXliff,
|
|
4282
|
+
"rails-yaml": detectRails,
|
|
4283
|
+
"i18next-json": detectI18next,
|
|
4284
|
+
"gettext-po": detectGettext,
|
|
4285
|
+
"apple-stringsdict": detectAppleStringsdict
|
|
4077
4286
|
};
|
|
4078
4287
|
}
|
|
4079
4288
|
});
|
|
@@ -4106,7 +4315,7 @@ var init_flatten = __esm({
|
|
|
4106
4315
|
});
|
|
4107
4316
|
|
|
4108
4317
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4109
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4318
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
|
|
4110
4319
|
import { join as join5 } from "path";
|
|
4111
4320
|
var LOCALE_RE2, vueI18nJson2;
|
|
4112
4321
|
var init_vue_i18n_json2 = __esm({
|
|
@@ -4127,7 +4336,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4127
4336
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4128
4337
|
let data;
|
|
4129
4338
|
try {
|
|
4130
|
-
data = JSON.parse(
|
|
4339
|
+
data = JSON.parse(readFileSync12(join5(localeRoot, file), "utf8"));
|
|
4131
4340
|
} catch (e) {
|
|
4132
4341
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4133
4342
|
continue;
|
|
@@ -4232,7 +4441,7 @@ var init_laravel_php2 = __esm({
|
|
|
4232
4441
|
});
|
|
4233
4442
|
|
|
4234
4443
|
// src/server/import/parsers/flutter-arb.ts
|
|
4235
|
-
import { readdirSync as readdirSync7, readFileSync as
|
|
4444
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync13 } from "fs";
|
|
4236
4445
|
import { join as join7 } from "path";
|
|
4237
4446
|
function localeFromArbName(file) {
|
|
4238
4447
|
const m = file.match(/^(.+)\.arb$/);
|
|
@@ -4273,7 +4482,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4273
4482
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4274
4483
|
let data;
|
|
4275
4484
|
try {
|
|
4276
|
-
data = JSON.parse(
|
|
4485
|
+
data = JSON.parse(readFileSync13(join7(localeRoot, file), "utf8"));
|
|
4277
4486
|
} catch (e) {
|
|
4278
4487
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4279
4488
|
continue;
|
|
@@ -4300,7 +4509,7 @@ var init_flutter_arb2 = __esm({
|
|
|
4300
4509
|
});
|
|
4301
4510
|
|
|
4302
4511
|
// src/server/import/parsers/apple-strings.ts
|
|
4303
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
4512
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync14, statSync as statSync5 } from "fs";
|
|
4304
4513
|
import { join as join8 } from "path";
|
|
4305
4514
|
function localeFromLproj(dir) {
|
|
4306
4515
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
@@ -4413,7 +4622,7 @@ var init_apple_strings2 = __esm({
|
|
|
4413
4622
|
let text;
|
|
4414
4623
|
try {
|
|
4415
4624
|
if (!statSync5(file).isFile()) continue;
|
|
4416
|
-
text =
|
|
4625
|
+
text = readFileSync14(file, "utf8");
|
|
4417
4626
|
} catch {
|
|
4418
4627
|
continue;
|
|
4419
4628
|
}
|
|
@@ -4432,6 +4641,773 @@ var init_apple_strings2 = __esm({
|
|
|
4432
4641
|
}
|
|
4433
4642
|
});
|
|
4434
4643
|
|
|
4644
|
+
// src/server/import/parsers/angular-xliff.ts
|
|
4645
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync15 } from "fs";
|
|
4646
|
+
import { join as join9 } from "path";
|
|
4647
|
+
function decodeEntities(s) {
|
|
4648
|
+
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, "&");
|
|
4649
|
+
}
|
|
4650
|
+
function parseAttrs(s) {
|
|
4651
|
+
const out = {};
|
|
4652
|
+
for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
|
|
4653
|
+
return out;
|
|
4654
|
+
}
|
|
4655
|
+
function decodeInline(raw, addMeta) {
|
|
4656
|
+
let out = "";
|
|
4657
|
+
let last = 0;
|
|
4658
|
+
for (const m of raw.matchAll(/<x\b([^>]*?)\/>/g)) {
|
|
4659
|
+
out += decodeEntities(raw.slice(last, m.index));
|
|
4660
|
+
const attrs = parseAttrs(m[1]);
|
|
4661
|
+
const id = attrs["id"] ?? "X";
|
|
4662
|
+
const equiv = attrs["equiv-text"];
|
|
4663
|
+
const simple = equiv?.match(/^\{\{\s*(\w+)\s*\}\}$/);
|
|
4664
|
+
if (simple) {
|
|
4665
|
+
out += `{${simple[1]}}`;
|
|
4666
|
+
} else {
|
|
4667
|
+
out += `{${id}}`;
|
|
4668
|
+
const meta = {};
|
|
4669
|
+
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4670
|
+
if (equiv !== void 0) meta.example = equiv;
|
|
4671
|
+
addMeta(id, meta);
|
|
4672
|
+
}
|
|
4673
|
+
last = m.index + m[0].length;
|
|
4674
|
+
}
|
|
4675
|
+
return out + decodeEntities(raw.slice(last));
|
|
4676
|
+
}
|
|
4677
|
+
var LOCALE_RE5, FILE_RE, angularXliff2;
|
|
4678
|
+
var init_angular_xliff2 = __esm({
|
|
4679
|
+
"src/server/import/parsers/angular-xliff.ts"() {
|
|
4680
|
+
"use strict";
|
|
4681
|
+
LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4682
|
+
FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4683
|
+
angularXliff2 = {
|
|
4684
|
+
name: "angular-xliff",
|
|
4685
|
+
parse(localeRoot, opts) {
|
|
4686
|
+
const warnings = [];
|
|
4687
|
+
const keys = {};
|
|
4688
|
+
const locales = [];
|
|
4689
|
+
const seen = (loc) => {
|
|
4690
|
+
if (!locales.includes(loc)) locales.push(loc);
|
|
4691
|
+
};
|
|
4692
|
+
const files = readdirSync9(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
|
|
4693
|
+
for (const file of files) {
|
|
4694
|
+
const fnameLocale = file.match(FILE_RE)[1];
|
|
4695
|
+
if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
|
|
4696
|
+
let xml;
|
|
4697
|
+
try {
|
|
4698
|
+
xml = readFileSync15(join9(localeRoot, file), "utf8");
|
|
4699
|
+
} catch (e) {
|
|
4700
|
+
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
4701
|
+
continue;
|
|
4702
|
+
}
|
|
4703
|
+
const sourceLocale = xml.match(/source-language="([^"]+)"/)?.[1];
|
|
4704
|
+
if (!sourceLocale) {
|
|
4705
|
+
warnings.push(`angular-xliff: ${file} has no source-language attribute; skipped`);
|
|
4706
|
+
continue;
|
|
4707
|
+
}
|
|
4708
|
+
const targetLocale = xml.match(/target-language="([^"]+)"/)?.[1] ?? fnameLocale;
|
|
4709
|
+
if (opts?.locales && !opts.locales.includes(targetLocale ?? sourceLocale)) continue;
|
|
4710
|
+
for (const unit of xml.matchAll(/<trans-unit\b([^>]*)>([\s\S]*?)<\/trans-unit>/g)) {
|
|
4711
|
+
const id = parseAttrs(unit[1])["id"];
|
|
4712
|
+
if (!id) {
|
|
4713
|
+
warnings.push(`angular-xliff: ${file} has a trans-unit without an id; skipped`);
|
|
4714
|
+
continue;
|
|
4715
|
+
}
|
|
4716
|
+
const body = unit[2];
|
|
4717
|
+
const src = body.match(/<source\b[^>]*>([\s\S]*?)<\/source>/);
|
|
4718
|
+
let tgt = body.match(/<target\b([^>]*)>([\s\S]*?)<\/target>/);
|
|
4719
|
+
if (tgt && /\bstate="new"/.test(tgt[1])) tgt = null;
|
|
4720
|
+
const entry = keys[id] ??= { values: {} };
|
|
4721
|
+
const addMeta = (name, meta) => {
|
|
4722
|
+
(entry.placeholders ??= {})[name] ??= meta;
|
|
4723
|
+
};
|
|
4724
|
+
if (src && entry.values[sourceLocale] === void 0) {
|
|
4725
|
+
entry.values[sourceLocale] = decodeInline(src[1], addMeta);
|
|
4726
|
+
seen(sourceLocale);
|
|
4727
|
+
}
|
|
4728
|
+
if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
|
|
4729
|
+
entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
|
|
4730
|
+
seen(targetLocale);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
return { locales, keys, warnings };
|
|
4735
|
+
}
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
4738
|
+
});
|
|
4739
|
+
|
|
4740
|
+
// src/server/import/parsers/gettext-po.ts
|
|
4741
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync16 } from "fs";
|
|
4742
|
+
import { join as join10 } from "path";
|
|
4743
|
+
function unescapePo(s) {
|
|
4744
|
+
return s.replace(
|
|
4745
|
+
/\\([\\"ntr])/g,
|
|
4746
|
+
(_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
|
|
4747
|
+
);
|
|
4748
|
+
}
|
|
4749
|
+
function parseEntries(text) {
|
|
4750
|
+
const entries = [];
|
|
4751
|
+
let cur = null;
|
|
4752
|
+
let append = null;
|
|
4753
|
+
const flush = () => {
|
|
4754
|
+
if (cur && cur.msgid !== void 0) entries.push(cur);
|
|
4755
|
+
cur = null;
|
|
4756
|
+
append = null;
|
|
4757
|
+
};
|
|
4758
|
+
for (const line of text.split("\n")) {
|
|
4759
|
+
if (line.trim() === "") {
|
|
4760
|
+
flush();
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
if (line.startsWith("#")) continue;
|
|
4764
|
+
const m = line.match(DIRECTIVE_RE);
|
|
4765
|
+
if (m) {
|
|
4766
|
+
const kw = m[1];
|
|
4767
|
+
const idx = m[2];
|
|
4768
|
+
const body = unescapePo(m[3]);
|
|
4769
|
+
if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
|
|
4770
|
+
cur ??= { plurals: /* @__PURE__ */ new Map() };
|
|
4771
|
+
const entry = cur;
|
|
4772
|
+
if (kw === "msgctxt") {
|
|
4773
|
+
entry.msgctxt = body;
|
|
4774
|
+
append = (c) => {
|
|
4775
|
+
entry.msgctxt = (entry.msgctxt ?? "") + c;
|
|
4776
|
+
};
|
|
4777
|
+
} else if (kw === "msgid") {
|
|
4778
|
+
entry.msgid = body;
|
|
4779
|
+
append = (c) => {
|
|
4780
|
+
entry.msgid = (entry.msgid ?? "") + c;
|
|
4781
|
+
};
|
|
4782
|
+
} else if (kw === "msgid_plural") {
|
|
4783
|
+
entry.msgidPlural = body;
|
|
4784
|
+
append = (c) => {
|
|
4785
|
+
entry.msgidPlural = (entry.msgidPlural ?? "") + c;
|
|
4786
|
+
};
|
|
4787
|
+
} else if (idx !== void 0) {
|
|
4788
|
+
const i = Number(idx);
|
|
4789
|
+
entry.plurals.set(i, body);
|
|
4790
|
+
append = (c) => {
|
|
4791
|
+
entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
|
|
4792
|
+
};
|
|
4793
|
+
} else {
|
|
4794
|
+
entry.msgstr = body;
|
|
4795
|
+
append = (c) => {
|
|
4796
|
+
entry.msgstr = (entry.msgstr ?? "") + c;
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
continue;
|
|
4800
|
+
}
|
|
4801
|
+
const cont = line.match(CONT_RE);
|
|
4802
|
+
if (cont && append) append(unescapePo(cont[1]));
|
|
4803
|
+
}
|
|
4804
|
+
flush();
|
|
4805
|
+
return entries;
|
|
4806
|
+
}
|
|
4807
|
+
function discoverPoFiles(root) {
|
|
4808
|
+
const found = [];
|
|
4809
|
+
const entries = readdirSync10(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
4810
|
+
for (const e of entries) {
|
|
4811
|
+
if (e.isFile() && e.name.endsWith(".po")) {
|
|
4812
|
+
const base = e.name.slice(0, -3);
|
|
4813
|
+
found.push({ path: join10(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
|
|
4814
|
+
} else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
|
|
4815
|
+
for (const sub of [join10(e.name, "LC_MESSAGES"), e.name]) {
|
|
4816
|
+
let names;
|
|
4817
|
+
try {
|
|
4818
|
+
names = readdirSync10(join10(root, sub)).sort();
|
|
4819
|
+
} catch {
|
|
4820
|
+
continue;
|
|
4821
|
+
}
|
|
4822
|
+
for (const f of names) {
|
|
4823
|
+
if (f.endsWith(".po")) found.push({ path: join10(root, sub, f), rel: join10(sub, f), locale: e.name });
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
return found;
|
|
4829
|
+
}
|
|
4830
|
+
var LOCALE_RE6, DIRECTIVE_RE, CONT_RE, gettextPo2;
|
|
4831
|
+
var init_gettext_po2 = __esm({
|
|
4832
|
+
"src/server/import/parsers/gettext-po.ts"() {
|
|
4833
|
+
"use strict";
|
|
4834
|
+
init_plurals();
|
|
4835
|
+
LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4836
|
+
DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
4837
|
+
CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
4838
|
+
gettextPo2 = {
|
|
4839
|
+
name: "gettext-po",
|
|
4840
|
+
parse(localeRoot, opts) {
|
|
4841
|
+
const warnings = [];
|
|
4842
|
+
const keys = {};
|
|
4843
|
+
const locales = [];
|
|
4844
|
+
for (const file of discoverPoFiles(localeRoot)) {
|
|
4845
|
+
let entries;
|
|
4846
|
+
try {
|
|
4847
|
+
entries = parseEntries(readFileSync16(file.path, "utf8"));
|
|
4848
|
+
} catch (e) {
|
|
4849
|
+
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
4850
|
+
continue;
|
|
4851
|
+
}
|
|
4852
|
+
const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
|
|
4853
|
+
const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
|
|
4854
|
+
const locale = file.locale ?? headerLang;
|
|
4855
|
+
if (!locale) {
|
|
4856
|
+
warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4859
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4860
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
4861
|
+
const cats = categoriesFor(locale);
|
|
4862
|
+
for (const entry of entries) {
|
|
4863
|
+
if (entry === header) continue;
|
|
4864
|
+
const key = entry.msgctxt ?? entry.msgid;
|
|
4865
|
+
if (!key) continue;
|
|
4866
|
+
if (entry.msgidPlural !== void 0) {
|
|
4867
|
+
const forms = {};
|
|
4868
|
+
for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
|
|
4869
|
+
if (body === "") continue;
|
|
4870
|
+
const cat = cats[i];
|
|
4871
|
+
if (!cat) {
|
|
4872
|
+
warnings.push(
|
|
4873
|
+
`gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
|
|
4874
|
+
);
|
|
4875
|
+
continue;
|
|
4876
|
+
}
|
|
4877
|
+
forms[cat] = body.split("%d").join("{count}");
|
|
4878
|
+
}
|
|
4879
|
+
if (!forms.other) continue;
|
|
4880
|
+
(keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
|
|
4881
|
+
} else {
|
|
4882
|
+
if (!entry.msgstr) continue;
|
|
4883
|
+
(keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
return { locales, keys, warnings };
|
|
4888
|
+
}
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
});
|
|
4892
|
+
|
|
4893
|
+
// src/server/import/parsers/i18next-json.ts
|
|
4894
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
4895
|
+
import { join as join11 } from "path";
|
|
4896
|
+
function safeIsDir2(p) {
|
|
4897
|
+
try {
|
|
4898
|
+
return statSync6(p).isDirectory();
|
|
4899
|
+
} catch {
|
|
4900
|
+
return false;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
function fromI18next(value) {
|
|
4904
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
4905
|
+
return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
|
|
4906
|
+
}
|
|
4907
|
+
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
4908
|
+
let data;
|
|
4909
|
+
try {
|
|
4910
|
+
data = JSON.parse(readFileSync17(path, "utf8"));
|
|
4911
|
+
} catch (e) {
|
|
4912
|
+
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
4913
|
+
return false;
|
|
4914
|
+
}
|
|
4915
|
+
const fileWarnings = [];
|
|
4916
|
+
const flat = flattenObject(data, "", fileWarnings);
|
|
4917
|
+
for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
|
|
4918
|
+
const families = /* @__PURE__ */ new Set();
|
|
4919
|
+
for (const [k, v] of Object.entries(flat)) {
|
|
4920
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
4921
|
+
if (m && m[2] === "other" && v !== "") families.add(m[1]);
|
|
4922
|
+
}
|
|
4923
|
+
const pluralForms = {};
|
|
4924
|
+
for (const [k, raw] of Object.entries(flat)) {
|
|
4925
|
+
if (raw === "") continue;
|
|
4926
|
+
const value = fromI18next(raw);
|
|
4927
|
+
const m = PLURAL_SUFFIX_RE.exec(k);
|
|
4928
|
+
if (m && families.has(m[1])) {
|
|
4929
|
+
(pluralForms[m[1]] ??= {})[m[2]] = value;
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
if (families.has(k)) {
|
|
4933
|
+
warnings.push(
|
|
4934
|
+
`i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
|
|
4935
|
+
);
|
|
4936
|
+
continue;
|
|
4937
|
+
}
|
|
4938
|
+
(keys[prefix + k] ??= { values: {} }).values[locale] = value;
|
|
4939
|
+
}
|
|
4940
|
+
for (const [base, forms] of Object.entries(pluralForms)) {
|
|
4941
|
+
(keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
|
|
4942
|
+
}
|
|
4943
|
+
return true;
|
|
4944
|
+
}
|
|
4945
|
+
var LOCALE_RE7, PLURAL_SUFFIX_RE, PLURAL_ARG, DEFAULT_NAMESPACE, i18nextJson2;
|
|
4946
|
+
var init_i18next_json2 = __esm({
|
|
4947
|
+
"src/server/import/parsers/i18next-json.ts"() {
|
|
4948
|
+
"use strict";
|
|
4949
|
+
init_flatten();
|
|
4950
|
+
init_plurals();
|
|
4951
|
+
init_placeholders();
|
|
4952
|
+
LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4953
|
+
PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
4954
|
+
PLURAL_ARG = "count";
|
|
4955
|
+
DEFAULT_NAMESPACE = "translation";
|
|
4956
|
+
i18nextJson2 = {
|
|
4957
|
+
name: "i18next-json",
|
|
4958
|
+
parse(localeRoot, opts) {
|
|
4959
|
+
const warnings = [];
|
|
4960
|
+
const keys = {};
|
|
4961
|
+
const locales = [];
|
|
4962
|
+
for (const entry of readdirSync11(localeRoot).sort()) {
|
|
4963
|
+
const full = join11(localeRoot, entry);
|
|
4964
|
+
if (safeIsDir2(full)) {
|
|
4965
|
+
if (!LOCALE_RE7.test(entry)) continue;
|
|
4966
|
+
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
4967
|
+
let any = false;
|
|
4968
|
+
for (const file of readdirSync11(full).sort()) {
|
|
4969
|
+
if (!file.endsWith(".json")) continue;
|
|
4970
|
+
const ns = file.slice(0, -".json".length);
|
|
4971
|
+
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
4972
|
+
if (ingestFile(join11(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
4973
|
+
}
|
|
4974
|
+
if (any && !locales.includes(entry)) locales.push(entry);
|
|
4975
|
+
} else if (entry.endsWith(".json")) {
|
|
4976
|
+
const locale = entry.slice(0, -".json".length);
|
|
4977
|
+
if (!LOCALE_RE7.test(locale)) continue;
|
|
4978
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4979
|
+
if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
|
|
4980
|
+
locales.push(locale);
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4984
|
+
return { locales, keys, warnings };
|
|
4985
|
+
}
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
});
|
|
4989
|
+
|
|
4990
|
+
// src/server/import/parsers/rails-yaml.ts
|
|
4991
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync18 } from "fs";
|
|
4992
|
+
import { join as join12 } from "path";
|
|
4993
|
+
function fromRuby(value) {
|
|
4994
|
+
return value.replace(/%\{(\w+)\}/g, "{$1}");
|
|
4995
|
+
}
|
|
4996
|
+
function makeNode() {
|
|
4997
|
+
return /* @__PURE__ */ Object.create(null);
|
|
4998
|
+
}
|
|
4999
|
+
function decodeDouble(body) {
|
|
5000
|
+
let out = "";
|
|
5001
|
+
for (let i = 0; i < body.length; i++) {
|
|
5002
|
+
const c = body[i];
|
|
5003
|
+
if (c !== "\\") {
|
|
5004
|
+
out += c;
|
|
5005
|
+
continue;
|
|
5006
|
+
}
|
|
5007
|
+
const n = body[++i];
|
|
5008
|
+
if (n === void 0) break;
|
|
5009
|
+
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
5010
|
+
}
|
|
5011
|
+
return out;
|
|
5012
|
+
}
|
|
5013
|
+
function scanQuoted(s, start) {
|
|
5014
|
+
const q = s[start];
|
|
5015
|
+
if (q === '"') {
|
|
5016
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
5017
|
+
if (s[i] === "\\") i++;
|
|
5018
|
+
else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
|
|
5019
|
+
}
|
|
5020
|
+
return null;
|
|
5021
|
+
}
|
|
5022
|
+
let out = "";
|
|
5023
|
+
for (let i = start + 1; i < s.length; i++) {
|
|
5024
|
+
if (s[i] === "'") {
|
|
5025
|
+
if (s[i + 1] === "'") {
|
|
5026
|
+
out += "'";
|
|
5027
|
+
i++;
|
|
5028
|
+
} else {
|
|
5029
|
+
return { text: out, end: i + 1 };
|
|
5030
|
+
}
|
|
5031
|
+
} else {
|
|
5032
|
+
out += s[i];
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
return null;
|
|
5036
|
+
}
|
|
5037
|
+
function stripPlainComment(s) {
|
|
5038
|
+
const m = /(^|\s)#/.exec(s);
|
|
5039
|
+
return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
|
|
5040
|
+
}
|
|
5041
|
+
function onlyTrailing(s) {
|
|
5042
|
+
return /^\s*(#.*)?$/.test(s);
|
|
5043
|
+
}
|
|
5044
|
+
function parseYamlSubset(text, file, warnings) {
|
|
5045
|
+
const roots = {};
|
|
5046
|
+
const lines = text.split(/\r?\n/);
|
|
5047
|
+
let stack = [];
|
|
5048
|
+
let skipDeeperThan = null;
|
|
5049
|
+
let lastLeafIndent = null;
|
|
5050
|
+
for (let n = 0; n < lines.length; n++) {
|
|
5051
|
+
const raw = lines[n];
|
|
5052
|
+
const lineNo = n + 1;
|
|
5053
|
+
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
5054
|
+
if (raw.trim() === "---") continue;
|
|
5055
|
+
const indentMatch = /^[ \t]*/.exec(raw)[0];
|
|
5056
|
+
if (indentMatch.includes(" ")) {
|
|
5057
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
|
|
5058
|
+
continue;
|
|
5059
|
+
}
|
|
5060
|
+
const indent = indentMatch.length;
|
|
5061
|
+
if (skipDeeperThan !== null) {
|
|
5062
|
+
if (indent > skipDeeperThan) continue;
|
|
5063
|
+
skipDeeperThan = null;
|
|
5064
|
+
}
|
|
5065
|
+
const content = raw.slice(indent);
|
|
5066
|
+
if (content.startsWith("- ") || content === "-") {
|
|
5067
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
|
|
5068
|
+
skipDeeperThan = indent;
|
|
5069
|
+
continue;
|
|
5070
|
+
}
|
|
5071
|
+
let key;
|
|
5072
|
+
let rest;
|
|
5073
|
+
if (content[0] === '"' || content[0] === "'") {
|
|
5074
|
+
const k = scanQuoted(content, 0);
|
|
5075
|
+
if (!k || content[k.end] !== ":") {
|
|
5076
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
|
|
5077
|
+
skipDeeperThan = indent;
|
|
5078
|
+
continue;
|
|
5079
|
+
}
|
|
5080
|
+
key = k.text;
|
|
5081
|
+
rest = content.slice(k.end + 1);
|
|
5082
|
+
} else {
|
|
5083
|
+
const m = /^(.*?):(?=\s|$)/.exec(content);
|
|
5084
|
+
if (!m) {
|
|
5085
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
|
|
5086
|
+
skipDeeperThan = indent;
|
|
5087
|
+
continue;
|
|
5088
|
+
}
|
|
5089
|
+
key = m[1].trim();
|
|
5090
|
+
rest = content.slice(m[0].length);
|
|
5091
|
+
}
|
|
5092
|
+
if (lastLeafIndent !== null && indent > lastLeafIndent) {
|
|
5093
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
|
|
5094
|
+
skipDeeperThan = indent - 1;
|
|
5095
|
+
continue;
|
|
5096
|
+
}
|
|
5097
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
5098
|
+
const trimmed = rest.trim();
|
|
5099
|
+
let value;
|
|
5100
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
5101
|
+
value = null;
|
|
5102
|
+
} else if (trimmed[0] === "&" || trimmed[0] === "*") {
|
|
5103
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
|
|
5104
|
+
skipDeeperThan = indent;
|
|
5105
|
+
continue;
|
|
5106
|
+
} else if (trimmed[0] === "|" || trimmed[0] === ">") {
|
|
5107
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
|
|
5108
|
+
skipDeeperThan = indent;
|
|
5109
|
+
continue;
|
|
5110
|
+
} else if (trimmed[0] === "[" || trimmed[0] === "{") {
|
|
5111
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
|
|
5112
|
+
skipDeeperThan = indent;
|
|
5113
|
+
continue;
|
|
5114
|
+
} else if (trimmed[0] === '"' || trimmed[0] === "'") {
|
|
5115
|
+
const v = scanQuoted(trimmed, 0);
|
|
5116
|
+
if (!v || !onlyTrailing(trimmed.slice(v.end))) {
|
|
5117
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
|
|
5118
|
+
continue;
|
|
5119
|
+
}
|
|
5120
|
+
value = v.text;
|
|
5121
|
+
} else {
|
|
5122
|
+
value = stripPlainComment(trimmed);
|
|
5123
|
+
}
|
|
5124
|
+
if (stack.length === 0) {
|
|
5125
|
+
if (value !== null) {
|
|
5126
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
|
|
5127
|
+
lastLeafIndent = indent;
|
|
5128
|
+
continue;
|
|
5129
|
+
}
|
|
5130
|
+
const root = roots[key] ??= makeNode();
|
|
5131
|
+
stack = [{ indent, node: root }];
|
|
5132
|
+
lastLeafIndent = null;
|
|
5133
|
+
continue;
|
|
5134
|
+
}
|
|
5135
|
+
const parent = stack[stack.length - 1].node;
|
|
5136
|
+
if (key in parent) {
|
|
5137
|
+
warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
|
|
5138
|
+
}
|
|
5139
|
+
if (value === null) {
|
|
5140
|
+
const child = makeNode();
|
|
5141
|
+
parent[key] = child;
|
|
5142
|
+
stack.push({ indent, node: child });
|
|
5143
|
+
lastLeafIndent = null;
|
|
5144
|
+
} else {
|
|
5145
|
+
parent[key] = value;
|
|
5146
|
+
lastLeafIndent = indent;
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
return { roots };
|
|
5150
|
+
}
|
|
5151
|
+
function asPluralForms(node) {
|
|
5152
|
+
const entries = Object.entries(node);
|
|
5153
|
+
if (entries.length === 0) return null;
|
|
5154
|
+
const forms = {};
|
|
5155
|
+
for (const [k, v] of entries) {
|
|
5156
|
+
if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
|
|
5157
|
+
if (v !== "") forms[k] = v;
|
|
5158
|
+
}
|
|
5159
|
+
if (!("other" in forms)) return null;
|
|
5160
|
+
return forms;
|
|
5161
|
+
}
|
|
5162
|
+
function synthesizeIcu(forms, file, key, warnings) {
|
|
5163
|
+
const parts = [];
|
|
5164
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
5165
|
+
const body = forms[cat];
|
|
5166
|
+
if (body === void 0) continue;
|
|
5167
|
+
if (body.includes("#")) {
|
|
5168
|
+
warnings.push(
|
|
5169
|
+
`rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
|
|
5170
|
+
);
|
|
5171
|
+
}
|
|
5172
|
+
parts.push(`${cat} {${fromRuby(body)}}`);
|
|
5173
|
+
}
|
|
5174
|
+
return `{count, plural, ${parts.join(" ")}}`;
|
|
5175
|
+
}
|
|
5176
|
+
var LOCALE_RE8, CATEGORY_SET, railsYaml2;
|
|
5177
|
+
var init_rails_yaml2 = __esm({
|
|
5178
|
+
"src/server/import/parsers/rails-yaml.ts"() {
|
|
5179
|
+
"use strict";
|
|
5180
|
+
init_schema();
|
|
5181
|
+
LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5182
|
+
CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5183
|
+
railsYaml2 = {
|
|
5184
|
+
name: "rails-yaml",
|
|
5185
|
+
parse(localeRoot, opts) {
|
|
5186
|
+
const warnings = [];
|
|
5187
|
+
const keys = {};
|
|
5188
|
+
const locales = [];
|
|
5189
|
+
const wanted = opts?.locales?.map((l) => l.toLowerCase());
|
|
5190
|
+
const addValue = (key, locale, value) => {
|
|
5191
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
5192
|
+
};
|
|
5193
|
+
const flatten = (node, prefix, locale, file) => {
|
|
5194
|
+
for (const [k, v] of Object.entries(node)) {
|
|
5195
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
5196
|
+
if (typeof v === "string") {
|
|
5197
|
+
if (v !== "") addValue(key, locale, fromRuby(v));
|
|
5198
|
+
continue;
|
|
5199
|
+
}
|
|
5200
|
+
const forms = asPluralForms(v);
|
|
5201
|
+
if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
|
|
5202
|
+
else flatten(v, key, locale, file);
|
|
5203
|
+
}
|
|
5204
|
+
};
|
|
5205
|
+
for (const file of readdirSync12(localeRoot).sort()) {
|
|
5206
|
+
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5207
|
+
let text;
|
|
5208
|
+
try {
|
|
5209
|
+
text = readFileSync18(join12(localeRoot, file), "utf8");
|
|
5210
|
+
} catch (e) {
|
|
5211
|
+
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5212
|
+
continue;
|
|
5213
|
+
}
|
|
5214
|
+
const { roots } = parseYamlSubset(text, file, warnings);
|
|
5215
|
+
for (const token of Object.keys(roots).sort()) {
|
|
5216
|
+
if (!LOCALE_RE8.test(token)) {
|
|
5217
|
+
warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
|
|
5218
|
+
continue;
|
|
5219
|
+
}
|
|
5220
|
+
if (wanted && !wanted.includes(token.toLowerCase())) continue;
|
|
5221
|
+
if (!locales.includes(token)) locales.push(token);
|
|
5222
|
+
flatten(roots[token], "", token, file);
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
return { locales, keys, warnings };
|
|
5226
|
+
}
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
});
|
|
5230
|
+
|
|
5231
|
+
// src/server/import/parsers/apple-stringsdict.ts
|
|
5232
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
|
|
5233
|
+
import { join as join13 } from "path";
|
|
5234
|
+
function localeFromLproj2(dir) {
|
|
5235
|
+
const m = dir.match(/^(.+)\.lproj$/);
|
|
5236
|
+
if (!m) return null;
|
|
5237
|
+
return LOCALE_RE9.test(m[1]) ? m[1] : null;
|
|
5238
|
+
}
|
|
5239
|
+
function decodeEntities2(s) {
|
|
5240
|
+
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, "&");
|
|
5241
|
+
}
|
|
5242
|
+
function parsePlistDict(xml) {
|
|
5243
|
+
let i = 0;
|
|
5244
|
+
const n = xml.length;
|
|
5245
|
+
const skipTrivia = () => {
|
|
5246
|
+
for (; ; ) {
|
|
5247
|
+
while (i < n && /\s/.test(xml[i])) i++;
|
|
5248
|
+
if (xml.startsWith("<!--", i)) {
|
|
5249
|
+
const end = xml.indexOf("-->", i + 4);
|
|
5250
|
+
if (end === -1) throw new Error("unterminated comment");
|
|
5251
|
+
i = end + 3;
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
|
|
5255
|
+
const end = xml.indexOf(">", i);
|
|
5256
|
+
if (end === -1) throw new Error("unterminated declaration");
|
|
5257
|
+
i = end + 1;
|
|
5258
|
+
continue;
|
|
5259
|
+
}
|
|
5260
|
+
break;
|
|
5261
|
+
}
|
|
5262
|
+
};
|
|
5263
|
+
const readTag = () => {
|
|
5264
|
+
if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
|
|
5265
|
+
const end = xml.indexOf(">", i);
|
|
5266
|
+
if (end === -1) throw new Error("unterminated tag");
|
|
5267
|
+
let body = xml.slice(i + 1, end).trim();
|
|
5268
|
+
i = end + 1;
|
|
5269
|
+
const closing = body.startsWith("/");
|
|
5270
|
+
if (closing) body = body.slice(1).trim();
|
|
5271
|
+
const selfClosing = body.endsWith("/");
|
|
5272
|
+
if (selfClosing) body = body.slice(0, -1).trim();
|
|
5273
|
+
const name = body.split(/\s/)[0];
|
|
5274
|
+
if (!name) throw new Error(`empty tag at offset ${end}`);
|
|
5275
|
+
return { name, closing, selfClosing };
|
|
5276
|
+
};
|
|
5277
|
+
const readElementText = (name) => {
|
|
5278
|
+
const re = new RegExp(`</${name}\\s*>`, "g");
|
|
5279
|
+
re.lastIndex = i;
|
|
5280
|
+
const m = re.exec(xml);
|
|
5281
|
+
if (!m) throw new Error(`unterminated <${name}>`);
|
|
5282
|
+
const text = xml.slice(i, m.index);
|
|
5283
|
+
i = m.index + m[0].length;
|
|
5284
|
+
return decodeEntities2(text);
|
|
5285
|
+
};
|
|
5286
|
+
const readValue = (tag2) => {
|
|
5287
|
+
if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
|
|
5288
|
+
if (tag2.name === "true" || tag2.name === "false") {
|
|
5289
|
+
if (!tag2.selfClosing) readElementText(tag2.name);
|
|
5290
|
+
return tag2.name;
|
|
5291
|
+
}
|
|
5292
|
+
if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
|
|
5293
|
+
return tag2.selfClosing ? "" : readElementText(tag2.name);
|
|
5294
|
+
}
|
|
5295
|
+
throw new Error(`unsupported plist element <${tag2.name}>`);
|
|
5296
|
+
};
|
|
5297
|
+
const readDict = () => {
|
|
5298
|
+
const out = {};
|
|
5299
|
+
for (; ; ) {
|
|
5300
|
+
skipTrivia();
|
|
5301
|
+
const tag2 = readTag();
|
|
5302
|
+
if (tag2.closing) {
|
|
5303
|
+
if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
|
|
5304
|
+
return out;
|
|
5305
|
+
}
|
|
5306
|
+
if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
|
|
5307
|
+
const key = readElementText("key");
|
|
5308
|
+
skipTrivia();
|
|
5309
|
+
const vt = readTag();
|
|
5310
|
+
if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
|
|
5311
|
+
out[key] = readValue(vt);
|
|
5312
|
+
}
|
|
5313
|
+
};
|
|
5314
|
+
skipTrivia();
|
|
5315
|
+
let tag = readTag();
|
|
5316
|
+
if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
|
|
5317
|
+
skipTrivia();
|
|
5318
|
+
tag = readTag();
|
|
5319
|
+
}
|
|
5320
|
+
if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
|
|
5321
|
+
return tag.selfClosing ? {} : readDict();
|
|
5322
|
+
}
|
|
5323
|
+
function entryToIcu(key, entry, file, warnings) {
|
|
5324
|
+
const warn = (msg) => {
|
|
5325
|
+
warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
|
|
5326
|
+
return null;
|
|
5327
|
+
};
|
|
5328
|
+
if (typeof entry !== "object") return warn("value is not a dict; skipped");
|
|
5329
|
+
const fmt = entry["NSStringLocalizedFormatKey"];
|
|
5330
|
+
if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
|
|
5331
|
+
const vars = [...fmt.matchAll(VAR_RE)];
|
|
5332
|
+
if (vars.length !== 1) {
|
|
5333
|
+
return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
|
|
5334
|
+
}
|
|
5335
|
+
const arg = vars[0][1];
|
|
5336
|
+
if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
|
|
5337
|
+
const prefix = fmt.slice(0, vars[0].index);
|
|
5338
|
+
const suffix = fmt.slice(vars[0].index + vars[0][0].length);
|
|
5339
|
+
const varDict = entry[arg];
|
|
5340
|
+
if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
|
|
5341
|
+
const specType = varDict["NSStringFormatSpecTypeKey"];
|
|
5342
|
+
if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
|
|
5343
|
+
return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
|
|
5344
|
+
}
|
|
5345
|
+
const valueType = varDict["NSStringFormatValueTypeKey"];
|
|
5346
|
+
const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
|
|
5347
|
+
const forms = {};
|
|
5348
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
5349
|
+
const body = varDict[cat];
|
|
5350
|
+
if (typeof body !== "string") continue;
|
|
5351
|
+
forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
|
|
5352
|
+
}
|
|
5353
|
+
if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
|
|
5354
|
+
return formsToIcu(arg, forms);
|
|
5355
|
+
}
|
|
5356
|
+
var LOCALE_RE9, TABLE2, VAR_RE, appleStringsdict2;
|
|
5357
|
+
var init_apple_stringsdict2 = __esm({
|
|
5358
|
+
"src/server/import/parsers/apple-stringsdict.ts"() {
|
|
5359
|
+
"use strict";
|
|
5360
|
+
init_schema();
|
|
5361
|
+
init_plurals();
|
|
5362
|
+
LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5363
|
+
TABLE2 = "Localizable.stringsdict";
|
|
5364
|
+
VAR_RE = /%#@([^@]*)@/g;
|
|
5365
|
+
appleStringsdict2 = {
|
|
5366
|
+
name: "apple-stringsdict",
|
|
5367
|
+
parse(localeRoot, opts) {
|
|
5368
|
+
const warnings = [];
|
|
5369
|
+
const keys = {};
|
|
5370
|
+
const locales = [];
|
|
5371
|
+
for (const dir of readdirSync13(localeRoot).sort()) {
|
|
5372
|
+
const locale = localeFromLproj2(dir);
|
|
5373
|
+
if (!locale) continue;
|
|
5374
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5375
|
+
const file = join13(localeRoot, dir, TABLE2);
|
|
5376
|
+
let text;
|
|
5377
|
+
try {
|
|
5378
|
+
if (!statSync7(file).isFile()) continue;
|
|
5379
|
+
text = readFileSync19(file, "utf8");
|
|
5380
|
+
} catch {
|
|
5381
|
+
continue;
|
|
5382
|
+
}
|
|
5383
|
+
locales.push(locale);
|
|
5384
|
+
const others = readdirSync13(join13(localeRoot, dir)).filter(
|
|
5385
|
+
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5386
|
+
);
|
|
5387
|
+
if (others.length) {
|
|
5388
|
+
warnings.push(
|
|
5389
|
+
`apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
|
|
5390
|
+
);
|
|
5391
|
+
}
|
|
5392
|
+
let root;
|
|
5393
|
+
try {
|
|
5394
|
+
root = parsePlistDict(text);
|
|
5395
|
+
} catch (e) {
|
|
5396
|
+
warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
|
|
5397
|
+
continue;
|
|
5398
|
+
}
|
|
5399
|
+
for (const key of Object.keys(root).sort()) {
|
|
5400
|
+
const icu = entryToIcu(key, root[key], file, warnings);
|
|
5401
|
+
if (icu === null) continue;
|
|
5402
|
+
(keys[key] ??= { values: {} }).values[locale] = icu;
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
return { locales, keys, warnings };
|
|
5406
|
+
}
|
|
5407
|
+
};
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5410
|
+
|
|
4435
5411
|
// src/server/import/parsers/index.ts
|
|
4436
5412
|
function getParser(name) {
|
|
4437
5413
|
const p = REGISTRY[name];
|
|
@@ -4446,11 +5422,21 @@ var init_parsers = __esm({
|
|
|
4446
5422
|
init_laravel_php2();
|
|
4447
5423
|
init_flutter_arb2();
|
|
4448
5424
|
init_apple_strings2();
|
|
5425
|
+
init_angular_xliff2();
|
|
5426
|
+
init_gettext_po2();
|
|
5427
|
+
init_i18next_json2();
|
|
5428
|
+
init_rails_yaml2();
|
|
5429
|
+
init_apple_stringsdict2();
|
|
4449
5430
|
REGISTRY = {
|
|
4450
5431
|
[vueI18nJson2.name]: vueI18nJson2,
|
|
4451
5432
|
[laravelPhp2.name]: laravelPhp2,
|
|
4452
5433
|
[flutterArb2.name]: flutterArb2,
|
|
4453
|
-
[appleStrings2.name]: appleStrings2
|
|
5434
|
+
[appleStrings2.name]: appleStrings2,
|
|
5435
|
+
[angularXliff2.name]: angularXliff2,
|
|
5436
|
+
[gettextPo2.name]: gettextPo2,
|
|
5437
|
+
[i18nextJson2.name]: i18nextJson2,
|
|
5438
|
+
[railsYaml2.name]: railsYaml2,
|
|
5439
|
+
[appleStringsdict2.name]: appleStringsdict2
|
|
4454
5440
|
};
|
|
4455
5441
|
}
|
|
4456
5442
|
});
|
|
@@ -4536,7 +5522,14 @@ var init_assemble = __esm({
|
|
|
4536
5522
|
"laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
|
|
4537
5523
|
"vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
|
|
4538
5524
|
"flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
|
|
4539
|
-
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
|
|
5525
|
+
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
|
|
5526
|
+
// skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
|
|
5527
|
+
// only writes the translation files back next to it.
|
|
5528
|
+
"angular-xliff": { adapter: "angular-xliff", path: "messages.{locale}.xlf", rootRelative: true, skipSourceLocale: true },
|
|
5529
|
+
"gettext-po": { adapter: "gettext-po", path: "{locale}.po", rootRelative: true },
|
|
5530
|
+
"i18next-json": { adapter: "i18next-json", path: "{locale}/translation.json", rootRelative: true },
|
|
5531
|
+
"rails-yaml": { adapter: "rails-yaml", path: "config/locales/{locale}.yml" },
|
|
5532
|
+
"apple-stringsdict": { adapter: "apple-stringsdict", path: "{locale}.lproj/Localizable.stringsdict", rootRelative: true }
|
|
4540
5533
|
};
|
|
4541
5534
|
}
|
|
4542
5535
|
});
|
|
@@ -4718,120 +5711,22 @@ var init_stats = __esm({
|
|
|
4718
5711
|
}
|
|
4719
5712
|
});
|
|
4720
5713
|
|
|
4721
|
-
// node_modules/dictionary-en/index.js
|
|
4722
|
-
var dictionary_en_exports = {};
|
|
4723
|
-
__export(dictionary_en_exports, {
|
|
4724
|
-
default: () => dictionary_en_default
|
|
4725
|
-
});
|
|
4726
|
-
import fs from "fs/promises";
|
|
4727
|
-
var aff, dic, dictionary, dictionary_en_default;
|
|
4728
|
-
var init_dictionary_en = __esm({
|
|
4729
|
-
async "node_modules/dictionary-en/index.js"() {
|
|
4730
|
-
"use strict";
|
|
4731
|
-
aff = await fs.readFile(new URL("index.aff", import.meta.url));
|
|
4732
|
-
dic = await fs.readFile(new URL("index.dic", import.meta.url));
|
|
4733
|
-
dictionary = { aff, dic };
|
|
4734
|
-
dictionary_en_default = dictionary;
|
|
4735
|
-
}
|
|
4736
|
-
});
|
|
4737
|
-
|
|
4738
|
-
// src/server/spell.ts
|
|
4739
|
-
import nspell from "nspell";
|
|
4740
|
-
async function loadDictionary(locale) {
|
|
4741
|
-
const key = norm(locale);
|
|
4742
|
-
if (instances.has(key) || unavailable.has(key)) return;
|
|
4743
|
-
const loader = LOADERS[key];
|
|
4744
|
-
if (!loader) {
|
|
4745
|
-
unavailable.add(key);
|
|
4746
|
-
return;
|
|
4747
|
-
}
|
|
4748
|
-
try {
|
|
4749
|
-
const { default: dict } = await loader();
|
|
4750
|
-
instances.set(key, nspell(dict));
|
|
4751
|
-
} catch {
|
|
4752
|
-
unavailable.add(key);
|
|
4753
|
-
} finally {
|
|
4754
|
-
loading.delete(key);
|
|
4755
|
-
}
|
|
4756
|
-
}
|
|
4757
|
-
function spellValue(locale, value, ignore) {
|
|
4758
|
-
const key = norm(locale);
|
|
4759
|
-
if (unavailable.has(key)) return [];
|
|
4760
|
-
const spell = instances.get(key);
|
|
4761
|
-
if (!spell) {
|
|
4762
|
-
if (!LOADERS[key]) {
|
|
4763
|
-
unavailable.add(key);
|
|
4764
|
-
return [];
|
|
4765
|
-
}
|
|
4766
|
-
if (!loading.has(key)) {
|
|
4767
|
-
loading.add(key);
|
|
4768
|
-
void loadDictionary(key);
|
|
4769
|
-
}
|
|
4770
|
-
return null;
|
|
4771
|
-
}
|
|
4772
|
-
const cacheKey = key + " " + value;
|
|
4773
|
-
let allBad = cache.get(cacheKey);
|
|
4774
|
-
if (!allBad) {
|
|
4775
|
-
const words = value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
|
|
4776
|
-
allBad = words.filter((w) => !spell.correct(w));
|
|
4777
|
-
cache.set(cacheKey, allBad);
|
|
4778
|
-
}
|
|
4779
|
-
return allBad.filter((w) => !ignore.has(w.toLowerCase()));
|
|
4780
|
-
}
|
|
4781
|
-
var LOADERS, instances, loading, unavailable, cache, norm, ICU_BLOCK, MASK, WORD;
|
|
4782
|
-
var init_spell = __esm({
|
|
4783
|
-
"src/server/spell.ts"() {
|
|
4784
|
-
"use strict";
|
|
4785
|
-
LOADERS = {
|
|
4786
|
-
en: () => init_dictionary_en().then(() => dictionary_en_exports),
|
|
4787
|
-
es: () => import("dictionary-es"),
|
|
4788
|
-
fr: () => import("dictionary-fr")
|
|
4789
|
-
};
|
|
4790
|
-
instances = /* @__PURE__ */ new Map();
|
|
4791
|
-
loading = /* @__PURE__ */ new Set();
|
|
4792
|
-
unavailable = /* @__PURE__ */ new Set();
|
|
4793
|
-
cache = /* @__PURE__ */ new Map();
|
|
4794
|
-
norm = (locale) => locale.toLowerCase();
|
|
4795
|
-
ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
4796
|
-
MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
|
|
4797
|
-
WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
|
|
4798
|
-
}
|
|
4799
|
-
});
|
|
4800
|
-
|
|
4801
5714
|
// src/server/checks.ts
|
|
4802
|
-
function contains(haystack, needle, caseSensitive) {
|
|
4803
|
-
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
4804
|
-
}
|
|
4805
5715
|
function runChecks(state, opts = {}) {
|
|
4806
5716
|
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
4807
5717
|
const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
|
|
4808
5718
|
const issues = [];
|
|
4809
5719
|
let spellPending = false;
|
|
4810
5720
|
const { sourceLocale } = state.config;
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
for (const t of Object.values(e.translations ?? {})) {
|
|
4816
|
-
for (const w of t.toLowerCase().split(/\s+/)) if (w) ignore.add(w);
|
|
5721
|
+
const ignore = ignoreWordsFor(state.glossary, state.config.spelling?.customWords);
|
|
5722
|
+
if (on("untranslated")) {
|
|
5723
|
+
for (const m of findMissing(state)) {
|
|
5724
|
+
issues.push({ key: m.key, locale: m.locale, check: "untranslated", message: "Not translated yet" });
|
|
4817
5725
|
}
|
|
4818
5726
|
}
|
|
4819
|
-
for (const word of state.config.spelling?.customWords ?? []) {
|
|
4820
|
-
const w = word.trim().toLowerCase();
|
|
4821
|
-
if (w) ignore.add(w);
|
|
4822
|
-
}
|
|
4823
|
-
const targetLocales = state.config.locales.filter((l) => l !== sourceLocale);
|
|
4824
5727
|
for (const key of Object.keys(state.keys).sort()) {
|
|
4825
5728
|
const entry = state.keys[key];
|
|
4826
5729
|
const source = entry.values[sourceLocale]?.value ?? "";
|
|
4827
|
-
if (on("untranslated") && !entry.skipTranslate) {
|
|
4828
|
-
for (const locale of targetLocales) {
|
|
4829
|
-
const translated = entry.plural ? (entry.values[locale]?.forms?.other ?? "").trim() !== "" : (entry.values[locale]?.value ?? "").trim() !== "";
|
|
4830
|
-
if (!translated) {
|
|
4831
|
-
issues.push({ key, locale, check: "untranslated", message: "Not translated yet" });
|
|
4832
|
-
}
|
|
4833
|
-
}
|
|
4834
|
-
}
|
|
4835
5730
|
if (entry.plural) {
|
|
4836
5731
|
if (on("placeholder")) {
|
|
4837
5732
|
const sourceForm = entry.values[sourceLocale]?.forms?.other ?? "";
|
|
@@ -4875,7 +5770,8 @@ function runChecks(state, opts = {}) {
|
|
|
4875
5770
|
});
|
|
4876
5771
|
}
|
|
4877
5772
|
if (on("spelling") && !blank) {
|
|
4878
|
-
const
|
|
5773
|
+
const dictId = state.config.lint?.spelling?.locales?.[locale] ?? locale;
|
|
5774
|
+
const bad = spellValue(dictId, value, ignore);
|
|
4879
5775
|
if (bad === null) spellPending = true;
|
|
4880
5776
|
else if (bad.length) {
|
|
4881
5777
|
issues.push({
|
|
@@ -4905,29 +5801,14 @@ function runChecks(state, opts = {}) {
|
|
|
4905
5801
|
});
|
|
4906
5802
|
}
|
|
4907
5803
|
if (on("glossary") && source) {
|
|
4908
|
-
for (const
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
message: `Do-not-translate term "${hint.term}" is missing from the translation`,
|
|
4917
|
-
detail: [hint.term]
|
|
4918
|
-
});
|
|
4919
|
-
}
|
|
4920
|
-
} else if (hint.forced) {
|
|
4921
|
-
if (!contains(value, hint.forced, cs)) {
|
|
4922
|
-
issues.push({
|
|
4923
|
-
key,
|
|
4924
|
-
locale,
|
|
4925
|
-
check: "glossary",
|
|
4926
|
-
message: `Should use "${hint.forced}" for "${hint.term}"`,
|
|
4927
|
-
detail: [hint.forced]
|
|
4928
|
-
});
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
5804
|
+
for (const viol of glossaryViolations(source, value, locale, state.glossary)) {
|
|
5805
|
+
issues.push({
|
|
5806
|
+
key,
|
|
5807
|
+
locale,
|
|
5808
|
+
check: "glossary",
|
|
5809
|
+
message: viol.kind === "do-not-translate" ? `Do-not-translate term "${viol.term}" is missing from the translation` : `Should use "${viol.expected}" for "${viol.term}"`,
|
|
5810
|
+
detail: [viol.expected]
|
|
5811
|
+
});
|
|
4931
5812
|
}
|
|
4932
5813
|
}
|
|
4933
5814
|
}
|
|
@@ -4943,7 +5824,8 @@ var init_checks = __esm({
|
|
|
4943
5824
|
"src/server/checks.ts"() {
|
|
4944
5825
|
"use strict";
|
|
4945
5826
|
init_placeholders();
|
|
4946
|
-
|
|
5827
|
+
init_glossary();
|
|
5828
|
+
init_scan();
|
|
4947
5829
|
init_spell();
|
|
4948
5830
|
init_suppress();
|
|
4949
5831
|
CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];
|
|
@@ -4958,12 +5840,12 @@ var init_checks = __esm({
|
|
|
4958
5840
|
});
|
|
4959
5841
|
|
|
4960
5842
|
// src/server/ui-prefs.ts
|
|
4961
|
-
import { readFileSync as
|
|
5843
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
4962
5844
|
import { homedir } from "os";
|
|
4963
|
-
import { join as
|
|
5845
|
+
import { join as join14 } from "path";
|
|
4964
5846
|
function readJson2(path) {
|
|
4965
5847
|
try {
|
|
4966
|
-
const parsed = JSON.parse(
|
|
5848
|
+
const parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
4967
5849
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4968
5850
|
} catch {
|
|
4969
5851
|
return {};
|
|
@@ -4988,7 +5870,7 @@ var init_ui_prefs = __esm({
|
|
|
4988
5870
|
THEMES = ["system", "light", "dark"];
|
|
4989
5871
|
isThemeMode = (v) => THEMES.includes(v);
|
|
4990
5872
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
4991
|
-
defaultUiPrefsPath = () =>
|
|
5873
|
+
defaultUiPrefsPath = () => join14(homedir(), ".glotfile", "ui.json");
|
|
4992
5874
|
DEFAULTS = { theme: "system" };
|
|
4993
5875
|
}
|
|
4994
5876
|
});
|
|
@@ -4996,13 +5878,13 @@ var init_ui_prefs = __esm({
|
|
|
4996
5878
|
// src/server/api.ts
|
|
4997
5879
|
import { Hono } from "hono";
|
|
4998
5880
|
import { streamSSE } from "hono/streaming";
|
|
4999
|
-
import { readFileSync as
|
|
5881
|
+
import { readFileSync as readFileSync21, existsSync as existsSync11, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync4 } from "fs";
|
|
5000
5882
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
5001
5883
|
function projectName(root) {
|
|
5002
5884
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
5003
5885
|
if (existsSync11(nameFile)) {
|
|
5004
5886
|
try {
|
|
5005
|
-
const name =
|
|
5887
|
+
const name = readFileSync21(nameFile, "utf8").trim();
|
|
5006
5888
|
if (name) return name;
|
|
5007
5889
|
} catch {
|
|
5008
5890
|
}
|
|
@@ -5127,7 +6009,7 @@ function createApi(deps) {
|
|
|
5127
6009
|
if (depth > 4) return;
|
|
5128
6010
|
let entries = [];
|
|
5129
6011
|
try {
|
|
5130
|
-
entries =
|
|
6012
|
+
entries = readdirSync14(dir);
|
|
5131
6013
|
} catch {
|
|
5132
6014
|
return;
|
|
5133
6015
|
}
|
|
@@ -5141,7 +6023,7 @@ function createApi(deps) {
|
|
|
5141
6023
|
filePath = abs;
|
|
5142
6024
|
} else {
|
|
5143
6025
|
try {
|
|
5144
|
-
if (
|
|
6026
|
+
if (statSync8(abs).isDirectory()) walk(abs, depth + 1);
|
|
5145
6027
|
} catch {
|
|
5146
6028
|
}
|
|
5147
6029
|
continue;
|
|
@@ -5959,7 +6841,7 @@ __export(server_exports, {
|
|
|
5959
6841
|
import { Hono as Hono2 } from "hono";
|
|
5960
6842
|
import { serve } from "@hono/node-server";
|
|
5961
6843
|
import { fileURLToPath } from "url";
|
|
5962
|
-
import { dirname as dirname4, join as
|
|
6844
|
+
import { dirname as dirname4, join as join15, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
5963
6845
|
import { readFile, stat } from "fs/promises";
|
|
5964
6846
|
import { createServer } from "net";
|
|
5965
6847
|
import open from "open";
|
|
@@ -6002,7 +6884,7 @@ function buildApp(opts) {
|
|
|
6002
6884
|
const file = await readFileResponse(target);
|
|
6003
6885
|
if (file) return file;
|
|
6004
6886
|
}
|
|
6005
|
-
const index = await readFileResponse(
|
|
6887
|
+
const index = await readFileResponse(join15(root, "index.html"));
|
|
6006
6888
|
if (index) return index;
|
|
6007
6889
|
return c.notFound();
|
|
6008
6890
|
});
|
|
@@ -6060,7 +6942,7 @@ var init_server = __esm({
|
|
|
6060
6942
|
init_scan();
|
|
6061
6943
|
init_scanner();
|
|
6062
6944
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
6063
|
-
DEFAULT_UI_DIR =
|
|
6945
|
+
DEFAULT_UI_DIR = join15(here, "..", "ui");
|
|
6064
6946
|
MIME = {
|
|
6065
6947
|
".html": "text/html; charset=utf-8",
|
|
6066
6948
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -6104,8 +6986,8 @@ init_scanner();
|
|
|
6104
6986
|
init_context();
|
|
6105
6987
|
init_run2();
|
|
6106
6988
|
init_outputs();
|
|
6107
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
6108
|
-
import { readFileSync as
|
|
6989
|
+
import { resolve as resolve11, dirname as dirname5, join as join16 } from "path";
|
|
6990
|
+
import { readFileSync as readFileSync22, existsSync as existsSync12, mkdirSync as mkdirSync4, cpSync } from "fs";
|
|
6109
6991
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6110
6992
|
|
|
6111
6993
|
// src/server/lint/locate.ts
|
|
@@ -6180,6 +7062,9 @@ function parseArgs(argv) {
|
|
|
6180
7062
|
if (first === "help" || first === "--help" || first === "-h") {
|
|
6181
7063
|
return isCommand(argv[1]) ? { command: argv[1], statePath, help: true } : { command: "help", statePath };
|
|
6182
7064
|
}
|
|
7065
|
+
if (first === "version" || first === "--version" || first === "-v") {
|
|
7066
|
+
return { command: "version", statePath };
|
|
7067
|
+
}
|
|
6183
7068
|
if (first !== void 0 && !first.startsWith("-") && !isCommand(first)) {
|
|
6184
7069
|
return { command: "serve", statePath, unknownCommand: first };
|
|
6185
7070
|
}
|
|
@@ -6413,7 +7298,7 @@ async function runLintCmd(args) {
|
|
|
6413
7298
|
}
|
|
6414
7299
|
return;
|
|
6415
7300
|
}
|
|
6416
|
-
const rawText = existsSync12(args.statePath) ?
|
|
7301
|
+
const rawText = existsSync12(args.statePath) ? readFileSync22(args.statePath, "utf8") : "";
|
|
6417
7302
|
const report = await runLint(state, {
|
|
6418
7303
|
locales: args.locales,
|
|
6419
7304
|
ruleIds: args.ruleIds,
|
|
@@ -6437,7 +7322,7 @@ async function runCheck(args) {
|
|
|
6437
7322
|
process.exitCode = 1;
|
|
6438
7323
|
return;
|
|
6439
7324
|
}
|
|
6440
|
-
const rawText = existsSync12(args.statePath) ?
|
|
7325
|
+
const rawText = existsSync12(args.statePath) ? readFileSync22(args.statePath, "utf8") : "";
|
|
6441
7326
|
const root = dirname5(resolve11(args.statePath));
|
|
6442
7327
|
const lint = await runLint(state, {});
|
|
6443
7328
|
const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
|
|
@@ -6598,10 +7483,10 @@ function runSplit(args) {
|
|
|
6598
7483
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
6599
7484
|
);
|
|
6600
7485
|
}
|
|
6601
|
-
var SKILL_SRC =
|
|
7486
|
+
var SKILL_SRC = join16(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
6602
7487
|
function runSkill(args) {
|
|
6603
7488
|
if (args.print) {
|
|
6604
|
-
console.log(
|
|
7489
|
+
console.log(readFileSync22(join16(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
6605
7490
|
return;
|
|
6606
7491
|
}
|
|
6607
7492
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
@@ -6732,12 +7617,16 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
6732
7617
|
formatOpts(commands),
|
|
6733
7618
|
"",
|
|
6734
7619
|
"Global options:",
|
|
6735
|
-
formatOpts(GLOBAL_OPTS),
|
|
7620
|
+
formatOpts([...GLOBAL_OPTS, ["-v, --version", "Print the glotfile version"]]),
|
|
6736
7621
|
"",
|
|
6737
7622
|
"Run `glotfile <command> --help` for a command's options."
|
|
6738
7623
|
].join("\n")
|
|
6739
7624
|
);
|
|
6740
7625
|
}
|
|
7626
|
+
function printVersion() {
|
|
7627
|
+
const pkgPath = join16(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
7628
|
+
console.log(JSON.parse(readFileSync22(pkgPath, "utf8")).version);
|
|
7629
|
+
}
|
|
6741
7630
|
async function main(argv) {
|
|
6742
7631
|
const args = parseArgs(argv);
|
|
6743
7632
|
if (args.unknownCommand) {
|
|
@@ -6746,6 +7635,7 @@ async function main(argv) {
|
|
|
6746
7635
|
return;
|
|
6747
7636
|
}
|
|
6748
7637
|
if (args.command === "help") return printHelp();
|
|
7638
|
+
if (args.command === "version") return printVersion();
|
|
6749
7639
|
if (args.help) return printHelp(args.command);
|
|
6750
7640
|
loadDotEnv();
|
|
6751
7641
|
if (args.command === "export") return runExport(args);
|