glotfile 0.4.1 → 0.4.3
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/bin/glotfile.js +0 -0
- package/dist/server/cli.js +646 -422
- package/dist/server/server.js +590 -87
- package/dist/ui/assets/index-CqpESIEu.css +1 -0
- package/dist/ui/assets/{index-DfZmbiXq.js → index-VdTDY_C8.js} +49 -19
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-DVTJ7ZX_.css +0 -1
package/dist/server/server.js
CHANGED
|
@@ -29,7 +29,7 @@ var init_dictionary_en = __esm({
|
|
|
29
29
|
import { Hono as Hono2 } from "hono";
|
|
30
30
|
import { serve } from "@hono/node-server";
|
|
31
31
|
import { fileURLToPath } from "url";
|
|
32
|
-
import { dirname as
|
|
32
|
+
import { dirname as dirname4, join as join9, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
33
33
|
import { readFile, stat } from "fs/promises";
|
|
34
34
|
import { createServer } from "net";
|
|
35
35
|
import open from "open";
|
|
@@ -92,6 +92,17 @@ var RULE_IDS = [
|
|
|
92
92
|
"whitespace",
|
|
93
93
|
"spelling"
|
|
94
94
|
];
|
|
95
|
+
var DEFAULT_SEVERITY = {
|
|
96
|
+
"empty-source": "error",
|
|
97
|
+
"empty-translation": "error",
|
|
98
|
+
"placeholder-mismatch": "error",
|
|
99
|
+
"icu-mismatch": "error",
|
|
100
|
+
"glossary-violation": "error",
|
|
101
|
+
"max-length": "warn",
|
|
102
|
+
"identical-to-source": "warn",
|
|
103
|
+
"whitespace": "warn",
|
|
104
|
+
"spelling": "warn"
|
|
105
|
+
};
|
|
95
106
|
|
|
96
107
|
// src/server/schema.ts
|
|
97
108
|
var CURRENT_VERSION = 1;
|
|
@@ -783,8 +794,10 @@ function findMissing(state) {
|
|
|
783
794
|
const targets = state.config.locales.filter((l) => l !== state.config.sourceLocale).sort();
|
|
784
795
|
const out = [];
|
|
785
796
|
for (const key of Object.keys(state.keys).sort()) {
|
|
797
|
+
const entry = state.keys[key];
|
|
798
|
+
if (entry.skipTranslate) continue;
|
|
786
799
|
for (const locale of targets) {
|
|
787
|
-
const v =
|
|
800
|
+
const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
|
|
788
801
|
if (!v) out.push({ key, locale });
|
|
789
802
|
}
|
|
790
803
|
}
|
|
@@ -1771,16 +1784,310 @@ function runChecks(state, opts = {}) {
|
|
|
1771
1784
|
return { issues, spellPending };
|
|
1772
1785
|
}
|
|
1773
1786
|
|
|
1787
|
+
// src/server/lint/spelling.ts
|
|
1788
|
+
function tokenize(text) {
|
|
1789
|
+
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
1790
|
+
}
|
|
1791
|
+
function buildAllowWords(glossary, dictionary2 = []) {
|
|
1792
|
+
const set = /* @__PURE__ */ new Set();
|
|
1793
|
+
const add = (s) => {
|
|
1794
|
+
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
1795
|
+
};
|
|
1796
|
+
for (const g of glossary) add(g.term);
|
|
1797
|
+
for (const w of dictionary2) add(w);
|
|
1798
|
+
return set;
|
|
1799
|
+
}
|
|
1800
|
+
var spellingRule = {
|
|
1801
|
+
id: "spelling",
|
|
1802
|
+
run(state, ctx) {
|
|
1803
|
+
const out = [];
|
|
1804
|
+
for (const key of Object.keys(state.keys)) {
|
|
1805
|
+
const entry = state.keys[key];
|
|
1806
|
+
for (const locale of ctx.targetLocales) {
|
|
1807
|
+
const speller = ctx.spellers.get(locale);
|
|
1808
|
+
if (!speller) continue;
|
|
1809
|
+
const value = entry.values[locale]?.value;
|
|
1810
|
+
if (!value) continue;
|
|
1811
|
+
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
1812
|
+
for (const word of tokenize(value)) {
|
|
1813
|
+
const lower = word.toLowerCase();
|
|
1814
|
+
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
1815
|
+
if (!speller.correct(word)) {
|
|
1816
|
+
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return out;
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
var defaultLoader = async (dictId) => {
|
|
1825
|
+
try {
|
|
1826
|
+
const nspellMod = await import("nspell");
|
|
1827
|
+
const nspell2 = nspellMod.default ?? nspellMod;
|
|
1828
|
+
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
1829
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
1830
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
1831
|
+
return nspell2(dict);
|
|
1832
|
+
} catch {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
// src/server/lint/rules.ts
|
|
1838
|
+
var emptySourceRule = {
|
|
1839
|
+
id: "empty-source",
|
|
1840
|
+
run(state, ctx) {
|
|
1841
|
+
const out = [];
|
|
1842
|
+
for (const key of Object.keys(state.keys)) {
|
|
1843
|
+
const entry = state.keys[key];
|
|
1844
|
+
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
1845
|
+
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
1846
|
+
}
|
|
1847
|
+
return out;
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
var emptyTranslationRule = {
|
|
1851
|
+
id: "empty-translation",
|
|
1852
|
+
run(state, ctx) {
|
|
1853
|
+
const out = [];
|
|
1854
|
+
for (const m of findMissing(state)) {
|
|
1855
|
+
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
1856
|
+
}
|
|
1857
|
+
for (const key of Object.keys(state.keys)) {
|
|
1858
|
+
for (const locale of ctx.targetLocales) {
|
|
1859
|
+
const v = state.keys[key].values[locale]?.value;
|
|
1860
|
+
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return out;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
var identicalToSourceRule = {
|
|
1867
|
+
id: "identical-to-source",
|
|
1868
|
+
run(state, ctx) {
|
|
1869
|
+
const out = [];
|
|
1870
|
+
for (const key of Object.keys(state.keys)) {
|
|
1871
|
+
const entry = state.keys[key];
|
|
1872
|
+
if (entry.skipTranslate) continue;
|
|
1873
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1874
|
+
if (!src) continue;
|
|
1875
|
+
for (const locale of ctx.targetLocales) {
|
|
1876
|
+
const v = entry.values[locale]?.value;
|
|
1877
|
+
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return out;
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
var whitespaceRule = {
|
|
1884
|
+
id: "whitespace",
|
|
1885
|
+
run(state, ctx) {
|
|
1886
|
+
const out = [];
|
|
1887
|
+
for (const key of Object.keys(state.keys)) {
|
|
1888
|
+
const entry = state.keys[key];
|
|
1889
|
+
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
1890
|
+
const srcEdge = src !== src.trim();
|
|
1891
|
+
for (const locale of ctx.targetLocales) {
|
|
1892
|
+
const v = entry.values[locale]?.value;
|
|
1893
|
+
if (!v) continue;
|
|
1894
|
+
if (v !== v.trim() !== srcEdge) {
|
|
1895
|
+
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return out;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
var placeholderMismatchRule = {
|
|
1903
|
+
id: "placeholder-mismatch",
|
|
1904
|
+
run(state, ctx) {
|
|
1905
|
+
const out = [];
|
|
1906
|
+
for (const key of Object.keys(state.keys)) {
|
|
1907
|
+
const entry = state.keys[key];
|
|
1908
|
+
if (entry.plural) {
|
|
1909
|
+
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
1910
|
+
if (!srcForm) continue;
|
|
1911
|
+
for (const locale of ctx.targetLocales) {
|
|
1912
|
+
const forms = entry.values[locale]?.forms;
|
|
1913
|
+
if (!forms) continue;
|
|
1914
|
+
const bad = Object.entries(forms).some(
|
|
1915
|
+
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
1916
|
+
);
|
|
1917
|
+
if (bad) {
|
|
1918
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1924
|
+
if (!src) continue;
|
|
1925
|
+
for (const locale of ctx.targetLocales) {
|
|
1926
|
+
const v = entry.values[locale]?.value;
|
|
1927
|
+
if (!v) continue;
|
|
1928
|
+
if (!placeholdersMatch(src, v)) {
|
|
1929
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return out;
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
var icuMismatchRule = {
|
|
1937
|
+
id: "icu-mismatch",
|
|
1938
|
+
run(state, ctx) {
|
|
1939
|
+
const out = [];
|
|
1940
|
+
for (const key of Object.keys(state.keys)) {
|
|
1941
|
+
const entry = state.keys[key];
|
|
1942
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1943
|
+
if (!src) continue;
|
|
1944
|
+
const srcIcu = isIcuPluralOrSelect(src);
|
|
1945
|
+
for (const locale of ctx.targetLocales) {
|
|
1946
|
+
const v = entry.values[locale]?.value;
|
|
1947
|
+
if (!v) continue;
|
|
1948
|
+
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
1949
|
+
out.push({
|
|
1950
|
+
ruleId: "icu-mismatch",
|
|
1951
|
+
key,
|
|
1952
|
+
locale,
|
|
1953
|
+
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
return out;
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
var maxLengthRule = {
|
|
1962
|
+
id: "max-length",
|
|
1963
|
+
run(state, ctx) {
|
|
1964
|
+
const out = [];
|
|
1965
|
+
for (const key of Object.keys(state.keys)) {
|
|
1966
|
+
const entry = state.keys[key];
|
|
1967
|
+
const max = entry.maxLength;
|
|
1968
|
+
if (max == null) continue;
|
|
1969
|
+
for (const locale of ctx.targetLocales) {
|
|
1970
|
+
const v = entry.values[locale]?.value;
|
|
1971
|
+
if (v && v.length > max) {
|
|
1972
|
+
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return out;
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
var glossaryViolationRule = {
|
|
1980
|
+
id: "glossary-violation",
|
|
1981
|
+
run(state, ctx) {
|
|
1982
|
+
const out = [];
|
|
1983
|
+
for (const key of Object.keys(state.keys)) {
|
|
1984
|
+
const entry = state.keys[key];
|
|
1985
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1986
|
+
if (!src) continue;
|
|
1987
|
+
for (const locale of ctx.targetLocales) {
|
|
1988
|
+
const v = entry.values[locale]?.value;
|
|
1989
|
+
if (!v) continue;
|
|
1990
|
+
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
1991
|
+
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
1992
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
1993
|
+
}
|
|
1994
|
+
if (hint.forced && !v.includes(hint.forced)) {
|
|
1995
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return out;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
var ALL_RULES = [
|
|
2004
|
+
emptySourceRule,
|
|
2005
|
+
emptyTranslationRule,
|
|
2006
|
+
placeholderMismatchRule,
|
|
2007
|
+
icuMismatchRule,
|
|
2008
|
+
glossaryViolationRule,
|
|
2009
|
+
maxLengthRule,
|
|
2010
|
+
identicalToSourceRule,
|
|
2011
|
+
whitespaceRule,
|
|
2012
|
+
spellingRule
|
|
2013
|
+
];
|
|
2014
|
+
|
|
2015
|
+
// src/server/lint/run.ts
|
|
2016
|
+
function resolveSeverity(id, config) {
|
|
2017
|
+
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
2018
|
+
}
|
|
2019
|
+
function sortFindings(findings) {
|
|
2020
|
+
return [...findings].sort(
|
|
2021
|
+
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
function countSeverities(findings) {
|
|
2025
|
+
let error = 0, warn = 0;
|
|
2026
|
+
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
2027
|
+
return { error, warn };
|
|
2028
|
+
}
|
|
2029
|
+
async function loadSpellers(locales, config, load, warn) {
|
|
2030
|
+
const map = /* @__PURE__ */ new Map();
|
|
2031
|
+
for (const locale of locales) {
|
|
2032
|
+
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
2033
|
+
const speller = await load(dictId);
|
|
2034
|
+
if (speller) map.set(locale, speller);
|
|
2035
|
+
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
2036
|
+
}
|
|
2037
|
+
return map;
|
|
2038
|
+
}
|
|
2039
|
+
async function runLint(state, options = {}) {
|
|
2040
|
+
const config = state.config.lint ?? {};
|
|
2041
|
+
const rules = options.rules ?? ALL_RULES;
|
|
2042
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
2043
|
+
const load = options.loadSpeller ?? defaultLoader;
|
|
2044
|
+
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
2045
|
+
const isActive = (rule) => {
|
|
2046
|
+
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
2047
|
+
return resolveSeverity(rule.id, config) !== "off";
|
|
2048
|
+
};
|
|
2049
|
+
const active = rules.filter(isActive);
|
|
2050
|
+
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2051
|
+
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2052
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
2053
|
+
const ctx = {
|
|
2054
|
+
config,
|
|
2055
|
+
sourceLocale: state.config.sourceLocale,
|
|
2056
|
+
targetLocales,
|
|
2057
|
+
glossary: state.glossary,
|
|
2058
|
+
spellers,
|
|
2059
|
+
allowWords
|
|
2060
|
+
};
|
|
2061
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2062
|
+
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2063
|
+
const findings = [];
|
|
2064
|
+
for (const rule of active) {
|
|
2065
|
+
const severity = resolveSeverity(rule.id, config);
|
|
2066
|
+
for (const raw of rule.run(state, ctx)) {
|
|
2067
|
+
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
2068
|
+
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
2069
|
+
findings.push({ ...raw, severity });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
const sorted = sortFindings(findings);
|
|
2073
|
+
const counts = countSeverities(sorted);
|
|
2074
|
+
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/server/lint/outputs.ts
|
|
2078
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
2079
|
+
import { resolve as resolve5 } from "path";
|
|
2080
|
+
|
|
1774
2081
|
// src/server/adapters/options.ts
|
|
1775
2082
|
function applyCase(canonical, style) {
|
|
1776
|
-
const
|
|
2083
|
+
const sep4 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
|
|
1777
2084
|
const lower = style === "lower-hyphen" || style === "lower-underscore";
|
|
1778
2085
|
return canonical.split(/[-_]/).map((p, i) => {
|
|
1779
2086
|
if (lower || i === 0) return p.toLowerCase();
|
|
1780
2087
|
if (/^[a-z]{4}$/i.test(p)) return p[0].toUpperCase() + p.slice(1).toLowerCase();
|
|
1781
2088
|
if (/^[a-z]{2}$/i.test(p)) return p.toUpperCase();
|
|
1782
2089
|
return p;
|
|
1783
|
-
}).join(
|
|
2090
|
+
}).join(sep4);
|
|
1784
2091
|
}
|
|
1785
2092
|
function resolveLocaleToken(output, canonical, adapterDefault) {
|
|
1786
2093
|
const mapped = output.localeMap?.[canonical];
|
|
@@ -2301,6 +2608,104 @@ var vueI18nJson = {
|
|
|
2301
2608
|
}
|
|
2302
2609
|
};
|
|
2303
2610
|
|
|
2611
|
+
// src/server/adapters/angular-xliff.ts
|
|
2612
|
+
function xmlEscape2(s) {
|
|
2613
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2614
|
+
}
|
|
2615
|
+
function renderInterpolations(text, ids) {
|
|
2616
|
+
let out = "";
|
|
2617
|
+
let last = 0;
|
|
2618
|
+
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
2619
|
+
const name = m[1];
|
|
2620
|
+
let id = ids.get(name);
|
|
2621
|
+
if (id === void 0) {
|
|
2622
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
2623
|
+
ids.set(name, id);
|
|
2624
|
+
}
|
|
2625
|
+
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
2626
|
+
last = m.index + m[0].length;
|
|
2627
|
+
}
|
|
2628
|
+
return out + xmlEscape2(text.slice(last));
|
|
2629
|
+
}
|
|
2630
|
+
function renderPluralIcu(forms, ids) {
|
|
2631
|
+
const cats = [
|
|
2632
|
+
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
2633
|
+
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
2634
|
+
];
|
|
2635
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
2636
|
+
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
2637
|
+
}
|
|
2638
|
+
function renderEmbeddedIcu(value) {
|
|
2639
|
+
const renamed = value.replace(
|
|
2640
|
+
/\{\s*\w+\s*,\s*(plural|select|selectordinal)\s*,/g,
|
|
2641
|
+
(_, type) => `{${type === "plural" ? "VAR_PLURAL" : "VAR_SELECT"}, ${type},`
|
|
2642
|
+
);
|
|
2643
|
+
return xmlEscape2(renamed);
|
|
2644
|
+
}
|
|
2645
|
+
function renderScalar(value, ids) {
|
|
2646
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
2647
|
+
}
|
|
2648
|
+
var DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
|
|
2649
|
+
var angularXliff = {
|
|
2650
|
+
name: "angular-xliff",
|
|
2651
|
+
capabilities: {
|
|
2652
|
+
plural: "native",
|
|
2653
|
+
select: "native",
|
|
2654
|
+
nesting: "flat",
|
|
2655
|
+
metadata: true,
|
|
2656
|
+
placeholderStyle: "icu",
|
|
2657
|
+
fileGrouping: "per-locale"
|
|
2658
|
+
},
|
|
2659
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE7,
|
|
2660
|
+
export(state, output) {
|
|
2661
|
+
const files = [];
|
|
2662
|
+
const warnings = [];
|
|
2663
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
|
|
2664
|
+
const sourceLocale = state.config.sourceLocale;
|
|
2665
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
|
|
2666
|
+
const emptyAs = resolveEmptyAs(output, "source");
|
|
2667
|
+
const keys = Object.keys(state.keys).sort();
|
|
2668
|
+
for (const locale of state.config.locales) {
|
|
2669
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
|
|
2670
|
+
const units = [];
|
|
2671
|
+
for (const key of keys) {
|
|
2672
|
+
const entry = state.keys[key];
|
|
2673
|
+
let source;
|
|
2674
|
+
let target;
|
|
2675
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2676
|
+
if (entry.plural) {
|
|
2677
|
+
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
2678
|
+
if (targetForms === null) continue;
|
|
2679
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
2680
|
+
target = renderPluralIcu(targetForms, ids);
|
|
2681
|
+
} else {
|
|
2682
|
+
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
2683
|
+
if (targetValue === null) continue;
|
|
2684
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
2685
|
+
target = renderScalar(targetValue, ids);
|
|
2686
|
+
}
|
|
2687
|
+
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
2688
|
+
units.push(` <source>${source}</source>`);
|
|
2689
|
+
units.push(` <target>${target}</target>`);
|
|
2690
|
+
if (entry.description) {
|
|
2691
|
+
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
2692
|
+
}
|
|
2693
|
+
units.push(` </trans-unit>`);
|
|
2694
|
+
}
|
|
2695
|
+
const contents = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
2696
|
+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
2697
|
+
<file source-language="${xmlEscape2(sourceToken)}" target-language="${xmlEscape2(token)}" datatype="plaintext" original="ng2.template">
|
|
2698
|
+
<body>
|
|
2699
|
+
` + (units.length ? units.join("\n") + "\n" : "") + ` </body>
|
|
2700
|
+
</file>
|
|
2701
|
+
</xliff>
|
|
2702
|
+
`;
|
|
2703
|
+
files.push({ path: resolvePath(output.path, token), contents });
|
|
2704
|
+
}
|
|
2705
|
+
return { files, warnings };
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2304
2709
|
// src/server/adapters/index.ts
|
|
2305
2710
|
function resolvePath(template, locale, namespace = "") {
|
|
2306
2711
|
return template.replaceAll("{locale}", locale).replaceAll("{namespace}", namespace);
|
|
@@ -2333,7 +2738,8 @@ function getRegistry() {
|
|
|
2333
2738
|
[i18nextJson.name]: i18nextJson,
|
|
2334
2739
|
[gettextPo.name]: gettextPo,
|
|
2335
2740
|
[appleStringsdict.name]: appleStringsdict,
|
|
2336
|
-
[vueI18nJson.name]: vueI18nJson
|
|
2741
|
+
[vueI18nJson.name]: vueI18nJson,
|
|
2742
|
+
[angularXliff.name]: angularXliff
|
|
2337
2743
|
};
|
|
2338
2744
|
}
|
|
2339
2745
|
function getAdapter(name) {
|
|
@@ -2342,9 +2748,27 @@ function getAdapter(name) {
|
|
|
2342
2748
|
return a;
|
|
2343
2749
|
}
|
|
2344
2750
|
|
|
2751
|
+
// src/server/lint/outputs.ts
|
|
2752
|
+
function checkOutputs(state, root) {
|
|
2753
|
+
const out = [];
|
|
2754
|
+
for (const output of state.config.outputs) {
|
|
2755
|
+
const result = getAdapter(output.adapter).export(state, output);
|
|
2756
|
+
for (const file of result.files) {
|
|
2757
|
+
const abs = resolve5(root, file.path);
|
|
2758
|
+
const current = existsSync7(abs) ? readFileSync7(abs, "utf8") : null;
|
|
2759
|
+
if (current === null) {
|
|
2760
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
2761
|
+
} else if (current !== file.contents) {
|
|
2762
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
return out;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2345
2769
|
// src/server/api.ts
|
|
2346
|
-
import { readFileSync as
|
|
2347
|
-
import { dirname as
|
|
2770
|
+
import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
|
|
2771
|
+
import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
|
|
2348
2772
|
|
|
2349
2773
|
// src/server/ai/anthropic.ts
|
|
2350
2774
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -2376,6 +2800,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
2376
2800
|
function buildBatchPrompt(reqs) {
|
|
2377
2801
|
const targetLocale = reqs[0]?.targetLocale ?? "";
|
|
2378
2802
|
const hasPluralItems = reqs.some((r) => r.plural !== void 0);
|
|
2803
|
+
const hasGlossaryItems = reqs.some((r) => r.glossary !== void 0 && r.glossary.length > 0);
|
|
2379
2804
|
const items = reqs.map((r) => {
|
|
2380
2805
|
const base = {
|
|
2381
2806
|
id: r.id,
|
|
@@ -2385,7 +2810,7 @@ function buildBatchPrompt(reqs) {
|
|
|
2385
2810
|
// Wrap in braces so the model sees "{site}" not "site" — makes the visual
|
|
2386
2811
|
// connection to the source string obvious and reduces rename errors.
|
|
2387
2812
|
placeholders: r.placeholders.map((p) => `{${p}}`),
|
|
2388
|
-
glossary: r.glossary
|
|
2813
|
+
...r.glossary?.length ? { glossary: r.glossary } : {},
|
|
2389
2814
|
hasScreenshot: r.image !== void 0
|
|
2390
2815
|
};
|
|
2391
2816
|
if (r.plural) {
|
|
@@ -2395,7 +2820,7 @@ function buildBatchPrompt(reqs) {
|
|
|
2395
2820
|
});
|
|
2396
2821
|
const returnFormat = hasPluralItems ? 'For a scalar item (has `source`) return {"id","translation"}; for a plural item (has `plural`) return {"id","forms"} with one string per required category.' : 'Return {"id","translation"} for each item.';
|
|
2397
2822
|
return `Translate every item below into the target locale: ${targetLocale}. All items share this one target language.
|
|
2398
|
-
Glossary entries are constraints you MUST apply. Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context. ${returnFormat} Return JSON {"items":[\u2026]}.
|
|
2823
|
+
` + (hasGlossaryItems ? "Glossary entries are constraints you MUST apply. " : "") + `Items with hasScreenshot:true have a screenshot supplied as a separate image block above; use it for context. ${returnFormat} Return JSON {"items":[\u2026]}.
|
|
2399
2824
|
` + JSON.stringify(items, null, 2);
|
|
2400
2825
|
}
|
|
2401
2826
|
function buildTranslateGemmaSystemPrompt(sourceLocale, targetLocale) {
|
|
@@ -2840,7 +3265,7 @@ function stripFences(s) {
|
|
|
2840
3265
|
return s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
2841
3266
|
}
|
|
2842
3267
|
function defaultSpawn(prompt, systemPrompt, model) {
|
|
2843
|
-
return new Promise((
|
|
3268
|
+
return new Promise((resolve11, reject) => {
|
|
2844
3269
|
const args = [
|
|
2845
3270
|
"--print",
|
|
2846
3271
|
"--output-format",
|
|
@@ -2872,7 +3297,7 @@ function defaultSpawn(prompt, systemPrompt, model) {
|
|
|
2872
3297
|
reject(new Error(`claude error: ${envelope.result ?? "unknown error"}`));
|
|
2873
3298
|
return;
|
|
2874
3299
|
}
|
|
2875
|
-
|
|
3300
|
+
resolve11(envelope.result ?? "");
|
|
2876
3301
|
} catch {
|
|
2877
3302
|
reject(new Error(`Failed to parse claude JSON output: ${stdout.slice(0, 200)}`));
|
|
2878
3303
|
}
|
|
@@ -2946,10 +3371,10 @@ function makeProvider(ai) {
|
|
|
2946
3371
|
}
|
|
2947
3372
|
|
|
2948
3373
|
// src/server/log.ts
|
|
2949
|
-
import { appendFileSync, readFileSync as
|
|
2950
|
-
import { resolve as
|
|
3374
|
+
import { appendFileSync, readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
|
|
3375
|
+
import { resolve as resolve6 } from "path";
|
|
2951
3376
|
function logPath(projectRoot) {
|
|
2952
|
-
return
|
|
3377
|
+
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
2953
3378
|
}
|
|
2954
3379
|
function appendLog(projectRoot, entry) {
|
|
2955
3380
|
ensureGlotfileDir(projectRoot);
|
|
@@ -2957,14 +3382,14 @@ function appendLog(projectRoot, entry) {
|
|
|
2957
3382
|
}
|
|
2958
3383
|
function readLog(projectRoot, limit = 100) {
|
|
2959
3384
|
const path = logPath(projectRoot);
|
|
2960
|
-
if (!
|
|
2961
|
-
const lines =
|
|
3385
|
+
if (!existsSync8(path)) return [];
|
|
3386
|
+
const lines = readFileSync8(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2962
3387
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2963
3388
|
return entries.reverse().slice(0, limit);
|
|
2964
3389
|
}
|
|
2965
3390
|
|
|
2966
3391
|
// src/server/import/detect.ts
|
|
2967
|
-
import { existsSync as
|
|
3392
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2968
3393
|
import { join as join4 } from "path";
|
|
2969
3394
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
2970
3395
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
@@ -2997,12 +3422,13 @@ function detectLaravel(root) {
|
|
|
2997
3422
|
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
2998
3423
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
2999
3424
|
}
|
|
3000
|
-
function detectVue(root) {
|
|
3425
|
+
function detectVue(root, forced = false) {
|
|
3001
3426
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3002
3427
|
const localeRoot = join4(root, rel);
|
|
3003
3428
|
if (!safeIsDir(localeRoot)) continue;
|
|
3004
3429
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3005
|
-
|
|
3430
|
+
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
3431
|
+
if (enough) {
|
|
3006
3432
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3007
3433
|
try {
|
|
3008
3434
|
return statSync2(join4(localeRoot, `${loc}.json`)).size;
|
|
@@ -3029,11 +3455,11 @@ function detectArb(root) {
|
|
|
3029
3455
|
var DETECTORS = [detectLaravel, detectVue, detectArb];
|
|
3030
3456
|
var BY_FORMAT = {
|
|
3031
3457
|
"laravel-php": detectLaravel,
|
|
3032
|
-
"vue-i18n-json": detectVue,
|
|
3458
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3033
3459
|
"flutter-arb": detectArb
|
|
3034
3460
|
};
|
|
3035
3461
|
function detect(root, formatOverride) {
|
|
3036
|
-
if (!
|
|
3462
|
+
if (!existsSync9(root)) return null;
|
|
3037
3463
|
if (formatOverride) {
|
|
3038
3464
|
const fn = BY_FORMAT[formatOverride];
|
|
3039
3465
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -3047,7 +3473,7 @@ function detect(root, formatOverride) {
|
|
|
3047
3473
|
}
|
|
3048
3474
|
|
|
3049
3475
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
3050
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
3476
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
|
|
3051
3477
|
import { join as join5 } from "path";
|
|
3052
3478
|
|
|
3053
3479
|
// src/server/import/flatten.ts
|
|
@@ -3087,7 +3513,7 @@ var vueI18nJson2 = {
|
|
|
3087
3513
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3088
3514
|
let data;
|
|
3089
3515
|
try {
|
|
3090
|
-
data = JSON.parse(
|
|
3516
|
+
data = JSON.parse(readFileSync9(join5(localeRoot, file), "utf8"));
|
|
3091
3517
|
} catch (e) {
|
|
3092
3518
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
3093
3519
|
continue;
|
|
@@ -3179,7 +3605,7 @@ var laravelPhp2 = {
|
|
|
3179
3605
|
};
|
|
3180
3606
|
|
|
3181
3607
|
// src/server/import/parsers/flutter-arb.ts
|
|
3182
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
3608
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
|
|
3183
3609
|
import { join as join7 } from "path";
|
|
3184
3610
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3185
3611
|
function localeFromArbName(file) {
|
|
@@ -3216,7 +3642,7 @@ var flutterArb2 = {
|
|
|
3216
3642
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3217
3643
|
let data;
|
|
3218
3644
|
try {
|
|
3219
|
-
data = JSON.parse(
|
|
3645
|
+
data = JSON.parse(readFileSync10(join7(localeRoot, file), "utf8"));
|
|
3220
3646
|
} catch (e) {
|
|
3221
3647
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
3222
3648
|
continue;
|
|
@@ -3370,8 +3796,8 @@ function runImport(opts) {
|
|
|
3370
3796
|
}
|
|
3371
3797
|
|
|
3372
3798
|
// src/server/export-run.ts
|
|
3373
|
-
import { readFileSync as
|
|
3374
|
-
import { resolve as
|
|
3799
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync7, rmdirSync, statSync as statSync4, unlinkSync } from "fs";
|
|
3800
|
+
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
3375
3801
|
function effectiveLocales(config) {
|
|
3376
3802
|
const limit = config.exportLocales;
|
|
3377
3803
|
if (!limit || limit.length === 0) return config.locales;
|
|
@@ -3382,18 +3808,86 @@ function narrowForExport(state) {
|
|
|
3382
3808
|
if (locales.length === state.config.locales.length) return state;
|
|
3383
3809
|
return { ...state, config: { ...state.config, locales } };
|
|
3384
3810
|
}
|
|
3811
|
+
var LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
3812
|
+
function escapeRegExp(s) {
|
|
3813
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3814
|
+
}
|
|
3815
|
+
function segmentRegExp(segment) {
|
|
3816
|
+
const pattern = escapeRegExp(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
3817
|
+
return new RegExp(`^${pattern}$`);
|
|
3818
|
+
}
|
|
3819
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
3820
|
+
let current = dir;
|
|
3821
|
+
while (current !== stopAt && current.startsWith(stopAt + sep)) {
|
|
3822
|
+
try {
|
|
3823
|
+
rmdirSync(current);
|
|
3824
|
+
} catch {
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
current = dirname2(current);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
3831
|
+
const segments = output.path.split("/").filter(Boolean);
|
|
3832
|
+
if (!segments.some((s) => s.includes("{locale}"))) return 0;
|
|
3833
|
+
const root = resolve7(projectRoot);
|
|
3834
|
+
let deleted = 0;
|
|
3835
|
+
const stale = (token) => token !== void 0 && !validTokens.has(token) && LOCALE_TOKEN.test(token);
|
|
3836
|
+
const visit = (dir, index, locale) => {
|
|
3837
|
+
const segment = segments[index];
|
|
3838
|
+
const isLast = index === segments.length - 1;
|
|
3839
|
+
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
3840
|
+
const next = resolve7(dir, segment);
|
|
3841
|
+
if (isLast) {
|
|
3842
|
+
if (stale(locale) && existsSync10(next) && statSync4(next).isFile()) {
|
|
3843
|
+
unlinkSync(next);
|
|
3844
|
+
deleted++;
|
|
3845
|
+
removeEmptyDirs(dir, root);
|
|
3846
|
+
}
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
visit(next, index + 1, locale);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
const re = segmentRegExp(segment);
|
|
3853
|
+
let entries;
|
|
3854
|
+
try {
|
|
3855
|
+
entries = readdirSync7(dir, { withFileTypes: true });
|
|
3856
|
+
} catch {
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
for (const entry of entries) {
|
|
3860
|
+
const m = entry.name.match(re);
|
|
3861
|
+
if (!m) continue;
|
|
3862
|
+
const token = m.groups?.locale ?? locale;
|
|
3863
|
+
if (isLast) {
|
|
3864
|
+
if (!entry.isFile() || !stale(token)) continue;
|
|
3865
|
+
unlinkSync(resolve7(dir, entry.name));
|
|
3866
|
+
deleted++;
|
|
3867
|
+
removeEmptyDirs(dir, root);
|
|
3868
|
+
} else if (entry.isDirectory()) {
|
|
3869
|
+
visit(resolve7(dir, entry.name), index + 1, token);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
visit(root, 0, void 0);
|
|
3874
|
+
return deleted;
|
|
3875
|
+
}
|
|
3385
3876
|
function exportToDisk(state, projectRoot, opts) {
|
|
3877
|
+
const allLocales = state.config.locales;
|
|
3386
3878
|
state = narrowForExport(state);
|
|
3387
3879
|
const outputs = opts?.adapter ? state.config.outputs.filter((o) => o.adapter === opts.adapter) : state.config.outputs;
|
|
3388
3880
|
const warnings = [];
|
|
3389
3881
|
let written = 0;
|
|
3390
3882
|
let skipped = 0;
|
|
3883
|
+
let deleted = 0;
|
|
3391
3884
|
for (const output of outputs) {
|
|
3392
|
-
const
|
|
3885
|
+
const adapter = getAdapter(output.adapter);
|
|
3886
|
+
const result = adapter.export(state, output);
|
|
3393
3887
|
warnings.push(...result.warnings);
|
|
3394
3888
|
const writtenPaths = /* @__PURE__ */ new Set();
|
|
3395
3889
|
for (const f of result.files) {
|
|
3396
|
-
const abs =
|
|
3890
|
+
const abs = resolve7(projectRoot, f.path);
|
|
3397
3891
|
if (writtenPaths.has(abs)) {
|
|
3398
3892
|
skipped++;
|
|
3399
3893
|
continue;
|
|
@@ -3401,7 +3895,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
3401
3895
|
writtenPaths.add(abs);
|
|
3402
3896
|
let current = null;
|
|
3403
3897
|
try {
|
|
3404
|
-
current =
|
|
3898
|
+
current = readFileSync11(abs, "utf8");
|
|
3405
3899
|
} catch {
|
|
3406
3900
|
}
|
|
3407
3901
|
if (current === f.contents) {
|
|
@@ -3411,12 +3905,14 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
3411
3905
|
writeFileAtomic(abs, f.contents);
|
|
3412
3906
|
written++;
|
|
3413
3907
|
}
|
|
3908
|
+
const validTokens = new Set(allLocales.map((l) => resolveLocaleToken(output, l, adapter.defaultLocaleCase)));
|
|
3909
|
+
deleted += pruneStaleLocaleFiles(output, validTokens, projectRoot);
|
|
3414
3910
|
}
|
|
3415
|
-
return { written, skipped, warnings };
|
|
3911
|
+
return { written, skipped, deleted, warnings };
|
|
3416
3912
|
}
|
|
3417
3913
|
|
|
3418
3914
|
// src/server/ui-prefs.ts
|
|
3419
|
-
import { readFileSync as
|
|
3915
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
3420
3916
|
import { homedir } from "os";
|
|
3421
3917
|
import { join as join8 } from "path";
|
|
3422
3918
|
var THEMES = ["system", "light", "dark"];
|
|
@@ -3425,7 +3921,7 @@ var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
|
3425
3921
|
var DEFAULTS = { theme: "system" };
|
|
3426
3922
|
function readJson(path) {
|
|
3427
3923
|
try {
|
|
3428
|
-
const parsed = JSON.parse(
|
|
3924
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
3429
3925
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3430
3926
|
} catch {
|
|
3431
3927
|
return {};
|
|
@@ -3441,8 +3937,8 @@ function saveUiPrefs(path, prefs) {
|
|
|
3441
3937
|
}
|
|
3442
3938
|
|
|
3443
3939
|
// src/server/local-settings.ts
|
|
3444
|
-
import { readFileSync as
|
|
3445
|
-
import { resolve as
|
|
3940
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
3941
|
+
import { resolve as resolve8 } from "path";
|
|
3446
3942
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
3447
3943
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
3448
3944
|
var DEFAULT_AI = {
|
|
@@ -3453,10 +3949,10 @@ var DEFAULT_AI = {
|
|
|
3453
3949
|
batchSize: 25
|
|
3454
3950
|
};
|
|
3455
3951
|
var DEFAULT_EDITOR = "vscode";
|
|
3456
|
-
var settingsPath = (projectRoot) =>
|
|
3952
|
+
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
3457
3953
|
function readJson2(path) {
|
|
3458
3954
|
try {
|
|
3459
|
-
const parsed = JSON.parse(
|
|
3955
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
3460
3956
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3461
3957
|
} catch {
|
|
3462
3958
|
return {};
|
|
@@ -3520,10 +4016,10 @@ function aiConfigError(ai) {
|
|
|
3520
4016
|
var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
3521
4017
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
3522
4018
|
function projectName(root) {
|
|
3523
|
-
const nameFile =
|
|
3524
|
-
if (
|
|
4019
|
+
const nameFile = resolve9(root, ".idea", ".name");
|
|
4020
|
+
if (existsSync11(nameFile)) {
|
|
3525
4021
|
try {
|
|
3526
|
-
const name =
|
|
4022
|
+
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3527
4023
|
if (name) return name;
|
|
3528
4024
|
} catch {
|
|
3529
4025
|
}
|
|
@@ -3533,7 +4029,7 @@ function projectName(root) {
|
|
|
3533
4029
|
function createApi(deps) {
|
|
3534
4030
|
const app = new Hono();
|
|
3535
4031
|
const load = () => loadState(deps.statePath);
|
|
3536
|
-
const projectRoot =
|
|
4032
|
+
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
3537
4033
|
let translateQueue = Promise.resolve();
|
|
3538
4034
|
const withTranslateLock = (fn) => {
|
|
3539
4035
|
const next = translateQueue.then(fn, fn);
|
|
@@ -3631,27 +4127,27 @@ function createApi(deps) {
|
|
|
3631
4127
|
found.set(deps.statePath, {
|
|
3632
4128
|
name: basename(deps.statePath),
|
|
3633
4129
|
path: deps.statePath,
|
|
3634
|
-
relDir: activeRel !== basename(activeRel) ?
|
|
4130
|
+
relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
|
|
3635
4131
|
});
|
|
3636
4132
|
function walk(dir, depth) {
|
|
3637
4133
|
if (depth > 4) return;
|
|
3638
4134
|
let entries = [];
|
|
3639
4135
|
try {
|
|
3640
|
-
entries =
|
|
4136
|
+
entries = readdirSync8(dir);
|
|
3641
4137
|
} catch {
|
|
3642
4138
|
return;
|
|
3643
4139
|
}
|
|
3644
4140
|
for (const name of entries) {
|
|
3645
4141
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3646
|
-
const abs =
|
|
4142
|
+
const abs = resolve9(dir, name);
|
|
3647
4143
|
let filePath = null;
|
|
3648
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
3649
|
-
filePath =
|
|
4144
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
|
|
4145
|
+
filePath = resolve9(dir, `${name}.json`);
|
|
3650
4146
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3651
4147
|
filePath = abs;
|
|
3652
4148
|
} else {
|
|
3653
4149
|
try {
|
|
3654
|
-
if (
|
|
4150
|
+
if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
|
|
3655
4151
|
} catch {
|
|
3656
4152
|
}
|
|
3657
4153
|
continue;
|
|
@@ -3660,7 +4156,7 @@ function createApi(deps) {
|
|
|
3660
4156
|
try {
|
|
3661
4157
|
loadState(filePath);
|
|
3662
4158
|
const rel = relative3(projectRoot, filePath);
|
|
3663
|
-
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ?
|
|
4159
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
|
|
3664
4160
|
} catch {
|
|
3665
4161
|
}
|
|
3666
4162
|
}
|
|
@@ -3676,10 +4172,10 @@ function createApi(deps) {
|
|
|
3676
4172
|
app.post("/file", async (c) => {
|
|
3677
4173
|
const { path } = await c.req.json();
|
|
3678
4174
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
3679
|
-
const resolved =
|
|
3680
|
-
const inside = resolved === projectRoot || resolved.startsWith(projectRoot +
|
|
4175
|
+
const resolved = resolve9(projectRoot, path);
|
|
4176
|
+
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
3681
4177
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
3682
|
-
if (!
|
|
4178
|
+
if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
|
|
3683
4179
|
loadState(resolved);
|
|
3684
4180
|
deps.statePath = resolved;
|
|
3685
4181
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -3736,11 +4232,11 @@ function createApi(deps) {
|
|
|
3736
4232
|
function removeOrphanScreenshot(s, screenshot) {
|
|
3737
4233
|
if (!screenshot) return;
|
|
3738
4234
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
3739
|
-
const root =
|
|
3740
|
-
const abs =
|
|
4235
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4236
|
+
const abs = resolve9(root, screenshot);
|
|
3741
4237
|
const rel = relative3(root, abs);
|
|
3742
|
-
const seg0 = rel.split(
|
|
3743
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4238
|
+
const seg0 = rel.split(sep2)[0] ?? "";
|
|
4239
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
|
|
3744
4240
|
try {
|
|
3745
4241
|
rmSync4(abs);
|
|
3746
4242
|
} catch {
|
|
@@ -3992,11 +4488,11 @@ function createApi(deps) {
|
|
|
3992
4488
|
const body = await c.req.parseBody();
|
|
3993
4489
|
const file = body["file"];
|
|
3994
4490
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
3995
|
-
const root =
|
|
4491
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
3996
4492
|
const dirName = screenshotDirName(deps.statePath);
|
|
3997
|
-
const dir =
|
|
4493
|
+
const dir = resolve9(root, dirName);
|
|
3998
4494
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
3999
|
-
writeFileAtomic(
|
|
4495
|
+
writeFileAtomic(resolve9(dir, filename), Buffer.from(await file.arrayBuffer()));
|
|
4000
4496
|
const path = `${dirName}/${filename}`;
|
|
4001
4497
|
const s = load();
|
|
4002
4498
|
const prev = s.keys[key]?.screenshot;
|
|
@@ -4028,6 +4524,23 @@ function createApi(deps) {
|
|
|
4028
4524
|
return c.json({ files, warnings });
|
|
4029
4525
|
});
|
|
4030
4526
|
app.get("/scan/missing", (c) => c.json(findMissing(load())));
|
|
4527
|
+
const spellerCache = /* @__PURE__ */ new Map();
|
|
4528
|
+
const cachedLoader = (dictId) => {
|
|
4529
|
+
let p = spellerCache.get(dictId);
|
|
4530
|
+
if (!p) {
|
|
4531
|
+
p = defaultLoader(dictId);
|
|
4532
|
+
spellerCache.set(dictId, p);
|
|
4533
|
+
}
|
|
4534
|
+
return p;
|
|
4535
|
+
};
|
|
4536
|
+
app.get("/lint", async (c) => {
|
|
4537
|
+
const state = load();
|
|
4538
|
+
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
4539
|
+
} });
|
|
4540
|
+
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
4541
|
+
const counts = countSeverities(findings);
|
|
4542
|
+
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
4543
|
+
});
|
|
4031
4544
|
app.get("/checks", (c) => {
|
|
4032
4545
|
const param = c.req.query("checks");
|
|
4033
4546
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4063,22 +4576,12 @@ function createApi(deps) {
|
|
|
4063
4576
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
4064
4577
|
});
|
|
4065
4578
|
app.post("/export", (c) => {
|
|
4066
|
-
const
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
const result = adapter.export(s, output);
|
|
4073
|
-
warnings.push(...result.warnings);
|
|
4074
|
-
for (const f of result.files) {
|
|
4075
|
-
const abs = resolve8(root, f.path);
|
|
4076
|
-
writeFileAtomic(abs, f.contents);
|
|
4077
|
-
count++;
|
|
4078
|
-
}
|
|
4079
|
-
}
|
|
4080
|
-
console.log(`[export] ${count} file(s)${warnings.length ? `, ${warnings.length} warning(s)` : ""}`);
|
|
4081
|
-
return c.json({ files: count, warnings });
|
|
4579
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4580
|
+
const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
|
|
4581
|
+
console.log(
|
|
4582
|
+
`[export] ${written + skipped} file(s)${deleted ? `, ${deleted} removed` : ""}${warnings.length ? `, ${warnings.length} warning(s)` : ""}`
|
|
4583
|
+
);
|
|
4584
|
+
return c.json({ files: written + skipped, warnings });
|
|
4082
4585
|
});
|
|
4083
4586
|
app.post("/translate/stream", async (c) => {
|
|
4084
4587
|
const signal = c.req.raw.signal;
|
|
@@ -4100,7 +4603,7 @@ function createApi(deps) {
|
|
|
4100
4603
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4101
4604
|
return;
|
|
4102
4605
|
}
|
|
4103
|
-
const { skipped } = attachScreenshotsForProvider(reqs, s,
|
|
4606
|
+
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4104
4607
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4105
4608
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4106
4609
|
let totalWritten = 0;
|
|
@@ -4178,7 +4681,7 @@ function createApi(deps) {
|
|
|
4178
4681
|
} catch (e) {
|
|
4179
4682
|
return c.json({ error: e.message }, 400);
|
|
4180
4683
|
}
|
|
4181
|
-
const { skipped } = attachScreenshotsForProvider(toTranslate, s,
|
|
4684
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4182
4685
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4183
4686
|
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4184
4687
|
const latest = load();
|
|
@@ -4230,7 +4733,7 @@ function createApi(deps) {
|
|
|
4230
4733
|
const refs = [];
|
|
4231
4734
|
const prefixRefs = [];
|
|
4232
4735
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
4233
|
-
const abs =
|
|
4736
|
+
const abs = resolve9(projectRoot, file);
|
|
4234
4737
|
for (const r of entry.refs) {
|
|
4235
4738
|
if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
|
|
4236
4739
|
}
|
|
@@ -4357,7 +4860,7 @@ function createApi(deps) {
|
|
|
4357
4860
|
}
|
|
4358
4861
|
|
|
4359
4862
|
// src/server/server.ts
|
|
4360
|
-
var here =
|
|
4863
|
+
var here = dirname4(fileURLToPath(import.meta.url));
|
|
4361
4864
|
var DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4362
4865
|
var MIME = {
|
|
4363
4866
|
".html": "text/html; charset=utf-8",
|
|
@@ -4386,16 +4889,16 @@ async function readFileResponse(absPath) {
|
|
|
4386
4889
|
}
|
|
4387
4890
|
function buildApp(opts) {
|
|
4388
4891
|
const app = new Hono2();
|
|
4389
|
-
|
|
4390
|
-
|
|
4892
|
+
const apiDeps = { statePath: opts.statePath, autoExport: true };
|
|
4893
|
+
app.route("/api", createApi(apiDeps));
|
|
4391
4894
|
app.get("/:dir/*", async (c, next) => {
|
|
4392
4895
|
const dirSeg = c.req.param("dir");
|
|
4393
4896
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4394
|
-
const shotsRoot =
|
|
4897
|
+
const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
|
|
4395
4898
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4396
4899
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4397
|
-
const target =
|
|
4398
|
-
const inside = target === shotsRoot || target.startsWith(shotsRoot +
|
|
4900
|
+
const target = resolve10(shotsRoot, "." + rest);
|
|
4901
|
+
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
|
|
4399
4902
|
if (inside) {
|
|
4400
4903
|
const file = await readFileResponse(target);
|
|
4401
4904
|
if (file) return file;
|
|
@@ -4403,11 +4906,11 @@ function buildApp(opts) {
|
|
|
4403
4906
|
return c.notFound();
|
|
4404
4907
|
});
|
|
4405
4908
|
if (!opts.dev) {
|
|
4406
|
-
const root =
|
|
4909
|
+
const root = resolve10(opts.uiDir ?? DEFAULT_UI_DIR);
|
|
4407
4910
|
app.get("/*", async (c) => {
|
|
4408
4911
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4409
|
-
const target =
|
|
4410
|
-
const inside = target === root || target.startsWith(root +
|
|
4912
|
+
const target = resolve10(root, "." + pathname);
|
|
4913
|
+
const inside = target === root || target.startsWith(root + sep3);
|
|
4411
4914
|
if (inside && pathname !== "/") {
|
|
4412
4915
|
const file = await readFileResponse(target);
|
|
4413
4916
|
if (file) return file;
|
|
@@ -4460,7 +4963,7 @@ async function startServer(opts) {
|
|
|
4460
4963
|
});
|
|
4461
4964
|
}
|
|
4462
4965
|
function backgroundScan(statePath) {
|
|
4463
|
-
const projectRoot =
|
|
4966
|
+
const projectRoot = dirname4(resolve10(statePath));
|
|
4464
4967
|
Promise.resolve().then(() => {
|
|
4465
4968
|
const state = loadState(statePath);
|
|
4466
4969
|
const existing = loadUsageCache(projectRoot);
|