glotfile 0.4.2 → 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.
@@ -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 dirname3, join as join9, resolve as resolve9, extname as extname3, sep as sep2 } from "path";
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 = state.keys[key].values[locale]?.value;
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 sep3 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
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(sep3);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 readFileSync13, existsSync as existsSync9, readdirSync as readdirSync7, statSync as statSync4, rmSync as rmSync4 } from "fs";
2347
- import { dirname as dirname2, resolve as resolve8, basename, relative as relative3, sep } from "path";
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";
@@ -2841,7 +3265,7 @@ function stripFences(s) {
2841
3265
  return s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
2842
3266
  }
2843
3267
  function defaultSpawn(prompt, systemPrompt, model) {
2844
- return new Promise((resolve10, reject) => {
3268
+ return new Promise((resolve11, reject) => {
2845
3269
  const args = [
2846
3270
  "--print",
2847
3271
  "--output-format",
@@ -2873,7 +3297,7 @@ function defaultSpawn(prompt, systemPrompt, model) {
2873
3297
  reject(new Error(`claude error: ${envelope.result ?? "unknown error"}`));
2874
3298
  return;
2875
3299
  }
2876
- resolve10(envelope.result ?? "");
3300
+ resolve11(envelope.result ?? "");
2877
3301
  } catch {
2878
3302
  reject(new Error(`Failed to parse claude JSON output: ${stdout.slice(0, 200)}`));
2879
3303
  }
@@ -2947,10 +3371,10 @@ function makeProvider(ai) {
2947
3371
  }
2948
3372
 
2949
3373
  // src/server/log.ts
2950
- import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
2951
- import { resolve as resolve5 } from "path";
3374
+ import { appendFileSync, readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3375
+ import { resolve as resolve6 } from "path";
2952
3376
  function logPath(projectRoot) {
2953
- return resolve5(projectRoot, ".glotfile", "log.jsonl");
3377
+ return resolve6(projectRoot, ".glotfile", "log.jsonl");
2954
3378
  }
2955
3379
  function appendLog(projectRoot, entry) {
2956
3380
  ensureGlotfileDir(projectRoot);
@@ -2958,14 +3382,14 @@ function appendLog(projectRoot, entry) {
2958
3382
  }
2959
3383
  function readLog(projectRoot, limit = 100) {
2960
3384
  const path = logPath(projectRoot);
2961
- if (!existsSync7(path)) return [];
2962
- const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
3385
+ if (!existsSync8(path)) return [];
3386
+ const lines = readFileSync8(path, "utf8").split("\n").filter((l) => l.trim() !== "");
2963
3387
  const entries = lines.map((l) => JSON.parse(l));
2964
3388
  return entries.reverse().slice(0, limit);
2965
3389
  }
2966
3390
 
2967
3391
  // src/server/import/detect.ts
2968
- import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
3392
+ import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
2969
3393
  import { join as join4 } from "path";
2970
3394
  var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
2971
3395
  var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
@@ -2998,12 +3422,13 @@ function detectLaravel(root) {
2998
3422
  const sourceLocale = pickSource(locales, (loc) => fileCount(join4(localeRoot, loc)));
2999
3423
  return { format: "laravel-php", localeRoot, locales, sourceLocale };
3000
3424
  }
3001
- function detectVue(root) {
3425
+ function detectVue(root, forced = false) {
3002
3426
  for (const rel of VUE_DIR_CANDIDATES) {
3003
3427
  const localeRoot = join4(root, rel);
3004
3428
  if (!safeIsDir(localeRoot)) continue;
3005
3429
  const locales = readdirSync3(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
3006
- if (locales.length >= 2) {
3430
+ const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
3431
+ if (enough) {
3007
3432
  const sourceLocale = pickSource(locales, (loc) => {
3008
3433
  try {
3009
3434
  return statSync2(join4(localeRoot, `${loc}.json`)).size;
@@ -3030,11 +3455,11 @@ function detectArb(root) {
3030
3455
  var DETECTORS = [detectLaravel, detectVue, detectArb];
3031
3456
  var BY_FORMAT = {
3032
3457
  "laravel-php": detectLaravel,
3033
- "vue-i18n-json": detectVue,
3458
+ "vue-i18n-json": (root) => detectVue(root, true),
3034
3459
  "flutter-arb": detectArb
3035
3460
  };
3036
3461
  function detect(root, formatOverride) {
3037
- if (!existsSync8(root)) return null;
3462
+ if (!existsSync9(root)) return null;
3038
3463
  if (formatOverride) {
3039
3464
  const fn = BY_FORMAT[formatOverride];
3040
3465
  if (!fn) throw new Error(`Unknown format: ${formatOverride}`);
@@ -3048,7 +3473,7 @@ function detect(root, formatOverride) {
3048
3473
  }
3049
3474
 
3050
3475
  // src/server/import/parsers/vue-i18n-json.ts
3051
- import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
3476
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
3052
3477
  import { join as join5 } from "path";
3053
3478
 
3054
3479
  // src/server/import/flatten.ts
@@ -3088,7 +3513,7 @@ var vueI18nJson2 = {
3088
3513
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3089
3514
  let data;
3090
3515
  try {
3091
- data = JSON.parse(readFileSync8(join5(localeRoot, file), "utf8"));
3516
+ data = JSON.parse(readFileSync9(join5(localeRoot, file), "utf8"));
3092
3517
  } catch (e) {
3093
3518
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
3094
3519
  continue;
@@ -3180,7 +3605,7 @@ var laravelPhp2 = {
3180
3605
  };
3181
3606
 
3182
3607
  // src/server/import/parsers/flutter-arb.ts
3183
- import { readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
3608
+ import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
3184
3609
  import { join as join7 } from "path";
3185
3610
  var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3186
3611
  function localeFromArbName(file) {
@@ -3217,7 +3642,7 @@ var flutterArb2 = {
3217
3642
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3218
3643
  let data;
3219
3644
  try {
3220
- data = JSON.parse(readFileSync9(join7(localeRoot, file), "utf8"));
3645
+ data = JSON.parse(readFileSync10(join7(localeRoot, file), "utf8"));
3221
3646
  } catch (e) {
3222
3647
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
3223
3648
  continue;
@@ -3371,8 +3796,8 @@ function runImport(opts) {
3371
3796
  }
3372
3797
 
3373
3798
  // src/server/export-run.ts
3374
- import { readFileSync as readFileSync10 } from "fs";
3375
- import { resolve as resolve6 } from "path";
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";
3376
3801
  function effectiveLocales(config) {
3377
3802
  const limit = config.exportLocales;
3378
3803
  if (!limit || limit.length === 0) return config.locales;
@@ -3383,18 +3808,86 @@ function narrowForExport(state) {
3383
3808
  if (locales.length === state.config.locales.length) return state;
3384
3809
  return { ...state, config: { ...state.config, locales } };
3385
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
+ }
3386
3876
  function exportToDisk(state, projectRoot, opts) {
3877
+ const allLocales = state.config.locales;
3387
3878
  state = narrowForExport(state);
3388
3879
  const outputs = opts?.adapter ? state.config.outputs.filter((o) => o.adapter === opts.adapter) : state.config.outputs;
3389
3880
  const warnings = [];
3390
3881
  let written = 0;
3391
3882
  let skipped = 0;
3883
+ let deleted = 0;
3392
3884
  for (const output of outputs) {
3393
- const result = getAdapter(output.adapter).export(state, output);
3885
+ const adapter = getAdapter(output.adapter);
3886
+ const result = adapter.export(state, output);
3394
3887
  warnings.push(...result.warnings);
3395
3888
  const writtenPaths = /* @__PURE__ */ new Set();
3396
3889
  for (const f of result.files) {
3397
- const abs = resolve6(projectRoot, f.path);
3890
+ const abs = resolve7(projectRoot, f.path);
3398
3891
  if (writtenPaths.has(abs)) {
3399
3892
  skipped++;
3400
3893
  continue;
@@ -3402,7 +3895,7 @@ function exportToDisk(state, projectRoot, opts) {
3402
3895
  writtenPaths.add(abs);
3403
3896
  let current = null;
3404
3897
  try {
3405
- current = readFileSync10(abs, "utf8");
3898
+ current = readFileSync11(abs, "utf8");
3406
3899
  } catch {
3407
3900
  }
3408
3901
  if (current === f.contents) {
@@ -3412,12 +3905,14 @@ function exportToDisk(state, projectRoot, opts) {
3412
3905
  writeFileAtomic(abs, f.contents);
3413
3906
  written++;
3414
3907
  }
3908
+ const validTokens = new Set(allLocales.map((l) => resolveLocaleToken(output, l, adapter.defaultLocaleCase)));
3909
+ deleted += pruneStaleLocaleFiles(output, validTokens, projectRoot);
3415
3910
  }
3416
- return { written, skipped, warnings };
3911
+ return { written, skipped, deleted, warnings };
3417
3912
  }
3418
3913
 
3419
3914
  // src/server/ui-prefs.ts
3420
- import { readFileSync as readFileSync11 } from "fs";
3915
+ import { readFileSync as readFileSync12 } from "fs";
3421
3916
  import { homedir } from "os";
3422
3917
  import { join as join8 } from "path";
3423
3918
  var THEMES = ["system", "light", "dark"];
@@ -3426,7 +3921,7 @@ var defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
3426
3921
  var DEFAULTS = { theme: "system" };
3427
3922
  function readJson(path) {
3428
3923
  try {
3429
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
3924
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
3430
3925
  return parsed && typeof parsed === "object" ? parsed : {};
3431
3926
  } catch {
3432
3927
  return {};
@@ -3442,8 +3937,8 @@ function saveUiPrefs(path, prefs) {
3442
3937
  }
3443
3938
 
3444
3939
  // src/server/local-settings.ts
3445
- import { readFileSync as readFileSync12 } from "fs";
3446
- import { resolve as resolve7 } from "path";
3940
+ import { readFileSync as readFileSync13 } from "fs";
3941
+ import { resolve as resolve8 } from "path";
3447
3942
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
3448
3943
  var isEditorId = (v) => EDITOR_IDS.includes(v);
3449
3944
  var DEFAULT_AI = {
@@ -3454,10 +3949,10 @@ var DEFAULT_AI = {
3454
3949
  batchSize: 25
3455
3950
  };
3456
3951
  var DEFAULT_EDITOR = "vscode";
3457
- var settingsPath = (projectRoot) => resolve7(projectRoot, ".glotfile", "settings.json");
3952
+ var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
3458
3953
  function readJson2(path) {
3459
3954
  try {
3460
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
3955
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
3461
3956
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3462
3957
  } catch {
3463
3958
  return {};
@@ -3521,10 +4016,10 @@ function aiConfigError(ai) {
3521
4016
  var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
3522
4017
  var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
3523
4018
  function projectName(root) {
3524
- const nameFile = resolve8(root, ".idea", ".name");
3525
- if (existsSync9(nameFile)) {
4019
+ const nameFile = resolve9(root, ".idea", ".name");
4020
+ if (existsSync11(nameFile)) {
3526
4021
  try {
3527
- const name = readFileSync13(nameFile, "utf8").trim();
4022
+ const name = readFileSync14(nameFile, "utf8").trim();
3528
4023
  if (name) return name;
3529
4024
  } catch {
3530
4025
  }
@@ -3534,7 +4029,7 @@ function projectName(root) {
3534
4029
  function createApi(deps) {
3535
4030
  const app = new Hono();
3536
4031
  const load = () => loadState(deps.statePath);
3537
- const projectRoot = dirname2(resolve8(deps.statePath));
4032
+ const projectRoot = dirname3(resolve9(deps.statePath));
3538
4033
  let translateQueue = Promise.resolve();
3539
4034
  const withTranslateLock = (fn) => {
3540
4035
  const next = translateQueue.then(fn, fn);
@@ -3632,27 +4127,27 @@ function createApi(deps) {
3632
4127
  found.set(deps.statePath, {
3633
4128
  name: basename(deps.statePath),
3634
4129
  path: deps.statePath,
3635
- relDir: activeRel !== basename(activeRel) ? dirname2(activeRel) : void 0
4130
+ relDir: activeRel !== basename(activeRel) ? dirname3(activeRel) : void 0
3636
4131
  });
3637
4132
  function walk(dir, depth) {
3638
4133
  if (depth > 4) return;
3639
4134
  let entries = [];
3640
4135
  try {
3641
- entries = readdirSync7(dir);
4136
+ entries = readdirSync8(dir);
3642
4137
  } catch {
3643
4138
  return;
3644
4139
  }
3645
4140
  for (const name of entries) {
3646
4141
  if (name.startsWith(".") || name === "node_modules") continue;
3647
- const abs = resolve8(dir, name);
4142
+ const abs = resolve9(dir, name);
3648
4143
  let filePath = null;
3649
- if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync9(resolve8(abs, "config.json"))) {
3650
- filePath = resolve8(dir, `${name}.json`);
4144
+ if ((name === "glotfile" || name.endsWith(".glotfile")) && existsSync11(resolve9(abs, "config.json"))) {
4145
+ filePath = resolve9(dir, `${name}.json`);
3651
4146
  } else if (name === "glotfile.json" || name.endsWith(".glotfile.json")) {
3652
4147
  filePath = abs;
3653
4148
  } else {
3654
4149
  try {
3655
- if (statSync4(abs).isDirectory()) walk(abs, depth + 1);
4150
+ if (statSync5(abs).isDirectory()) walk(abs, depth + 1);
3656
4151
  } catch {
3657
4152
  }
3658
4153
  continue;
@@ -3661,7 +4156,7 @@ function createApi(deps) {
3661
4156
  try {
3662
4157
  loadState(filePath);
3663
4158
  const rel = relative3(projectRoot, filePath);
3664
- found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname2(rel) : void 0 });
4159
+ found.set(filePath, { name: basename(filePath), path: filePath, relDir: rel !== basename(filePath) ? dirname3(rel) : void 0 });
3665
4160
  } catch {
3666
4161
  }
3667
4162
  }
@@ -3677,10 +4172,10 @@ function createApi(deps) {
3677
4172
  app.post("/file", async (c) => {
3678
4173
  const { path } = await c.req.json();
3679
4174
  if (typeof path !== "string") return c.json({ error: "path must be a string" }, 400);
3680
- const resolved = resolve8(projectRoot, path);
3681
- const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep);
4175
+ const resolved = resolve9(projectRoot, path);
4176
+ const inside = resolved === projectRoot || resolved.startsWith(projectRoot + sep2);
3682
4177
  if (!inside) return c.json({ error: "file is outside the project" }, 400);
3683
- if (!existsSync9(resolved)) return c.json({ error: "file not found" }, 400);
4178
+ if (!existsSync11(resolved)) return c.json({ error: "file not found" }, 400);
3684
4179
  loadState(resolved);
3685
4180
  deps.statePath = resolved;
3686
4181
  return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
@@ -3737,11 +4232,11 @@ function createApi(deps) {
3737
4232
  function removeOrphanScreenshot(s, screenshot) {
3738
4233
  if (!screenshot) return;
3739
4234
  for (const e of Object.values(s.keys)) if (e.screenshot === screenshot) return;
3740
- const root = dirname2(resolve8(deps.statePath));
3741
- const abs = resolve8(root, screenshot);
4235
+ const root = dirname3(resolve9(deps.statePath));
4236
+ const abs = resolve9(root, screenshot);
3742
4237
  const rel = relative3(root, abs);
3743
- const seg0 = rel.split(sep)[0] ?? "";
3744
- if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync9(abs)) {
4238
+ const seg0 = rel.split(sep2)[0] ?? "";
4239
+ if (!rel.startsWith("..") && seg0.endsWith("-screenshots") && existsSync11(abs)) {
3745
4240
  try {
3746
4241
  rmSync4(abs);
3747
4242
  } catch {
@@ -3993,11 +4488,11 @@ function createApi(deps) {
3993
4488
  const body = await c.req.parseBody();
3994
4489
  const file = body["file"];
3995
4490
  if (!file || typeof file === "string") return c.json({ error: "no file uploaded" }, 400);
3996
- const root = dirname2(resolve8(deps.statePath));
4491
+ const root = dirname3(resolve9(deps.statePath));
3997
4492
  const dirName = screenshotDirName(deps.statePath);
3998
- const dir = resolve8(root, dirName);
4493
+ const dir = resolve9(root, dirName);
3999
4494
  const filename = `${sanitize(key)}__${sanitize(file.name)}`;
4000
- writeFileAtomic(resolve8(dir, filename), Buffer.from(await file.arrayBuffer()));
4495
+ writeFileAtomic(resolve9(dir, filename), Buffer.from(await file.arrayBuffer()));
4001
4496
  const path = `${dirName}/${filename}`;
4002
4497
  const s = load();
4003
4498
  const prev = s.keys[key]?.screenshot;
@@ -4029,6 +4524,23 @@ function createApi(deps) {
4029
4524
  return c.json({ files, warnings });
4030
4525
  });
4031
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
+ });
4032
4544
  app.get("/checks", (c) => {
4033
4545
  const param = c.req.query("checks");
4034
4546
  const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
@@ -4064,22 +4576,12 @@ function createApi(deps) {
4064
4576
  return c.json({ keyCount: result.keyCount, localeCount: result.localeCount, warnings: result.warnings });
4065
4577
  });
4066
4578
  app.post("/export", (c) => {
4067
- const s = narrowForExport(load());
4068
- const root = dirname2(resolve8(deps.statePath));
4069
- const warnings = [];
4070
- let count = 0;
4071
- for (const output of s.config.outputs) {
4072
- const adapter = getAdapter(output.adapter);
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 });
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 });
4083
4585
  });
4084
4586
  app.post("/translate/stream", async (c) => {
4085
4587
  const signal = c.req.raw.signal;
@@ -4101,7 +4603,7 @@ function createApi(deps) {
4101
4603
  await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
4102
4604
  return;
4103
4605
  }
4104
- const { skipped } = attachScreenshotsForProvider(reqs, s, dirname2(resolve8(deps.statePath)), provider.supportsVision());
4606
+ const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
4105
4607
  if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
4106
4608
  console.log(`[translate] ${reqs.length} string(s) \u2192 ${aiCfg.model}`);
4107
4609
  let totalWritten = 0;
@@ -4179,7 +4681,7 @@ function createApi(deps) {
4179
4681
  } catch (e) {
4180
4682
  return c.json({ error: e.message }, 400);
4181
4683
  }
4182
- const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname2(resolve8(deps.statePath)), provider.supportsVision());
4684
+ const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
4183
4685
  if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
4184
4686
  const results = await runLocaleParallel(toTranslate, provider, {}, aiCfg.concurrency);
4185
4687
  const latest = load();
@@ -4231,7 +4733,7 @@ function createApi(deps) {
4231
4733
  const refs = [];
4232
4734
  const prefixRefs = [];
4233
4735
  for (const [file, entry] of Object.entries(cache2.files)) {
4234
- const abs = resolve8(projectRoot, file);
4736
+ const abs = resolve9(projectRoot, file);
4235
4737
  for (const r of entry.refs) {
4236
4738
  if (r.key === key) refs.push({ file, abs, line: r.line, col: r.col, scanner: r.scanner });
4237
4739
  }
@@ -4358,7 +4860,7 @@ function createApi(deps) {
4358
4860
  }
4359
4861
 
4360
4862
  // src/server/server.ts
4361
- var here = dirname3(fileURLToPath(import.meta.url));
4863
+ var here = dirname4(fileURLToPath(import.meta.url));
4362
4864
  var DEFAULT_UI_DIR = join9(here, "..", "ui");
4363
4865
  var MIME = {
4364
4866
  ".html": "text/html; charset=utf-8",
@@ -4392,11 +4894,11 @@ function buildApp(opts) {
4392
4894
  app.get("/:dir/*", async (c, next) => {
4393
4895
  const dirSeg = c.req.param("dir");
4394
4896
  if (!dirSeg.endsWith("-screenshots")) return next();
4395
- const shotsRoot = resolve9(dirname3(resolve9(apiDeps.statePath)), dirSeg);
4897
+ const shotsRoot = resolve10(dirname4(resolve10(apiDeps.statePath)), dirSeg);
4396
4898
  const pathname = decodeURIComponent(new URL(c.req.url).pathname);
4397
4899
  const rest = pathname.slice(`/${dirSeg}`.length);
4398
- const target = resolve9(shotsRoot, "." + rest);
4399
- const inside = target === shotsRoot || target.startsWith(shotsRoot + sep2);
4900
+ const target = resolve10(shotsRoot, "." + rest);
4901
+ const inside = target === shotsRoot || target.startsWith(shotsRoot + sep3);
4400
4902
  if (inside) {
4401
4903
  const file = await readFileResponse(target);
4402
4904
  if (file) return file;
@@ -4404,11 +4906,11 @@ function buildApp(opts) {
4404
4906
  return c.notFound();
4405
4907
  });
4406
4908
  if (!opts.dev) {
4407
- const root = resolve9(opts.uiDir ?? DEFAULT_UI_DIR);
4909
+ const root = resolve10(opts.uiDir ?? DEFAULT_UI_DIR);
4408
4910
  app.get("/*", async (c) => {
4409
4911
  const pathname = decodeURIComponent(new URL(c.req.url).pathname);
4410
- const target = resolve9(root, "." + pathname);
4411
- const inside = target === root || target.startsWith(root + sep2);
4912
+ const target = resolve10(root, "." + pathname);
4913
+ const inside = target === root || target.startsWith(root + sep3);
4412
4914
  if (inside && pathname !== "/") {
4413
4915
  const file = await readFileResponse(target);
4414
4916
  if (file) return file;
@@ -4461,7 +4963,7 @@ async function startServer(opts) {
4461
4963
  });
4462
4964
  }
4463
4965
  function backgroundScan(statePath) {
4464
- const projectRoot = dirname3(resolve9(statePath));
4966
+ const projectRoot = dirname4(resolve10(statePath));
4465
4967
  Promise.resolve().then(() => {
4466
4968
  const state = loadState(statePath);
4467
4969
  const existing = loadUsageCache(projectRoot);