glotfile 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/server/cli.js +1228 -307
- package/dist/server/server.js +997 -297
- package/dist/ui/assets/index-Bjwiz6KQ.css +1 -0
- package/dist/ui/assets/{index-B7j9yFsz.js → index-Dwn9g3g-.js} +113 -15
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BaHu118N.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -58,6 +58,14 @@ var init_atomic_write = __esm({
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
// src/server/lint/registry.ts
|
|
61
|
+
function unknownRuleIds(ids) {
|
|
62
|
+
const valid = new Set(RULE_IDS);
|
|
63
|
+
return ids.filter((id) => !valid.has(id));
|
|
64
|
+
}
|
|
65
|
+
function suggestRuleId(unknown) {
|
|
66
|
+
const lower = unknown.toLowerCase();
|
|
67
|
+
return RULE_IDS.find((id) => id.includes(lower) || lower.includes(id));
|
|
68
|
+
}
|
|
61
69
|
var RULE_IDS, DEFAULT_SEVERITY;
|
|
62
70
|
var init_registry = __esm({
|
|
63
71
|
"src/server/lint/registry.ts"() {
|
|
@@ -271,7 +279,8 @@ function validate(raw) {
|
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
if (raw.glossary !== void 0 && !Array.isArray(raw.glossary)) fail("glossary must be an array");
|
|
274
|
-
|
|
282
|
+
if (raw.glossarySuggestions !== void 0 && !Array.isArray(raw.glossarySuggestions)) fail("glossarySuggestions must be an array");
|
|
283
|
+
const state = { glossary: [], glossarySuggestions: [], ...raw };
|
|
275
284
|
return state;
|
|
276
285
|
}
|
|
277
286
|
function defaultState() {
|
|
@@ -290,6 +299,7 @@ function defaultState() {
|
|
|
290
299
|
autoExport: true
|
|
291
300
|
},
|
|
292
301
|
glossary: [],
|
|
302
|
+
glossarySuggestions: [],
|
|
293
303
|
keys: {}
|
|
294
304
|
};
|
|
295
305
|
}
|
|
@@ -807,6 +817,39 @@ function upsertGlossaryEntry(state, entry) {
|
|
|
807
817
|
function deleteGlossaryEntry(state, term) {
|
|
808
818
|
state.glossary = state.glossary.filter((e) => e.term !== term);
|
|
809
819
|
}
|
|
820
|
+
function normGlossaryTerm(term) {
|
|
821
|
+
return term.trim().toLowerCase();
|
|
822
|
+
}
|
|
823
|
+
function mergeGlossarySuggestions(state, found) {
|
|
824
|
+
const known = /* @__PURE__ */ new Set();
|
|
825
|
+
for (const g of state.glossary) known.add(normGlossaryTerm(g.term));
|
|
826
|
+
for (const s of state.glossarySuggestions) known.add(normGlossaryTerm(s.term));
|
|
827
|
+
const added = [];
|
|
828
|
+
for (const f of found) {
|
|
829
|
+
const term = f.term.trim();
|
|
830
|
+
if (!term) continue;
|
|
831
|
+
const key = normGlossaryTerm(term);
|
|
832
|
+
if (known.has(key)) continue;
|
|
833
|
+
known.add(key);
|
|
834
|
+
const sug = { term, status: "pending" };
|
|
835
|
+
if (f.note?.trim()) sug.note = f.note.trim();
|
|
836
|
+
if (f.doNotTranslate) sug.doNotTranslate = true;
|
|
837
|
+
if (f.caseSensitive) sug.caseSensitive = true;
|
|
838
|
+
if (f.wholeWord === false) sug.wholeWord = false;
|
|
839
|
+
state.glossarySuggestions.push(sug);
|
|
840
|
+
added.push(sug);
|
|
841
|
+
}
|
|
842
|
+
return added;
|
|
843
|
+
}
|
|
844
|
+
function dismissGlossarySuggestion(state, term) {
|
|
845
|
+
const key = normGlossaryTerm(term);
|
|
846
|
+
const s = state.glossarySuggestions.find((x) => normGlossaryTerm(x.term) === key);
|
|
847
|
+
if (s) s.status = "dismissed";
|
|
848
|
+
}
|
|
849
|
+
function removeGlossarySuggestion(state, term) {
|
|
850
|
+
const key = normGlossaryTerm(term);
|
|
851
|
+
state.glossarySuggestions = state.glossarySuggestions.filter((x) => normGlossaryTerm(x.term) !== key);
|
|
852
|
+
}
|
|
810
853
|
function addCustomWord(state, word) {
|
|
811
854
|
const w = word.trim();
|
|
812
855
|
if (!w) return;
|
|
@@ -1807,6 +1850,71 @@ var init_vue_i18n_json = __esm({
|
|
|
1807
1850
|
}
|
|
1808
1851
|
});
|
|
1809
1852
|
|
|
1853
|
+
// src/server/adapters/next-intl-json.ts
|
|
1854
|
+
var DEFAULT_LOCALE_CASE8, nextIntlJson;
|
|
1855
|
+
var init_next_intl_json = __esm({
|
|
1856
|
+
"src/server/adapters/next-intl-json.ts"() {
|
|
1857
|
+
"use strict";
|
|
1858
|
+
init_adapters();
|
|
1859
|
+
init_shared();
|
|
1860
|
+
init_options();
|
|
1861
|
+
init_format();
|
|
1862
|
+
init_plurals();
|
|
1863
|
+
DEFAULT_LOCALE_CASE8 = "lower-hyphen";
|
|
1864
|
+
nextIntlJson = {
|
|
1865
|
+
name: "next-intl-json",
|
|
1866
|
+
capabilities: {
|
|
1867
|
+
plural: "native",
|
|
1868
|
+
select: "native",
|
|
1869
|
+
nesting: "both",
|
|
1870
|
+
metadata: false,
|
|
1871
|
+
placeholderStyle: "icu",
|
|
1872
|
+
fileGrouping: "per-locale"
|
|
1873
|
+
},
|
|
1874
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE8,
|
|
1875
|
+
export(state, output) {
|
|
1876
|
+
const files = [];
|
|
1877
|
+
const warnings = [];
|
|
1878
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE8));
|
|
1879
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
1880
|
+
const fmt = { indent, sortKeys: true, finalNewline };
|
|
1881
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
1882
|
+
const flatOutput = output.style === "flat";
|
|
1883
|
+
for (const locale of state.config.locales) {
|
|
1884
|
+
const flat = {};
|
|
1885
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
1886
|
+
if (entry.plural) {
|
|
1887
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1888
|
+
if (!forms) continue;
|
|
1889
|
+
flat[key] = formsToIcu(entry.plural.arg, forms);
|
|
1890
|
+
} else {
|
|
1891
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
1892
|
+
if (raw === null) continue;
|
|
1893
|
+
flat[key] = raw;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
let payload = flat;
|
|
1897
|
+
if (!flatOutput) {
|
|
1898
|
+
const { tree, collisions } = nestKeys(flat);
|
|
1899
|
+
for (const key of collisions) {
|
|
1900
|
+
warnings.push({
|
|
1901
|
+
code: "key-collision",
|
|
1902
|
+
key,
|
|
1903
|
+
locale,
|
|
1904
|
+
message: "key is both a leaf and a parent; dropped from nested output"
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
payload = tree;
|
|
1908
|
+
}
|
|
1909
|
+
files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8)), contents: serializeJson(payload, fmt) });
|
|
1910
|
+
}
|
|
1911
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
1912
|
+
return { files, warnings };
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1810
1918
|
// src/server/adapters/angular-xliff.ts
|
|
1811
1919
|
function xmlEscape2(s) {
|
|
1812
1920
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1862,7 +1970,7 @@ function renderEmbeddedIcu(value) {
|
|
|
1862
1970
|
function renderScalar(value, ids, placeholders) {
|
|
1863
1971
|
return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
|
|
1864
1972
|
}
|
|
1865
|
-
var
|
|
1973
|
+
var DEFAULT_LOCALE_CASE9, angularXliff;
|
|
1866
1974
|
var init_angular_xliff = __esm({
|
|
1867
1975
|
"src/server/adapters/angular-xliff.ts"() {
|
|
1868
1976
|
"use strict";
|
|
@@ -1870,7 +1978,7 @@ var init_angular_xliff = __esm({
|
|
|
1870
1978
|
init_options();
|
|
1871
1979
|
init_placeholders();
|
|
1872
1980
|
init_schema();
|
|
1873
|
-
|
|
1981
|
+
DEFAULT_LOCALE_CASE9 = "bcp47-hyphen";
|
|
1874
1982
|
angularXliff = {
|
|
1875
1983
|
name: "angular-xliff",
|
|
1876
1984
|
capabilities: {
|
|
@@ -1881,18 +1989,18 @@ var init_angular_xliff = __esm({
|
|
|
1881
1989
|
placeholderStyle: "icu",
|
|
1882
1990
|
fileGrouping: "per-locale"
|
|
1883
1991
|
},
|
|
1884
|
-
defaultLocaleCase:
|
|
1992
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE9,
|
|
1885
1993
|
export(state, output) {
|
|
1886
1994
|
const files = [];
|
|
1887
1995
|
const warnings = [];
|
|
1888
|
-
warnings.push(...localeCollisionWarnings(output, state.config.locales,
|
|
1996
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE9));
|
|
1889
1997
|
const sourceLocale = state.config.sourceLocale;
|
|
1890
|
-
const sourceToken = resolveLocaleToken(output, sourceLocale,
|
|
1998
|
+
const sourceToken = resolveLocaleToken(output, sourceLocale, DEFAULT_LOCALE_CASE9);
|
|
1891
1999
|
const emptyAs = resolveEmptyAs(output, "source");
|
|
1892
2000
|
const keys = Object.keys(state.keys).sort();
|
|
1893
2001
|
for (const locale of state.config.locales) {
|
|
1894
2002
|
if (output.skipSourceLocale && locale === sourceLocale) continue;
|
|
1895
|
-
const token = resolveLocaleToken(output, locale,
|
|
2003
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE9);
|
|
1896
2004
|
const units = [];
|
|
1897
2005
|
for (const key of keys) {
|
|
1898
2006
|
const entry = state.keys[key];
|
|
@@ -1957,7 +2065,7 @@ function yamlMap(node, indent, level) {
|
|
|
1957
2065
|
}
|
|
1958
2066
|
return lines;
|
|
1959
2067
|
}
|
|
1960
|
-
var RESERVED_KEYS,
|
|
2068
|
+
var RESERVED_KEYS, DEFAULT_LOCALE_CASE10, railsYaml;
|
|
1961
2069
|
var init_rails_yaml = __esm({
|
|
1962
2070
|
"src/server/adapters/rails-yaml.ts"() {
|
|
1963
2071
|
"use strict";
|
|
@@ -1967,7 +2075,7 @@ var init_rails_yaml = __esm({
|
|
|
1967
2075
|
init_placeholders();
|
|
1968
2076
|
init_schema();
|
|
1969
2077
|
RESERVED_KEYS = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "on", "off", "null", "y", "n"]);
|
|
1970
|
-
|
|
2078
|
+
DEFAULT_LOCALE_CASE10 = "bcp47-hyphen";
|
|
1971
2079
|
railsYaml = {
|
|
1972
2080
|
name: "rails-yaml",
|
|
1973
2081
|
capabilities: {
|
|
@@ -1978,10 +2086,10 @@ var init_rails_yaml = __esm({
|
|
|
1978
2086
|
placeholderStyle: "named",
|
|
1979
2087
|
fileGrouping: "per-locale"
|
|
1980
2088
|
},
|
|
1981
|
-
defaultLocaleCase:
|
|
2089
|
+
defaultLocaleCase: DEFAULT_LOCALE_CASE10,
|
|
1982
2090
|
export(state, output) {
|
|
1983
2091
|
const warnings = [];
|
|
1984
|
-
warnings.push(...localeCollisionWarnings(output, state.config.locales,
|
|
2092
|
+
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE10));
|
|
1985
2093
|
const { indent, finalNewline } = resolveFormat(state, output);
|
|
1986
2094
|
const emptyAs = resolveEmptyAs(output, "omit");
|
|
1987
2095
|
const files = [];
|
|
@@ -2013,7 +2121,7 @@ var init_rails_yaml = __esm({
|
|
|
2013
2121
|
for (const c of collisions) {
|
|
2014
2122
|
warnings.push({ code: "key-collision", key: c, locale, message: "key is both a leaf and a parent; dropped from nested output" });
|
|
2015
2123
|
}
|
|
2016
|
-
const token = resolveLocaleToken(output, locale,
|
|
2124
|
+
const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE10);
|
|
2017
2125
|
const body = [`${yamlKey(token)}:`, ...yamlMap(nested, indent, 1)].join("\n");
|
|
2018
2126
|
files.push({ path: resolvePath(output.path, token), contents: finalNewline ? body + "\n" : body });
|
|
2019
2127
|
}
|
|
@@ -2057,6 +2165,7 @@ function getRegistry() {
|
|
|
2057
2165
|
[appleStringsdict.name]: appleStringsdict,
|
|
2058
2166
|
[appleStrings.name]: appleStrings,
|
|
2059
2167
|
[vueI18nJson.name]: vueI18nJson,
|
|
2168
|
+
[nextIntlJson.name]: nextIntlJson,
|
|
2060
2169
|
[angularXliff.name]: angularXliff,
|
|
2061
2170
|
[railsYaml.name]: railsYaml
|
|
2062
2171
|
};
|
|
@@ -2078,6 +2187,7 @@ var init_adapters = __esm({
|
|
|
2078
2187
|
init_apple_stringsdict();
|
|
2079
2188
|
init_apple_strings();
|
|
2080
2189
|
init_vue_i18n_json();
|
|
2190
|
+
init_next_intl_json();
|
|
2081
2191
|
init_angular_xliff();
|
|
2082
2192
|
init_rails_yaml();
|
|
2083
2193
|
}
|
|
@@ -2461,6 +2571,54 @@ var init_batch = __esm({
|
|
|
2461
2571
|
}
|
|
2462
2572
|
});
|
|
2463
2573
|
|
|
2574
|
+
// src/server/ai/price-cache.ts
|
|
2575
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2576
|
+
import { homedir } from "os";
|
|
2577
|
+
import { join as join3 } from "path";
|
|
2578
|
+
function isModelPrice(v) {
|
|
2579
|
+
if (!v || typeof v !== "object") return false;
|
|
2580
|
+
const p = v;
|
|
2581
|
+
return typeof p.inputPerMTok === "number" && typeof p.outputPerMTok === "number";
|
|
2582
|
+
}
|
|
2583
|
+
function loadPriceCache(path = defaultPriceCachePath()) {
|
|
2584
|
+
let parsed;
|
|
2585
|
+
try {
|
|
2586
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2587
|
+
} catch {
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2591
|
+
const raw = parsed;
|
|
2592
|
+
if (!raw.models || typeof raw.models !== "object") return null;
|
|
2593
|
+
const models = {};
|
|
2594
|
+
for (const [id, price] of Object.entries(raw.models)) {
|
|
2595
|
+
if (isModelPrice(price)) models[id] = price;
|
|
2596
|
+
}
|
|
2597
|
+
return {
|
|
2598
|
+
source: typeof raw.source === "string" ? raw.source : "unknown",
|
|
2599
|
+
fetchedAt: typeof raw.fetchedAt === "string" ? raw.fetchedAt : "",
|
|
2600
|
+
models
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function savePriceCache(cache2, path = defaultPriceCachePath()) {
|
|
2604
|
+
writeFileAtomic(path, JSON.stringify(cache2, null, 2) + "\n");
|
|
2605
|
+
}
|
|
2606
|
+
function getPriceCache() {
|
|
2607
|
+
if (memo === void 0) memo = loadPriceCache();
|
|
2608
|
+
return memo;
|
|
2609
|
+
}
|
|
2610
|
+
function invalidatePriceCache() {
|
|
2611
|
+
memo = void 0;
|
|
2612
|
+
}
|
|
2613
|
+
var defaultPriceCachePath, memo;
|
|
2614
|
+
var init_price_cache = __esm({
|
|
2615
|
+
"src/server/ai/price-cache.ts"() {
|
|
2616
|
+
"use strict";
|
|
2617
|
+
init_atomic_write();
|
|
2618
|
+
defaultPriceCachePath = () => process.env.GLOTFILE_PRICES_PATH || join3(homedir(), ".glotfile", "model-prices.json");
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2464
2622
|
// src/server/ai/pricing.ts
|
|
2465
2623
|
function addUsage(into, add) {
|
|
2466
2624
|
into.inputTokens += add.inputTokens;
|
|
@@ -2474,7 +2632,9 @@ function usageCostUsd(usage, ai, multiplier = 1) {
|
|
|
2474
2632
|
return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
|
|
2475
2633
|
}
|
|
2476
2634
|
function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
|
|
2477
|
-
const
|
|
2635
|
+
const writeRate = pricing.cacheWritePerMTok ?? pricing.inputPerMTok * CACHE_WRITE_MULTIPLIER;
|
|
2636
|
+
const readRate = pricing.cacheReadPerMTok ?? pricing.inputPerMTok * CACHE_READ_MULTIPLIER;
|
|
2637
|
+
const inputCost = usage.inputTokens * pricing.inputPerMTok + usage.cacheCreationInputTokens * writeRate + usage.cacheReadInputTokens * readRate;
|
|
2478
2638
|
return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
|
|
2479
2639
|
}
|
|
2480
2640
|
function bareModelId(model) {
|
|
@@ -2485,12 +2645,27 @@ function bareModelId(model) {
|
|
|
2485
2645
|
if (anth !== -1) id = id.slice(anth + "anthropic.".length);
|
|
2486
2646
|
return id;
|
|
2487
2647
|
}
|
|
2488
|
-
function
|
|
2648
|
+
function lookupCachePrice(cache2, id) {
|
|
2649
|
+
const exact = cache2.models[id];
|
|
2650
|
+
if (exact) return { source: "cache", ...exact };
|
|
2651
|
+
let best;
|
|
2652
|
+
for (const [cid, price] of Object.entries(cache2.models)) {
|
|
2653
|
+
if (id.startsWith(cid) && (!best || cid.length > best.id.length)) {
|
|
2654
|
+
best = { id: cid, price: { source: "cache", ...price } };
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return best ? best.price : null;
|
|
2658
|
+
}
|
|
2659
|
+
function resolvePricing(ai, cache2 = getPriceCache()) {
|
|
2489
2660
|
if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
|
|
2490
2661
|
return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
|
|
2491
2662
|
}
|
|
2492
2663
|
if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
|
|
2493
2664
|
const id = bareModelId(ai.model);
|
|
2665
|
+
if (cache2) {
|
|
2666
|
+
const cached = lookupCachePrice(cache2, id);
|
|
2667
|
+
if (cached) return cached;
|
|
2668
|
+
}
|
|
2494
2669
|
let best;
|
|
2495
2670
|
for (const row of PRICE_TABLE) {
|
|
2496
2671
|
if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
|
|
@@ -2501,6 +2676,7 @@ var BATCH_PRICE_MULTIPLIER, CACHE_WRITE_MULTIPLIER, CACHE_READ_MULTIPLIER, PRICE
|
|
|
2501
2676
|
var init_pricing = __esm({
|
|
2502
2677
|
"src/server/ai/pricing.ts"() {
|
|
2503
2678
|
"use strict";
|
|
2679
|
+
init_price_cache();
|
|
2504
2680
|
BATCH_PRICE_MULTIPLIER = 0.5;
|
|
2505
2681
|
CACHE_WRITE_MULTIPLIER = 1.25;
|
|
2506
2682
|
CACHE_READ_MULTIPLIER = 0.1;
|
|
@@ -3149,11 +3325,11 @@ var init_glotfile_dir = __esm({
|
|
|
3149
3325
|
});
|
|
3150
3326
|
|
|
3151
3327
|
// src/server/local-settings.ts
|
|
3152
|
-
import { readFileSync as
|
|
3328
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3153
3329
|
import { resolve as resolve3 } from "path";
|
|
3154
3330
|
function readJson(path) {
|
|
3155
3331
|
try {
|
|
3156
|
-
const parsed = JSON.parse(
|
|
3332
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3157
3333
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3158
3334
|
} catch {
|
|
3159
3335
|
return {};
|
|
@@ -3240,13 +3416,23 @@ var init_local_settings = __esm({
|
|
|
3240
3416
|
});
|
|
3241
3417
|
|
|
3242
3418
|
// src/server/glossary.ts
|
|
3243
|
-
function
|
|
3244
|
-
return
|
|
3419
|
+
function escapeRegExp2(s) {
|
|
3420
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3421
|
+
}
|
|
3422
|
+
function contains(haystack, needle, caseSensitive, wholeWord) {
|
|
3423
|
+
if (!wholeWord) {
|
|
3424
|
+
return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
|
|
3425
|
+
}
|
|
3426
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp2(needle)}(?![\\p{L}\\p{N}])`, caseSensitive ? "u" : "iu");
|
|
3427
|
+
return re.test(haystack);
|
|
3428
|
+
}
|
|
3429
|
+
function termInSource(source, entry) {
|
|
3430
|
+
return contains(source, entry.term, entry.caseSensitive, entry.wholeWord ?? true);
|
|
3245
3431
|
}
|
|
3246
3432
|
function relevantGlossary(source, targetLocale, glossary) {
|
|
3247
3433
|
const hints = [];
|
|
3248
3434
|
for (const entry of glossary) {
|
|
3249
|
-
if (!
|
|
3435
|
+
if (!termInSource(source, entry)) continue;
|
|
3250
3436
|
hints.push({
|
|
3251
3437
|
term: entry.term,
|
|
3252
3438
|
doNotTranslate: entry.doNotTranslate,
|
|
@@ -3256,10 +3442,20 @@ function relevantGlossary(source, targetLocale, glossary) {
|
|
|
3256
3442
|
}
|
|
3257
3443
|
return hints;
|
|
3258
3444
|
}
|
|
3445
|
+
function sourceKeysForTerm(state, term, opts = {}) {
|
|
3446
|
+
const pseudo = { term, caseSensitive: opts.caseSensitive, wholeWord: opts.wholeWord };
|
|
3447
|
+
const out = [];
|
|
3448
|
+
for (const [key, entry] of Object.entries(state.keys)) {
|
|
3449
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
3450
|
+
const text = lv?.value ?? lv?.forms?.other ?? "";
|
|
3451
|
+
if (text && termInSource(text, pseudo)) out.push(key);
|
|
3452
|
+
}
|
|
3453
|
+
return out;
|
|
3454
|
+
}
|
|
3259
3455
|
function glossaryViolations(source, value, targetLocale, glossary) {
|
|
3260
3456
|
const out = [];
|
|
3261
3457
|
for (const entry of glossary) {
|
|
3262
|
-
if (!
|
|
3458
|
+
if (!termInSource(source, entry)) continue;
|
|
3263
3459
|
if (entry.doNotTranslate) {
|
|
3264
3460
|
if (!contains(value, entry.term, entry.caseSensitive)) {
|
|
3265
3461
|
out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
|
|
@@ -3280,7 +3476,7 @@ var init_glossary = __esm({
|
|
|
3280
3476
|
});
|
|
3281
3477
|
|
|
3282
3478
|
// src/server/ai/run.ts
|
|
3283
|
-
import { readFileSync as
|
|
3479
|
+
import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
3284
3480
|
import { resolve as resolve4, extname } from "path";
|
|
3285
3481
|
function selectRequests(state, opts) {
|
|
3286
3482
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
@@ -3354,7 +3550,7 @@ function attachScreenshots(reqs, state, projectRoot) {
|
|
|
3354
3550
|
if (!existsSync5(abs)) {
|
|
3355
3551
|
cache2.set(screenshot, null);
|
|
3356
3552
|
} else {
|
|
3357
|
-
const buf =
|
|
3553
|
+
const buf = readFileSync6(abs);
|
|
3358
3554
|
cache2.set(screenshot, buf.length > MAX_IMAGE_BYTES ? null : { mediaType, base64: buf.toString("base64") });
|
|
3359
3555
|
}
|
|
3360
3556
|
}
|
|
@@ -3500,16 +3696,16 @@ var init_run = __esm({
|
|
|
3500
3696
|
});
|
|
3501
3697
|
|
|
3502
3698
|
// src/server/ai/pending-batch.ts
|
|
3503
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as
|
|
3504
|
-
import { join as
|
|
3699
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, rmSync as rmSync4 } from "fs";
|
|
3700
|
+
import { join as join4 } from "path";
|
|
3505
3701
|
function pendingBatchPath(projectRoot) {
|
|
3506
|
-
return
|
|
3702
|
+
return join4(projectRoot, ".glotfile", "batch.json");
|
|
3507
3703
|
}
|
|
3508
3704
|
function loadPendingBatch(projectRoot) {
|
|
3509
3705
|
const path = pendingBatchPath(projectRoot);
|
|
3510
3706
|
if (!existsSync6(path)) return void 0;
|
|
3511
3707
|
try {
|
|
3512
|
-
const parsed = JSON.parse(
|
|
3708
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3513
3709
|
if (parsed?.version !== 1) return void 0;
|
|
3514
3710
|
return parsed;
|
|
3515
3711
|
} catch {
|
|
@@ -3517,9 +3713,9 @@ function loadPendingBatch(projectRoot) {
|
|
|
3517
3713
|
}
|
|
3518
3714
|
}
|
|
3519
3715
|
function savePendingBatch(projectRoot, pending) {
|
|
3520
|
-
const dir =
|
|
3716
|
+
const dir = join4(projectRoot, ".glotfile");
|
|
3521
3717
|
mkdirSync4(dir, { recursive: true });
|
|
3522
|
-
const gitignore =
|
|
3718
|
+
const gitignore = join4(dir, ".gitignore");
|
|
3523
3719
|
if (!existsSync6(gitignore)) writeFileSync3(gitignore, "*\n");
|
|
3524
3720
|
writeFileSync3(pendingBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3525
3721
|
}
|
|
@@ -3533,7 +3729,7 @@ var init_pending_batch = __esm({
|
|
|
3533
3729
|
});
|
|
3534
3730
|
|
|
3535
3731
|
// src/server/log.ts
|
|
3536
|
-
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as
|
|
3732
|
+
import { appendFileSync, existsSync as existsSync7, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync8 } from "fs";
|
|
3537
3733
|
import { resolve as resolve5 } from "path";
|
|
3538
3734
|
function logPath(projectRoot) {
|
|
3539
3735
|
return resolve5(projectRoot, ".glotfile", "log.jsonl");
|
|
@@ -3546,7 +3742,7 @@ function appendLog(projectRoot, entry) {
|
|
|
3546
3742
|
}
|
|
3547
3743
|
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
3548
3744
|
if (!existsSync7(path) || statSync2(path).size <= maxBytes) return;
|
|
3549
|
-
const lines =
|
|
3745
|
+
const lines = readFileSync8(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3550
3746
|
const kept = [];
|
|
3551
3747
|
let bytes = 0;
|
|
3552
3748
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -3745,7 +3941,7 @@ var init_batch_run = __esm({
|
|
|
3745
3941
|
});
|
|
3746
3942
|
|
|
3747
3943
|
// src/server/ai/context.ts
|
|
3748
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3944
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
|
|
3749
3945
|
import { resolve as resolve6 } from "path";
|
|
3750
3946
|
function globToRegExp2(glob) {
|
|
3751
3947
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -3761,7 +3957,7 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
3761
3957
|
const absPath = resolve6(projectRoot, ref.file);
|
|
3762
3958
|
if (!fileCache.has(ref.file)) {
|
|
3763
3959
|
if (!existsSync8(absPath)) continue;
|
|
3764
|
-
const content =
|
|
3960
|
+
const content = readFileSync9(absPath, "utf8");
|
|
3765
3961
|
fileCache.set(ref.file, content.split("\n"));
|
|
3766
3962
|
}
|
|
3767
3963
|
const lines = fileCache.get(ref.file);
|
|
@@ -3777,6 +3973,21 @@ function extractSnippets(refs, projectRoot, fileCache) {
|
|
|
3777
3973
|
}
|
|
3778
3974
|
return snippets;
|
|
3779
3975
|
}
|
|
3976
|
+
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
3977
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
3978
|
+
for (const target of targets) {
|
|
3979
|
+
const allRefs = Object.entries(cache2.files).flatMap(
|
|
3980
|
+
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
3981
|
+
key: r.key,
|
|
3982
|
+
file,
|
|
3983
|
+
line: r.line,
|
|
3984
|
+
col: r.col,
|
|
3985
|
+
scanner: r.scanner
|
|
3986
|
+
}))
|
|
3987
|
+
);
|
|
3988
|
+
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3780
3991
|
function buildUsageIndex(cache2) {
|
|
3781
3992
|
const index = /* @__PURE__ */ new Map();
|
|
3782
3993
|
for (const [file, entry] of Object.entries(cache2.files)) {
|
|
@@ -3915,16 +4126,16 @@ var init_context = __esm({
|
|
|
3915
4126
|
});
|
|
3916
4127
|
|
|
3917
4128
|
// src/server/ai/pending-context-batch.ts
|
|
3918
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as
|
|
3919
|
-
import { join as
|
|
4129
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync4, rmSync as rmSync5 } from "fs";
|
|
4130
|
+
import { join as join5 } from "path";
|
|
3920
4131
|
function pendingContextBatchPath(projectRoot) {
|
|
3921
|
-
return
|
|
4132
|
+
return join5(projectRoot, ".glotfile", "context-batch.json");
|
|
3922
4133
|
}
|
|
3923
4134
|
function loadPendingContextBatch(projectRoot) {
|
|
3924
4135
|
const path = pendingContextBatchPath(projectRoot);
|
|
3925
4136
|
if (!existsSync9(path)) return void 0;
|
|
3926
4137
|
try {
|
|
3927
|
-
const parsed = JSON.parse(
|
|
4138
|
+
const parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
3928
4139
|
if (parsed?.version !== 1) return void 0;
|
|
3929
4140
|
return parsed;
|
|
3930
4141
|
} catch {
|
|
@@ -3932,9 +4143,9 @@ function loadPendingContextBatch(projectRoot) {
|
|
|
3932
4143
|
}
|
|
3933
4144
|
}
|
|
3934
4145
|
function savePendingContextBatch(projectRoot, pending) {
|
|
3935
|
-
const dir =
|
|
4146
|
+
const dir = join5(projectRoot, ".glotfile");
|
|
3936
4147
|
mkdirSync5(dir, { recursive: true });
|
|
3937
|
-
const gitignore =
|
|
4148
|
+
const gitignore = join5(dir, ".gitignore");
|
|
3938
4149
|
if (!existsSync9(gitignore)) writeFileSync4(gitignore, "*\n");
|
|
3939
4150
|
writeFileSync4(pendingContextBatchPath(projectRoot), JSON.stringify(pending, null, 2) + "\n");
|
|
3940
4151
|
}
|
|
@@ -4055,6 +4266,109 @@ var init_context_batch_run = __esm({
|
|
|
4055
4266
|
}
|
|
4056
4267
|
});
|
|
4057
4268
|
|
|
4269
|
+
// src/server/ai/glossary-suggest.ts
|
|
4270
|
+
function globToRegExp3(glob) {
|
|
4271
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4272
|
+
return new RegExp(`^${escaped}$`);
|
|
4273
|
+
}
|
|
4274
|
+
function selectGlossarySources(state, opts) {
|
|
4275
|
+
const keyRe = opts.keyGlob ? globToRegExp3(opts.keyGlob) : null;
|
|
4276
|
+
let rows = [];
|
|
4277
|
+
for (const key of Object.keys(state.keys)) {
|
|
4278
|
+
if (keyRe && !keyRe.test(key)) continue;
|
|
4279
|
+
const entry = state.keys[key];
|
|
4280
|
+
if (opts.since) {
|
|
4281
|
+
if (!entry.createdAt || entry.createdAt < opts.since) continue;
|
|
4282
|
+
}
|
|
4283
|
+
const lv = entry.values[state.config.sourceLocale];
|
|
4284
|
+
const source = (lv?.value ?? lv?.forms?.other ?? "").trim();
|
|
4285
|
+
if (!source) continue;
|
|
4286
|
+
rows.push({ key, source });
|
|
4287
|
+
}
|
|
4288
|
+
rows.sort((a, b) => {
|
|
4289
|
+
const ta = state.keys[a.key].createdAt ?? "";
|
|
4290
|
+
const tb = state.keys[b.key].createdAt ?? "";
|
|
4291
|
+
return tb.localeCompare(ta) || a.key.localeCompare(b.key);
|
|
4292
|
+
});
|
|
4293
|
+
if (opts.limit !== void 0) rows = rows.slice(0, opts.limit);
|
|
4294
|
+
return rows;
|
|
4295
|
+
}
|
|
4296
|
+
function knownTermList(state) {
|
|
4297
|
+
const out = /* @__PURE__ */ new Set();
|
|
4298
|
+
for (const g of state.glossary) out.add(g.term);
|
|
4299
|
+
for (const s of state.glossarySuggestions) out.add(s.term);
|
|
4300
|
+
return [...out];
|
|
4301
|
+
}
|
|
4302
|
+
function buildGlossarySuggestSystemPrompt() {
|
|
4303
|
+
return [
|
|
4304
|
+
"You identify GLOSSARY-CANDIDATE terms in a UI string catalog so they translate consistently.",
|
|
4305
|
+
"A glossary term is a brand or product name, a feature or module name, an acronym, a piece of domain/industry jargon, or any noun phrase that should translate the SAME way everywhere (or stay verbatim).",
|
|
4306
|
+
"You are given source strings (the app's original language). Return the candidate terms you find.",
|
|
4307
|
+
"Rules:",
|
|
4308
|
+
"- Only surface terms a translator would benefit from pinning. IGNORE ordinary words, verbs, and generic UI labels (e.g. 'Save', 'Cancel', 'Welcome').",
|
|
4309
|
+
"- Prefer terms that recur or are clearly proper nouns / product names / acronyms.",
|
|
4310
|
+
"- Set doNotTranslate: true for brand/product names, code identifiers, and acronyms that must stay verbatim in every language.",
|
|
4311
|
+
"- Set caseSensitive: true only when casing is meaningful (e.g. an all-caps acronym that must not match a lowercase common word).",
|
|
4312
|
+
"- Set wholeWord: false ONLY if the term should also match inside larger words; otherwise omit it (whole-word is the default).",
|
|
4313
|
+
"- note: one short phrase on why it's a term (e.g. 'product name', 'industry acronym', 'recurring UI concept'). Keep it under 80 characters.",
|
|
4314
|
+
"- Do NOT return any term in the provided 'Already known' list.",
|
|
4315
|
+
"- Return the term exactly as it appears in the source (preserve casing)."
|
|
4316
|
+
].join("\n");
|
|
4317
|
+
}
|
|
4318
|
+
function buildGlossarySuggestBatchPrompt(sources, knownTerms) {
|
|
4319
|
+
const known = knownTerms.length ? knownTerms.join(", ") : "(none yet)";
|
|
4320
|
+
const lines = sources.map((s) => `- [${s.key}] ${s.source}`).join("\n");
|
|
4321
|
+
return [
|
|
4322
|
+
`Already known (do NOT return these): ${known}`,
|
|
4323
|
+
"",
|
|
4324
|
+
"Source strings:",
|
|
4325
|
+
lines,
|
|
4326
|
+
"",
|
|
4327
|
+
'Return JSON {"terms":[{"term","note?","doNotTranslate?","caseSensitive?","wholeWord?"}]}. Return an empty array if you find no good candidates.'
|
|
4328
|
+
].join("\n");
|
|
4329
|
+
}
|
|
4330
|
+
function dedupeTerms(terms) {
|
|
4331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4332
|
+
const out = [];
|
|
4333
|
+
for (const t of terms) {
|
|
4334
|
+
const term = t.term?.trim();
|
|
4335
|
+
if (!term) continue;
|
|
4336
|
+
const key = term.toLowerCase();
|
|
4337
|
+
if (seen.has(key)) continue;
|
|
4338
|
+
seen.add(key);
|
|
4339
|
+
out.push({ ...t, term });
|
|
4340
|
+
}
|
|
4341
|
+
return out;
|
|
4342
|
+
}
|
|
4343
|
+
var GLOSSARY_SUGGEST_SCHEMA;
|
|
4344
|
+
var init_glossary_suggest = __esm({
|
|
4345
|
+
"src/server/ai/glossary-suggest.ts"() {
|
|
4346
|
+
"use strict";
|
|
4347
|
+
GLOSSARY_SUGGEST_SCHEMA = {
|
|
4348
|
+
type: "object",
|
|
4349
|
+
properties: {
|
|
4350
|
+
terms: {
|
|
4351
|
+
type: "array",
|
|
4352
|
+
items: {
|
|
4353
|
+
type: "object",
|
|
4354
|
+
properties: {
|
|
4355
|
+
term: { type: "string" },
|
|
4356
|
+
note: { type: "string" },
|
|
4357
|
+
doNotTranslate: { type: "boolean" },
|
|
4358
|
+
caseSensitive: { type: "boolean" },
|
|
4359
|
+
wholeWord: { type: "boolean" }
|
|
4360
|
+
},
|
|
4361
|
+
required: ["term"],
|
|
4362
|
+
additionalProperties: false
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
},
|
|
4366
|
+
required: ["terms"],
|
|
4367
|
+
additionalProperties: false
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
});
|
|
4371
|
+
|
|
4058
4372
|
// src/server/ai/estimate.ts
|
|
4059
4373
|
function estimateTokens(text) {
|
|
4060
4374
|
const cjk = text.match(CJK_RE)?.length ?? 0;
|
|
@@ -4103,29 +4417,131 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4103
4417
|
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4104
4418
|
};
|
|
4105
4419
|
}
|
|
4106
|
-
|
|
4420
|
+
function estimateContext(targets, ai) {
|
|
4421
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4422
|
+
const batches = chunk(targets, batchSize);
|
|
4423
|
+
const system = buildContextSystemPrompt();
|
|
4424
|
+
let inputTokens = 0;
|
|
4425
|
+
let outputTokens = 0;
|
|
4426
|
+
for (const batch of batches) {
|
|
4427
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildContextBatchPrompt(batch));
|
|
4428
|
+
outputTokens += batch.length * (CONTEXT_REPLY_OVERHEAD + TYPICAL_CONTEXT_TOKENS);
|
|
4429
|
+
}
|
|
4430
|
+
const pricing = resolvePricing(ai);
|
|
4431
|
+
return {
|
|
4432
|
+
keys: targets.length,
|
|
4433
|
+
batches: batches.length,
|
|
4434
|
+
inputTokens,
|
|
4435
|
+
outputTokens,
|
|
4436
|
+
pricing,
|
|
4437
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
function estimateGlossarySuggest(sources, knownTerms, ai) {
|
|
4441
|
+
const batchSize = Math.max(1, ai.contextBatchSize ?? ai.batchSize ?? 10);
|
|
4442
|
+
const batches = chunk(sources, batchSize);
|
|
4443
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
4444
|
+
let inputTokens = 0;
|
|
4445
|
+
let outputTokens = 0;
|
|
4446
|
+
for (const batch of batches) {
|
|
4447
|
+
inputTokens += estimateTokens(system) + estimateTokens(buildGlossarySuggestBatchPrompt(batch, knownTerms));
|
|
4448
|
+
outputTokens += Math.ceil(batch.length * TERM_YIELD) * TERM_REPLY_TOKENS;
|
|
4449
|
+
}
|
|
4450
|
+
const pricing = resolvePricing(ai);
|
|
4451
|
+
return {
|
|
4452
|
+
sources: sources.length,
|
|
4453
|
+
batches: batches.length,
|
|
4454
|
+
inputTokens,
|
|
4455
|
+
outputTokens,
|
|
4456
|
+
pricing,
|
|
4457
|
+
estimatedCost: pricing ? (inputTokens * pricing.inputPerMTok + outputTokens * pricing.outputPerMTok) / 1e6 : null
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
var CJK_RE, EXPANSION, ITEM_REPLY_OVERHEAD, FORM_REPLY_OVERHEAD, CONTEXT_REPLY_OVERHEAD, TYPICAL_CONTEXT_TOKENS, TERM_REPLY_TOKENS, TERM_YIELD;
|
|
4107
4461
|
var init_estimate = __esm({
|
|
4108
4462
|
"src/server/ai/estimate.ts"() {
|
|
4109
4463
|
"use strict";
|
|
4110
4464
|
init_run();
|
|
4111
4465
|
init_provider();
|
|
4466
|
+
init_context();
|
|
4467
|
+
init_glossary_suggest();
|
|
4112
4468
|
init_batch();
|
|
4113
4469
|
init_pricing();
|
|
4114
4470
|
CJK_RE = /[ -鿿가-豈-]/g;
|
|
4115
4471
|
EXPANSION = 1.2;
|
|
4116
4472
|
ITEM_REPLY_OVERHEAD = 16;
|
|
4117
4473
|
FORM_REPLY_OVERHEAD = 8;
|
|
4474
|
+
CONTEXT_REPLY_OVERHEAD = 16;
|
|
4475
|
+
TYPICAL_CONTEXT_TOKENS = 35;
|
|
4476
|
+
TERM_REPLY_TOKENS = 24;
|
|
4477
|
+
TERM_YIELD = 0.15;
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4481
|
+
// src/server/ai/price-fetch.ts
|
|
4482
|
+
function normalizeModelsDevPrices(api) {
|
|
4483
|
+
const out = {};
|
|
4484
|
+
const ranks = {};
|
|
4485
|
+
if (!api || typeof api !== "object") return out;
|
|
4486
|
+
for (const [provId, prov] of Object.entries(api)) {
|
|
4487
|
+
const models = prov?.models;
|
|
4488
|
+
if (!models || typeof models !== "object") continue;
|
|
4489
|
+
const rank = providerRank(provId);
|
|
4490
|
+
for (const [modelKey, model] of Object.entries(models)) {
|
|
4491
|
+
const cost = model?.cost;
|
|
4492
|
+
if (!cost || typeof cost.input !== "number" || typeof cost.output !== "number") continue;
|
|
4493
|
+
const bareId = bareModelId(modelKey);
|
|
4494
|
+
if (!bareId) continue;
|
|
4495
|
+
const existingRank = ranks[bareId];
|
|
4496
|
+
if (existingRank !== void 0 && existingRank <= rank) continue;
|
|
4497
|
+
const price = { inputPerMTok: cost.input, outputPerMTok: cost.output };
|
|
4498
|
+
if (typeof cost.cache_read === "number") price.cacheReadPerMTok = cost.cache_read;
|
|
4499
|
+
if (typeof cost.cache_write === "number") price.cacheWritePerMTok = cost.cache_write;
|
|
4500
|
+
out[bareId] = price;
|
|
4501
|
+
ranks[bareId] = rank;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
return out;
|
|
4505
|
+
}
|
|
4506
|
+
async function refreshPrices(opts = {}) {
|
|
4507
|
+
const url = opts.url ?? priceUrl();
|
|
4508
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
4509
|
+
const res = await doFetch(url);
|
|
4510
|
+
if (!res.ok) throw new Error(`Failed to fetch prices from ${url}: HTTP ${res.status}`);
|
|
4511
|
+
const api = await res.json();
|
|
4512
|
+
const models = normalizeModelsDevPrices(api);
|
|
4513
|
+
const modelCount = Object.keys(models).length;
|
|
4514
|
+
if (modelCount === 0) throw new Error(`No model prices found in response from ${url}`);
|
|
4515
|
+
const cache2 = { source: "models.dev", fetchedAt: (opts.now ?? defaultNow)(), models };
|
|
4516
|
+
const path = opts.path ?? defaultPriceCachePath();
|
|
4517
|
+
savePriceCache(cache2, path);
|
|
4518
|
+
return { source: cache2.source, fetchedAt: cache2.fetchedAt, modelCount, path };
|
|
4519
|
+
}
|
|
4520
|
+
var MODELS_DEV_URL, priceUrl, PROVIDER_PREFERENCE, providerRank, defaultNow;
|
|
4521
|
+
var init_price_fetch = __esm({
|
|
4522
|
+
"src/server/ai/price-fetch.ts"() {
|
|
4523
|
+
"use strict";
|
|
4524
|
+
init_pricing();
|
|
4525
|
+
init_price_cache();
|
|
4526
|
+
MODELS_DEV_URL = "https://models.dev/api.json";
|
|
4527
|
+
priceUrl = () => process.env.GLOTFILE_PRICES_URL || MODELS_DEV_URL;
|
|
4528
|
+
PROVIDER_PREFERENCE = ["anthropic", "openai", "google", "meta-llama", "mistral", "deepseek", "xai", "groq"];
|
|
4529
|
+
providerRank = (provId) => {
|
|
4530
|
+
const i = PROVIDER_PREFERENCE.indexOf(provId);
|
|
4531
|
+
return i === -1 ? PROVIDER_PREFERENCE.length : i;
|
|
4532
|
+
};
|
|
4533
|
+
defaultNow = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
4118
4534
|
}
|
|
4119
4535
|
});
|
|
4120
4536
|
|
|
4121
4537
|
// src/server/scan.ts
|
|
4122
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4538
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
|
|
4123
4539
|
import { resolve as resolve7 } from "path";
|
|
4124
4540
|
function loadUsageCache(projectRoot) {
|
|
4125
4541
|
const path = resolve7(projectRoot, ".glotfile", "usage.json");
|
|
4126
4542
|
if (!existsSync10(path)) return null;
|
|
4127
4543
|
try {
|
|
4128
|
-
return JSON.parse(
|
|
4544
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
4129
4545
|
} catch {
|
|
4130
4546
|
return null;
|
|
4131
4547
|
}
|
|
@@ -4190,8 +4606,8 @@ var init_scan = __esm({
|
|
|
4190
4606
|
});
|
|
4191
4607
|
|
|
4192
4608
|
// src/server/scanner.ts
|
|
4193
|
-
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as
|
|
4194
|
-
import { join as
|
|
4609
|
+
import { readdirSync as readdirSync3, statSync as statSync3, readFileSync as readFileSync12 } from "fs";
|
|
4610
|
+
import { join as join6, extname as extname2, relative } from "path";
|
|
4195
4611
|
function scannerForExt(ext) {
|
|
4196
4612
|
return EXT_SCANNER[ext] ?? null;
|
|
4197
4613
|
}
|
|
@@ -4230,6 +4646,69 @@ function customPatterns(opts) {
|
|
|
4230
4646
|
}
|
|
4231
4647
|
return out;
|
|
4232
4648
|
}
|
|
4649
|
+
function isNextIntlFile(content) {
|
|
4650
|
+
return NEXT_INTL_IMPORT.test(content);
|
|
4651
|
+
}
|
|
4652
|
+
function nextIntlBindings(content) {
|
|
4653
|
+
const out = [];
|
|
4654
|
+
for (const m of content.matchAll(NI_BIND)) out.push({ name: m[1], ns: m[2] ?? "", index: m.index });
|
|
4655
|
+
for (const m of content.matchAll(NI_BIND_OBJ)) out.push({ name: m[1], ns: m[2], index: m.index });
|
|
4656
|
+
return out;
|
|
4657
|
+
}
|
|
4658
|
+
function nsForBindingAt(bindings, name, index) {
|
|
4659
|
+
let best = null;
|
|
4660
|
+
for (const b of bindings) {
|
|
4661
|
+
if (b.name === name && b.index < index && (!best || b.index > best.index)) best = b;
|
|
4662
|
+
}
|
|
4663
|
+
return best ? best.ns : null;
|
|
4664
|
+
}
|
|
4665
|
+
function joinKey(ns, rel) {
|
|
4666
|
+
return ns ? `${ns}.${rel}` : rel;
|
|
4667
|
+
}
|
|
4668
|
+
function uniqueBindingNames(bindings) {
|
|
4669
|
+
return [...new Set(bindings.map((b) => b.name))];
|
|
4670
|
+
}
|
|
4671
|
+
function nextIntlRefMatches(content) {
|
|
4672
|
+
const bindings = nextIntlBindings(content);
|
|
4673
|
+
const out = [];
|
|
4674
|
+
for (const name of uniqueBindingNames(bindings)) {
|
|
4675
|
+
const re = new RegExp(
|
|
4676
|
+
`\\b${escapeRe2(name)}${NI_METHOD}\\s*\\(\\s*(?:'([^'\\n]+)'|"([^"\\n]+)"|\`([^\`$\\n]+)\`)`,
|
|
4677
|
+
"g"
|
|
4678
|
+
);
|
|
4679
|
+
let m;
|
|
4680
|
+
while ((m = re.exec(content)) !== null) {
|
|
4681
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
4682
|
+
if (ns === null) continue;
|
|
4683
|
+
out.push({ key: joinKey(ns, m[1] ?? m[2] ?? m[3]), index: m.index });
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return out;
|
|
4687
|
+
}
|
|
4688
|
+
function nextIntlPrefixMatches(content) {
|
|
4689
|
+
const bindings = nextIntlBindings(content);
|
|
4690
|
+
const out = [];
|
|
4691
|
+
for (const name of uniqueBindingNames(bindings)) {
|
|
4692
|
+
const ev = escapeRe2(name);
|
|
4693
|
+
const headConcat = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*(?:'([^'\\n]*)'|"([^"\\n]*)")\\s*\\+`, "g");
|
|
4694
|
+
const headTemplate = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*\`([^\`$\\n]*)\\$\\{`, "g");
|
|
4695
|
+
const dynamicArg = new RegExp(`\\b${ev}${NI_METHOD}\\s*\\(\\s*[A-Za-z_$][\\w$.]*\\s*[),]`, "g");
|
|
4696
|
+
let m;
|
|
4697
|
+
for (const re of [headConcat, headTemplate]) {
|
|
4698
|
+
while ((m = re.exec(content)) !== null) {
|
|
4699
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
4700
|
+
if (ns === null) continue;
|
|
4701
|
+
out.push({ prefix: joinKey(ns, m[1] ?? m[2] ?? ""), index: m.index });
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
while ((m = dynamicArg.exec(content)) !== null) {
|
|
4705
|
+
const ns = nsForBindingAt(bindings, name, m.index);
|
|
4706
|
+
if (!ns) continue;
|
|
4707
|
+
out.push({ prefix: `${ns}.`, index: m.index });
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
return out;
|
|
4711
|
+
}
|
|
4233
4712
|
function lineStartOffsets(content) {
|
|
4234
4713
|
const starts = [0];
|
|
4235
4714
|
let idx = content.indexOf("\n");
|
|
@@ -4250,49 +4729,56 @@ function offsetToLineCol(starts, offset) {
|
|
|
4250
4729
|
return { line: lo + 1, col: offset - starts[lo] + 1 };
|
|
4251
4730
|
}
|
|
4252
4731
|
function extractRefs(content, scanner, opts) {
|
|
4253
|
-
const
|
|
4254
|
-
const
|
|
4255
|
-
if (patterns.length === 0) return [];
|
|
4732
|
+
const useNextIntl = scanner === "next-intl" || scanner === "js-i18n" && isNextIntlFile(content);
|
|
4733
|
+
const effScanner = useNextIntl ? "next-intl" : scanner;
|
|
4256
4734
|
const starts = lineStartOffsets(content);
|
|
4257
4735
|
const result = [];
|
|
4258
4736
|
const seen = /* @__PURE__ */ new Set();
|
|
4259
|
-
|
|
4260
|
-
const
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
const { line, col } = offsetToLineCol(starts, m.index);
|
|
4266
|
-
const dedup = `${line}:${col}:${key}`;
|
|
4267
|
-
if (!seen.has(dedup)) {
|
|
4268
|
-
seen.add(dedup);
|
|
4269
|
-
result.push({ key, line, col, scanner });
|
|
4270
|
-
}
|
|
4737
|
+
const push = (key, index) => {
|
|
4738
|
+
const { line, col } = offsetToLineCol(starts, index);
|
|
4739
|
+
const dedup = `${line}:${col}:${key}`;
|
|
4740
|
+
if (!seen.has(dedup)) {
|
|
4741
|
+
seen.add(dedup);
|
|
4742
|
+
result.push({ key, line, col, scanner: effScanner });
|
|
4271
4743
|
}
|
|
4744
|
+
};
|
|
4745
|
+
if (useNextIntl) {
|
|
4746
|
+
for (const r of nextIntlRefMatches(content)) push(r.key, r.index);
|
|
4747
|
+
} else {
|
|
4748
|
+
const base = scanner === "flutter" ? flutterPatterns(content, opts) : PATTERNS[scanner] ?? [];
|
|
4749
|
+
for (const pattern of base) eachMatch(content, pattern, push);
|
|
4272
4750
|
}
|
|
4751
|
+
for (const pattern of customPatterns(opts)) eachMatch(content, pattern, push);
|
|
4273
4752
|
result.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
4274
4753
|
return result;
|
|
4275
4754
|
}
|
|
4755
|
+
function eachMatch(content, pattern, fn) {
|
|
4756
|
+
const re = new RegExp(pattern.source, "g");
|
|
4757
|
+
let m;
|
|
4758
|
+
while ((m = re.exec(content)) !== null) {
|
|
4759
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
4760
|
+
fn(m[1], m.index);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4276
4763
|
function extractPrefixes(content, scanner) {
|
|
4277
|
-
const
|
|
4278
|
-
|
|
4764
|
+
const useNextIntl = scanner === "next-intl" || scanner === "js-i18n" && isNextIntlFile(content);
|
|
4765
|
+
const effScanner = useNextIntl ? "next-intl" : scanner;
|
|
4279
4766
|
const starts = lineStartOffsets(content);
|
|
4280
4767
|
const result = [];
|
|
4281
4768
|
const seen = /* @__PURE__ */ new Set();
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
const { line, col } = offsetToLineCol(starts, m.index);
|
|
4290
|
-
const dedup = `${line}:${col}:${prefix}`;
|
|
4291
|
-
if (!seen.has(dedup)) {
|
|
4292
|
-
seen.add(dedup);
|
|
4293
|
-
result.push({ prefix, line, col, scanner });
|
|
4294
|
-
}
|
|
4769
|
+
const push = (prefix, index) => {
|
|
4770
|
+
if (!prefix) return;
|
|
4771
|
+
const { line, col } = offsetToLineCol(starts, index);
|
|
4772
|
+
const dedup = `${line}:${col}:${prefix}`;
|
|
4773
|
+
if (!seen.has(dedup)) {
|
|
4774
|
+
seen.add(dedup);
|
|
4775
|
+
result.push({ prefix, line, col, scanner: effScanner });
|
|
4295
4776
|
}
|
|
4777
|
+
};
|
|
4778
|
+
if (useNextIntl) {
|
|
4779
|
+
for (const p of nextIntlPrefixMatches(content)) push(p.prefix, p.index);
|
|
4780
|
+
} else {
|
|
4781
|
+
for (const pattern of PREFIX_PATTERNS[scanner] ?? []) eachMatch(content, pattern, push);
|
|
4296
4782
|
}
|
|
4297
4783
|
result.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
4298
4784
|
return result;
|
|
@@ -4343,7 +4829,7 @@ function* walkFiles(dir, root, exclude) {
|
|
|
4343
4829
|
}
|
|
4344
4830
|
for (const name of entries) {
|
|
4345
4831
|
if (ALWAYS_EXCLUDE.has(name)) continue;
|
|
4346
|
-
const abs =
|
|
4832
|
+
const abs = join6(dir, name);
|
|
4347
4833
|
const rel = relative(root, abs);
|
|
4348
4834
|
let st;
|
|
4349
4835
|
try {
|
|
@@ -4373,7 +4859,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4373
4859
|
const ext = extname2(relPath);
|
|
4374
4860
|
const scanner = scannerForExt(ext);
|
|
4375
4861
|
if (!scanner) continue;
|
|
4376
|
-
const abs =
|
|
4862
|
+
const abs = join6(projectRoot, relPath);
|
|
4377
4863
|
let st;
|
|
4378
4864
|
try {
|
|
4379
4865
|
st = statSync3(abs);
|
|
@@ -4389,7 +4875,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4389
4875
|
}
|
|
4390
4876
|
let content;
|
|
4391
4877
|
try {
|
|
4392
|
-
content =
|
|
4878
|
+
content = readFileSync12(abs, "utf8");
|
|
4393
4879
|
} catch {
|
|
4394
4880
|
continue;
|
|
4395
4881
|
}
|
|
@@ -4404,7 +4890,7 @@ function runScan(projectRoot, opts, existing) {
|
|
|
4404
4890
|
saveUsageCache(projectRoot, cache2);
|
|
4405
4891
|
return cache2;
|
|
4406
4892
|
}
|
|
4407
|
-
var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, KEY_SHAPE, STRING_LITERALS;
|
|
4893
|
+
var PATTERNS, PREFIX_PATTERNS, CACHE_VERSION, EXT_SCANNER, ALWAYS_EXCLUDE, FLUTTER_ACCESSOR_DEFAULTS, NEXT_INTL_IMPORT, NI_BIND, NI_BIND_OBJ, NI_METHOD, KEY_SHAPE, STRING_LITERALS;
|
|
4408
4894
|
var init_scanner = __esm({
|
|
4409
4895
|
"src/server/scanner.ts"() {
|
|
4410
4896
|
"use strict";
|
|
@@ -4475,7 +4961,7 @@ var init_scanner = __esm({
|
|
|
4475
4961
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
4476
4962
|
]
|
|
4477
4963
|
};
|
|
4478
|
-
CACHE_VERSION =
|
|
4964
|
+
CACHE_VERSION = 8;
|
|
4479
4965
|
EXT_SCANNER = {
|
|
4480
4966
|
".php": "laravel",
|
|
4481
4967
|
".vue": "js-i18n",
|
|
@@ -4509,6 +4995,10 @@ var init_scanner = __esm({
|
|
|
4509
4995
|
"__pycache__"
|
|
4510
4996
|
]);
|
|
4511
4997
|
FLUTTER_ACCESSOR_DEFAULTS = ["l10n", "loc", "localizations", "translations"];
|
|
4998
|
+
NEXT_INTL_IMPORT = /(?:from\s*|require\(\s*)['"]next-intl(?:\/[\w-]+)?['"]/;
|
|
4999
|
+
NI_BIND = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\s*\(\s*(?:['"]([^'"]*)['"])?\s*\)/g;
|
|
5000
|
+
NI_BIND_OBJ = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?getTranslations\s*\(\s*\{[^}]*?\bnamespace\s*:\s*['"]([^'"]+)['"][^}]*?\}\s*\)/g;
|
|
5001
|
+
NI_METHOD = "(?:\\.(?:rich|markup|raw|has))?";
|
|
4512
5002
|
KEY_SHAPE = /^[A-Za-z0-9_][A-Za-z0-9_/-]*(?:\.(?:[A-Za-z0-9_-]+|%[sd]))+\.?$/;
|
|
4513
5003
|
STRING_LITERALS = [
|
|
4514
5004
|
/'([^'\\\n]+)'/g,
|
|
@@ -4519,8 +5009,8 @@ var init_scanner = __esm({
|
|
|
4519
5009
|
});
|
|
4520
5010
|
|
|
4521
5011
|
// src/server/import/detect.ts
|
|
4522
|
-
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as
|
|
4523
|
-
import { join as
|
|
5012
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
|
|
5013
|
+
import { join as join7 } from "path";
|
|
4524
5014
|
function safeIsDir(p) {
|
|
4525
5015
|
try {
|
|
4526
5016
|
return statSync4(p).isDirectory();
|
|
@@ -4529,7 +5019,7 @@ function safeIsDir(p) {
|
|
|
4529
5019
|
}
|
|
4530
5020
|
}
|
|
4531
5021
|
function listDirs(dir) {
|
|
4532
|
-
return readdirSync4(dir).filter((e) => safeIsDir(
|
|
5022
|
+
return readdirSync4(dir).filter((e) => safeIsDir(join7(dir, e)));
|
|
4533
5023
|
}
|
|
4534
5024
|
function fileCount(dir) {
|
|
4535
5025
|
try {
|
|
@@ -4543,23 +5033,23 @@ function pickSource(locales, sizeOf) {
|
|
|
4543
5033
|
return [...locales].sort((a, b) => sizeOf(b) - sizeOf(a) || a.localeCompare(b))[0] ?? "en";
|
|
4544
5034
|
}
|
|
4545
5035
|
function detectLaravel(root) {
|
|
4546
|
-
const localeRoot = [
|
|
5036
|
+
const localeRoot = [join7(root, "resources", "lang"), join7(root, "lang")].find(safeIsDir);
|
|
4547
5037
|
if (!localeRoot) return null;
|
|
4548
5038
|
const locales = listDirs(localeRoot).filter((d) => LOCALE_RE.test(d));
|
|
4549
5039
|
if (locales.length === 0) return null;
|
|
4550
|
-
const sourceLocale = pickSource(locales, (loc) => fileCount(
|
|
5040
|
+
const sourceLocale = pickSource(locales, (loc) => fileCount(join7(localeRoot, loc)));
|
|
4551
5041
|
return { format: "laravel-php", localeRoot, locales, sourceLocale };
|
|
4552
5042
|
}
|
|
4553
5043
|
function detectVue(root, forced = false) {
|
|
4554
5044
|
for (const rel of VUE_DIR_CANDIDATES) {
|
|
4555
|
-
const localeRoot =
|
|
5045
|
+
const localeRoot = join7(root, rel);
|
|
4556
5046
|
if (!safeIsDir(localeRoot)) continue;
|
|
4557
5047
|
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
4558
5048
|
const enough = locales.length >= 2 || locales.length === 1 && (forced || locales[0] === "en" || locales[0].startsWith("en-") || locales[0].startsWith("en_"));
|
|
4559
5049
|
if (enough) {
|
|
4560
5050
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4561
5051
|
try {
|
|
4562
|
-
return statSync4(
|
|
5052
|
+
return statSync4(join7(localeRoot, `${loc}.json`)).size;
|
|
4563
5053
|
} catch {
|
|
4564
5054
|
return 0;
|
|
4565
5055
|
}
|
|
@@ -4569,22 +5059,60 @@ function detectVue(root, forced = false) {
|
|
|
4569
5059
|
}
|
|
4570
5060
|
return null;
|
|
4571
5061
|
}
|
|
4572
|
-
function
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
5062
|
+
function hasNextIntlSignal(root) {
|
|
5063
|
+
if (NEXT_INTL_CONFIG_CANDIDATES.some((rel) => existsSync11(join7(root, rel)))) return true;
|
|
5064
|
+
try {
|
|
5065
|
+
const pkg = JSON.parse(readFileSync13(join7(root, "package.json"), "utf8"));
|
|
5066
|
+
if (pkg.dependencies?.["next-intl"] || pkg.devDependencies?.["next-intl"]) return true;
|
|
5067
|
+
} catch {
|
|
5068
|
+
}
|
|
5069
|
+
return false;
|
|
5070
|
+
}
|
|
5071
|
+
function nextIntlDefaultLocale(root) {
|
|
5072
|
+
for (const rel of NEXT_INTL_ROUTING_CANDIDATES) {
|
|
5073
|
+
try {
|
|
5074
|
+
const m = readFileSync13(join7(root, rel), "utf8").match(/defaultLocale\s*:\s*['"]([^'"]+)['"]/);
|
|
5075
|
+
if (m) return m[1];
|
|
5076
|
+
} catch {
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
return void 0;
|
|
5080
|
+
}
|
|
5081
|
+
function detectNextIntl(root, forced = false) {
|
|
5082
|
+
if (!forced && !hasNextIntlSignal(root)) return null;
|
|
5083
|
+
for (const rel of NEXT_INTL_DIR_CANDIDATES) {
|
|
5084
|
+
const localeRoot = join7(root, rel);
|
|
5085
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
5086
|
+
const locales = readdirSync4(localeRoot).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((l) => LOCALE_RE.test(l));
|
|
5087
|
+
if (locales.length === 0) continue;
|
|
5088
|
+
const def = nextIntlDefaultLocale(root);
|
|
5089
|
+
const sourceLocale = def && locales.includes(def) ? def : pickSource(locales, (loc) => {
|
|
5090
|
+
try {
|
|
5091
|
+
return statSync4(join7(localeRoot, `${loc}.json`)).size;
|
|
5092
|
+
} catch {
|
|
5093
|
+
return 0;
|
|
5094
|
+
}
|
|
5095
|
+
});
|
|
5096
|
+
return { format: "next-intl-json", localeRoot, locales, sourceLocale };
|
|
5097
|
+
}
|
|
5098
|
+
return null;
|
|
5099
|
+
}
|
|
5100
|
+
function detectArb(root) {
|
|
5101
|
+
for (const rel of ["lib/l10n", "l10n", "lib/src/l10n"]) {
|
|
5102
|
+
const localeRoot = join7(root, rel);
|
|
5103
|
+
if (!safeIsDir(localeRoot)) continue;
|
|
5104
|
+
const locales = readdirSync4(localeRoot).map((f) => f.match(/^(?:app_)?(.+)\.arb$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
|
|
5105
|
+
if (locales.length >= 1) {
|
|
5106
|
+
return { format: "flutter-arb", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
5107
|
+
}
|
|
4580
5108
|
}
|
|
4581
5109
|
return null;
|
|
4582
5110
|
}
|
|
4583
5111
|
function lprojLocales(dir) {
|
|
4584
|
-
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5112
|
+
return listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.strings")));
|
|
4585
5113
|
}
|
|
4586
5114
|
function detectApple(root) {
|
|
4587
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5115
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4588
5116
|
let best = null;
|
|
4589
5117
|
for (const dir of candidates) {
|
|
4590
5118
|
const locales = lprojLocales(dir);
|
|
@@ -4596,7 +5124,7 @@ function detectApple(root) {
|
|
|
4596
5124
|
locales,
|
|
4597
5125
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4598
5126
|
try {
|
|
4599
|
-
return statSync4(
|
|
5127
|
+
return statSync4(join7(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4600
5128
|
} catch {
|
|
4601
5129
|
return 0;
|
|
4602
5130
|
}
|
|
@@ -4608,7 +5136,7 @@ function detectApple(root) {
|
|
|
4608
5136
|
}
|
|
4609
5137
|
function detectAngularXliff(root) {
|
|
4610
5138
|
for (const rel of ANGULAR_DIR_CANDIDATES) {
|
|
4611
|
-
const localeRoot = rel === "." ? root :
|
|
5139
|
+
const localeRoot = rel === "." ? root : join7(root, rel);
|
|
4612
5140
|
if (!safeIsDir(localeRoot)) continue;
|
|
4613
5141
|
const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
|
|
4614
5142
|
if (files.length === 0) continue;
|
|
@@ -4616,7 +5144,7 @@ function detectAngularXliff(root) {
|
|
|
4616
5144
|
const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
|
|
4617
5145
|
let sourceLocale;
|
|
4618
5146
|
try {
|
|
4619
|
-
sourceLocale =
|
|
5147
|
+
sourceLocale = readFileSync13(join7(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
|
|
4620
5148
|
} catch {
|
|
4621
5149
|
}
|
|
4622
5150
|
if (!sourceLocale && locales.length === 0) continue;
|
|
@@ -4627,14 +5155,14 @@ function detectAngularXliff(root) {
|
|
|
4627
5155
|
return null;
|
|
4628
5156
|
}
|
|
4629
5157
|
function detectRails(root) {
|
|
4630
|
-
const localeRoot =
|
|
5158
|
+
const localeRoot = join7(root, "config", "locales");
|
|
4631
5159
|
if (!safeIsDir(localeRoot)) return null;
|
|
4632
5160
|
const locales = [];
|
|
4633
5161
|
for (const file of readdirSync4(localeRoot).sort()) {
|
|
4634
5162
|
if (!/\.ya?ml$/.test(file)) continue;
|
|
4635
5163
|
let text;
|
|
4636
5164
|
try {
|
|
4637
|
-
text =
|
|
5165
|
+
text = readFileSync13(join7(localeRoot, file), "utf8");
|
|
4638
5166
|
} catch {
|
|
4639
5167
|
continue;
|
|
4640
5168
|
}
|
|
@@ -4648,15 +5176,15 @@ function detectRails(root) {
|
|
|
4648
5176
|
}
|
|
4649
5177
|
function detectI18next(root) {
|
|
4650
5178
|
for (const rel of I18NEXT_DIR_CANDIDATES) {
|
|
4651
|
-
const localeRoot =
|
|
5179
|
+
const localeRoot = join7(root, rel);
|
|
4652
5180
|
if (!safeIsDir(localeRoot)) continue;
|
|
4653
5181
|
const locales = listDirs(localeRoot).filter(
|
|
4654
|
-
(d) => LOCALE_RE.test(d) && readdirSync4(
|
|
5182
|
+
(d) => LOCALE_RE.test(d) && readdirSync4(join7(localeRoot, d)).some((f) => f.endsWith(".json"))
|
|
4655
5183
|
);
|
|
4656
5184
|
if (locales.length === 0) continue;
|
|
4657
5185
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4658
5186
|
try {
|
|
4659
|
-
return readdirSync4(
|
|
5187
|
+
return readdirSync4(join7(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync4(join7(localeRoot, loc, f)).size, 0);
|
|
4660
5188
|
} catch {
|
|
4661
5189
|
return 0;
|
|
4662
5190
|
}
|
|
@@ -4673,8 +5201,8 @@ function gettextLocales(dir) {
|
|
|
4673
5201
|
if (!locales.includes(flat)) locales.push(flat);
|
|
4674
5202
|
continue;
|
|
4675
5203
|
}
|
|
4676
|
-
if (!LOCALE_RE.test(entry) || !safeIsDir(
|
|
4677
|
-
const sub =
|
|
5204
|
+
if (!LOCALE_RE.test(entry) || !safeIsDir(join7(dir, entry))) continue;
|
|
5205
|
+
const sub = join7(dir, entry);
|
|
4678
5206
|
const hasPo = (d) => {
|
|
4679
5207
|
try {
|
|
4680
5208
|
return readdirSync4(d).some((f) => f.endsWith(".po"));
|
|
@@ -4682,7 +5210,7 @@ function gettextLocales(dir) {
|
|
|
4682
5210
|
return false;
|
|
4683
5211
|
}
|
|
4684
5212
|
};
|
|
4685
|
-
if (hasPo(
|
|
5213
|
+
if (hasPo(join7(sub, "LC_MESSAGES")) || hasPo(sub)) {
|
|
4686
5214
|
if (!locales.includes(entry)) locales.push(entry);
|
|
4687
5215
|
}
|
|
4688
5216
|
}
|
|
@@ -4690,7 +5218,7 @@ function gettextLocales(dir) {
|
|
|
4690
5218
|
}
|
|
4691
5219
|
function detectGettext(root) {
|
|
4692
5220
|
for (const rel of GETTEXT_DIR_CANDIDATES) {
|
|
4693
|
-
const localeRoot =
|
|
5221
|
+
const localeRoot = join7(root, rel);
|
|
4694
5222
|
if (!safeIsDir(localeRoot)) continue;
|
|
4695
5223
|
const locales = gettextLocales(localeRoot);
|
|
4696
5224
|
if (locales.length === 0) continue;
|
|
@@ -4699,10 +5227,10 @@ function detectGettext(root) {
|
|
|
4699
5227
|
return null;
|
|
4700
5228
|
}
|
|
4701
5229
|
function detectAppleStringsdict(root) {
|
|
4702
|
-
const candidates = [root, ...listDirs(root).map((d) =>
|
|
5230
|
+
const candidates = [root, ...listDirs(root).map((d) => join7(root, d))];
|
|
4703
5231
|
let best = null;
|
|
4704
5232
|
for (const dir of candidates) {
|
|
4705
|
-
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(
|
|
5233
|
+
const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync11(join7(dir, `${l}.lproj`, "Localizable.stringsdict")));
|
|
4706
5234
|
if (locales.length === 0) continue;
|
|
4707
5235
|
if (!best || locales.length > best.locales.length) {
|
|
4708
5236
|
best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
|
|
@@ -4723,17 +5251,21 @@ function detect(root, formatOverride) {
|
|
|
4723
5251
|
}
|
|
4724
5252
|
return null;
|
|
4725
5253
|
}
|
|
4726
|
-
var LOCALE_RE, VUE_DIR_CANDIDATES, ANGULAR_DIR_CANDIDATES, I18NEXT_DIR_CANDIDATES, GETTEXT_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
|
|
5254
|
+
var LOCALE_RE, VUE_DIR_CANDIDATES, NEXT_INTL_CONFIG_CANDIDATES, NEXT_INTL_ROUTING_CANDIDATES, NEXT_INTL_DIR_CANDIDATES, ANGULAR_DIR_CANDIDATES, I18NEXT_DIR_CANDIDATES, GETTEXT_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
|
|
4727
5255
|
var init_detect = __esm({
|
|
4728
5256
|
"src/server/import/detect.ts"() {
|
|
4729
5257
|
"use strict";
|
|
4730
5258
|
LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4731
5259
|
VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
5260
|
+
NEXT_INTL_CONFIG_CANDIDATES = ["src/i18n/request.ts", "i18n/request.ts", "src/i18n/request.js", "i18n/request.js"];
|
|
5261
|
+
NEXT_INTL_ROUTING_CANDIDATES = ["src/i18n/routing.ts", "i18n/routing.ts", "src/i18n/routing.js", "i18n/routing.js"];
|
|
5262
|
+
NEXT_INTL_DIR_CANDIDATES = ["messages", "src/messages", "locales", "src/locales", "src/i18n/messages"];
|
|
4732
5263
|
ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
|
|
4733
5264
|
I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
|
|
4734
5265
|
GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
|
|
4735
5266
|
DETECTORS = [
|
|
4736
5267
|
detectLaravel,
|
|
5268
|
+
detectNextIntl,
|
|
4737
5269
|
detectVue,
|
|
4738
5270
|
detectArb,
|
|
4739
5271
|
detectApple,
|
|
@@ -4745,6 +5277,7 @@ var init_detect = __esm({
|
|
|
4745
5277
|
];
|
|
4746
5278
|
BY_FORMAT = {
|
|
4747
5279
|
"laravel-php": detectLaravel,
|
|
5280
|
+
"next-intl-json": (root) => detectNextIntl(root, true),
|
|
4748
5281
|
"vue-i18n-json": (root) => detectVue(root, true),
|
|
4749
5282
|
"flutter-arb": detectArb,
|
|
4750
5283
|
"apple-strings": detectApple,
|
|
@@ -4784,8 +5317,8 @@ var init_flatten = __esm({
|
|
|
4784
5317
|
});
|
|
4785
5318
|
|
|
4786
5319
|
// src/server/import/parsers/vue-i18n-json.ts
|
|
4787
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
4788
|
-
import { join as
|
|
5320
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14 } from "fs";
|
|
5321
|
+
import { join as join8 } from "path";
|
|
4789
5322
|
function fromVueI18n(value) {
|
|
4790
5323
|
return value.replace(/\{'([^']*)'\}/g, "'$1'");
|
|
4791
5324
|
}
|
|
@@ -4808,7 +5341,7 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4808
5341
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4809
5342
|
let data;
|
|
4810
5343
|
try {
|
|
4811
|
-
data = JSON.parse(
|
|
5344
|
+
data = JSON.parse(readFileSync14(join8(localeRoot, file), "utf8"));
|
|
4812
5345
|
} catch (e) {
|
|
4813
5346
|
warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
|
|
4814
5347
|
continue;
|
|
@@ -4824,6 +5357,44 @@ var init_vue_i18n_json2 = __esm({
|
|
|
4824
5357
|
}
|
|
4825
5358
|
});
|
|
4826
5359
|
|
|
5360
|
+
// src/server/import/parsers/next-intl-json.ts
|
|
5361
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
|
|
5362
|
+
import { join as join9 } from "path";
|
|
5363
|
+
var LOCALE_RE3, nextIntlJson2;
|
|
5364
|
+
var init_next_intl_json2 = __esm({
|
|
5365
|
+
"src/server/import/parsers/next-intl-json.ts"() {
|
|
5366
|
+
"use strict";
|
|
5367
|
+
init_flatten();
|
|
5368
|
+
LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5369
|
+
nextIntlJson2 = {
|
|
5370
|
+
name: "next-intl-json",
|
|
5371
|
+
parse(localeRoot, opts) {
|
|
5372
|
+
const warnings = [];
|
|
5373
|
+
const keys = {};
|
|
5374
|
+
const locales = [];
|
|
5375
|
+
for (const file of readdirSync6(localeRoot).sort()) {
|
|
5376
|
+
if (!file.endsWith(".json")) continue;
|
|
5377
|
+
const locale = file.slice(0, -".json".length);
|
|
5378
|
+
if (!LOCALE_RE3.test(locale)) continue;
|
|
5379
|
+
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5380
|
+
let data;
|
|
5381
|
+
try {
|
|
5382
|
+
data = JSON.parse(readFileSync15(join9(localeRoot, file), "utf8"));
|
|
5383
|
+
} catch (e) {
|
|
5384
|
+
warnings.push(`next-intl-json: failed to parse ${file}: ${e.message}`);
|
|
5385
|
+
continue;
|
|
5386
|
+
}
|
|
5387
|
+
if (!locales.includes(locale)) locales.push(locale);
|
|
5388
|
+
for (const [key, value] of Object.entries(flattenObject(data, "", warnings))) {
|
|
5389
|
+
(keys[key] ??= { values: {} }).values[locale] = value;
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
return { locales, keys, warnings };
|
|
5393
|
+
}
|
|
5394
|
+
};
|
|
5395
|
+
}
|
|
5396
|
+
});
|
|
5397
|
+
|
|
4827
5398
|
// src/server/import/placeholders.ts
|
|
4828
5399
|
function markBareBracesLiteral(value) {
|
|
4829
5400
|
return value.replace(/(?<!%)\{(\w+)\}/g, "'{$1}'");
|
|
@@ -4841,17 +5412,17 @@ var init_placeholders2 = __esm({
|
|
|
4841
5412
|
});
|
|
4842
5413
|
|
|
4843
5414
|
// src/server/import/parsers/laravel-php.ts
|
|
4844
|
-
import { readdirSync as
|
|
4845
|
-
import { join as
|
|
5415
|
+
import { readdirSync as readdirSync7, statSync as statSync5 } from "fs";
|
|
5416
|
+
import { join as join10, relative as relative2 } from "path";
|
|
4846
5417
|
import { execFileSync } from "child_process";
|
|
4847
5418
|
function listDirs2(dir) {
|
|
4848
|
-
return
|
|
5419
|
+
return readdirSync7(dir).filter((e) => statSync5(join10(dir, e)).isDirectory());
|
|
4849
5420
|
}
|
|
4850
5421
|
function listPhpFiles(dir) {
|
|
4851
5422
|
const out = [];
|
|
4852
5423
|
const walk = (d) => {
|
|
4853
|
-
for (const e of
|
|
4854
|
-
const full =
|
|
5424
|
+
for (const e of readdirSync7(d)) {
|
|
5425
|
+
const full = join10(d, e);
|
|
4855
5426
|
if (statSync5(full).isDirectory()) walk(full);
|
|
4856
5427
|
else if (e.endsWith(".php")) out.push(full);
|
|
4857
5428
|
}
|
|
@@ -4894,7 +5465,7 @@ var init_laravel_php2 = __esm({
|
|
|
4894
5465
|
for (const locale of listDirs2(localeRoot).sort()) {
|
|
4895
5466
|
if (locale === "vendor") continue;
|
|
4896
5467
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4897
|
-
const localeDir =
|
|
5468
|
+
const localeDir = join10(localeRoot, locale);
|
|
4898
5469
|
locales.push(locale);
|
|
4899
5470
|
for (const file of listPhpFiles(localeDir)) {
|
|
4900
5471
|
const group = relative2(localeDir, file).replace(/\\/g, "/").replace(/\.php$/, "");
|
|
@@ -4919,14 +5490,14 @@ var init_laravel_php2 = __esm({
|
|
|
4919
5490
|
});
|
|
4920
5491
|
|
|
4921
5492
|
// src/server/import/parsers/flutter-arb.ts
|
|
4922
|
-
import { readdirSync as
|
|
4923
|
-
import { join as
|
|
5493
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
5494
|
+
import { join as join11 } from "path";
|
|
4924
5495
|
function localeFromArbName(file) {
|
|
4925
5496
|
const m = file.match(/^(.+)\.arb$/);
|
|
4926
5497
|
if (!m) return null;
|
|
4927
5498
|
let locale = m[1];
|
|
4928
5499
|
if (locale.startsWith("app_")) locale = locale.slice(4);
|
|
4929
|
-
return
|
|
5500
|
+
return LOCALE_RE4.test(locale) ? locale : null;
|
|
4930
5501
|
}
|
|
4931
5502
|
function placeholderMeta(raw) {
|
|
4932
5503
|
if (!raw || typeof raw !== "object") return void 0;
|
|
@@ -4942,25 +5513,25 @@ function placeholderMeta(raw) {
|
|
|
4942
5513
|
}
|
|
4943
5514
|
return Object.keys(out).length ? out : void 0;
|
|
4944
5515
|
}
|
|
4945
|
-
var
|
|
5516
|
+
var LOCALE_RE4, flutterArb2;
|
|
4946
5517
|
var init_flutter_arb2 = __esm({
|
|
4947
5518
|
"src/server/import/parsers/flutter-arb.ts"() {
|
|
4948
5519
|
"use strict";
|
|
4949
|
-
|
|
5520
|
+
LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4950
5521
|
flutterArb2 = {
|
|
4951
5522
|
name: "flutter-arb",
|
|
4952
5523
|
parse(localeRoot, opts) {
|
|
4953
5524
|
const warnings = [];
|
|
4954
5525
|
const keys = {};
|
|
4955
5526
|
const locales = [];
|
|
4956
|
-
for (const file of
|
|
5527
|
+
for (const file of readdirSync8(localeRoot).sort()) {
|
|
4957
5528
|
if (!file.endsWith(".arb")) continue;
|
|
4958
5529
|
const locale = localeFromArbName(file);
|
|
4959
5530
|
if (!locale) continue;
|
|
4960
5531
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
4961
5532
|
let data;
|
|
4962
5533
|
try {
|
|
4963
|
-
data = JSON.parse(
|
|
5534
|
+
data = JSON.parse(readFileSync16(join11(localeRoot, file), "utf8"));
|
|
4964
5535
|
} catch (e) {
|
|
4965
5536
|
warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
|
|
4966
5537
|
continue;
|
|
@@ -4987,12 +5558,12 @@ var init_flutter_arb2 = __esm({
|
|
|
4987
5558
|
});
|
|
4988
5559
|
|
|
4989
5560
|
// src/server/import/parsers/apple-strings.ts
|
|
4990
|
-
import { readdirSync as
|
|
4991
|
-
import { join as
|
|
5561
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
5562
|
+
import { join as join12 } from "path";
|
|
4992
5563
|
function localeFromLproj(dir) {
|
|
4993
5564
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
4994
5565
|
if (!m) return null;
|
|
4995
|
-
return
|
|
5566
|
+
return LOCALE_RE5.test(m[1]) ? m[1] : null;
|
|
4996
5567
|
}
|
|
4997
5568
|
function printfToCanonical(s) {
|
|
4998
5569
|
return s.replace(/%%/g, "%");
|
|
@@ -5092,11 +5663,11 @@ function parseStrings(text, file, warnings) {
|
|
|
5092
5663
|
}
|
|
5093
5664
|
return pairs;
|
|
5094
5665
|
}
|
|
5095
|
-
var
|
|
5666
|
+
var LOCALE_RE5, TABLE, appleStrings2;
|
|
5096
5667
|
var init_apple_strings2 = __esm({
|
|
5097
5668
|
"src/server/import/parsers/apple-strings.ts"() {
|
|
5098
5669
|
"use strict";
|
|
5099
|
-
|
|
5670
|
+
LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5100
5671
|
TABLE = "Localizable.strings";
|
|
5101
5672
|
appleStrings2 = {
|
|
5102
5673
|
name: "apple-strings",
|
|
@@ -5104,20 +5675,20 @@ var init_apple_strings2 = __esm({
|
|
|
5104
5675
|
const warnings = [];
|
|
5105
5676
|
const keys = {};
|
|
5106
5677
|
const locales = [];
|
|
5107
|
-
for (const dir of
|
|
5678
|
+
for (const dir of readdirSync9(localeRoot).sort()) {
|
|
5108
5679
|
const locale = localeFromLproj(dir);
|
|
5109
5680
|
if (!locale) continue;
|
|
5110
5681
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5111
|
-
const file =
|
|
5682
|
+
const file = join12(localeRoot, dir, TABLE);
|
|
5112
5683
|
let text;
|
|
5113
5684
|
try {
|
|
5114
5685
|
if (!statSync6(file).isFile()) continue;
|
|
5115
|
-
text =
|
|
5686
|
+
text = readFileSync17(file, "utf8");
|
|
5116
5687
|
} catch {
|
|
5117
5688
|
continue;
|
|
5118
5689
|
}
|
|
5119
5690
|
locales.push(locale);
|
|
5120
|
-
const others =
|
|
5691
|
+
const others = readdirSync9(join12(localeRoot, dir)).filter((f) => f.endsWith(".strings") && f !== TABLE);
|
|
5121
5692
|
if (others.length) {
|
|
5122
5693
|
warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
|
|
5123
5694
|
}
|
|
@@ -5132,8 +5703,8 @@ var init_apple_strings2 = __esm({
|
|
|
5132
5703
|
});
|
|
5133
5704
|
|
|
5134
5705
|
// src/server/import/parsers/angular-xliff.ts
|
|
5135
|
-
import { readdirSync as
|
|
5136
|
-
import { join as
|
|
5706
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync18 } from "fs";
|
|
5707
|
+
import { join as join13 } from "path";
|
|
5137
5708
|
function decodeEntities(s) {
|
|
5138
5709
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
5139
5710
|
}
|
|
@@ -5182,11 +5753,11 @@ function decodeInline(raw, addMeta) {
|
|
|
5182
5753
|
}
|
|
5183
5754
|
return out + decodeEntities(raw.slice(last));
|
|
5184
5755
|
}
|
|
5185
|
-
var
|
|
5756
|
+
var LOCALE_RE6, FILE_RE, ANGULAR_CONVENTION_ID, angularXliff2;
|
|
5186
5757
|
var init_angular_xliff2 = __esm({
|
|
5187
5758
|
"src/server/import/parsers/angular-xliff.ts"() {
|
|
5188
5759
|
"use strict";
|
|
5189
|
-
|
|
5760
|
+
LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5190
5761
|
FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
5191
5762
|
ANGULAR_CONVENTION_ID = /^[A-Z][A-Z0-9_]*$/;
|
|
5192
5763
|
angularXliff2 = {
|
|
@@ -5198,13 +5769,13 @@ var init_angular_xliff2 = __esm({
|
|
|
5198
5769
|
const seen = (loc) => {
|
|
5199
5770
|
if (!locales.includes(loc)) locales.push(loc);
|
|
5200
5771
|
};
|
|
5201
|
-
const files =
|
|
5772
|
+
const files = readdirSync10(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
|
|
5202
5773
|
for (const file of files) {
|
|
5203
5774
|
const fnameLocale = file.match(FILE_RE)[1];
|
|
5204
|
-
if (fnameLocale !== void 0 && !
|
|
5775
|
+
if (fnameLocale !== void 0 && !LOCALE_RE6.test(fnameLocale)) continue;
|
|
5205
5776
|
let xml;
|
|
5206
5777
|
try {
|
|
5207
|
-
xml =
|
|
5778
|
+
xml = readFileSync18(join13(localeRoot, file), "utf8");
|
|
5208
5779
|
} catch (e) {
|
|
5209
5780
|
warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
|
|
5210
5781
|
continue;
|
|
@@ -5251,8 +5822,8 @@ var init_angular_xliff2 = __esm({
|
|
|
5251
5822
|
});
|
|
5252
5823
|
|
|
5253
5824
|
// src/server/import/parsers/gettext-po.ts
|
|
5254
|
-
import { readdirSync as
|
|
5255
|
-
import { join as
|
|
5825
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
|
|
5826
|
+
import { join as join14 } from "path";
|
|
5256
5827
|
function unescapePo(s) {
|
|
5257
5828
|
return s.replace(
|
|
5258
5829
|
/\\([\\"ntr])/g,
|
|
@@ -5337,33 +5908,33 @@ function parseEntries(text) {
|
|
|
5337
5908
|
}
|
|
5338
5909
|
function discoverPoFiles(root) {
|
|
5339
5910
|
const found = [];
|
|
5340
|
-
const entries =
|
|
5911
|
+
const entries = readdirSync11(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
5341
5912
|
for (const e of entries) {
|
|
5342
5913
|
if (e.isFile() && e.name.endsWith(".po")) {
|
|
5343
5914
|
const base = e.name.slice(0, -3);
|
|
5344
|
-
found.push({ path:
|
|
5345
|
-
} else if (e.isDirectory() &&
|
|
5346
|
-
for (const sub of [
|
|
5915
|
+
found.push({ path: join14(root, e.name), rel: e.name, locale: LOCALE_RE7.test(base) ? base : null });
|
|
5916
|
+
} else if (e.isDirectory() && LOCALE_RE7.test(e.name)) {
|
|
5917
|
+
for (const sub of [join14(e.name, "LC_MESSAGES"), e.name]) {
|
|
5347
5918
|
let names;
|
|
5348
5919
|
try {
|
|
5349
|
-
names =
|
|
5920
|
+
names = readdirSync11(join14(root, sub)).sort();
|
|
5350
5921
|
} catch {
|
|
5351
5922
|
continue;
|
|
5352
5923
|
}
|
|
5353
5924
|
for (const f of names) {
|
|
5354
|
-
if (f.endsWith(".po")) found.push({ path:
|
|
5925
|
+
if (f.endsWith(".po")) found.push({ path: join14(root, sub, f), rel: join14(sub, f), locale: e.name });
|
|
5355
5926
|
}
|
|
5356
5927
|
}
|
|
5357
5928
|
}
|
|
5358
5929
|
}
|
|
5359
5930
|
return found;
|
|
5360
5931
|
}
|
|
5361
|
-
var
|
|
5932
|
+
var LOCALE_RE7, DIRECTIVE_RE, CONT_RE, gettextPo2;
|
|
5362
5933
|
var init_gettext_po2 = __esm({
|
|
5363
5934
|
"src/server/import/parsers/gettext-po.ts"() {
|
|
5364
5935
|
"use strict";
|
|
5365
5936
|
init_plurals();
|
|
5366
|
-
|
|
5937
|
+
LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5367
5938
|
DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
|
|
5368
5939
|
CONT_RE = /^[ \t]*"(.*)"\s*$/;
|
|
5369
5940
|
gettextPo2 = {
|
|
@@ -5375,7 +5946,7 @@ var init_gettext_po2 = __esm({
|
|
|
5375
5946
|
for (const file of discoverPoFiles(localeRoot)) {
|
|
5376
5947
|
let entries;
|
|
5377
5948
|
try {
|
|
5378
|
-
entries = parseEntries(
|
|
5949
|
+
entries = parseEntries(readFileSync19(file.path, "utf8"));
|
|
5379
5950
|
} catch (e) {
|
|
5380
5951
|
warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
|
|
5381
5952
|
continue;
|
|
@@ -5422,8 +5993,8 @@ var init_gettext_po2 = __esm({
|
|
|
5422
5993
|
});
|
|
5423
5994
|
|
|
5424
5995
|
// src/server/import/parsers/i18next-json.ts
|
|
5425
|
-
import { readdirSync as
|
|
5426
|
-
import { join as
|
|
5996
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync20, statSync as statSync7 } from "fs";
|
|
5997
|
+
import { join as join15 } from "path";
|
|
5427
5998
|
function safeIsDir2(p) {
|
|
5428
5999
|
try {
|
|
5429
6000
|
return statSync7(p).isDirectory();
|
|
@@ -5438,7 +6009,7 @@ function fromI18next(value) {
|
|
|
5438
6009
|
function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
5439
6010
|
let data;
|
|
5440
6011
|
try {
|
|
5441
|
-
data = JSON.parse(
|
|
6012
|
+
data = JSON.parse(readFileSync20(path, "utf8"));
|
|
5442
6013
|
} catch (e) {
|
|
5443
6014
|
warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
|
|
5444
6015
|
return false;
|
|
@@ -5473,14 +6044,14 @@ function ingestFile(path, label, prefix, locale, keys, warnings) {
|
|
|
5473
6044
|
}
|
|
5474
6045
|
return true;
|
|
5475
6046
|
}
|
|
5476
|
-
var
|
|
6047
|
+
var LOCALE_RE8, PLURAL_SUFFIX_RE, PLURAL_ARG, DEFAULT_NAMESPACE, i18nextJson2;
|
|
5477
6048
|
var init_i18next_json2 = __esm({
|
|
5478
6049
|
"src/server/import/parsers/i18next-json.ts"() {
|
|
5479
6050
|
"use strict";
|
|
5480
6051
|
init_flatten();
|
|
5481
6052
|
init_plurals();
|
|
5482
6053
|
init_placeholders();
|
|
5483
|
-
|
|
6054
|
+
LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5484
6055
|
PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
5485
6056
|
PLURAL_ARG = "count";
|
|
5486
6057
|
DEFAULT_NAMESPACE = "translation";
|
|
@@ -5490,22 +6061,22 @@ var init_i18next_json2 = __esm({
|
|
|
5490
6061
|
const warnings = [];
|
|
5491
6062
|
const keys = {};
|
|
5492
6063
|
const locales = [];
|
|
5493
|
-
for (const entry of
|
|
5494
|
-
const full =
|
|
6064
|
+
for (const entry of readdirSync12(localeRoot).sort()) {
|
|
6065
|
+
const full = join15(localeRoot, entry);
|
|
5495
6066
|
if (safeIsDir2(full)) {
|
|
5496
|
-
if (!
|
|
6067
|
+
if (!LOCALE_RE8.test(entry)) continue;
|
|
5497
6068
|
if (opts?.locales && !opts.locales.includes(entry)) continue;
|
|
5498
6069
|
let any = false;
|
|
5499
|
-
for (const file of
|
|
6070
|
+
for (const file of readdirSync12(full).sort()) {
|
|
5500
6071
|
if (!file.endsWith(".json")) continue;
|
|
5501
6072
|
const ns = file.slice(0, -".json".length);
|
|
5502
6073
|
const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
|
|
5503
|
-
if (ingestFile(
|
|
6074
|
+
if (ingestFile(join15(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
|
|
5504
6075
|
}
|
|
5505
6076
|
if (any && !locales.includes(entry)) locales.push(entry);
|
|
5506
6077
|
} else if (entry.endsWith(".json")) {
|
|
5507
6078
|
const locale = entry.slice(0, -".json".length);
|
|
5508
|
-
if (!
|
|
6079
|
+
if (!LOCALE_RE8.test(locale)) continue;
|
|
5509
6080
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5510
6081
|
if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
|
|
5511
6082
|
locales.push(locale);
|
|
@@ -5519,8 +6090,8 @@ var init_i18next_json2 = __esm({
|
|
|
5519
6090
|
});
|
|
5520
6091
|
|
|
5521
6092
|
// src/server/import/parsers/rails-yaml.ts
|
|
5522
|
-
import { readdirSync as
|
|
5523
|
-
import { join as
|
|
6093
|
+
import { readdirSync as readdirSync13, readFileSync as readFileSync21 } from "fs";
|
|
6094
|
+
import { join as join16 } from "path";
|
|
5524
6095
|
function makeNode() {
|
|
5525
6096
|
return /* @__PURE__ */ Object.create(null);
|
|
5526
6097
|
}
|
|
@@ -5710,13 +6281,13 @@ function synthesizeIcu(forms, file, key, warnings) {
|
|
|
5710
6281
|
}
|
|
5711
6282
|
return `{count, plural, ${parts.join(" ")}}`;
|
|
5712
6283
|
}
|
|
5713
|
-
var
|
|
6284
|
+
var LOCALE_RE9, CATEGORY_SET, railsYaml2;
|
|
5714
6285
|
var init_rails_yaml2 = __esm({
|
|
5715
6286
|
"src/server/import/parsers/rails-yaml.ts"() {
|
|
5716
6287
|
"use strict";
|
|
5717
6288
|
init_schema();
|
|
5718
6289
|
init_placeholders2();
|
|
5719
|
-
|
|
6290
|
+
LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
|
|
5720
6291
|
CATEGORY_SET = new Set(PLURAL_CATEGORIES);
|
|
5721
6292
|
railsYaml2 = {
|
|
5722
6293
|
name: "rails-yaml",
|
|
@@ -5740,18 +6311,18 @@ var init_rails_yaml2 = __esm({
|
|
|
5740
6311
|
else flatten(v, key, locale, file);
|
|
5741
6312
|
}
|
|
5742
6313
|
};
|
|
5743
|
-
for (const file of
|
|
6314
|
+
for (const file of readdirSync13(localeRoot).sort()) {
|
|
5744
6315
|
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
5745
6316
|
let text;
|
|
5746
6317
|
try {
|
|
5747
|
-
text =
|
|
6318
|
+
text = readFileSync21(join16(localeRoot, file), "utf8");
|
|
5748
6319
|
} catch (e) {
|
|
5749
6320
|
warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
|
|
5750
6321
|
continue;
|
|
5751
6322
|
}
|
|
5752
6323
|
const { roots } = parseYamlSubset(text, file, warnings);
|
|
5753
6324
|
for (const token of Object.keys(roots).sort()) {
|
|
5754
|
-
if (!
|
|
6325
|
+
if (!LOCALE_RE9.test(token)) {
|
|
5755
6326
|
warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
|
|
5756
6327
|
continue;
|
|
5757
6328
|
}
|
|
@@ -5767,12 +6338,12 @@ var init_rails_yaml2 = __esm({
|
|
|
5767
6338
|
});
|
|
5768
6339
|
|
|
5769
6340
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5770
|
-
import { readdirSync as
|
|
5771
|
-
import { join as
|
|
6341
|
+
import { readdirSync as readdirSync14, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
|
|
6342
|
+
import { join as join17 } from "path";
|
|
5772
6343
|
function localeFromLproj2(dir) {
|
|
5773
6344
|
const m = dir.match(/^(.+)\.lproj$/);
|
|
5774
6345
|
if (!m) return null;
|
|
5775
|
-
return
|
|
6346
|
+
return LOCALE_RE10.test(m[1]) ? m[1] : null;
|
|
5776
6347
|
}
|
|
5777
6348
|
function decodeEntities2(s) {
|
|
5778
6349
|
return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
@@ -5909,13 +6480,13 @@ function entryToIcu(key, entry, file, warnings) {
|
|
|
5909
6480
|
if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
|
|
5910
6481
|
return formsToIcu(arg, forms);
|
|
5911
6482
|
}
|
|
5912
|
-
var
|
|
6483
|
+
var LOCALE_RE10, TABLE2, VAR_RE, appleStringsdict2;
|
|
5913
6484
|
var init_apple_stringsdict2 = __esm({
|
|
5914
6485
|
"src/server/import/parsers/apple-stringsdict.ts"() {
|
|
5915
6486
|
"use strict";
|
|
5916
6487
|
init_schema();
|
|
5917
6488
|
init_plurals();
|
|
5918
|
-
|
|
6489
|
+
LOCALE_RE10 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5919
6490
|
TABLE2 = "Localizable.stringsdict";
|
|
5920
6491
|
VAR_RE = /%#@([^@]*)@/g;
|
|
5921
6492
|
appleStringsdict2 = {
|
|
@@ -5924,20 +6495,20 @@ var init_apple_stringsdict2 = __esm({
|
|
|
5924
6495
|
const warnings = [];
|
|
5925
6496
|
const keys = {};
|
|
5926
6497
|
const locales = [];
|
|
5927
|
-
for (const dir of
|
|
6498
|
+
for (const dir of readdirSync14(localeRoot).sort()) {
|
|
5928
6499
|
const locale = localeFromLproj2(dir);
|
|
5929
6500
|
if (!locale) continue;
|
|
5930
6501
|
if (opts?.locales && !opts.locales.includes(locale)) continue;
|
|
5931
|
-
const file =
|
|
6502
|
+
const file = join17(localeRoot, dir, TABLE2);
|
|
5932
6503
|
let text;
|
|
5933
6504
|
try {
|
|
5934
6505
|
if (!statSync8(file).isFile()) continue;
|
|
5935
|
-
text =
|
|
6506
|
+
text = readFileSync22(file, "utf8");
|
|
5936
6507
|
} catch {
|
|
5937
6508
|
continue;
|
|
5938
6509
|
}
|
|
5939
6510
|
locales.push(locale);
|
|
5940
|
-
const others =
|
|
6511
|
+
const others = readdirSync14(join17(localeRoot, dir)).filter(
|
|
5941
6512
|
(f) => f.endsWith(".stringsdict") && f !== TABLE2
|
|
5942
6513
|
);
|
|
5943
6514
|
if (others.length) {
|
|
@@ -5975,6 +6546,7 @@ var init_parsers = __esm({
|
|
|
5975
6546
|
"src/server/import/parsers/index.ts"() {
|
|
5976
6547
|
"use strict";
|
|
5977
6548
|
init_vue_i18n_json2();
|
|
6549
|
+
init_next_intl_json2();
|
|
5978
6550
|
init_laravel_php2();
|
|
5979
6551
|
init_flutter_arb2();
|
|
5980
6552
|
init_apple_strings2();
|
|
@@ -5985,6 +6557,7 @@ var init_parsers = __esm({
|
|
|
5985
6557
|
init_apple_stringsdict2();
|
|
5986
6558
|
REGISTRY = {
|
|
5987
6559
|
[vueI18nJson2.name]: vueI18nJson2,
|
|
6560
|
+
[nextIntlJson2.name]: nextIntlJson2,
|
|
5988
6561
|
[laravelPhp2.name]: laravelPhp2,
|
|
5989
6562
|
[flutterArb2.name]: flutterArb2,
|
|
5990
6563
|
[appleStrings2.name]: appleStrings2,
|
|
@@ -6409,7 +6982,7 @@ var init_run2 = __esm({
|
|
|
6409
6982
|
});
|
|
6410
6983
|
|
|
6411
6984
|
// src/server/lint/outputs.ts
|
|
6412
|
-
import { readFileSync as
|
|
6985
|
+
import { readFileSync as readFileSync23, existsSync as existsSync12 } from "fs";
|
|
6413
6986
|
import { resolve as resolve8 } from "path";
|
|
6414
6987
|
function checkOutputs(state, root) {
|
|
6415
6988
|
const out = [];
|
|
@@ -6417,7 +6990,7 @@ function checkOutputs(state, root) {
|
|
|
6417
6990
|
const result = getAdapter(output.adapter).export(state, output);
|
|
6418
6991
|
for (const file of result.files) {
|
|
6419
6992
|
const abs = resolve8(root, file.path);
|
|
6420
|
-
const current = existsSync12(abs) ?
|
|
6993
|
+
const current = existsSync12(abs) ? readFileSync23(abs, "utf8") : null;
|
|
6421
6994
|
if (current === null) {
|
|
6422
6995
|
out.push({ ruleId: "output-stale", key: file.path, locale: "", severity: "error", message: "output file is missing; run `glotfile export`" });
|
|
6423
6996
|
} else if (current !== file.contents) {
|
|
@@ -6525,6 +7098,7 @@ function assemble2(parsed, opts) {
|
|
|
6525
7098
|
spelling: { customWords: [] }
|
|
6526
7099
|
},
|
|
6527
7100
|
glossary: [],
|
|
7101
|
+
glossarySuggestions: [],
|
|
6528
7102
|
keys,
|
|
6529
7103
|
warnings
|
|
6530
7104
|
};
|
|
@@ -6541,6 +7115,9 @@ var init_assemble = __esm({
|
|
|
6541
7115
|
OUTPUT_BY_FORMAT = {
|
|
6542
7116
|
"laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
|
|
6543
7117
|
"vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
|
|
7118
|
+
// rootRelative: write back to wherever the messages dir was found (messages/,
|
|
7119
|
+
// src/messages/, …) rather than assuming the conventional root-level location.
|
|
7120
|
+
"next-intl-json": { adapter: "next-intl-json", path: "{locale}.json", rootRelative: true },
|
|
6544
7121
|
"flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
|
|
6545
7122
|
"apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
|
|
6546
7123
|
// skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
|
|
@@ -6865,13 +7442,63 @@ var init_checks = __esm({
|
|
|
6865
7442
|
}
|
|
6866
7443
|
});
|
|
6867
7444
|
|
|
7445
|
+
// src/server/ai/explain-error.ts
|
|
7446
|
+
function rawMessage(err) {
|
|
7447
|
+
if (err instanceof Error && err.message) return err.message;
|
|
7448
|
+
if (typeof err === "string") return err;
|
|
7449
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
7450
|
+
return err.message;
|
|
7451
|
+
}
|
|
7452
|
+
return String(err ?? "Unknown error");
|
|
7453
|
+
}
|
|
7454
|
+
function explainProviderError(provider, err) {
|
|
7455
|
+
const raw = rawMessage(err);
|
|
7456
|
+
const m = raw.toLowerCase();
|
|
7457
|
+
if (provider === "bedrock") {
|
|
7458
|
+
if (/could not load credentials|unable to locate credentials|credentialsprovider|credentials from any providers/.test(m)) {
|
|
7459
|
+
return "No AWS credentials found. Set AWS_PROFILE (or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) in your shell or a .env file in the directory you started glotfile from, or use an SSO / instance role. If you just edited .env, restart glotfile so it reloads. For SSO, run `aws sso login`.";
|
|
7460
|
+
}
|
|
7461
|
+
if (/on-demand throughput isn.?t supported/.test(m) || /inference profile/.test(m)) {
|
|
7462
|
+
return 'This Bedrock model needs an inference profile for on-demand use. Prefix the model id with your region group \u2014 e.g. "eu.anthropic.claude-3-5-sonnet-20241022-v2:0" (or "us." / "apac." for your region).';
|
|
7463
|
+
}
|
|
7464
|
+
if (/not authorized to perform/.test(m) || /no identity-based policy/.test(m) || /bedrock:invoke/.test(m)) {
|
|
7465
|
+
return "Your AWS credentials authenticated, but their IAM policy doesn't allow this action. Add bedrock:InvokeModel (and bedrock:InvokeModelWithResponseStream) for this model to the IAM policy on this user/role.";
|
|
7466
|
+
}
|
|
7467
|
+
if (/access to the model|don.?t have access to the model/.test(m)) {
|
|
7468
|
+
return "Your account doesn't have access to this model in this region. Enable it in the Bedrock console under Model access, for the region you configured.";
|
|
7469
|
+
}
|
|
7470
|
+
if (/access ?denied/.test(m)) {
|
|
7471
|
+
return "Bedrock denied access. Either the model isn't enabled for your account/region (enable it in the Bedrock console under Model access) or your IAM policy is missing bedrock:InvokeModel for this model.";
|
|
7472
|
+
}
|
|
7473
|
+
if (/region/.test(m)) {
|
|
7474
|
+
return "No AWS region set for Bedrock. Set the Region in AI settings, or AWS_REGION in your environment.";
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
const keyEnv = KEY_ENV[provider];
|
|
7478
|
+
if (keyEnv && /api key|unauthorized|\b401\b|authentication|incorrect api key|invalid x-api-key/.test(m)) {
|
|
7479
|
+
return `${provider} rejected the request \u2014 check ${keyEnv}. Set it in your environment or a .env file in the directory you started glotfile from.`;
|
|
7480
|
+
}
|
|
7481
|
+
return raw;
|
|
7482
|
+
}
|
|
7483
|
+
var KEY_ENV;
|
|
7484
|
+
var init_explain_error = __esm({
|
|
7485
|
+
"src/server/ai/explain-error.ts"() {
|
|
7486
|
+
"use strict";
|
|
7487
|
+
KEY_ENV = {
|
|
7488
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
7489
|
+
openai: "OPENAI_API_KEY",
|
|
7490
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
});
|
|
7494
|
+
|
|
6868
7495
|
// src/server/ui-prefs.ts
|
|
6869
|
-
import { readFileSync as
|
|
6870
|
-
import { homedir } from "os";
|
|
6871
|
-
import { join as
|
|
7496
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
7497
|
+
import { homedir as homedir2 } from "os";
|
|
7498
|
+
import { join as join18 } from "path";
|
|
6872
7499
|
function readJson2(path) {
|
|
6873
7500
|
try {
|
|
6874
|
-
const parsed = JSON.parse(
|
|
7501
|
+
const parsed = JSON.parse(readFileSync24(path, "utf8"));
|
|
6875
7502
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
6876
7503
|
} catch {
|
|
6877
7504
|
return {};
|
|
@@ -6896,7 +7523,7 @@ var init_ui_prefs = __esm({
|
|
|
6896
7523
|
THEMES = ["system", "light", "dark"];
|
|
6897
7524
|
isThemeMode = (v) => THEMES.includes(v);
|
|
6898
7525
|
isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
|
|
6899
|
-
defaultUiPrefsPath = () =>
|
|
7526
|
+
defaultUiPrefsPath = () => join18(homedir2(), ".glotfile", "ui.json");
|
|
6900
7527
|
DEFAULTS = { theme: "system" };
|
|
6901
7528
|
}
|
|
6902
7529
|
});
|
|
@@ -6929,8 +7556,8 @@ var init_events = __esm({
|
|
|
6929
7556
|
});
|
|
6930
7557
|
|
|
6931
7558
|
// src/server/watch.ts
|
|
6932
|
-
import { statSync as statSync9, readdirSync as
|
|
6933
|
-
import { join as
|
|
7559
|
+
import { statSync as statSync9, readdirSync as readdirSync15 } from "fs";
|
|
7560
|
+
import { join as join19 } from "path";
|
|
6934
7561
|
import { createHash as createHash2 } from "crypto";
|
|
6935
7562
|
function hashState(state) {
|
|
6936
7563
|
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
@@ -6946,15 +7573,15 @@ function signature(statePath) {
|
|
|
6946
7573
|
const parts = [];
|
|
6947
7574
|
for (const rel of ["config.json", "keys.json"]) {
|
|
6948
7575
|
try {
|
|
6949
|
-
const s = statSync9(
|
|
7576
|
+
const s = statSync9(join19(dir, rel));
|
|
6950
7577
|
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6951
7578
|
} catch {
|
|
6952
7579
|
}
|
|
6953
7580
|
}
|
|
6954
7581
|
try {
|
|
6955
|
-
for (const name of
|
|
7582
|
+
for (const name of readdirSync15(join19(dir, "locales")).sort()) {
|
|
6956
7583
|
if (!name.endsWith(".json")) continue;
|
|
6957
|
-
const s = statSync9(
|
|
7584
|
+
const s = statSync9(join19(dir, "locales", name));
|
|
6958
7585
|
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6959
7586
|
}
|
|
6960
7587
|
} catch {
|
|
@@ -7033,34 +7660,19 @@ var init_watch = __esm({
|
|
|
7033
7660
|
// src/server/api.ts
|
|
7034
7661
|
import { Hono } from "hono";
|
|
7035
7662
|
import { streamSSE } from "hono/streaming";
|
|
7036
|
-
import { readFileSync as
|
|
7663
|
+
import { readFileSync as readFileSync25, existsSync as existsSync13, readdirSync as readdirSync16, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
7037
7664
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
7038
7665
|
function projectName(root) {
|
|
7039
7666
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
7040
7667
|
if (existsSync13(nameFile)) {
|
|
7041
7668
|
try {
|
|
7042
|
-
const name =
|
|
7669
|
+
const name = readFileSync25(nameFile, "utf8").trim();
|
|
7043
7670
|
if (name) return name;
|
|
7044
7671
|
} catch {
|
|
7045
7672
|
}
|
|
7046
7673
|
}
|
|
7047
7674
|
return basename(root);
|
|
7048
7675
|
}
|
|
7049
|
-
function attachUsageSnippets(targets, cache2, projectRoot) {
|
|
7050
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
7051
|
-
for (const target of targets) {
|
|
7052
|
-
const allRefs = Object.entries(cache2.files).flatMap(
|
|
7053
|
-
([file, entry]) => entry.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
7054
|
-
key: r.key,
|
|
7055
|
-
file,
|
|
7056
|
-
line: r.line,
|
|
7057
|
-
col: r.col,
|
|
7058
|
-
scanner: r.scanner
|
|
7059
|
-
}))
|
|
7060
|
-
);
|
|
7061
|
-
target.usageSnippets = extractSnippets(allRefs, projectRoot, fileCache);
|
|
7062
|
-
}
|
|
7063
|
-
}
|
|
7064
7676
|
function createApi(deps) {
|
|
7065
7677
|
const app = new Hono();
|
|
7066
7678
|
const load = () => loadState(deps.statePath);
|
|
@@ -7192,6 +7804,61 @@ function createApi(deps) {
|
|
|
7192
7804
|
}
|
|
7193
7805
|
return c.json({ ok: true });
|
|
7194
7806
|
});
|
|
7807
|
+
app.post("/ai-test", async (c) => {
|
|
7808
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
7809
|
+
const meta = { provider: aiCfg.provider, model: aiCfg.model };
|
|
7810
|
+
let provider;
|
|
7811
|
+
try {
|
|
7812
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7813
|
+
} catch (e) {
|
|
7814
|
+
return c.json({ ok: false, ...meta, error: explainProviderError(aiCfg.provider, e) });
|
|
7815
|
+
}
|
|
7816
|
+
const controller = new AbortController();
|
|
7817
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
7818
|
+
try {
|
|
7819
|
+
const probe = {
|
|
7820
|
+
id: "probe",
|
|
7821
|
+
key: "glotfile.connection-test",
|
|
7822
|
+
source: "Hello",
|
|
7823
|
+
sourceLocale: "en",
|
|
7824
|
+
targetLocale: "es",
|
|
7825
|
+
placeholders: []
|
|
7826
|
+
};
|
|
7827
|
+
await provider.translate([probe], void 0, controller.signal);
|
|
7828
|
+
return c.json({ ok: true, ...meta });
|
|
7829
|
+
} catch (e) {
|
|
7830
|
+
const error = controller.signal.aborted ? "Connection test timed out after 30s \u2014 the provider didn't respond." : explainProviderError(aiCfg.provider, e);
|
|
7831
|
+
return c.json({ ok: false, ...meta, error });
|
|
7832
|
+
} finally {
|
|
7833
|
+
clearTimeout(timer);
|
|
7834
|
+
}
|
|
7835
|
+
});
|
|
7836
|
+
app.get("/prices", (c) => {
|
|
7837
|
+
const cache2 = loadPriceCache();
|
|
7838
|
+
const ai = loadLocalSettings(projectRoot).ai;
|
|
7839
|
+
const pricing = resolvePricing(ai, cache2);
|
|
7840
|
+
return c.json({
|
|
7841
|
+
source: cache2?.source ?? null,
|
|
7842
|
+
fetchedAt: cache2?.fetchedAt ?? null,
|
|
7843
|
+
modelCount: cache2 ? Object.keys(cache2.models).length : 0,
|
|
7844
|
+
path: defaultPriceCachePath(),
|
|
7845
|
+
resolved: pricing ? { provider: ai.provider, model: ai.model, ...pricing } : null
|
|
7846
|
+
});
|
|
7847
|
+
});
|
|
7848
|
+
app.get("/prices/list", (c) => {
|
|
7849
|
+
const cache2 = loadPriceCache();
|
|
7850
|
+
const models = cache2 ? Object.entries(cache2.models).map(([id, p]) => ({ id, ...p })).sort((a, b) => a.id.localeCompare(b.id)) : [];
|
|
7851
|
+
return c.json({ source: cache2?.source ?? null, fetchedAt: cache2?.fetchedAt ?? null, models });
|
|
7852
|
+
});
|
|
7853
|
+
app.post("/prices/refresh", async (c) => {
|
|
7854
|
+
try {
|
|
7855
|
+
const res = await refreshPrices();
|
|
7856
|
+
invalidatePriceCache();
|
|
7857
|
+
return c.json({ ok: true, ...res });
|
|
7858
|
+
} catch (e) {
|
|
7859
|
+
return c.json({ error: e.message }, 502);
|
|
7860
|
+
}
|
|
7861
|
+
});
|
|
7195
7862
|
app.get("/file", (c) => c.json({ path: deps.statePath, name: basename(deps.statePath), dir: projectRoot, project: basename(projectRoot) }));
|
|
7196
7863
|
app.get("/files", (c) => {
|
|
7197
7864
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -7205,7 +7872,7 @@ function createApi(deps) {
|
|
|
7205
7872
|
if (depth > 4) return;
|
|
7206
7873
|
let entries = [];
|
|
7207
7874
|
try {
|
|
7208
|
-
entries =
|
|
7875
|
+
entries = readdirSync16(dir);
|
|
7209
7876
|
} catch {
|
|
7210
7877
|
return;
|
|
7211
7878
|
}
|
|
@@ -7556,6 +8223,90 @@ function createApi(deps) {
|
|
|
7556
8223
|
logChange({ kind: "glossary", summary: `Deleted glossary term "${term}"`, before });
|
|
7557
8224
|
return c.json({ ok: true });
|
|
7558
8225
|
});
|
|
8226
|
+
app.get("/glossary/suggestions", (c) => {
|
|
8227
|
+
const s = load();
|
|
8228
|
+
const pending = s.glossarySuggestions.filter((x) => x.status === "pending");
|
|
8229
|
+
return c.json(pending.map((x) => ({
|
|
8230
|
+
...x,
|
|
8231
|
+
occurrences: sourceKeysForTerm(s, x.term, { caseSensitive: x.caseSensitive, wholeWord: x.wholeWord }).length
|
|
8232
|
+
})));
|
|
8233
|
+
});
|
|
8234
|
+
app.post("/glossary/suggestions/dismiss", async (c) => {
|
|
8235
|
+
const { term } = await c.req.json();
|
|
8236
|
+
if (typeof term !== "string") return c.json({ error: "term must be a string" }, 400);
|
|
8237
|
+
const s = load();
|
|
8238
|
+
dismissGlossarySuggestion(s, term);
|
|
8239
|
+
persist(s);
|
|
8240
|
+
logChange({ kind: "glossary", summary: `Dismissed suggested term "${term}"` });
|
|
8241
|
+
return c.json({ ok: true });
|
|
8242
|
+
});
|
|
8243
|
+
app.delete("/glossary/suggestions/:term", (c) => {
|
|
8244
|
+
const s = load();
|
|
8245
|
+
const term = decodeURIComponent(c.req.param("term"));
|
|
8246
|
+
removeGlossarySuggestion(s, term);
|
|
8247
|
+
persist(s);
|
|
8248
|
+
return c.json({ ok: true });
|
|
8249
|
+
});
|
|
8250
|
+
app.post("/glossary/suggest", async (c) => {
|
|
8251
|
+
const signal = c.req.raw.signal;
|
|
8252
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8253
|
+
return streamSSE(c, async (stream) => {
|
|
8254
|
+
const s0 = load();
|
|
8255
|
+
const sources = selectGlossarySources(s0, { keyGlob: body.keyGlob, limit: body.limit, since: body.since });
|
|
8256
|
+
if (!sources.length) {
|
|
8257
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: 0, terms: [] }) });
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8261
|
+
let provider;
|
|
8262
|
+
try {
|
|
8263
|
+
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
8264
|
+
} catch (e) {
|
|
8265
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: e.message }) });
|
|
8266
|
+
return;
|
|
8267
|
+
}
|
|
8268
|
+
const known = knownTermList(s0);
|
|
8269
|
+
await stream.writeSSE({ event: "start", data: JSON.stringify({ total: sources.length }) });
|
|
8270
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
8271
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
8272
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
8273
|
+
const chunks = [];
|
|
8274
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
8275
|
+
const all = [];
|
|
8276
|
+
let done = 0;
|
|
8277
|
+
let next = 0;
|
|
8278
|
+
async function worker() {
|
|
8279
|
+
while (next < chunks.length) {
|
|
8280
|
+
if (signal?.aborted) break;
|
|
8281
|
+
const chunkRows = chunks[next++];
|
|
8282
|
+
try {
|
|
8283
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
8284
|
+
all.push(...raw.terms ?? []);
|
|
8285
|
+
} catch (e) {
|
|
8286
|
+
void stream.writeSSE({ event: "warn", data: JSON.stringify({ error: e.message }) });
|
|
8287
|
+
}
|
|
8288
|
+
done += chunkRows.length;
|
|
8289
|
+
void stream.writeSSE({ event: "progress", data: JSON.stringify({ done, total: sources.length }) });
|
|
8290
|
+
}
|
|
8291
|
+
}
|
|
8292
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
8293
|
+
if (signal?.aborted) return;
|
|
8294
|
+
const fresh = load();
|
|
8295
|
+
const added = mergeGlossarySuggestions(fresh, dedupeTerms(all));
|
|
8296
|
+
const usage = provider.takeUsage?.();
|
|
8297
|
+
persist(fresh);
|
|
8298
|
+
appendLog(projectRoot, {
|
|
8299
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8300
|
+
kind: "glossary",
|
|
8301
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
8302
|
+
model: aiCfg.model,
|
|
8303
|
+
system,
|
|
8304
|
+
usage,
|
|
8305
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8306
|
+
});
|
|
8307
|
+
await stream.writeSSE({ event: "done", data: JSON.stringify({ added: added.length, terms: added }) });
|
|
8308
|
+
});
|
|
8309
|
+
});
|
|
7559
8310
|
app.post("/keys/:key/screenshot", async (c) => {
|
|
7560
8311
|
const key = c.req.param("key");
|
|
7561
8312
|
const body = await c.req.parseBody();
|
|
@@ -7741,7 +8492,7 @@ function createApi(deps) {
|
|
|
7741
8492
|
try {
|
|
7742
8493
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7743
8494
|
} catch (e) {
|
|
7744
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ error:
|
|
8495
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7745
8496
|
return;
|
|
7746
8497
|
}
|
|
7747
8498
|
const { skipped } = attachScreenshotsForProvider(reqs, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
@@ -7758,58 +8509,65 @@ function createApi(deps) {
|
|
|
7758
8509
|
event: "start",
|
|
7759
8510
|
data: JSON.stringify({ total: reqs.length, locales: [...localeTotals].map(([locale, total]) => ({ locale, total })) })
|
|
7760
8511
|
});
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
8512
|
+
try {
|
|
8513
|
+
await runLocaleParallel(reqs, provider, {
|
|
8514
|
+
// Announce a language the moment a worker picks it up — this is the
|
|
8515
|
+
// signal that "something is happening" during the long first LLM call.
|
|
8516
|
+
onLocaleStart: (locale) => {
|
|
8517
|
+
void stream.writeSSE({ event: "locale-start", data: JSON.stringify({ locale }) });
|
|
8518
|
+
},
|
|
8519
|
+
onBatchComplete: (done, total, batchResults, locale) => {
|
|
8520
|
+
const fresh = load();
|
|
8521
|
+
const { written, errors } = applyResults(fresh, reqs, batchResults);
|
|
8522
|
+
persist(fresh);
|
|
8523
|
+
totalWritten += written;
|
|
8524
|
+
allErrors.push(...errors);
|
|
8525
|
+
const usage = provider.takeUsage?.();
|
|
8526
|
+
appendLog(projectRoot, {
|
|
8527
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8528
|
+
kind: "translate",
|
|
8529
|
+
summary: `Translated ${batchResults.length} item(s)`,
|
|
8530
|
+
model: aiCfg.model,
|
|
8531
|
+
system,
|
|
8532
|
+
items: batchResults.map((r) => {
|
|
8533
|
+
const req = reqById.get(r.id);
|
|
8534
|
+
return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? fresh.keys[req.key]?.screenshot : void 0 };
|
|
8535
|
+
}),
|
|
8536
|
+
results: batchResults,
|
|
8537
|
+
usage,
|
|
8538
|
+
estimatedCostUsd: usageCostUsd(usage, aiCfg)
|
|
8539
|
+
});
|
|
8540
|
+
const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
|
|
8541
|
+
localeDone.set(locale, ld);
|
|
8542
|
+
console.log(`[translate] ${done}/${total}`);
|
|
8543
|
+
void stream.writeSSE({
|
|
8544
|
+
event: "progress",
|
|
8545
|
+
data: JSON.stringify({ done, total, written: totalWritten, errors, locale, localeDone: ld, localeTotal: localeTotals.get(locale) ?? 0 })
|
|
8546
|
+
});
|
|
8547
|
+
},
|
|
8548
|
+
onLocaleDone: (locale) => {
|
|
8549
|
+
void stream.writeSSE({ event: "locale-done", data: JSON.stringify({ locale }) });
|
|
8550
|
+
},
|
|
8551
|
+
// Record the raw reply so an unparseable model response is diagnosable
|
|
8552
|
+
// from the activity log instead of vanishing into per-item errors.
|
|
8553
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8554
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8555
|
+
appendLog(projectRoot, {
|
|
8556
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8557
|
+
kind: "translate",
|
|
8558
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8559
|
+
model: aiCfg.model,
|
|
8560
|
+
locale,
|
|
8561
|
+
raw
|
|
8562
|
+
});
|
|
8563
|
+
}
|
|
8564
|
+
}, aiCfg.concurrency, signal, aiCfg.batchSize);
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
if (!signal?.aborted) {
|
|
8567
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: explainProviderError(aiCfg.provider, e) }) });
|
|
7811
8568
|
}
|
|
7812
|
-
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
7813
8571
|
if (!signal?.aborted) {
|
|
7814
8572
|
console.log(`[translate] done \u2014 wrote ${totalWritten}, ${allErrors.length} error(s)`);
|
|
7815
8573
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ written: totalWritten, errors: allErrors }) });
|
|
@@ -7836,23 +8594,28 @@ function createApi(deps) {
|
|
|
7836
8594
|
try {
|
|
7837
8595
|
provider = deps.makeProvider ? deps.makeProvider() : makeProvider(aiCfg);
|
|
7838
8596
|
} catch (e) {
|
|
7839
|
-
return c.json({ error:
|
|
8597
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 400);
|
|
7840
8598
|
}
|
|
7841
8599
|
const { skipped } = attachScreenshotsForProvider(toTranslate, s, dirname3(resolve9(deps.statePath)), provider.supportsVision());
|
|
7842
8600
|
if (skipped) console.warn(`Model "${aiCfg.model}" has no vision support; ${skipped} screenshot(s) ignored.`);
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
8601
|
+
let results;
|
|
8602
|
+
try {
|
|
8603
|
+
results = await runLocaleParallel(toTranslate, provider, {
|
|
8604
|
+
onMalformedReply: (raw, batchSize, locale) => {
|
|
8605
|
+
console.error(`[translate] malformed model reply (${locale}, batch of ${batchSize})${batchSize > 1 ? " \u2014 splitting batch and retrying" : ""}`);
|
|
8606
|
+
appendLog(projectRoot, {
|
|
8607
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8608
|
+
kind: "translate",
|
|
8609
|
+
summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
|
|
8610
|
+
model: aiCfg.model,
|
|
8611
|
+
locale,
|
|
8612
|
+
raw
|
|
8613
|
+
});
|
|
8614
|
+
}
|
|
8615
|
+
}, aiCfg.concurrency, void 0, aiCfg.batchSize);
|
|
8616
|
+
} catch (e) {
|
|
8617
|
+
return c.json({ error: explainProviderError(aiCfg.provider, e) }, 502);
|
|
8618
|
+
}
|
|
7856
8619
|
const latest = load();
|
|
7857
8620
|
({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
|
|
7858
8621
|
const usage = provider.takeUsage?.();
|
|
@@ -8123,6 +8886,22 @@ function createApi(deps) {
|
|
|
8123
8886
|
await stream.writeSSE({ event: "done", data: JSON.stringify({ requested: targets.length, written: totalWritten, errors: allErrors }) });
|
|
8124
8887
|
});
|
|
8125
8888
|
});
|
|
8889
|
+
app.post("/context/estimate", async (c) => {
|
|
8890
|
+
const body = await c.req.json().catch(() => ({}));
|
|
8891
|
+
const cache2 = loadUsageCache(projectRoot);
|
|
8892
|
+
if (!cache2) return c.json({ error: "No usage index found. Run 'glotfile scan' first." }, 400);
|
|
8893
|
+
const targets = selectContextTargets(load(), {
|
|
8894
|
+
all: body.all,
|
|
8895
|
+
keyGlob: body.keyGlob,
|
|
8896
|
+
limit: body.limit,
|
|
8897
|
+
since: body.since,
|
|
8898
|
+
keys: body.keys,
|
|
8899
|
+
force: body.force
|
|
8900
|
+
}, cache2, body.lastRunAt);
|
|
8901
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8902
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
8903
|
+
return c.json(estimateContext(targets, aiCfg));
|
|
8904
|
+
});
|
|
8126
8905
|
app.get("/context/batch/status", async (c) => {
|
|
8127
8906
|
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
8128
8907
|
let supported = false;
|
|
@@ -8227,6 +9006,8 @@ var init_api = __esm({
|
|
|
8227
9006
|
"src/server/api.ts"() {
|
|
8228
9007
|
"use strict";
|
|
8229
9008
|
init_state();
|
|
9009
|
+
init_glossary_suggest();
|
|
9010
|
+
init_glossary();
|
|
8230
9011
|
init_accept();
|
|
8231
9012
|
init_scan();
|
|
8232
9013
|
init_scanner();
|
|
@@ -8240,12 +9021,15 @@ var init_api = __esm({
|
|
|
8240
9021
|
init_ai();
|
|
8241
9022
|
init_run();
|
|
8242
9023
|
init_provider();
|
|
9024
|
+
init_explain_error();
|
|
8243
9025
|
init_batch_run();
|
|
8244
9026
|
init_pending_batch();
|
|
8245
9027
|
init_context_batch_run();
|
|
8246
9028
|
init_pending_context_batch();
|
|
8247
9029
|
init_estimate();
|
|
8248
9030
|
init_pricing();
|
|
9031
|
+
init_price_fetch();
|
|
9032
|
+
init_price_cache();
|
|
8249
9033
|
init_log();
|
|
8250
9034
|
init_schema();
|
|
8251
9035
|
init_run3();
|
|
@@ -8270,7 +9054,7 @@ __export(server_exports, {
|
|
|
8270
9054
|
import { Hono as Hono2 } from "hono";
|
|
8271
9055
|
import { serve } from "@hono/node-server";
|
|
8272
9056
|
import { fileURLToPath } from "url";
|
|
8273
|
-
import { dirname as dirname4, join as
|
|
9057
|
+
import { dirname as dirname4, join as join20, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
8274
9058
|
import { readFile, stat } from "fs/promises";
|
|
8275
9059
|
import { createServer } from "net";
|
|
8276
9060
|
import open from "open";
|
|
@@ -8326,7 +9110,7 @@ function buildApp(opts) {
|
|
|
8326
9110
|
const file = await readFileResponse(target);
|
|
8327
9111
|
if (file) return file;
|
|
8328
9112
|
}
|
|
8329
|
-
const index = await readFileResponse(
|
|
9113
|
+
const index = await readFileResponse(join20(root, "index.html"));
|
|
8330
9114
|
if (index) return index;
|
|
8331
9115
|
return c.notFound();
|
|
8332
9116
|
});
|
|
@@ -8396,7 +9180,7 @@ var init_server = __esm({
|
|
|
8396
9180
|
init_scanner();
|
|
8397
9181
|
init_usage();
|
|
8398
9182
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
8399
|
-
DEFAULT_UI_DIR =
|
|
9183
|
+
DEFAULT_UI_DIR = join20(here, "..", "ui");
|
|
8400
9184
|
MIME = {
|
|
8401
9185
|
".html": "text/html; charset=utf-8",
|
|
8402
9186
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -8428,8 +9212,8 @@ var init_server = __esm({
|
|
|
8428
9212
|
// src/server/cli.ts
|
|
8429
9213
|
init_state();
|
|
8430
9214
|
init_stats();
|
|
8431
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
8432
|
-
import { readFileSync as
|
|
9215
|
+
import { resolve as resolve11, dirname as dirname5, join as join21, basename as basename2 } from "path";
|
|
9216
|
+
import { readFileSync as readFileSync26, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
8433
9217
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8434
9218
|
|
|
8435
9219
|
// src/server/agent-cli.ts
|
|
@@ -8561,13 +9345,17 @@ init_pending_batch();
|
|
|
8561
9345
|
init_context_batch_run();
|
|
8562
9346
|
init_pending_context_batch();
|
|
8563
9347
|
init_estimate();
|
|
9348
|
+
init_glossary_suggest();
|
|
8564
9349
|
init_pricing();
|
|
9350
|
+
init_price_fetch();
|
|
9351
|
+
init_price_cache();
|
|
8565
9352
|
init_log();
|
|
8566
9353
|
init_scan();
|
|
8567
9354
|
init_scanner();
|
|
8568
9355
|
init_usage();
|
|
8569
9356
|
init_context();
|
|
8570
9357
|
init_run2();
|
|
9358
|
+
init_registry();
|
|
8571
9359
|
init_outputs();
|
|
8572
9360
|
|
|
8573
9361
|
// src/server/lint/locate.ts
|
|
@@ -8644,7 +9432,7 @@ function formatSarif(report, ctx) {
|
|
|
8644
9432
|
}
|
|
8645
9433
|
|
|
8646
9434
|
// src/server/cli.ts
|
|
8647
|
-
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch", "get", "stats", "set", "set-state", "clear", "apply"];
|
|
9435
|
+
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "suggest-glossary", "scan", "prune", "split", "skill", "batch", "prices", "get", "stats", "set", "set-state", "clear", "apply"];
|
|
8648
9436
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
8649
9437
|
function parseArgs(argv) {
|
|
8650
9438
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -8720,6 +9508,7 @@ function parseArgs(argv) {
|
|
|
8720
9508
|
else if (flag === "--batch") args.batch = true;
|
|
8721
9509
|
else if (flag === "--wait") args.wait = true;
|
|
8722
9510
|
else if (flag === "--print") args.print = true;
|
|
9511
|
+
else if (flag === "--refresh") args.refresh = true;
|
|
8723
9512
|
else if (flag === "--state" && next) {
|
|
8724
9513
|
args.states = next.split(",");
|
|
8725
9514
|
i++;
|
|
@@ -8815,7 +9604,7 @@ function translateSelection(args) {
|
|
|
8815
9604
|
}
|
|
8816
9605
|
function readStdin() {
|
|
8817
9606
|
try {
|
|
8818
|
-
return
|
|
9607
|
+
return readFileSync26(0, "utf8");
|
|
8819
9608
|
} catch {
|
|
8820
9609
|
return "";
|
|
8821
9610
|
}
|
|
@@ -9083,15 +9872,15 @@ async function runContextBatchAction(args, pending, action, projectRoot) {
|
|
|
9083
9872
|
function sarifContextFor(statePath) {
|
|
9084
9873
|
if (detectFormat(statePath) === "split") {
|
|
9085
9874
|
const dir = splitDirFor(statePath);
|
|
9086
|
-
const keysPath =
|
|
9875
|
+
const keysPath = join21(dir, "keys.json");
|
|
9087
9876
|
return {
|
|
9088
9877
|
keysUri: `${basename2(dir)}/keys.json`,
|
|
9089
|
-
keysRawText: existsSync14(keysPath) ?
|
|
9878
|
+
keysRawText: existsSync14(keysPath) ? readFileSync26(keysPath, "utf8") : ""
|
|
9090
9879
|
};
|
|
9091
9880
|
}
|
|
9092
9881
|
return {
|
|
9093
9882
|
keysUri: basename2(statePath),
|
|
9094
|
-
keysRawText: existsSync14(statePath) ?
|
|
9883
|
+
keysRawText: existsSync14(statePath) ? readFileSync26(statePath, "utf8") : ""
|
|
9095
9884
|
};
|
|
9096
9885
|
}
|
|
9097
9886
|
function printReport(report, format, statePath) {
|
|
@@ -9100,6 +9889,18 @@ function printReport(report, format, statePath) {
|
|
|
9100
9889
|
else console.log(formatText(report).trimEnd());
|
|
9101
9890
|
}
|
|
9102
9891
|
async function runLintCmd(args) {
|
|
9892
|
+
if (args.ruleIds) {
|
|
9893
|
+
const unknown = unknownRuleIds(args.ruleIds);
|
|
9894
|
+
if (unknown.length > 0) {
|
|
9895
|
+
for (const id of unknown) {
|
|
9896
|
+
const hint = suggestRuleId(id);
|
|
9897
|
+
console.error(`Unknown --rule '${id}'.${hint ? ` Did you mean '${hint}'?` : ""}`);
|
|
9898
|
+
}
|
|
9899
|
+
console.error(`Valid rules: ${RULE_IDS.join(", ")}.`);
|
|
9900
|
+
process.exitCode = 1;
|
|
9901
|
+
return;
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9103
9904
|
const state = loadState(args.statePath);
|
|
9104
9905
|
if (args.accept) {
|
|
9105
9906
|
const { acceptFindings: acceptFindings2 } = await Promise.resolve().then(() => (init_accept(), accept_exports));
|
|
@@ -9241,29 +10042,30 @@ async function runBuildContext(args) {
|
|
|
9241
10042
|
console.log("No keys need context.");
|
|
9242
10043
|
return;
|
|
9243
10044
|
}
|
|
10045
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10046
|
+
attachUsageSnippets(targets, cache2, projectRoot);
|
|
10047
|
+
if (args.estimate) {
|
|
10048
|
+
const est = estimateContext(targets, aiCfg);
|
|
10049
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10050
|
+
console.log(`Estimate for ${fmt(est.keys)} key(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10051
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10052
|
+
if (est.pricing) {
|
|
10053
|
+
const cost = est.estimatedCost;
|
|
10054
|
+
console.log(`Estimated cost: ~$${cost >= 0.1 ? cost.toFixed(2) : cost.toFixed(4)} (\xB120%, ${est.pricing.source} pricing $${est.pricing.inputPerMTok}/$${est.pricing.outputPerMTok} per MTok)`);
|
|
10055
|
+
} else {
|
|
10056
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10057
|
+
}
|
|
10058
|
+
return;
|
|
10059
|
+
}
|
|
9244
10060
|
let provider;
|
|
9245
10061
|
try {
|
|
9246
|
-
provider = makeProvider(
|
|
10062
|
+
provider = makeProvider(aiCfg);
|
|
9247
10063
|
} catch (e) {
|
|
9248
10064
|
console.error(e.message);
|
|
9249
10065
|
process.exitCode = 1;
|
|
9250
10066
|
return;
|
|
9251
10067
|
}
|
|
9252
|
-
const fileCache = /* @__PURE__ */ new Map();
|
|
9253
|
-
for (const target of targets) {
|
|
9254
|
-
const refs = Object.values(cache2.files).flatMap(
|
|
9255
|
-
(f) => f.refs.filter((r) => r.key === target.key).map((r) => ({
|
|
9256
|
-
key: r.key,
|
|
9257
|
-
file: Object.keys(cache2.files).find((path) => cache2.files[path]?.refs.includes(r)) ?? "",
|
|
9258
|
-
line: r.line,
|
|
9259
|
-
col: r.col,
|
|
9260
|
-
scanner: r.scanner
|
|
9261
|
-
}))
|
|
9262
|
-
);
|
|
9263
|
-
target.usageSnippets = extractSnippets(refs, projectRoot, fileCache);
|
|
9264
|
-
}
|
|
9265
10068
|
const system = buildContextSystemPrompt();
|
|
9266
|
-
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
9267
10069
|
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
9268
10070
|
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
9269
10071
|
if (args.batch) {
|
|
@@ -9319,6 +10121,71 @@ async function runBuildContext(args) {
|
|
|
9319
10121
|
console.log(`Wrote context for ${written} key(s).`);
|
|
9320
10122
|
for (const e of errors) console.warn(`skip ${e.key}: ${e.error}`);
|
|
9321
10123
|
}
|
|
10124
|
+
async function runSuggestGlossary(args) {
|
|
10125
|
+
const state = loadState(args.statePath);
|
|
10126
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10127
|
+
const sources = selectGlossarySources(state, { keyGlob: args.keyGlob, limit: args.limit, since: args.since });
|
|
10128
|
+
if (!sources.length) {
|
|
10129
|
+
console.log("No source strings to scan.");
|
|
10130
|
+
return;
|
|
10131
|
+
}
|
|
10132
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10133
|
+
const known = knownTermList(state);
|
|
10134
|
+
if (args.estimate) {
|
|
10135
|
+
const est = estimateGlossarySuggest(sources, known, aiCfg);
|
|
10136
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
10137
|
+
console.log(`Estimate for ${fmt(est.sources)} source string(s) in ${fmt(est.batches)} batch(es) \u2014 ${aiCfg.provider} \xB7 ${aiCfg.model}`);
|
|
10138
|
+
console.log(`Totals: ~${fmt(est.inputTokens)} input / ~${fmt(est.outputTokens)} output tokens`);
|
|
10139
|
+
if (est.pricing) {
|
|
10140
|
+
const cost = est.estimatedCost;
|
|
10141
|
+
console.log(`Estimated cost: ~$${cost >= 0.1 ? cost.toFixed(2) : cost.toFixed(4)} (\xB120%, ${est.pricing.source} pricing $${est.pricing.inputPerMTok}/$${est.pricing.outputPerMTok} per MTok)`);
|
|
10142
|
+
} else {
|
|
10143
|
+
console.log("No pricing known for this model \u2014 set inputPricePerMTok/outputPricePerMTok in your AI settings for a dollar estimate.");
|
|
10144
|
+
}
|
|
10145
|
+
return;
|
|
10146
|
+
}
|
|
10147
|
+
let provider;
|
|
10148
|
+
try {
|
|
10149
|
+
provider = makeProvider(aiCfg);
|
|
10150
|
+
} catch (e) {
|
|
10151
|
+
console.error(e.message);
|
|
10152
|
+
process.exitCode = 1;
|
|
10153
|
+
return;
|
|
10154
|
+
}
|
|
10155
|
+
const system = buildGlossarySuggestSystemPrompt();
|
|
10156
|
+
const batchSize = aiCfg.contextBatchSize ?? aiCfg.batchSize ?? 10;
|
|
10157
|
+
const concurrency = aiCfg.contextConcurrency ?? aiCfg.concurrency ?? 3;
|
|
10158
|
+
const chunks = [];
|
|
10159
|
+
for (let i = 0; i < sources.length; i += batchSize) chunks.push(sources.slice(i, i + batchSize));
|
|
10160
|
+
const all = [];
|
|
10161
|
+
let done = 0;
|
|
10162
|
+
let next = 0;
|
|
10163
|
+
async function worker() {
|
|
10164
|
+
while (next < chunks.length) {
|
|
10165
|
+
const chunkRows = chunks[next++];
|
|
10166
|
+
try {
|
|
10167
|
+
const raw = await provider.complete({ system, content: [{ type: "text", text: buildGlossarySuggestBatchPrompt(chunkRows, known) }], schema: GLOSSARY_SUGGEST_SCHEMA });
|
|
10168
|
+
const batch = raw;
|
|
10169
|
+
all.push(...batch.terms ?? []);
|
|
10170
|
+
} catch (e) {
|
|
10171
|
+
console.warn(`batch failed: ${e.message}`);
|
|
10172
|
+
}
|
|
10173
|
+
done += chunkRows.length;
|
|
10174
|
+
console.log(`[${done}/${sources.length}] scanned`);
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
|
|
10178
|
+
const added = mergeGlossarySuggestions(state, dedupeTerms(all));
|
|
10179
|
+
saveState(args.statePath, state);
|
|
10180
|
+
appendLog(projectRoot, {
|
|
10181
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10182
|
+
kind: "glossary",
|
|
10183
|
+
summary: `Suggested ${added.length} glossary term(s)`,
|
|
10184
|
+
model: aiCfg.model
|
|
10185
|
+
});
|
|
10186
|
+
console.log(`Found ${added.length} new candidate term(s). Review them in the glossary UI.`);
|
|
10187
|
+
for (const s of added) console.log(` \u2022 ${s.term}${s.note ? ` \u2014 ${s.note}` : ""}`);
|
|
10188
|
+
}
|
|
9322
10189
|
async function runScanCmd(args) {
|
|
9323
10190
|
const state = loadState(args.statePath);
|
|
9324
10191
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
@@ -9392,10 +10259,10 @@ function runSplit(args) {
|
|
|
9392
10259
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
9393
10260
|
);
|
|
9394
10261
|
}
|
|
9395
|
-
var SKILL_SRC =
|
|
10262
|
+
var SKILL_SRC = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
9396
10263
|
function runSkill(args) {
|
|
9397
10264
|
if (args.print) {
|
|
9398
|
-
console.log(
|
|
10265
|
+
console.log(readFileSync26(join21(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
9399
10266
|
return;
|
|
9400
10267
|
}
|
|
9401
10268
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
@@ -9574,6 +10441,39 @@ function runApply(args) {
|
|
|
9574
10441
|
console.log(JSON.stringify({ applied: r.applied, keysTouched: r.keysTouched, saved, dryRun: !!args.dryRun, errors: r.errors }, null, 2));
|
|
9575
10442
|
if (r.errors.length) process.exitCode = 1;
|
|
9576
10443
|
}
|
|
10444
|
+
async function runPrices(args) {
|
|
10445
|
+
const projectRoot = dirname5(resolve11(args.statePath));
|
|
10446
|
+
if (args.refresh) {
|
|
10447
|
+
try {
|
|
10448
|
+
const res = await refreshPrices();
|
|
10449
|
+
invalidatePriceCache();
|
|
10450
|
+
console.log(`Updated ${res.modelCount} model price(s) from ${res.source}.`);
|
|
10451
|
+
console.log(`Fetched ${new Date(res.fetchedAt).toLocaleString()} \u2192 ${res.path}`);
|
|
10452
|
+
} catch (e) {
|
|
10453
|
+
console.error(`Could not refresh prices: ${e.message}`);
|
|
10454
|
+
console.error("Existing cached prices (if any) are unchanged.");
|
|
10455
|
+
process.exitCode = 1;
|
|
10456
|
+
}
|
|
10457
|
+
return;
|
|
10458
|
+
}
|
|
10459
|
+
const cache2 = loadPriceCache();
|
|
10460
|
+
if (cache2) {
|
|
10461
|
+
const when = cache2.fetchedAt ? new Date(cache2.fetchedAt).toLocaleString() : "unknown time";
|
|
10462
|
+
console.log(`Price cache: ${Object.keys(cache2.models).length} model(s) from ${cache2.source}, fetched ${when}.`);
|
|
10463
|
+
console.log(`Location: ${defaultPriceCachePath()}`);
|
|
10464
|
+
} else {
|
|
10465
|
+
console.log("No price cache yet. Run `glotfile prices --refresh` to fetch the latest from models.dev.");
|
|
10466
|
+
}
|
|
10467
|
+
const aiCfg = loadLocalSettings(projectRoot).ai;
|
|
10468
|
+
const pricing = resolvePricing(aiCfg, cache2);
|
|
10469
|
+
if (pricing) {
|
|
10470
|
+
console.log(`
|
|
10471
|
+
${aiCfg.provider} \xB7 ${aiCfg.model}: $${pricing.inputPerMTok}/$${pricing.outputPerMTok} per MTok (${pricing.source}).`);
|
|
10472
|
+
} else {
|
|
10473
|
+
console.log(`
|
|
10474
|
+
No price known for ${aiCfg.provider} \xB7 ${aiCfg.model}. Set inputPricePerMTok/outputPricePerMTok in AI settings, or refresh.`);
|
|
10475
|
+
}
|
|
10476
|
+
}
|
|
9577
10477
|
var GLOBAL_OPTS = [
|
|
9578
10478
|
["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
|
|
9579
10479
|
["-h, --help", "Show this help"]
|
|
@@ -9652,12 +10552,24 @@ var COMMAND_HELP = {
|
|
|
9652
10552
|
},
|
|
9653
10553
|
"build-context": {
|
|
9654
10554
|
summary: "AI-generate per-key context to improve translation (requires a prior scan).",
|
|
9655
|
-
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]",
|
|
10555
|
+
usage: "glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--estimate] [--batch]",
|
|
9656
10556
|
options: [
|
|
9657
10557
|
["--all", "(Re)build context for every key, not just those missing it"],
|
|
9658
10558
|
["--key <glob>", "Only keys matching this glob"],
|
|
9659
10559
|
["--limit <n>", "Process at most n keys"],
|
|
9660
|
-
["--since <date>", "Only keys added or changed since this date"]
|
|
10560
|
+
["--since <date>", "Only keys added or changed since this date"],
|
|
10561
|
+
["--estimate", "Print batches, tokens and estimated cost without building"],
|
|
10562
|
+
["--batch", "Submit via the provider's batch API (50% cost, async; anthropic only)"]
|
|
10563
|
+
]
|
|
10564
|
+
},
|
|
10565
|
+
"suggest-glossary": {
|
|
10566
|
+
summary: "AI-scan source strings for candidate glossary terms (adds a review queue; existing terms are skipped).",
|
|
10567
|
+
usage: "glotfile suggest-glossary [--key <glob>] [--limit <n>] [--since <date>] [--estimate]",
|
|
10568
|
+
options: [
|
|
10569
|
+
["--key <glob>", "Only scan keys matching this glob"],
|
|
10570
|
+
["--limit <n>", "Scan at most n source strings"],
|
|
10571
|
+
["--since <date>", "Only keys added since this date"],
|
|
10572
|
+
["--estimate", "Print batches, tokens and estimated cost without scanning"]
|
|
9661
10573
|
]
|
|
9662
10574
|
},
|
|
9663
10575
|
scan: {
|
|
@@ -9696,6 +10608,13 @@ var COMMAND_HELP = {
|
|
|
9696
10608
|
["cancel", "Cancel the pending batch and discard the handle"]
|
|
9697
10609
|
]
|
|
9698
10610
|
},
|
|
10611
|
+
prices: {
|
|
10612
|
+
summary: "Show or refresh the model price cache used for cost estimates (models.dev).",
|
|
10613
|
+
usage: "glotfile prices [--refresh]",
|
|
10614
|
+
options: [
|
|
10615
|
+
["--refresh", "Fetch the latest prices from models.dev into the cache (the only command that hits the network)"]
|
|
10616
|
+
]
|
|
10617
|
+
},
|
|
9699
10618
|
get: {
|
|
9700
10619
|
summary: "Extract values from the catalog (filtered) without loading the whole file. Prints JSON.",
|
|
9701
10620
|
usage: "glotfile get [<key-glob>\u2026] [--key <glob>] [--locale <list>] [--state <list>] [--fields <list>] [--keys-only] [--format json|ndjson]",
|
|
@@ -9785,8 +10704,8 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
9785
10704
|
);
|
|
9786
10705
|
}
|
|
9787
10706
|
function printVersion() {
|
|
9788
|
-
const pkgPath =
|
|
9789
|
-
console.log(JSON.parse(
|
|
10707
|
+
const pkgPath = join21(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
10708
|
+
console.log(JSON.parse(readFileSync26(pkgPath, "utf8")).version);
|
|
9790
10709
|
}
|
|
9791
10710
|
async function main(argv) {
|
|
9792
10711
|
const args = parseArgs(argv);
|
|
@@ -9806,11 +10725,13 @@ async function main(argv) {
|
|
|
9806
10725
|
if (args.command === "import") return runImportCmd(args);
|
|
9807
10726
|
if (args.command === "sync") return runSyncCmd(args);
|
|
9808
10727
|
if (args.command === "build-context") return runBuildContext(args);
|
|
10728
|
+
if (args.command === "suggest-glossary") return runSuggestGlossary(args);
|
|
9809
10729
|
if (args.command === "scan") return runScanCmd(args);
|
|
9810
10730
|
if (args.command === "prune") return runPrune(args);
|
|
9811
10731
|
if (args.command === "split") return runSplit(args);
|
|
9812
10732
|
if (args.command === "skill") return runSkill(args);
|
|
9813
10733
|
if (args.command === "batch") return runBatch(args);
|
|
10734
|
+
if (args.command === "prices") return runPrices(args);
|
|
9814
10735
|
if (args.command === "get") return runGetCmd(args);
|
|
9815
10736
|
if (args.command === "stats") return runStatsCmd(args);
|
|
9816
10737
|
if (args.command === "set") return runSet(args);
|