glotfile 0.4.2 → 0.4.4
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 +971 -499
- package/dist/server/server.js +778 -84
- package/dist/ui/assets/index-3IIAIpZW.css +1 -0
- package/dist/ui/assets/{index-CJ_nmOjf.js → index-pl7PaD7b.js} +62 -19
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-De8z0F8Y.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
|
}
|
|
@@ -1384,6 +1397,10 @@ function toI18next(value) {
|
|
|
1384
1397
|
if (isIcuPluralOrSelect(value)) return value;
|
|
1385
1398
|
return value.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}");
|
|
1386
1399
|
}
|
|
1400
|
+
function toRuby(value) {
|
|
1401
|
+
if (isIcuPluralOrSelect(value)) return value;
|
|
1402
|
+
return value.replace(/(?<!%)\{(\w+)\}/g, "%{$1}");
|
|
1403
|
+
}
|
|
1387
1404
|
function placeholdersMatch(source, translation) {
|
|
1388
1405
|
const a = extractPlaceholders(source).sort();
|
|
1389
1406
|
const b = extractPlaceholders(translation).sort();
|
|
@@ -1771,16 +1788,310 @@ function runChecks(state, opts = {}) {
|
|
|
1771
1788
|
return { issues, spellPending };
|
|
1772
1789
|
}
|
|
1773
1790
|
|
|
1791
|
+
// src/server/lint/spelling.ts
|
|
1792
|
+
function tokenize(text) {
|
|
1793
|
+
return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
|
|
1794
|
+
}
|
|
1795
|
+
function buildAllowWords(glossary, dictionary2 = []) {
|
|
1796
|
+
const set = /* @__PURE__ */ new Set();
|
|
1797
|
+
const add = (s) => {
|
|
1798
|
+
for (const w of tokenize(s)) set.add(w.toLowerCase());
|
|
1799
|
+
};
|
|
1800
|
+
for (const g of glossary) add(g.term);
|
|
1801
|
+
for (const w of dictionary2) add(w);
|
|
1802
|
+
return set;
|
|
1803
|
+
}
|
|
1804
|
+
var spellingRule = {
|
|
1805
|
+
id: "spelling",
|
|
1806
|
+
run(state, ctx) {
|
|
1807
|
+
const out = [];
|
|
1808
|
+
for (const key of Object.keys(state.keys)) {
|
|
1809
|
+
const entry = state.keys[key];
|
|
1810
|
+
for (const locale of ctx.targetLocales) {
|
|
1811
|
+
const speller = ctx.spellers.get(locale);
|
|
1812
|
+
if (!speller) continue;
|
|
1813
|
+
const value = entry.values[locale]?.value;
|
|
1814
|
+
if (!value) continue;
|
|
1815
|
+
const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
|
|
1816
|
+
for (const word of tokenize(value)) {
|
|
1817
|
+
const lower = word.toLowerCase();
|
|
1818
|
+
if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
|
|
1819
|
+
if (!speller.correct(word)) {
|
|
1820
|
+
out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return out;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
var defaultLoader = async (dictId) => {
|
|
1829
|
+
try {
|
|
1830
|
+
const nspellMod = await import("nspell");
|
|
1831
|
+
const nspell2 = nspellMod.default ?? nspellMod;
|
|
1832
|
+
const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
|
|
1833
|
+
const dictExport = dictMod.default ?? dictMod;
|
|
1834
|
+
const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
|
|
1835
|
+
return nspell2(dict);
|
|
1836
|
+
} catch {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
// src/server/lint/rules.ts
|
|
1842
|
+
var emptySourceRule = {
|
|
1843
|
+
id: "empty-source",
|
|
1844
|
+
run(state, ctx) {
|
|
1845
|
+
const out = [];
|
|
1846
|
+
for (const key of Object.keys(state.keys)) {
|
|
1847
|
+
const entry = state.keys[key];
|
|
1848
|
+
const v = entry.plural ? entry.values[ctx.sourceLocale]?.forms?.other : entry.values[ctx.sourceLocale]?.value;
|
|
1849
|
+
if (!v || !v.trim()) out.push({ ruleId: "empty-source", key, locale: "", message: "source value is empty" });
|
|
1850
|
+
}
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
var emptyTranslationRule = {
|
|
1855
|
+
id: "empty-translation",
|
|
1856
|
+
run(state, ctx) {
|
|
1857
|
+
const out = [];
|
|
1858
|
+
for (const m of findMissing(state)) {
|
|
1859
|
+
out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
|
|
1860
|
+
}
|
|
1861
|
+
for (const key of Object.keys(state.keys)) {
|
|
1862
|
+
for (const locale of ctx.targetLocales) {
|
|
1863
|
+
const v = state.keys[key].values[locale]?.value;
|
|
1864
|
+
if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return out;
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
var identicalToSourceRule = {
|
|
1871
|
+
id: "identical-to-source",
|
|
1872
|
+
run(state, ctx) {
|
|
1873
|
+
const out = [];
|
|
1874
|
+
for (const key of Object.keys(state.keys)) {
|
|
1875
|
+
const entry = state.keys[key];
|
|
1876
|
+
if (entry.skipTranslate) continue;
|
|
1877
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1878
|
+
if (!src) continue;
|
|
1879
|
+
for (const locale of ctx.targetLocales) {
|
|
1880
|
+
const v = entry.values[locale]?.value;
|
|
1881
|
+
if (v && v === src) out.push({ ruleId: "identical-to-source", key, locale, message: "translation is identical to the source" });
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return out;
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
var whitespaceRule = {
|
|
1888
|
+
id: "whitespace",
|
|
1889
|
+
run(state, ctx) {
|
|
1890
|
+
const out = [];
|
|
1891
|
+
for (const key of Object.keys(state.keys)) {
|
|
1892
|
+
const entry = state.keys[key];
|
|
1893
|
+
const src = entry.values[ctx.sourceLocale]?.value ?? "";
|
|
1894
|
+
const srcEdge = src !== src.trim();
|
|
1895
|
+
for (const locale of ctx.targetLocales) {
|
|
1896
|
+
const v = entry.values[locale]?.value;
|
|
1897
|
+
if (!v) continue;
|
|
1898
|
+
if (v !== v.trim() !== srcEdge) {
|
|
1899
|
+
out.push({ ruleId: "whitespace", key, locale, message: "leading/trailing whitespace differs from the source" });
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
var placeholderMismatchRule = {
|
|
1907
|
+
id: "placeholder-mismatch",
|
|
1908
|
+
run(state, ctx) {
|
|
1909
|
+
const out = [];
|
|
1910
|
+
for (const key of Object.keys(state.keys)) {
|
|
1911
|
+
const entry = state.keys[key];
|
|
1912
|
+
if (entry.plural) {
|
|
1913
|
+
const srcForm = entry.values[ctx.sourceLocale]?.forms?.other;
|
|
1914
|
+
if (!srcForm) continue;
|
|
1915
|
+
for (const locale of ctx.targetLocales) {
|
|
1916
|
+
const forms = entry.values[locale]?.forms;
|
|
1917
|
+
if (!forms) continue;
|
|
1918
|
+
const bad = Object.entries(forms).some(
|
|
1919
|
+
([cat, form]) => form && !pluralFormPlaceholdersMatch(cat, srcForm, form)
|
|
1920
|
+
);
|
|
1921
|
+
if (bad) {
|
|
1922
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1928
|
+
if (!src) continue;
|
|
1929
|
+
for (const locale of ctx.targetLocales) {
|
|
1930
|
+
const v = entry.values[locale]?.value;
|
|
1931
|
+
if (!v) continue;
|
|
1932
|
+
if (!placeholdersMatch(src, v)) {
|
|
1933
|
+
out.push({ ruleId: "placeholder-mismatch", key, locale, message: "placeholders differ from the source" });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return out;
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
var icuMismatchRule = {
|
|
1941
|
+
id: "icu-mismatch",
|
|
1942
|
+
run(state, ctx) {
|
|
1943
|
+
const out = [];
|
|
1944
|
+
for (const key of Object.keys(state.keys)) {
|
|
1945
|
+
const entry = state.keys[key];
|
|
1946
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1947
|
+
if (!src) continue;
|
|
1948
|
+
const srcIcu = isIcuPluralOrSelect(src);
|
|
1949
|
+
for (const locale of ctx.targetLocales) {
|
|
1950
|
+
const v = entry.values[locale]?.value;
|
|
1951
|
+
if (!v) continue;
|
|
1952
|
+
if (isIcuPluralOrSelect(v) !== srcIcu) {
|
|
1953
|
+
out.push({
|
|
1954
|
+
ruleId: "icu-mismatch",
|
|
1955
|
+
key,
|
|
1956
|
+
locale,
|
|
1957
|
+
message: srcIcu ? "source is an ICU plural/select but the translation is not" : "translation is an ICU plural/select but the source is not"
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return out;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
var maxLengthRule = {
|
|
1966
|
+
id: "max-length",
|
|
1967
|
+
run(state, ctx) {
|
|
1968
|
+
const out = [];
|
|
1969
|
+
for (const key of Object.keys(state.keys)) {
|
|
1970
|
+
const entry = state.keys[key];
|
|
1971
|
+
const max = entry.maxLength;
|
|
1972
|
+
if (max == null) continue;
|
|
1973
|
+
for (const locale of ctx.targetLocales) {
|
|
1974
|
+
const v = entry.values[locale]?.value;
|
|
1975
|
+
if (v && v.length > max) {
|
|
1976
|
+
out.push({ ruleId: "max-length", key, locale, message: `length ${v.length} exceeds maxLength ${max}` });
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return out;
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
var glossaryViolationRule = {
|
|
1984
|
+
id: "glossary-violation",
|
|
1985
|
+
run(state, ctx) {
|
|
1986
|
+
const out = [];
|
|
1987
|
+
for (const key of Object.keys(state.keys)) {
|
|
1988
|
+
const entry = state.keys[key];
|
|
1989
|
+
const src = entry.values[ctx.sourceLocale]?.value;
|
|
1990
|
+
if (!src) continue;
|
|
1991
|
+
for (const locale of ctx.targetLocales) {
|
|
1992
|
+
const v = entry.values[locale]?.value;
|
|
1993
|
+
if (!v) continue;
|
|
1994
|
+
for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
|
|
1995
|
+
if (hint.doNotTranslate && !v.includes(hint.term)) {
|
|
1996
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
|
|
1997
|
+
}
|
|
1998
|
+
if (hint.forced && !v.includes(hint.forced)) {
|
|
1999
|
+
out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return out;
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
var ALL_RULES = [
|
|
2008
|
+
emptySourceRule,
|
|
2009
|
+
emptyTranslationRule,
|
|
2010
|
+
placeholderMismatchRule,
|
|
2011
|
+
icuMismatchRule,
|
|
2012
|
+
glossaryViolationRule,
|
|
2013
|
+
maxLengthRule,
|
|
2014
|
+
identicalToSourceRule,
|
|
2015
|
+
whitespaceRule,
|
|
2016
|
+
spellingRule
|
|
2017
|
+
];
|
|
2018
|
+
|
|
2019
|
+
// src/server/lint/run.ts
|
|
2020
|
+
function resolveSeverity(id, config) {
|
|
2021
|
+
return config.rules?.[id] ?? DEFAULT_SEVERITY[id];
|
|
2022
|
+
}
|
|
2023
|
+
function sortFindings(findings) {
|
|
2024
|
+
return [...findings].sort(
|
|
2025
|
+
(a, b) => a.key.localeCompare(b.key) || a.locale.localeCompare(b.locale) || a.ruleId.localeCompare(b.ruleId)
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
function countSeverities(findings) {
|
|
2029
|
+
let error = 0, warn = 0;
|
|
2030
|
+
for (const f of findings) f.severity === "error" ? error++ : warn++;
|
|
2031
|
+
return { error, warn };
|
|
2032
|
+
}
|
|
2033
|
+
async function loadSpellers(locales, config, load, warn) {
|
|
2034
|
+
const map = /* @__PURE__ */ new Map();
|
|
2035
|
+
for (const locale of locales) {
|
|
2036
|
+
const dictId = config.spelling?.locales?.[locale] ?? locale;
|
|
2037
|
+
const speller = await load(dictId);
|
|
2038
|
+
if (speller) map.set(locale, speller);
|
|
2039
|
+
else warn(`no dictionary for "${locale}", skipping spelling`);
|
|
2040
|
+
}
|
|
2041
|
+
return map;
|
|
2042
|
+
}
|
|
2043
|
+
async function runLint(state, options = {}) {
|
|
2044
|
+
const config = state.config.lint ?? {};
|
|
2045
|
+
const rules = options.rules ?? ALL_RULES;
|
|
2046
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
2047
|
+
const load = options.loadSpeller ?? defaultLoader;
|
|
2048
|
+
const targetLocales = state.config.locales.filter((l) => l !== state.config.sourceLocale);
|
|
2049
|
+
const isActive = (rule) => {
|
|
2050
|
+
if (options.ruleIds && !options.ruleIds.includes(rule.id)) return false;
|
|
2051
|
+
return resolveSeverity(rule.id, config) !== "off";
|
|
2052
|
+
};
|
|
2053
|
+
const active = rules.filter(isActive);
|
|
2054
|
+
const spellingOn = active.some((r) => r.id === "spelling");
|
|
2055
|
+
const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
|
|
2056
|
+
const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
|
|
2057
|
+
const ctx = {
|
|
2058
|
+
config,
|
|
2059
|
+
sourceLocale: state.config.sourceLocale,
|
|
2060
|
+
targetLocales,
|
|
2061
|
+
glossary: state.glossary,
|
|
2062
|
+
spellers,
|
|
2063
|
+
allowWords
|
|
2064
|
+
};
|
|
2065
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp2);
|
|
2066
|
+
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
2067
|
+
const findings = [];
|
|
2068
|
+
for (const rule of active) {
|
|
2069
|
+
const severity = resolveSeverity(rule.id, config);
|
|
2070
|
+
for (const raw of rule.run(state, ctx)) {
|
|
2071
|
+
if (ignoreRes.some((re) => re.test(raw.key))) continue;
|
|
2072
|
+
if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
|
|
2073
|
+
findings.push({ ...raw, severity });
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
const sorted = sortFindings(findings);
|
|
2077
|
+
const counts = countSeverities(sorted);
|
|
2078
|
+
return { findings: sorted, counts, ok: counts.error === 0 };
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/server/lint/outputs.ts
|
|
2082
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
2083
|
+
import { resolve as resolve5 } from "path";
|
|
2084
|
+
|
|
1774
2085
|
// src/server/adapters/options.ts
|
|
1775
2086
|
function applyCase(canonical, style) {
|
|
1776
|
-
const
|
|
2087
|
+
const sep4 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
|
|
1777
2088
|
const lower = style === "lower-hyphen" || style === "lower-underscore";
|
|
1778
2089
|
return canonical.split(/[-_]/).map((p, i) => {
|
|
1779
2090
|
if (lower || i === 0) return p.toLowerCase();
|
|
1780
2091
|
if (/^[a-z]{4}$/i.test(p)) return p[0].toUpperCase() + p.slice(1).toLowerCase();
|
|
1781
2092
|
if (/^[a-z]{2}$/i.test(p)) return p.toUpperCase();
|
|
1782
2093
|
return p;
|
|
1783
|
-
}).join(
|
|
2094
|
+
}).join(sep4);
|
|
1784
2095
|
}
|
|
1785
2096
|
function resolveLocaleToken(output, canonical, adapterDefault) {
|
|
1786
2097
|
const mapped = output.localeMap?.[canonical];
|
|
@@ -2301,6 +2612,182 @@ var vueI18nJson = {
|
|
|
2301
2612
|
}
|
|
2302
2613
|
};
|
|
2303
2614
|
|
|
2615
|
+
// src/server/adapters/angular-xliff.ts
|
|
2616
|
+
function xmlEscape2(s) {
|
|
2617
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2618
|
+
}
|
|
2619
|
+
function renderInterpolations(text, ids) {
|
|
2620
|
+
let out = "";
|
|
2621
|
+
let last = 0;
|
|
2622
|
+
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
2623
|
+
const name = m[1];
|
|
2624
|
+
let id = ids.get(name);
|
|
2625
|
+
if (id === void 0) {
|
|
2626
|
+
id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
|
|
2627
|
+
ids.set(name, id);
|
|
2628
|
+
}
|
|
2629
|
+
out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
|
|
2630
|
+
last = m.index + m[0].length;
|
|
2631
|
+
}
|
|
2632
|
+
return out + xmlEscape2(text.slice(last));
|
|
2633
|
+
}
|
|
2634
|
+
function renderPluralIcu(forms, ids) {
|
|
2635
|
+
const cats = [
|
|
2636
|
+
...Object.keys(forms).filter((c) => c.startsWith("=")),
|
|
2637
|
+
...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
|
|
2638
|
+
];
|
|
2639
|
+
const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
|
|
2640
|
+
return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
|
|
2641
|
+
}
|
|
2642
|
+
function renderEmbeddedIcu(value) {
|
|
2643
|
+
const renamed = value.replace(
|
|
2644
|
+
/\{\s*\w+\s*,\s*(plural|select|selectordinal)\s*,/g,
|
|
2645
|
+
(_, type) => `{${type === "plural" ? "VAR_PLURAL" : "VAR_SELECT"}, ${type},`
|
|
2646
|
+
);
|
|
2647
|
+
return xmlEscape2(renamed);
|
|
2648
|
+
}
|
|
2649
|
+
function renderScalar(value, ids) {
|
|
2650
|
+
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
|
|
2651
|
+
}
|
|
2652
|
+
var DEFAULT_LOCALE_CASE7 = "bcp47-hyphen";
|
|
2653
|
+
var angularXliff = {
|
|
2654
|
+
name: "angular-xliff",
|
|
2655
|
+
capabilities: {
|
|
2656
|
+
plural: "native",
|
|
2657
|
+
select: "native",
|
|
2658
|
+
nesting: "flat",
|
|
2659
|
+
metadata: true,
|
|
2660
|
+
placeholderStyle: "icu",
|
|
2661
|
+
fileGrouping: "per-locale"
|
|
2662
|
+
},
|
|
2663
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE7,
|
|
2664
|
+
export(state, output) {
|
|
2665
|
+
const files = [];
|
|
2666
|
+
const warnings = [];
|
|
2667
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE7));
|
|
2668
|
+
const sourceLocale = state.config.sourceLocale;
|
|
2669
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE7);
|
|
2670
|
+
const emptyAs = resolveEmptyAs(output, "source");
|
|
2671
|
+
const keys = Object.keys(state.keys).sort();
|
|
2672
|
+
for (const locale of state.config.locales) {
|
|
2673
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE7);
|
|
2674
|
+
const units = [];
|
|
2675
|
+
for (const key of keys) {
|
|
2676
|
+
const entry = state.keys[key];
|
|
2677
|
+
let source;
|
|
2678
|
+
let target;
|
|
2679
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2680
|
+
if (entry.plural) {
|
|
2681
|
+
const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
|
|
2682
|
+
if (targetForms === null) continue;
|
|
2683
|
+
source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
|
|
2684
|
+
target = renderPluralIcu(targetForms, ids);
|
|
2685
|
+
} else {
|
|
2686
|
+
const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
|
|
2687
|
+
if (targetValue === null) continue;
|
|
2688
|
+
source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
|
|
2689
|
+
target = renderScalar(targetValue, ids);
|
|
2690
|
+
}
|
|
2691
|
+
units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
|
|
2692
|
+
units.push(` <source>${source}</source>`);
|
|
2693
|
+
units.push(` <target>${target}</target>`);
|
|
2694
|
+
if (entry.description) {
|
|
2695
|
+
units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
|
|
2696
|
+
}
|
|
2697
|
+
units.push(` </trans-unit>`);
|
|
2698
|
+
}
|
|
2699
|
+
const contents = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
2700
|
+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
2701
|
+
<file source-language="${xmlEscape2(sourceToken)}" target-language="${xmlEscape2(token)}" datatype="plaintext" original="ng2.template">
|
|
2702
|
+
<body>
|
|
2703
|
+
` + (units.length ? units.join("\n") + "\n" : "") + ` </body>
|
|
2704
|
+
</file>
|
|
2705
|
+
</xliff>
|
|
2706
|
+
`;
|
|
2707
|
+
files.push({ path: resolvePath(output.path, token), contents });
|
|
2708
|
+
}
|
|
2709
|
+
return { files, warnings };
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2713
|
+
// src/server/adapters/rails-yaml.ts
|
|
2714
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "on", "off", "null", "y", "n"]);
|
|
2715
|
+
function yamlString(s) {
|
|
2716
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") + '"';
|
|
2717
|
+
}
|
|
2718
|
+
function yamlKey(k) {
|
|
2719
|
+
if (RESERVED_KEYS.has(k.toLowerCase())) return yamlString(k);
|
|
2720
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(k) ? k : yamlString(k);
|
|
2721
|
+
}
|
|
2722
|
+
function yamlMap(node, indent, level) {
|
|
2723
|
+
const pad = " ".repeat(indent * level);
|
|
2724
|
+
const lines = [];
|
|
2725
|
+
for (const key of Object.keys(node).sort()) {
|
|
2726
|
+
const v = node[key];
|
|
2727
|
+
if (v && typeof v === "object") {
|
|
2728
|
+
lines.push(`${pad}${yamlKey(key)}:`);
|
|
2729
|
+
lines.push(...yamlMap(v, indent, level + 1));
|
|
2730
|
+
} else {
|
|
2731
|
+
lines.push(`${pad}${yamlKey(key)}: ${yamlString(String(v))}`);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return lines;
|
|
2735
|
+
}
|
|
2736
|
+
var DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
|
|
2737
|
+
var railsYaml = {
|
|
2738
|
+
name: "rails-yaml",
|
|
2739
|
+
capabilities: {
|
|
2740
|
+
plural: "native",
|
|
2741
|
+
select: "lossy",
|
|
2742
|
+
nesting: "nested",
|
|
2743
|
+
metadata: false,
|
|
2744
|
+
placeholderStyle: "named",
|
|
2745
|
+
fileGrouping: "per-locale"
|
|
2746
|
+
},
|
|
2747
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE8,
|
|
2748
|
+
export(state, output) {
|
|
2749
|
+
const warnings = [];
|
|
2750
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
|
|
2751
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
2752
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
2753
|
+
const files = [];
|
|
2754
|
+
for (const locale of state.config.locales) {
|
|
2755
|
+
const flat = {};
|
|
2756
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
2757
|
+
if (entry.plural) {
|
|
2758
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2759
|
+
if (!forms) continue;
|
|
2760
|
+
for (const cat of PLURAL_CATEGORIES) {
|
|
2761
|
+
const body2 = forms[cat];
|
|
2762
|
+
if (body2 !== void 0) flat[`${key}.${cat}`] = toRuby(body2);
|
|
2763
|
+
}
|
|
2764
|
+
} else {
|
|
2765
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2766
|
+
if (raw === null) continue;
|
|
2767
|
+
if (raw && isIcuPluralOrSelect(raw)) {
|
|
2768
|
+
warnings.push({
|
|
2769
|
+
code: "lossy-plural",
|
|
2770
|
+
key,
|
|
2771
|
+
locale,
|
|
2772
|
+
message: "rails-yaml cannot represent ICU plural/select; written unconverted"
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
flat[key] = toRuby(raw);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
const { tree: nested, collisions } = nestKeys(flat);
|
|
2779
|
+
for (const c of collisions) {
|
|
2780
|
+
warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
|
|
2781
|
+
}
|
|
2782
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
|
|
2783
|
+
const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
|
|
2784
|
+
files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
|
|
2785
|
+
}
|
|
2786
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2787
|
+
return { files, warnings };
|
|
2788
|
+
}
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2304
2791
|
// src/server/adapters/index.ts
|
|
2305
2792
|
function resolvePath(template, locale, namespace = "") {
|
|
2306
2793
|
return template.replaceAll("{locale}", locale).replaceAll("{namespace}", namespace);
|
|
@@ -2333,7 +2820,9 @@ function getRegistry() {
|
|
|
2333
2820
|
[i18nextJson.name]: i18nextJson,
|
|
2334
2821
|
[gettextPo.name]: gettextPo,
|
|
2335
2822
|
[appleStringsdict.name]: appleStringsdict,
|
|
2336
|
-
[vueI18nJson.name]: vueI18nJson
|
|
2823
|
+
[vueI18nJson.name]: vueI18nJson,
|
|
2824
|
+
[angularXliff.name]: angularXliff,
|
|
2825
|
+
[railsYaml.name]: railsYaml
|
|
2337
2826
|
};
|
|
2338
2827
|
}
|
|
2339
2828
|
function getAdapter(name) {
|
|
@@ -2342,9 +2831,27 @@ function getAdapter(name) {
|
|
|
2342
2831
|
return a;
|
|
2343
2832
|
}
|
|
2344
2833
|
|
|
2834
|
+
// src/server/lint/outputs.ts
|
|
2835
|
+
function checkOutputs(state, root) {
|
|
2836
|
+
const out = [];
|
|
2837
|
+
for (const output of state.config.outputs) {
|
|
2838
|
+
const result = getAdapter(output.adapter).export(state, output);
|
|
2839
|
+
for (const file of result.files) {
|
|
2840
|
+
const abs = resolve5(root, file.path);
|
|
2841
|
+
const current = existsSync7(abs) ? readFileSync7(abs, "utf8") : null;
|
|
2842
|
+
if (current === null) {
|
|
2843
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
2844
|
+
} else if (current !== file.contents) {
|
|
2845
|
+
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is out of date; run `glotfile export`" });
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
return out;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2345
2852
|
// src/server/api.ts
|
|
2346
|
-
import { readFileSync as
|
|
2347
|
-
import { dirname as
|
|
2853
|
+
import { readFileSync as readFileSync14, existsSync as existsSync11, readdirSync as readdirSync8, statSync as statSync5, rmSync as rmSync4 } from "fs";
|
|
2854
|
+
import { dirname as dirname3, resolve as resolve9, basename, relative as relative3, sep as sep2 } from "path";
|
|
2348
2855
|
|
|
2349
2856
|
// src/server/ai/anthropic.ts
|
|
2350
2857
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -2841,7 +3348,7 @@ function stripFences(s) {
|
|
|
2841
3348
|
return s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
2842
3349
|
}
|
|
2843
3350
|
function defaultSpawn(prompt, systemPrompt, model) {
|
|
2844
|
-
return new Promise((
|
|
3351
|
+
return new Promise((resolve11, reject) => {
|
|
2845
3352
|
const args = [
|
|
2846
3353
|
"--print",
|
|
2847
3354
|
"--output-format",
|
|
@@ -2873,7 +3380,7 @@ function defaultSpawn(prompt, systemPrompt, model) {
|
|
|
2873
3380
|
reject(new Error(`claude error: ${envelope.result ?? "unknown error"}`));
|
|
2874
3381
|
return;
|
|
2875
3382
|
}
|
|
2876
|
-
|
|
3383
|
+
resolve11(envelope.result ?? "");
|
|
2877
3384
|
} catch {
|
|
2878
3385
|
reject(new Error(`Failed to parse claude JSON output: ${stdout.slice(0, 200)}`));
|
|
2879
3386
|
}
|
|
@@ -2946,11 +3453,107 @@ function makeProvider(ai) {
|
|
|
2946
3453
|
}
|
|
2947
3454
|
}
|
|
2948
3455
|
|
|
3456
|
+
// src/server/ai/pricing.ts
|
|
3457
|
+
var PRICE_TABLE = [
|
|
3458
|
+
["claude-fable-5", 10, 50],
|
|
3459
|
+
["claude-mythos-5", 10, 50],
|
|
3460
|
+
// Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
|
|
3461
|
+
// the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
|
|
3462
|
+
["claude-opus-4-1", 15, 75],
|
|
3463
|
+
["claude-opus-4-0", 15, 75],
|
|
3464
|
+
["claude-opus-4-2025", 15, 75],
|
|
3465
|
+
["claude-opus-4", 5, 25],
|
|
3466
|
+
["claude-sonnet-4", 3, 15],
|
|
3467
|
+
["claude-haiku-4", 1, 5],
|
|
3468
|
+
["claude-3-5-haiku", 0.8, 4],
|
|
3469
|
+
["gpt-5.5-pro", 30, 180],
|
|
3470
|
+
["gpt-5.5", 5, 30],
|
|
3471
|
+
["gpt-5.4-pro", 30, 180],
|
|
3472
|
+
["gpt-5.4-mini", 0.75, 4.5],
|
|
3473
|
+
["gpt-5.4-nano", 0.2, 1.25],
|
|
3474
|
+
["gpt-5.4", 2.5, 15],
|
|
3475
|
+
["gpt-5.3-codex", 1.75, 14]
|
|
3476
|
+
];
|
|
3477
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
|
|
3478
|
+
function bareModelId(model) {
|
|
3479
|
+
let id = model.trim().toLowerCase();
|
|
3480
|
+
const slash = id.lastIndexOf("/");
|
|
3481
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
3482
|
+
const anth = id.lastIndexOf("anthropic.");
|
|
3483
|
+
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
3484
|
+
return id;
|
|
3485
|
+
}
|
|
3486
|
+
function resolvePricing(ai) {
|
|
3487
|
+
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
3488
|
+
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
3489
|
+
}
|
|
3490
|
+
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
3491
|
+
const id = bareModelId(ai.model);
|
|
3492
|
+
let best;
|
|
3493
|
+
for (const row of PRICE_TABLE) {
|
|
3494
|
+
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
3495
|
+
}
|
|
3496
|
+
return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// src/server/ai/estimate.ts
|
|
3500
|
+
var CJK_RE = /[ -鿿가-豈-]/g;
|
|
3501
|
+
function estimateTokens(text) {
|
|
3502
|
+
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
3503
|
+
return Math.ceil((text.length - cjk) / 4 + cjk / 2);
|
|
3504
|
+
}
|
|
3505
|
+
var EXPANSION = 1.2;
|
|
3506
|
+
var ITEM_REPLY_OVERHEAD = 16;
|
|
3507
|
+
var FORM_REPLY_OVERHEAD = 8;
|
|
3508
|
+
function estimateOutputTokens(req) {
|
|
3509
|
+
const translated = Math.ceil(estimateTokens(req.source) * EXPANSION);
|
|
3510
|
+
if (req.plural) {
|
|
3511
|
+
return ITEM_REPLY_OVERHEAD + req.plural.categories.length * (translated + FORM_REPLY_OVERHEAD);
|
|
3512
|
+
}
|
|
3513
|
+
return ITEM_REPLY_OVERHEAD + translated;
|
|
3514
|
+
}
|
|
3515
|
+
function estimateTranslation(state, ai, opts) {
|
|
3516
|
+
const reqs = selectRequests(state, opts);
|
|
3517
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
3518
|
+
for (const r of reqs) {
|
|
3519
|
+
let group = byLocale.get(r.targetLocale);
|
|
3520
|
+
if (!group) {
|
|
3521
|
+
group = [];
|
|
3522
|
+
byLocale.set(r.targetLocale, group);
|
|
3523
|
+
}
|
|
3524
|
+
group.push(r);
|
|
3525
|
+
}
|
|
3526
|
+
const perLocale = [];
|
|
3527
|
+
for (const [locale, group] of byLocale) {
|
|
3528
|
+
let inputTokens2 = 0;
|
|
3529
|
+
let outputTokens2 = 0;
|
|
3530
|
+
const batches = chunk(group, Math.max(1, ai.batchSize));
|
|
3531
|
+
for (const batch of batches) {
|
|
3532
|
+
const system = buildSystemPrompt(batch.some((r) => r.plural !== void 0));
|
|
3533
|
+
inputTokens2 += estimateTokens(system) + estimateTokens(buildBatchPrompt(batch));
|
|
3534
|
+
for (const r of batch) outputTokens2 += estimateOutputTokens(r);
|
|
3535
|
+
}
|
|
3536
|
+
perLocale.push({ locale, requests: group.length, batches: batches.length, inputTokens: inputTokens2, outputTokens: outputTokens2 });
|
|
3537
|
+
}
|
|
3538
|
+
const inputTokens = perLocale.reduce((n, l) => n + l.inputTokens, 0);
|
|
3539
|
+
const outputTokens = perLocale.reduce((n, l) => n + l.outputTokens, 0);
|
|
3540
|
+
const pricing = resolvePricing(ai);
|
|
3541
|
+
return {
|
|
3542
|
+
requests: reqs.length,
|
|
3543
|
+
batches: perLocale.reduce((n, l) => n + l.batches, 0),
|
|
3544
|
+
perLocale,
|
|
3545
|
+
inputTokens,
|
|
3546
|
+
outputTokens,
|
|
3547
|
+
pricing,
|
|
3548
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
|
|
2949
3552
|
// src/server/log.ts
|
|
2950
|
-
import { appendFileSync, readFileSync as
|
|
2951
|
-
import { resolve as
|
|
3553
|
+
import { appendFileSync, readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
|
|
3554
|
+
import { resolve as resolve6 } from "path";
|
|
2952
3555
|
function logPath(projectRoot) {
|
|
2953
|
-
return
|
|
3556
|
+
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
2954
3557
|
}
|
|
2955
3558
|
function appendLog(projectRoot, entry) {
|
|
2956
3559
|
ensureGlotfileDir(projectRoot);
|
|
@@ -2958,14 +3561,14 @@ function appendLog(projectRoot, entry) {
|
|
|
2958
3561
|
}
|
|
2959
3562
|
function readLog(projectRoot, limit = 100) {
|
|
2960
3563
|
const path = logPath(projectRoot);
|
|
2961
|
-
if (!
|
|
2962
|
-
const lines =
|
|
3564
|
+
if (!existsSync8(path)) return [];
|
|
3565
|
+
const lines = readFileSync8(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
2963
3566
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2964
3567
|
return entries.reverse().slice(0, limit);
|
|
2965
3568
|
}
|
|
2966
3569
|
|
|
2967
3570
|
// src/server/import/detect.ts
|
|
2968
|
-
import { existsSync as
|
|
3571
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
2969
3572
|
import { join as join4 } from "path";
|
|
2970
3573
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
2971
3574
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
@@ -2998,12 +3601,13 @@ function detectLaravel(root) {
|
|
|
2998
3601
|
const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
|
|
2999
3602
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
3000
3603
|
}
|
|
3001
|
-
function detectVue(root) {
|
|
3604
|
+
function detectVue(root, forced = false) {
|
|
3002
3605
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
3003
3606
|
const localeRoot = join4(root, rel);
|
|
3004
3607
|
if (!safeIsDir(localeRoot)) continue;
|
|
3005
3608
|
const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
3006
|
-
|
|
3609
|
+
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
3610
|
+
if (enough) {
|
|
3007
3611
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
3008
3612
|
try {
|
|
3009
3613
|
return statSync2(join4(localeRoot, `${loc}.json`)).size;
|
|
@@ -3030,11 +3634,11 @@ function detectArb(root) {
|
|
|
3030
3634
|
var DETECTORS = [detectLaravel, detectVue, detectArb];
|
|
3031
3635
|
var BY_FORMAT = {
|
|
3032
3636
|
"laravel-php": detectLaravel,
|
|
3033
|
-
"vue-i18n-json": detectVue,
|
|
3637
|
+
"vue-i18n-json": (root) => detectVue(root, true),
|
|
3034
3638
|
"flutter-arb": detectArb
|
|
3035
3639
|
};
|
|
3036
3640
|
function detect(root, formatOverride) {
|
|
3037
|
-
if (!
|
|
3641
|
+
if (!existsSync9(root)) return null;
|
|
3038
3642
|
if (formatOverride) {
|
|
3039
3643
|
const fn = BY_FORMAT[formatOverride];
|
|
3040
3644
|
if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
|
|
@@ -3048,7 +3652,7 @@ function detect(root, formatOverride) {
|
|
|
3048
3652
|
}
|
|
3049
3653
|
|
|
3050
3654
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
3051
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
3655
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
|
|
3052
3656
|
import { join as join5 } from "path";
|
|
3053
3657
|
|
|
3054
3658
|
// src/server/import/flatten.ts
|
|
@@ -3088,7 +3692,7 @@ var vueI18nJson2 = {
|
|
|
3088
3692
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3089
3693
|
let data;
|
|
3090
3694
|
try {
|
|
3091
|
-
data = JSON.parse(
|
|
3695
|
+
data = JSON.parse(readFileSync9(join5(localeRoot, file), "utf8"));
|
|
3092
3696
|
} catch (e) {
|
|
3093
3697
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
3094
3698
|
continue;
|
|
@@ -3180,7 +3784,7 @@ var laravelPhp2 = {
|
|
|
3180
3784
|
};
|
|
3181
3785
|
|
|
3182
3786
|
// src/server/import/parsers/flutter-arb.ts
|
|
3183
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
3787
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
|
|
3184
3788
|
import { join as join7 } from "path";
|
|
3185
3789
|
var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
3186
3790
|
function localeFromArbName(file) {
|
|
@@ -3217,7 +3821,7 @@ var flutterArb2 = {
|
|
|
3217
3821
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
3218
3822
|
let data;
|
|
3219
3823
|
try {
|
|
3220
|
-
data = JSON.parse(
|
|
3824
|
+
data = JSON.parse(readFileSync10(join7(localeRoot, file), "utf8"));
|
|
3221
3825
|
} catch (e) {
|
|
3222
3826
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
3223
3827
|
continue;
|
|
@@ -3371,8 +3975,8 @@ function runImport(opts) {
|
|
|
3371
3975
|
}
|
|
3372
3976
|
|
|
3373
3977
|
// src/server/export-run.ts
|
|
3374
|
-
import { readFileSync as
|
|
3375
|
-
import { resolve as
|
|
3978
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync7, rmdirSync, statSync as statSync4, unlinkSync } from "fs";
|
|
3979
|
+
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
3376
3980
|
function effectiveLocales(config) {
|
|
3377
3981
|
const limit = config.exportLocales;
|
|
3378
3982
|
if (!limit || limit.length === 0) return config.locales;
|
|
@@ -3383,18 +3987,86 @@ function narrowForExport(state) {
|
|
|
3383
3987
|
if (locales.length === state.config.locales.length) return state;
|
|
3384
3988
|
return { ...state, config: { ...state.config, locales } };
|
|
3385
3989
|
}
|
|
3990
|
+
var LOCALE_TOKEN = /^[a-z]{2,3}([_-][a-z0-9]+)*$/i;
|
|
3991
|
+
function escapeRegExp(s) {
|
|
3992
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3993
|
+
}
|
|
3994
|
+
function segmentRegExp(segment) {
|
|
3995
|
+
const pattern = escapeRegExp(segment).replaceAll("\\{locale\\}", "(?<locale>[A-Za-z0-9_-]+)").replaceAll("\\{namespace\\}", "[^/]*");
|
|
3996
|
+
return new RegExp(`^${pattern}$`);
|
|
3997
|
+
}
|
|
3998
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
3999
|
+
let current = dir;
|
|
4000
|
+
while (current !== stopAt && current.startsWith(stopAt + sep)) {
|
|
4001
|
+
try {
|
|
4002
|
+
rmdirSync(current);
|
|
4003
|
+
} catch {
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
current = dirname2(current);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
4010
|
+
const segments = output.path.split("/").filter(Boolean);
|
|
4011
|
+
if (!segments.some((s) => s.includes("{locale}"))) return 0;
|
|
4012
|
+
const root = resolve7(projectRoot);
|
|
4013
|
+
let deleted = 0;
|
|
4014
|
+
const stale = (token) => token !== void 0 && !validTokens.has(token) && LOCALE_TOKEN.test(token);
|
|
4015
|
+
const visit = (dir, index, locale) => {
|
|
4016
|
+
const segment = segments[index];
|
|
4017
|
+
const isLast = index === segments.length - 1;
|
|
4018
|
+
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
4019
|
+
const next = resolve7(dir, segment);
|
|
4020
|
+
if (isLast) {
|
|
4021
|
+
if (stale(locale) && existsSync10(next) && statSync4(next).isFile()) {
|
|
4022
|
+
unlinkSync(next);
|
|
4023
|
+
deleted++;
|
|
4024
|
+
removeEmptyDirs(dir, root);
|
|
4025
|
+
}
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
visit(next, index + 1, locale);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
const re = segmentRegExp(segment);
|
|
4032
|
+
let entries;
|
|
4033
|
+
try {
|
|
4034
|
+
entries = readdirSync7(dir, { withFileTypes: true });
|
|
4035
|
+
} catch {
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
for (const entry of entries) {
|
|
4039
|
+
const m = entry.name.match(re);
|
|
4040
|
+
if (!m) continue;
|
|
4041
|
+
const token = m.groups?.locale ?? locale;
|
|
4042
|
+
if (isLast) {
|
|
4043
|
+
if (!entry.isFile() || !stale(token)) continue;
|
|
4044
|
+
unlinkSync(resolve7(dir, entry.name));
|
|
4045
|
+
deleted++;
|
|
4046
|
+
removeEmptyDirs(dir, root);
|
|
4047
|
+
} else if (entry.isDirectory()) {
|
|
4048
|
+
visit(resolve7(dir, entry.name), index + 1, token);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
visit(root, 0, void 0);
|
|
4053
|
+
return deleted;
|
|
4054
|
+
}
|
|
3386
4055
|
function exportToDisk(state, projectRoot, opts) {
|
|
4056
|
+
const allLocales = state.config.locales;
|
|
3387
4057
|
state = narrowForExport(state);
|
|
3388
4058
|
const outputs = opts?.adapter ? state.config.outputs.filter((o) => o.adapter === opts.adapter) : state.config.outputs;
|
|
3389
4059
|
const warnings = [];
|
|
3390
4060
|
let written = 0;
|
|
3391
4061
|
let skipped = 0;
|
|
4062
|
+
let deleted = 0;
|
|
3392
4063
|
for (const output of outputs) {
|
|
3393
|
-
const
|
|
4064
|
+
const adapter = getAdapter(output.adapter);
|
|
4065
|
+
const result = adapter.export(state, output);
|
|
3394
4066
|
warnings.push(...result.warnings);
|
|
3395
4067
|
const writtenPaths = /* @__PURE__ */ new Set();
|
|
3396
4068
|
for (const f of result.files) {
|
|
3397
|
-
const abs =
|
|
4069
|
+
const abs = resolve7(projectRoot, f.path);
|
|
3398
4070
|
if (writtenPaths.has(abs)) {
|
|
3399
4071
|
skipped++;
|
|
3400
4072
|
continue;
|
|
@@ -3402,7 +4074,7 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
3402
4074
|
writtenPaths.add(abs);
|
|
3403
4075
|
let current = null;
|
|
3404
4076
|
try {
|
|
3405
|
-
current =
|
|
4077
|
+
current = readFileSync11(abs, "utf8");
|
|
3406
4078
|
} catch {
|
|
3407
4079
|
}
|
|
3408
4080
|
if (current === f.contents) {
|
|
@@ -3412,12 +4084,14 @@ function exportToDisk(state, projectRoot, opts) {
|
|
|
3412
4084
|
writeFileAtomic(abs, f.contents);
|
|
3413
4085
|
written++;
|
|
3414
4086
|
}
|
|
4087
|
+
const validTokens = new Set(allLocales.map((l) => resolveLocaleToken(output, l, adapter.defaultLocaleCase)));
|
|
4088
|
+
deleted += pruneStaleLocaleFiles(output, validTokens, projectRoot);
|
|
3415
4089
|
}
|
|
3416
|
-
return { written, skipped, warnings };
|
|
4090
|
+
return { written, skipped, deleted, warnings };
|
|
3417
4091
|
}
|
|
3418
4092
|
|
|
3419
4093
|
// src/server/ui-prefs.ts
|
|
3420
|
-
import { readFileSync as
|
|
4094
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
3421
4095
|
import { homedir } from "os";
|
|
3422
4096
|
import { join as join8 } from "path";
|
|
3423
4097
|
var THEMES = ["system", "light", "dark"];
|
|
@@ -3426,7 +4100,7 @@ var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
|
|
|
3426
4100
|
var DEFAULTS = { theme: "system" };
|
|
3427
4101
|
function readJson(path) {
|
|
3428
4102
|
try {
|
|
3429
|
-
const parsed = JSON.parse(
|
|
4103
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
3430
4104
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3431
4105
|
} catch {
|
|
3432
4106
|
return {};
|
|
@@ -3442,8 +4116,8 @@ function saveUiPrefs(path, prefs) {
|
|
|
3442
4116
|
}
|
|
3443
4117
|
|
|
3444
4118
|
// src/server/local-settings.ts
|
|
3445
|
-
import { readFileSync as
|
|
3446
|
-
import { resolve as
|
|
4119
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
4120
|
+
import { resolve as resolve8 } from "path";
|
|
3447
4121
|
var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
|
|
3448
4122
|
var isEditorId = (v) => EDITOR_IDS.includes(v);
|
|
3449
4123
|
var DEFAULT_AI = {
|
|
@@ -3454,10 +4128,10 @@ var DEFAULT_AI = {
|
|
|
3454
4128
|
batchSize: 25
|
|
3455
4129
|
};
|
|
3456
4130
|
var DEFAULT_EDITOR = "vscode";
|
|
3457
|
-
var settingsPath = (projectRoot) =>
|
|
4131
|
+
var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
|
|
3458
4132
|
function readJson2(path) {
|
|
3459
4133
|
try {
|
|
3460
|
-
const parsed = JSON.parse(
|
|
4134
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
3461
4135
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3462
4136
|
} catch {
|
|
3463
4137
|
return {};
|
|
@@ -3475,7 +4149,9 @@ function coerceAi(raw) {
|
|
|
3475
4149
|
contextBatchSize: typeof a.contextBatchSize === "number" && a.contextBatchSize > 0 ? a.contextBatchSize : void 0,
|
|
3476
4150
|
contextConcurrency: typeof a.contextConcurrency === "number" && a.contextConcurrency > 0 ? a.contextConcurrency : void 0,
|
|
3477
4151
|
vision: typeof a.vision === "boolean" ? a.vision : void 0,
|
|
3478
|
-
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0
|
|
4152
|
+
promptStyle: PROMPT_STYLES.includes(a.promptStyle) ? a.promptStyle : void 0,
|
|
4153
|
+
inputPricePerMTok: typeof a.inputPricePerMTok === "number" && a.inputPricePerMTok >= 0 ? a.inputPricePerMTok : void 0,
|
|
4154
|
+
outputPricePerMTok: typeof a.outputPricePerMTok === "number" && a.outputPricePerMTok >= 0 ? a.outputPricePerMTok : void 0
|
|
3479
4155
|
};
|
|
3480
4156
|
}
|
|
3481
4157
|
function coerceProfiles(raw) {
|
|
@@ -3514,6 +4190,10 @@ function aiConfigError(ai) {
|
|
|
3514
4190
|
if (!(a.endpoint === null || a.endpoint === void 0 || typeof a.endpoint === "string")) return "ai.endpoint must be a string or null";
|
|
3515
4191
|
if (!(a.region === void 0 || a.region === null || typeof a.region === "string")) return "ai.region must be a string or null";
|
|
3516
4192
|
if (typeof a.batchSize !== "number") return "ai.batchSize must be a number";
|
|
4193
|
+
for (const f of ["inputPricePerMTok", "outputPricePerMTok"]) {
|
|
4194
|
+
const v = a[f];
|
|
4195
|
+
if (!(v === void 0 || v === null || typeof v === "number" && v >= 0)) return `ai.${f} must be a non-negative number`;
|
|
4196
|
+
}
|
|
3517
4197
|
return null;
|
|
3518
4198
|
}
|
|
3519
4199
|
|
|
@@ -3521,10 +4201,10 @@ function aiConfigError(ai) {
|
|
|
3521
4201
|
var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
3522
4202
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
3523
4203
|
function projectName(root) {
|
|
3524
|
-
const nameFile =
|
|
3525
|
-
if (
|
|
4204
|
+
const nameFile = resolve9(root, ".idea", ".name");
|
|
4205
|
+
if (existsSync11(nameFile)) {
|
|
3526
4206
|
try {
|
|
3527
|
-
const name =
|
|
4207
|
+
const name = readFileSync14(nameFile, "utf8").trim();
|
|
3528
4208
|
if (name) return name;
|
|
3529
4209
|
} catch {
|
|
3530
4210
|
}
|
|
@@ -3534,7 +4214,7 @@ function projectName(root) {
|
|
|
3534
4214
|
function createApi(deps) {
|
|
3535
4215
|
const app = new Hono();
|
|
3536
4216
|
const load = () => loadState(deps.statePath);
|
|
3537
|
-
const projectRoot =
|
|
4217
|
+
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
3538
4218
|
let translateQueue = Promise.resolve();
|
|
3539
4219
|
const withTranslateLock = (fn) => {
|
|
3540
4220
|
const next = translateQueue.then(fn, fn);
|
|
@@ -3632,27 +4312,27 @@ function createApi(deps) {
|
|
|
3632
4312
|
found.set(deps.statePath, {
|
|
3633
4313
|
name: basename(deps.statePath),
|
|
3634
4314
|
path: deps.statePath,
|
|
3635
|
-
relDir: activeRel !== basename(activeRel) ?
|
|
4315
|
+
relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
|
|
3636
4316
|
});
|
|
3637
4317
|
function walk(dir, depth) {
|
|
3638
4318
|
if (depth > 4) return;
|
|
3639
4319
|
let entries = [];
|
|
3640
4320
|
try {
|
|
3641
|
-
entries =
|
|
4321
|
+
entries = readdirSync8(dir);
|
|
3642
4322
|
} catch {
|
|
3643
4323
|
return;
|
|
3644
4324
|
}
|
|
3645
4325
|
for (const name of entries) {
|
|
3646
4326
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3647
|
-
const abs =
|
|
4327
|
+
const abs = resolve9(dir, name);
|
|
3648
4328
|
let filePath = null;
|
|
3649
|
-
if ((name === "glotfile" || name.endsWith(".glotfile")) &&
|
|
3650
|
-
filePath =
|
|
4329
|
+
if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
|
|
4330
|
+
filePath = resolve9(dir, `${name}.json`);
|
|
3651
4331
|
} else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
|
|
3652
4332
|
filePath = abs;
|
|
3653
4333
|
} else {
|
|
3654
4334
|
try {
|
|
3655
|
-
if (
|
|
4335
|
+
if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
|
|
3656
4336
|
} catch {
|
|
3657
4337
|
}
|
|
3658
4338
|
continue;
|
|
@@ -3661,7 +4341,7 @@ function createApi(deps) {
|
|
|
3661
4341
|
try {
|
|
3662
4342
|
loadState(filePath);
|
|
3663
4343
|
const rel = relative3(projectRoot, filePath);
|
|
3664
|
-
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ?
|
|
4344
|
+
found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
|
|
3665
4345
|
} catch {
|
|
3666
4346
|
}
|
|
3667
4347
|
}
|
|
@@ -3677,10 +4357,10 @@ function createApi(deps) {
|
|
|
3677
4357
|
app.post("/file", async (c) => {
|
|
3678
4358
|
const { path } = await c.req.json();
|
|
3679
4359
|
if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
|
|
3680
|
-
const resolved =
|
|
3681
|
-
const inside = resolved === projectRoot || resolved.startsWith(projectRoot +
|
|
4360
|
+
const resolved = resolve9(projectRoot, path);
|
|
4361
|
+
const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
|
|
3682
4362
|
if (!inside) return c.json({ error: "file is outside the project" }, 400);
|
|
3683
|
-
if (!
|
|
4363
|
+
if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
|
|
3684
4364
|
loadState(resolved);
|
|
3685
4365
|
deps.statePath = resolved;
|
|
3686
4366
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
@@ -3737,11 +4417,11 @@ function createApi(deps) {
|
|
|
3737
4417
|
function removeOrphanScreenshot(s, screenshot) {
|
|
3738
4418
|
if (!screenshot) return;
|
|
3739
4419
|
for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
|
|
3740
|
-
const root =
|
|
3741
|
-
const abs =
|
|
4420
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4421
|
+
const abs = resolve9(root, screenshot);
|
|
3742
4422
|
const rel = relative3(root, abs);
|
|
3743
|
-
const seg0 = rel.split(
|
|
3744
|
-
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") &&
|
|
4423
|
+
const seg0 = rel.split(sep2)[0] ?? "";
|
|
4424
|
+
if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
|
|
3745
4425
|
try {
|
|
3746
4426
|
rmSync4(abs);
|
|
3747
4427
|
} catch {
|
|
@@ -3993,11 +4673,11 @@ function createApi(deps) {
|
|
|
3993
4673
|
const body = await c.req.parseBody();
|
|
3994
4674
|
const file = body["file"];
|
|
3995
4675
|
if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
|
|
3996
|
-
const root =
|
|
4676
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
3997
4677
|
const dirName = screenshotDirName(deps.statePath);
|
|
3998
|
-
const dir =
|
|
4678
|
+
const dir = resolve9(root, dirName);
|
|
3999
4679
|
const filename = `${sanitize(key)}__${sanitize(file.name)}`;
|
|
4000
|
-
writeFileAtomic(
|
|
4680
|
+
writeFileAtomic(resolve9(dir, filename), Buffer.from(await file.arrayBuffer()));
|
|
4001
4681
|
const path = `${dirName}/${filename}`;
|
|
4002
4682
|
const s = load();
|
|
4003
4683
|
const prev = s.keys[key]?.screenshot;
|
|
@@ -4029,6 +4709,23 @@ function createApi(deps) {
|
|
|
4029
4709
|
return c.json({ files, warnings });
|
|
4030
4710
|
});
|
|
4031
4711
|
app.get("/scan/missing", (c) => c.json(findMissing(load())));
|
|
4712
|
+
const spellerCache = /* @__PURE__ */ new Map();
|
|
4713
|
+
const cachedLoader = (dictId) => {
|
|
4714
|
+
let p = spellerCache.get(dictId);
|
|
4715
|
+
if (!p) {
|
|
4716
|
+
p = defaultLoader(dictId);
|
|
4717
|
+
spellerCache.set(dictId, p);
|
|
4718
|
+
}
|
|
4719
|
+
return p;
|
|
4720
|
+
};
|
|
4721
|
+
app.get("/lint", async (c) => {
|
|
4722
|
+
const state = load();
|
|
4723
|
+
const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
|
|
4724
|
+
} });
|
|
4725
|
+
const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
|
|
4726
|
+
const counts = countSeverities(findings);
|
|
4727
|
+
return c.json({ findings, counts, ok: counts.error === 0 });
|
|
4728
|
+
});
|
|
4032
4729
|
app.get("/checks", (c) => {
|
|
4033
4730
|
const param = c.req.query("checks");
|
|
4034
4731
|
const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
|
|
@@ -4064,22 +4761,12 @@ function createApi(deps) {
|
|
|
4064
4761
|
return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
|
|
4065
4762
|
});
|
|
4066
4763
|
app.post("/export", (c) => {
|
|
4067
|
-
const
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
const result = adapter.export(s, output);
|
|
4074
|
-
warnings.push(...result.warnings);
|
|
4075
|
-
for (const f of result.files) {
|
|
4076
|
-
const abs = resolve8(root, f.path);
|
|
4077
|
-
writeFileAtomic(abs, f.contents);
|
|
4078
|
-
count++;
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
console.log(`[export] ${count} file(s)${warnings.length ? `, ${warnings.length} warning(s)` : ""}`);
|
|
4082
|
-
return c.json({ files: count, warnings });
|
|
4764
|
+
const root = dirname3(resolve9(deps.statePath));
|
|
4765
|
+
const { written, skipped, deleted, warnings } = exportToDisk(load(), root);
|
|
4766
|
+
console.log(
|
|
4767
|
+
`[export] ${written + skipped} file(s)${deleted ? `, ${deleted} removed` : ""}${warnings.length ? `, ${warnings.length} warning(s)` : ""}`
|
|
4768
|
+
);
|
|
4769
|
+
return c.json({ files: written + skipped, warnings });
|
|
4083
4770
|
});
|
|
4084
4771
|
app.post("/translate/stream", async (c) => {
|
|
4085
4772
|
const signal = c.req.raw.signal;
|
|
@@ -4101,7 +4788,7 @@ function createApi(deps) {
|
|
|
4101
4788
|
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
4102
4789
|
return;
|
|
4103
4790
|
}
|
|
4104
|
-
const { skipped } = attachScreenshotsForProvider(reqs, s,
|
|
4791
|
+
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4105
4792
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4106
4793
|
console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
|
|
4107
4794
|
let totalWritten = 0;
|
|
@@ -4179,7 +4866,7 @@ function createApi(deps) {
|
|
|
4179
4866
|
} catch (e) {
|
|
4180
4867
|
return c.json({ error: e.message }, 400);
|
|
4181
4868
|
}
|
|
4182
|
-
const { skipped } = attachScreenshotsForProvider(toTranslate, s,
|
|
4869
|
+
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
4183
4870
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
4184
4871
|
const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
|
|
4185
4872
|
const latest = load();
|
|
@@ -4207,6 +4894,13 @@ function createApi(deps) {
|
|
|
4207
4894
|
}
|
|
4208
4895
|
return c.json({ requested: reqs.length, written, errors });
|
|
4209
4896
|
}));
|
|
4897
|
+
app.post("/translate/estimate", async (c) => {
|
|
4898
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4899
|
+
const keys = Array.isArray(body.keys) && body.keys.length ? body.keys.filter(Boolean) : void 0;
|
|
4900
|
+
const locales = Array.isArray(body.locales) && body.locales.length ? body.locales.filter(Boolean) : void 0;
|
|
4901
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
4902
|
+
return c.json(estimateTranslation(load(), ai, { onlyMissing: body.onlyMissing ?? true, keys, locales }));
|
|
4903
|
+
});
|
|
4210
4904
|
app.get("/log", (c) => c.json(readLog(projectRoot, 100)));
|
|
4211
4905
|
app.post("/scan", async (c) => {
|
|
4212
4906
|
const s = load();
|
|
@@ -4231,7 +4925,7 @@ function createApi(deps) {
|
|
|
4231
4925
|
const refs = [];
|
|
4232
4926
|
const prefixRefs = [];
|
|
4233
4927
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
4234
|
-
const abs =
|
|
4928
|
+
const abs = resolve9(projectRoot, file);
|
|
4235
4929
|
for (const r of entry.refs) {
|
|
4236
4930
|
if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
|
|
4237
4931
|
}
|
|
@@ -4358,7 +5052,7 @@ function createApi(deps) {
|
|
|
4358
5052
|
}
|
|
4359
5053
|
|
|
4360
5054
|
// src/server/server.ts
|
|
4361
|
-
var here =
|
|
5055
|
+
var here = dirname4(fileURLToPath(import.meta.url));
|
|
4362
5056
|
var DEFAULT_UI_DIR = join9(here, "..", "ui");
|
|
4363
5057
|
var MIME = {
|
|
4364
5058
|
".html": "text/html; charset=utf-8",
|
|
@@ -4392,11 +5086,11 @@ function buildApp(opts) {
|
|
|
4392
5086
|
app.get("/:dir/*", async (c, next) => {
|
|
4393
5087
|
const dirSeg = c.req.param("dir");
|
|
4394
5088
|
if (!dirSeg.endsWith("-screenshots")) return next();
|
|
4395
|
-
const shotsRoot =
|
|
5089
|
+
const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
|
|
4396
5090
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4397
5091
|
const rest = pathname.slice(`/${dirSeg}`.length);
|
|
4398
|
-
const target =
|
|
4399
|
-
const inside = target === shotsRoot || target.startsWith(shotsRoot +
|
|
5092
|
+
const target = resolve10(shotsRoot, "." + rest);
|
|
5093
|
+
const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
|
|
4400
5094
|
if (inside) {
|
|
4401
5095
|
const file = await readFileResponse(target);
|
|
4402
5096
|
if (file) return file;
|
|
@@ -4404,11 +5098,11 @@ function buildApp(opts) {
|
|
|
4404
5098
|
return c.notFound();
|
|
4405
5099
|
});
|
|
4406
5100
|
if (!opts.dev) {
|
|
4407
|
-
const root =
|
|
5101
|
+
const root = resolve10(opts.uiDir ?? DEFAULT_UI_DIR);
|
|
4408
5102
|
app.get("/*", async (c) => {
|
|
4409
5103
|
const pathname = decodeURIComponent(new URL(c.req.url).pathname);
|
|
4410
|
-
const target =
|
|
4411
|
-
const inside = target === root || target.startsWith(root +
|
|
5104
|
+
const target = resolve10(root, "." + pathname);
|
|
5105
|
+
const inside = target === root || target.startsWith(root + sep3);
|
|
4412
5106
|
if (inside && pathname !== "/") {
|
|
4413
5107
|
const file = await readFileResponse(target);
|
|
4414
5108
|
if (file) return file;
|
|
@@ -4461,7 +5155,7 @@ async function startServer(opts) {
|
|
|
4461
5155
|
});
|
|
4462
5156
|
}
|
|
4463
5157
|
function backgroundScan(statePath) {
|
|
4464
|
-
const projectRoot =
|
|
5158
|
+
const projectRoot = dirname4(resolve10(statePath));
|
|
4465
5159
|
Promise.resolve().then(() => {
|
|
4466
5160
|
const state = loadState(statePath);
|
|
4467
5161
|
const existing = loadUsageCache(projectRoot);
|