glotfile 0.8.7 → 0.8.9
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 +5 -5
- package/dist/server/cli.js +574 -152
- package/dist/server/server.js +146 -113
- package/dist/ui/assets/{index-CVA535xu.js → index-B64rdzp7.js} +326 -14
- package/dist/ui/index.html +1 -1
- package/package.json +2 -2
- package/skill/SKILL.md +28 -3
- package/skill/references/cli-reference.md +84 -1
- package/skill/references/conventions.md +17 -0
- package/skill/references/schema.md +21 -2
- package/skill/references/workflows.md +61 -16
package/dist/server/cli.js
CHANGED
|
@@ -852,6 +852,153 @@ var init_state = __esm({
|
|
|
852
852
|
}
|
|
853
853
|
});
|
|
854
854
|
|
|
855
|
+
// src/server/stats.ts
|
|
856
|
+
function countWords(text) {
|
|
857
|
+
const t = text.trim();
|
|
858
|
+
return t === "" ? 0 : t.split(/\s+/).length;
|
|
859
|
+
}
|
|
860
|
+
function namespaceOf(key) {
|
|
861
|
+
const i = key.indexOf(".");
|
|
862
|
+
return i === -1 ? "(root)" : key.slice(0, i);
|
|
863
|
+
}
|
|
864
|
+
function pct(n, d) {
|
|
865
|
+
return d === 0 ? 0 : Math.round(n / d * 1e3) / 10;
|
|
866
|
+
}
|
|
867
|
+
function sourceText(entry, sourceLocale) {
|
|
868
|
+
const lv = entry.values[sourceLocale];
|
|
869
|
+
if (!lv) return "";
|
|
870
|
+
return entry.plural ? lv.forms?.other ?? "" : lv.value ?? "";
|
|
871
|
+
}
|
|
872
|
+
function isPresent(entry, locale) {
|
|
873
|
+
const lv = entry.values[locale];
|
|
874
|
+
if (!lv) return false;
|
|
875
|
+
return entry.plural ? (lv.forms?.other ?? "").trim() !== "" : (lv.value ?? "").trim() !== "";
|
|
876
|
+
}
|
|
877
|
+
function classify(entry, locale) {
|
|
878
|
+
if (!isPresent(entry, locale)) return "missing";
|
|
879
|
+
const st = entry.values[locale].state;
|
|
880
|
+
if (st === "reviewed") return "reviewed";
|
|
881
|
+
if (st === "needs-review") return "needsReview";
|
|
882
|
+
return "machine";
|
|
883
|
+
}
|
|
884
|
+
function groupCompletion(state, keys, targets, name) {
|
|
885
|
+
let translated = 0;
|
|
886
|
+
let reviewed = 0;
|
|
887
|
+
for (const k of keys) {
|
|
888
|
+
const entry = state.keys[k];
|
|
889
|
+
for (const locale of targets) {
|
|
890
|
+
const bucket = classify(entry, locale);
|
|
891
|
+
if (bucket !== "missing") translated++;
|
|
892
|
+
if (bucket === "reviewed") reviewed++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const cells = keys.length * targets.length;
|
|
896
|
+
return { name, total: keys.length, translatedPct: pct(translated, cells), reviewedPct: pct(reviewed, cells) };
|
|
897
|
+
}
|
|
898
|
+
function worstFirst(a, b) {
|
|
899
|
+
return a.translatedPct - b.translatedPct || a.name.localeCompare(b.name);
|
|
900
|
+
}
|
|
901
|
+
function computeStats(state) {
|
|
902
|
+
const { sourceLocale, locales } = state.config;
|
|
903
|
+
const targets = locales.filter((l) => l !== sourceLocale);
|
|
904
|
+
const allKeys = Object.keys(state.keys);
|
|
905
|
+
const expected = allKeys.filter((k) => !state.keys[k].skipTranslate);
|
|
906
|
+
const locales_ = targets.map((locale) => {
|
|
907
|
+
const counts = { reviewed: 0, needsReview: 0, machine: 0, missing: 0 };
|
|
908
|
+
let sourceWords = 0;
|
|
909
|
+
let missingWords = 0;
|
|
910
|
+
for (const k of expected) {
|
|
911
|
+
const entry = state.keys[k];
|
|
912
|
+
const w = countWords(sourceText(entry, sourceLocale));
|
|
913
|
+
sourceWords += w;
|
|
914
|
+
const bucket = classify(entry, locale);
|
|
915
|
+
counts[bucket]++;
|
|
916
|
+
if (bucket === "missing") missingWords += w;
|
|
917
|
+
}
|
|
918
|
+
const total = expected.length;
|
|
919
|
+
const translated = counts.reviewed + counts.needsReview + counts.machine;
|
|
920
|
+
return {
|
|
921
|
+
locale,
|
|
922
|
+
total,
|
|
923
|
+
counts,
|
|
924
|
+
translated,
|
|
925
|
+
reviewed: counts.reviewed,
|
|
926
|
+
translatedPct: pct(translated, total),
|
|
927
|
+
reviewedPct: pct(counts.reviewed, total),
|
|
928
|
+
words: { source: sourceWords, missing: missingWords }
|
|
929
|
+
};
|
|
930
|
+
});
|
|
931
|
+
const cells = expected.length * targets.length;
|
|
932
|
+
let translatedCells = 0;
|
|
933
|
+
let reviewedCells = 0;
|
|
934
|
+
for (const ls of locales_) {
|
|
935
|
+
translatedCells += ls.translated;
|
|
936
|
+
reviewedCells += ls.reviewed;
|
|
937
|
+
}
|
|
938
|
+
const nsMap = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const k of expected) {
|
|
940
|
+
const ns = namespaceOf(k);
|
|
941
|
+
(nsMap.get(ns) ?? nsMap.set(ns, []).get(ns)).push(k);
|
|
942
|
+
}
|
|
943
|
+
const byNamespace = [...nsMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
|
|
944
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const k of expected) {
|
|
946
|
+
for (const tag of state.keys[k].tags ?? []) {
|
|
947
|
+
(tagMap.get(tag) ?? tagMap.set(tag, []).get(tag)).push(k);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const byTag = [...tagMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
|
|
951
|
+
return {
|
|
952
|
+
totals: {
|
|
953
|
+
keys: allKeys.length,
|
|
954
|
+
locales: targets.length,
|
|
955
|
+
translatedPct: pct(translatedCells, cells),
|
|
956
|
+
reviewedPct: pct(reviewedCells, cells),
|
|
957
|
+
sourceWords: expected.reduce((sum, k) => sum + countWords(sourceText(state.keys[k], sourceLocale)), 0)
|
|
958
|
+
},
|
|
959
|
+
locales: locales_,
|
|
960
|
+
byNamespace,
|
|
961
|
+
byTag
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
var init_stats = __esm({
|
|
965
|
+
"src/server/stats.ts"() {
|
|
966
|
+
"use strict";
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// src/server/cell-state.ts
|
|
971
|
+
function cellState(entry, locale, sourceLocale) {
|
|
972
|
+
const lv = entry.values[locale];
|
|
973
|
+
if (locale === sourceLocale) {
|
|
974
|
+
const has = entry.plural ? !!lv?.forms?.other?.trim() : !!lv?.value?.trim();
|
|
975
|
+
return has ? "source" : "missing";
|
|
976
|
+
}
|
|
977
|
+
const present = entry.plural ? categoriesFor(locale).every((c) => (lv?.forms?.[c] ?? "") !== "") : !!lv?.value;
|
|
978
|
+
if (!present) return "missing";
|
|
979
|
+
const st = lv.state;
|
|
980
|
+
return st === "reviewed" ? "reviewed" : st === "needs-review" ? "needs-review" : "machine";
|
|
981
|
+
}
|
|
982
|
+
var EFFECTIVE_STATES;
|
|
983
|
+
var init_cell_state = __esm({
|
|
984
|
+
"src/server/cell-state.ts"() {
|
|
985
|
+
"use strict";
|
|
986
|
+
init_plurals();
|
|
987
|
+
EFFECTIVE_STATES = ["source", "missing", "machine", "needs-review", "reviewed"];
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// src/server/glob.ts
|
|
992
|
+
function globToRegExp(glob) {
|
|
993
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
994
|
+
return new RegExp(`^${escaped}$`);
|
|
995
|
+
}
|
|
996
|
+
var init_glob = __esm({
|
|
997
|
+
"src/server/glob.ts"() {
|
|
998
|
+
"use strict";
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
855
1002
|
// src/server/adapters/options.ts
|
|
856
1003
|
function applyCase(canonical, style) {
|
|
857
1004
|
const sep4 = style === "lower-underscore" || style === "bcp47-underscore" ? "_" : "-";
|
|
@@ -999,6 +1146,9 @@ function extractLiterals(value) {
|
|
|
999
1146
|
});
|
|
1000
1147
|
return out;
|
|
1001
1148
|
}
|
|
1149
|
+
function quotedLiterals(value) {
|
|
1150
|
+
return extractLiterals(value).map((content) => `'${content}'`);
|
|
1151
|
+
}
|
|
1002
1152
|
function toLaravel(value) {
|
|
1003
1153
|
if (isIcuPluralOrSelect(value)) return value;
|
|
1004
1154
|
return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
|
|
@@ -2070,10 +2220,11 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
2070
2220
|
"You are a professional software localization engine for a UI string catalog.",
|
|
2071
2221
|
"Your goal: translate each source UI string into its target locale accurately and idiomatically, as a native speaker would phrase it in a real app interface.",
|
|
2072
2222
|
"",
|
|
2073
|
-
"You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
|
|
2223
|
+
"You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, an optional `literals` list, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
|
|
2074
2224
|
"",
|
|
2075
2225
|
"Hard rules:",
|
|
2076
2226
|
"- Preserve every interpolation placeholder EXACTLY as written: {name}, {{count}}, %s, %d, :name. Never translate, rename, reorder, or remove them.",
|
|
2227
|
+
"- Reproduce every entry of the item's `literals` array EXACTLY, including its surrounding apostrophes (e.g. '{{visitor}}', '{name}'). These are app-managed literal tokens, not prose: translate the words around them, but never translate, rename, unquote, or drop them. The apostrophes are required \u2014 a result with bare {{visitor}} instead of '{{visitor}}' is wrong.",
|
|
2077
2228
|
"- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
|
|
2078
2229
|
"- Glossary: a term marked do-not-translate MUST appear unchanged in the translation. A term with a forced translation for the target locale MUST use that exact translation.",
|
|
2079
2230
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
@@ -2102,6 +2253,7 @@ function buildBatchPrompt(reqs) {
|
|
|
2102
2253
|
// Wrap in braces so the model sees "{site}" not "site" — makes the visual
|
|
2103
2254
|
// connection to the source string obvious and reduces rename errors.
|
|
2104
2255
|
placeholders: r.placeholders.map((p) => `{${p}}`),
|
|
2256
|
+
...r.literals?.length ? { literals: r.literals } : {},
|
|
2105
2257
|
...r.glossary?.length ? { glossary: r.glossary } : {},
|
|
2106
2258
|
hasScreenshot: r.image !== void 0
|
|
2107
2259
|
};
|
|
@@ -3127,17 +3279,6 @@ var init_glossary = __esm({
|
|
|
3127
3279
|
}
|
|
3128
3280
|
});
|
|
3129
3281
|
|
|
3130
|
-
// src/server/glob.ts
|
|
3131
|
-
function globToRegExp(glob) {
|
|
3132
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3133
|
-
return new RegExp(`^${escaped}$`);
|
|
3134
|
-
}
|
|
3135
|
-
var init_glob = __esm({
|
|
3136
|
-
"src/server/glob.ts"() {
|
|
3137
|
-
"use strict";
|
|
3138
|
-
}
|
|
3139
|
-
});
|
|
3140
|
-
|
|
3141
3282
|
// src/server/ai/run.ts
|
|
3142
3283
|
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
3143
3284
|
import { resolve as resolve4, extname } from "path";
|
|
@@ -3145,6 +3286,8 @@ function selectRequests(state, opts) {
|
|
|
3145
3286
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3146
3287
|
const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
|
|
3147
3288
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3289
|
+
const stateSet = opts.states ? new Set(opts.states) : null;
|
|
3290
|
+
const skip = (st) => stateSet ? !stateSet.has(st) : !!opts.onlyMissing && st !== "missing";
|
|
3148
3291
|
const reqs = [];
|
|
3149
3292
|
let id = 0;
|
|
3150
3293
|
for (const key of Object.keys(state.keys).sort()) {
|
|
@@ -3158,10 +3301,9 @@ function selectRequests(state, opts) {
|
|
|
3158
3301
|
const other = sourceForms?.other;
|
|
3159
3302
|
if (!sourceForms || !other) continue;
|
|
3160
3303
|
for (const locale of targets) {
|
|
3161
|
-
|
|
3162
|
-
const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
|
|
3163
|
-
if (opts.onlyMissing && complete) continue;
|
|
3304
|
+
if (skip(cellState(entry, locale, state.config.sourceLocale))) continue;
|
|
3164
3305
|
const glossary = relevantGlossary(other, locale, state.glossary);
|
|
3306
|
+
const literals = quotedLiterals(other);
|
|
3165
3307
|
reqs.push({
|
|
3166
3308
|
id: String(id++),
|
|
3167
3309
|
key,
|
|
@@ -3171,6 +3313,7 @@ function selectRequests(state, opts) {
|
|
|
3171
3313
|
targetLocale: locale,
|
|
3172
3314
|
maxLength: entry.maxLength,
|
|
3173
3315
|
placeholders: extractPlaceholders(other),
|
|
3316
|
+
...literals.length ? { literals } : {},
|
|
3174
3317
|
...glossary.length ? { glossary } : {},
|
|
3175
3318
|
plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
|
|
3176
3319
|
});
|
|
@@ -3180,9 +3323,9 @@ function selectRequests(state, opts) {
|
|
|
3180
3323
|
const source = sourceLv?.value;
|
|
3181
3324
|
if (!source) continue;
|
|
3182
3325
|
for (const locale of targets) {
|
|
3183
|
-
|
|
3184
|
-
if (opts.onlyMissing && existing) continue;
|
|
3326
|
+
if (skip(cellState(entry, locale, state.config.sourceLocale))) continue;
|
|
3185
3327
|
const glossary = relevantGlossary(source, locale, state.glossary);
|
|
3328
|
+
const literals = quotedLiterals(source);
|
|
3186
3329
|
reqs.push({
|
|
3187
3330
|
id: String(id++),
|
|
3188
3331
|
key,
|
|
@@ -3192,6 +3335,7 @@ function selectRequests(state, opts) {
|
|
|
3192
3335
|
targetLocale: locale,
|
|
3193
3336
|
maxLength: entry.maxLength,
|
|
3194
3337
|
placeholders: extractPlaceholders(source),
|
|
3338
|
+
...literals.length ? { literals } : {},
|
|
3195
3339
|
...glossary.length ? { glossary } : {}
|
|
3196
3340
|
});
|
|
3197
3341
|
}
|
|
@@ -3340,6 +3484,7 @@ var init_run = __esm({
|
|
|
3340
3484
|
init_placeholders();
|
|
3341
3485
|
init_plurals();
|
|
3342
3486
|
init_state();
|
|
3487
|
+
init_cell_state();
|
|
3343
3488
|
init_glob();
|
|
3344
3489
|
init_batch();
|
|
3345
3490
|
MEDIA_TYPES = {
|
|
@@ -3683,7 +3828,8 @@ function buildContextSystemPrompt() {
|
|
|
3683
3828
|
"- Do NOT restate the source string itself.",
|
|
3684
3829
|
"- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
|
|
3685
3830
|
"- Keep it under 500 characters.",
|
|
3686
|
-
"- If no code snippets are available, infer from the key path and source value."
|
|
3831
|
+
"- If no code snippets are available, infer from the key path and source value.",
|
|
3832
|
+
"- Tokens: a source may contain interpolation placeholders ({name}, {{name}}, :name, %s) and ICU-apostrophe-quoted LITERAL tokens (e.g. '{{visitor}}', '{name}') that the app fills at runtime. Any provided `literals` are literal tokens, NOT plain placeholders. If you reference a token, write it EXACTLY as it appears in the source \u2014 keep apostrophe-quoted literals quoted, and never relabel a quoted literal as a placeholder or strip its quotes. The translation engine needs these to survive verbatim, so a note may simply remind translators to reproduce them exactly."
|
|
3687
3833
|
].join("\n");
|
|
3688
3834
|
}
|
|
3689
3835
|
function buildContextBatchPrompt(reqs) {
|
|
@@ -3695,7 +3841,14 @@ function buildContextBatchPrompt(reqs) {
|
|
|
3695
3841
|
${s.lines}
|
|
3696
3842
|
\`\`\``;
|
|
3697
3843
|
}).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
|
|
3698
|
-
|
|
3844
|
+
const literals = quotedLiterals(r.source);
|
|
3845
|
+
return {
|
|
3846
|
+
id: r.id,
|
|
3847
|
+
key: r.key,
|
|
3848
|
+
source: r.source,
|
|
3849
|
+
...literals.length ? { literals } : {},
|
|
3850
|
+
codeSnippets: snippetText
|
|
3851
|
+
};
|
|
3699
3852
|
});
|
|
3700
3853
|
return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
|
|
3701
3854
|
}
|
|
@@ -3733,6 +3886,7 @@ var init_context = __esm({
|
|
|
3733
3886
|
"src/server/ai/context.ts"() {
|
|
3734
3887
|
"use strict";
|
|
3735
3888
|
init_state();
|
|
3889
|
+
init_placeholders();
|
|
3736
3890
|
MAX_CONTEXT_LENGTH = 500;
|
|
3737
3891
|
SNIPPET_WINDOW = 15;
|
|
3738
3892
|
MAX_SNIPPETS = 3;
|
|
@@ -6583,121 +6737,6 @@ var init_run3 = __esm({
|
|
|
6583
6737
|
}
|
|
6584
6738
|
});
|
|
6585
6739
|
|
|
6586
|
-
// src/server/stats.ts
|
|
6587
|
-
function countWords(text) {
|
|
6588
|
-
const t = text.trim();
|
|
6589
|
-
return t === "" ? 0 : t.split(/\s+/).length;
|
|
6590
|
-
}
|
|
6591
|
-
function namespaceOf(key) {
|
|
6592
|
-
const i = key.indexOf(".");
|
|
6593
|
-
return i === -1 ? "(root)" : key.slice(0, i);
|
|
6594
|
-
}
|
|
6595
|
-
function pct(n, d) {
|
|
6596
|
-
return d === 0 ? 0 : Math.round(n / d * 1e3) / 10;
|
|
6597
|
-
}
|
|
6598
|
-
function sourceText(entry, sourceLocale) {
|
|
6599
|
-
const lv = entry.values[sourceLocale];
|
|
6600
|
-
if (!lv) return "";
|
|
6601
|
-
return entry.plural ? lv.forms?.other ?? "" : lv.value ?? "";
|
|
6602
|
-
}
|
|
6603
|
-
function isPresent(entry, locale) {
|
|
6604
|
-
const lv = entry.values[locale];
|
|
6605
|
-
if (!lv) return false;
|
|
6606
|
-
return entry.plural ? (lv.forms?.other ?? "").trim() !== "" : (lv.value ?? "").trim() !== "";
|
|
6607
|
-
}
|
|
6608
|
-
function classify(entry, locale) {
|
|
6609
|
-
if (!isPresent(entry, locale)) return "missing";
|
|
6610
|
-
const st = entry.values[locale].state;
|
|
6611
|
-
if (st === "reviewed") return "reviewed";
|
|
6612
|
-
if (st === "needs-review") return "needsReview";
|
|
6613
|
-
return "machine";
|
|
6614
|
-
}
|
|
6615
|
-
function groupCompletion(state, keys, targets, name) {
|
|
6616
|
-
let translated = 0;
|
|
6617
|
-
let reviewed = 0;
|
|
6618
|
-
for (const k of keys) {
|
|
6619
|
-
const entry = state.keys[k];
|
|
6620
|
-
for (const locale of targets) {
|
|
6621
|
-
const bucket = classify(entry, locale);
|
|
6622
|
-
if (bucket !== "missing") translated++;
|
|
6623
|
-
if (bucket === "reviewed") reviewed++;
|
|
6624
|
-
}
|
|
6625
|
-
}
|
|
6626
|
-
const cells = keys.length * targets.length;
|
|
6627
|
-
return { name, total: keys.length, translatedPct: pct(translated, cells), reviewedPct: pct(reviewed, cells) };
|
|
6628
|
-
}
|
|
6629
|
-
function worstFirst(a, b) {
|
|
6630
|
-
return a.translatedPct - b.translatedPct || a.name.localeCompare(b.name);
|
|
6631
|
-
}
|
|
6632
|
-
function computeStats(state) {
|
|
6633
|
-
const { sourceLocale, locales } = state.config;
|
|
6634
|
-
const targets = locales.filter((l) => l !== sourceLocale);
|
|
6635
|
-
const allKeys = Object.keys(state.keys);
|
|
6636
|
-
const expected = allKeys.filter((k) => !state.keys[k].skipTranslate);
|
|
6637
|
-
const locales_ = targets.map((locale) => {
|
|
6638
|
-
const counts = { reviewed: 0, needsReview: 0, machine: 0, missing: 0 };
|
|
6639
|
-
let sourceWords = 0;
|
|
6640
|
-
let missingWords = 0;
|
|
6641
|
-
for (const k of expected) {
|
|
6642
|
-
const entry = state.keys[k];
|
|
6643
|
-
const w = countWords(sourceText(entry, sourceLocale));
|
|
6644
|
-
sourceWords += w;
|
|
6645
|
-
const bucket = classify(entry, locale);
|
|
6646
|
-
counts[bucket]++;
|
|
6647
|
-
if (bucket === "missing") missingWords += w;
|
|
6648
|
-
}
|
|
6649
|
-
const total = expected.length;
|
|
6650
|
-
const translated = counts.reviewed + counts.needsReview + counts.machine;
|
|
6651
|
-
return {
|
|
6652
|
-
locale,
|
|
6653
|
-
total,
|
|
6654
|
-
counts,
|
|
6655
|
-
translated,
|
|
6656
|
-
reviewed: counts.reviewed,
|
|
6657
|
-
translatedPct: pct(translated, total),
|
|
6658
|
-
reviewedPct: pct(counts.reviewed, total),
|
|
6659
|
-
words: { source: sourceWords, missing: missingWords }
|
|
6660
|
-
};
|
|
6661
|
-
});
|
|
6662
|
-
const cells = expected.length * targets.length;
|
|
6663
|
-
let translatedCells = 0;
|
|
6664
|
-
let reviewedCells = 0;
|
|
6665
|
-
for (const ls of locales_) {
|
|
6666
|
-
translatedCells += ls.translated;
|
|
6667
|
-
reviewedCells += ls.reviewed;
|
|
6668
|
-
}
|
|
6669
|
-
const nsMap = /* @__PURE__ */ new Map();
|
|
6670
|
-
for (const k of expected) {
|
|
6671
|
-
const ns = namespaceOf(k);
|
|
6672
|
-
(nsMap.get(ns) ?? nsMap.set(ns, []).get(ns)).push(k);
|
|
6673
|
-
}
|
|
6674
|
-
const byNamespace = [...nsMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
|
|
6675
|
-
const tagMap = /* @__PURE__ */ new Map();
|
|
6676
|
-
for (const k of expected) {
|
|
6677
|
-
for (const tag of state.keys[k].tags ?? []) {
|
|
6678
|
-
(tagMap.get(tag) ?? tagMap.set(tag, []).get(tag)).push(k);
|
|
6679
|
-
}
|
|
6680
|
-
}
|
|
6681
|
-
const byTag = [...tagMap.entries()].map(([name, keys]) => groupCompletion(state, keys, targets, name)).sort(worstFirst);
|
|
6682
|
-
return {
|
|
6683
|
-
totals: {
|
|
6684
|
-
keys: allKeys.length,
|
|
6685
|
-
locales: targets.length,
|
|
6686
|
-
translatedPct: pct(translatedCells, cells),
|
|
6687
|
-
reviewedPct: pct(reviewedCells, cells),
|
|
6688
|
-
sourceWords: expected.reduce((sum, k) => sum + countWords(sourceText(state.keys[k], sourceLocale)), 0)
|
|
6689
|
-
},
|
|
6690
|
-
locales: locales_,
|
|
6691
|
-
byNamespace,
|
|
6692
|
-
byTag
|
|
6693
|
-
};
|
|
6694
|
-
}
|
|
6695
|
-
var init_stats = __esm({
|
|
6696
|
-
"src/server/stats.ts"() {
|
|
6697
|
-
"use strict";
|
|
6698
|
-
}
|
|
6699
|
-
});
|
|
6700
|
-
|
|
6701
6740
|
// src/server/checks.ts
|
|
6702
6741
|
function runChecks(state, opts = {}) {
|
|
6703
6742
|
const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
|
|
@@ -8388,6 +8427,129 @@ var init_server = __esm({
|
|
|
8388
8427
|
|
|
8389
8428
|
// src/server/cli.ts
|
|
8390
8429
|
init_state();
|
|
8430
|
+
init_stats();
|
|
8431
|
+
import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
|
|
8432
|
+
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
8433
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8434
|
+
|
|
8435
|
+
// src/server/agent-cli.ts
|
|
8436
|
+
init_state();
|
|
8437
|
+
init_cell_state();
|
|
8438
|
+
init_glob();
|
|
8439
|
+
function projectCell(cell, fields) {
|
|
8440
|
+
const out = {};
|
|
8441
|
+
for (const f of fields) {
|
|
8442
|
+
if (f === "value" && cell.value !== void 0) out.value = cell.value;
|
|
8443
|
+
else if (f === "value" && cell.forms !== void 0) out.forms = cell.forms;
|
|
8444
|
+
else if (f === "state") out.state = cell.state;
|
|
8445
|
+
else if (f === "updatedAt" && cell.updatedAt !== void 0) out.updatedAt = cell.updatedAt;
|
|
8446
|
+
}
|
|
8447
|
+
return out;
|
|
8448
|
+
}
|
|
8449
|
+
function runGet(state, opts) {
|
|
8450
|
+
const { sourceLocale } = state.config;
|
|
8451
|
+
const shown = (opts.locales?.length ? opts.locales : state.config.locales).map(canonLocale);
|
|
8452
|
+
const targetsShown = shown.filter((l) => l !== sourceLocale);
|
|
8453
|
+
const res = opts.keyGlobs?.length ? opts.keyGlobs.map(globToRegExp) : null;
|
|
8454
|
+
const stateSet = opts.states?.length ? new Set(opts.states) : null;
|
|
8455
|
+
const fields = opts.fields?.length ? opts.fields : ["value", "state"];
|
|
8456
|
+
const fullEntry = fields.includes("all");
|
|
8457
|
+
const keys = [];
|
|
8458
|
+
const json = {};
|
|
8459
|
+
const ndjson = [];
|
|
8460
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
8461
|
+
if (res && !res.some((re) => re.test(key))) continue;
|
|
8462
|
+
const entry = state.keys[key];
|
|
8463
|
+
if (stateSet && !targetsShown.some((l) => stateSet.has(cellState(entry, l, sourceLocale)))) continue;
|
|
8464
|
+
keys.push(key);
|
|
8465
|
+
if (fullEntry) {
|
|
8466
|
+
const values = {};
|
|
8467
|
+
for (const l of shown) if (entry.values[l]) values[l] = entry.values[l];
|
|
8468
|
+
json[key] = { ...entry, values };
|
|
8469
|
+
ndjson.push({ key, ...entry, values });
|
|
8470
|
+
continue;
|
|
8471
|
+
}
|
|
8472
|
+
const cells = {};
|
|
8473
|
+
for (const locale of shown) {
|
|
8474
|
+
const st = cellState(entry, locale, sourceLocale);
|
|
8475
|
+
if (stateSet && locale !== sourceLocale && !stateSet.has(st)) continue;
|
|
8476
|
+
const lv = entry.values[locale];
|
|
8477
|
+
const cell = entry.plural ? { forms: lv?.forms ?? {}, state: st, updatedAt: lv?.updatedAt } : { value: lv?.value ?? "", state: st, updatedAt: lv?.updatedAt };
|
|
8478
|
+
const projected = projectCell(cell, fields);
|
|
8479
|
+
cells[locale] = projected;
|
|
8480
|
+
ndjson.push({ key, locale, ...projected });
|
|
8481
|
+
}
|
|
8482
|
+
json[key] = cells;
|
|
8483
|
+
}
|
|
8484
|
+
return { keys, json, ndjson };
|
|
8485
|
+
}
|
|
8486
|
+
function applyOne(state, op, clock) {
|
|
8487
|
+
switch (op.op) {
|
|
8488
|
+
case "create":
|
|
8489
|
+
createKey(state, op.key, op.value, clock);
|
|
8490
|
+
return;
|
|
8491
|
+
case "set-source":
|
|
8492
|
+
setSourceValue(state, op.key, op.value);
|
|
8493
|
+
return;
|
|
8494
|
+
case "set-target":
|
|
8495
|
+
setTargetValue(state, op.key, op.locale, op.value, clock);
|
|
8496
|
+
if (op.state && op.state !== "reviewed") setKeyState(state, op.key, op.locale, op.state);
|
|
8497
|
+
return;
|
|
8498
|
+
case "set-source-forms":
|
|
8499
|
+
setSourcePluralForms(state, op.key, op.forms);
|
|
8500
|
+
return;
|
|
8501
|
+
case "set-forms":
|
|
8502
|
+
setPluralForms(state, op.key, op.locale, op.forms, clock);
|
|
8503
|
+
if (op.state && op.state !== "reviewed") setKeyState(state, op.key, op.locale, op.state);
|
|
8504
|
+
return;
|
|
8505
|
+
case "set-state":
|
|
8506
|
+
setKeyState(state, op.key, op.locale, op.state);
|
|
8507
|
+
return;
|
|
8508
|
+
case "clear":
|
|
8509
|
+
clearValue(state, op.key, op.locale);
|
|
8510
|
+
return;
|
|
8511
|
+
default:
|
|
8512
|
+
throw new Error(`Unknown op: ${op.op ?? "(missing)"}`);
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
function parseOps(raw) {
|
|
8516
|
+
let data;
|
|
8517
|
+
try {
|
|
8518
|
+
data = JSON.parse(raw);
|
|
8519
|
+
} catch (e) {
|
|
8520
|
+
throw new Error(`apply expects a JSON array of operations on stdin: ${e.message}`);
|
|
8521
|
+
}
|
|
8522
|
+
if (!Array.isArray(data)) throw new Error("apply expects a JSON array of operations on stdin");
|
|
8523
|
+
return data.map((o, i) => {
|
|
8524
|
+
if (!o || typeof o !== "object" || typeof o.op !== "string") {
|
|
8525
|
+
throw new Error(`operation ${i} is not an { "op": ... } object`);
|
|
8526
|
+
}
|
|
8527
|
+
return o;
|
|
8528
|
+
});
|
|
8529
|
+
}
|
|
8530
|
+
function applyOps(state, ops, opts = {}) {
|
|
8531
|
+
const clock = opts.clock ?? systemClock;
|
|
8532
|
+
const touched = /* @__PURE__ */ new Set();
|
|
8533
|
+
const errors = [];
|
|
8534
|
+
let applied = 0;
|
|
8535
|
+
for (let i = 0; i < ops.length; i++) {
|
|
8536
|
+
const op = ops[i];
|
|
8537
|
+
try {
|
|
8538
|
+
applyOne(state, op, clock);
|
|
8539
|
+
applied++;
|
|
8540
|
+
if (op.key) touched.add(op.key);
|
|
8541
|
+
} catch (e) {
|
|
8542
|
+
errors.push({ index: i, op: op.op, key: op.key, error: e instanceof Error ? e.message : String(e) });
|
|
8543
|
+
if (!opts.continueOnError) break;
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
return { applied, keysTouched: [...touched].sort(), errors };
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
// src/server/cli.ts
|
|
8550
|
+
init_cell_state();
|
|
8551
|
+
init_glob();
|
|
8552
|
+
init_schema();
|
|
8391
8553
|
init_export_run();
|
|
8392
8554
|
init_storage();
|
|
8393
8555
|
init_ai();
|
|
@@ -8407,9 +8569,6 @@ init_usage();
|
|
|
8407
8569
|
init_context();
|
|
8408
8570
|
init_run2();
|
|
8409
8571
|
init_outputs();
|
|
8410
|
-
import { resolve as resolve11, dirname as dirname5, join as join19, basename as basename2 } from "path";
|
|
8411
|
-
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
8412
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8413
8572
|
|
|
8414
8573
|
// src/server/lint/locate.ts
|
|
8415
8574
|
function locate(rawText, key) {
|
|
@@ -8485,7 +8644,7 @@ function formatSarif(report, ctx) {
|
|
|
8485
8644
|
}
|
|
8486
8645
|
|
|
8487
8646
|
// src/server/cli.ts
|
|
8488
|
-
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch"];
|
|
8647
|
+
var COMMANDS = ["serve", "export", "translate", "lint", "check", "import", "sync", "build-context", "scan", "prune", "split", "skill", "batch", "get", "stats", "set", "set-state", "clear", "apply"];
|
|
8489
8648
|
var isCommand = (s) => s != null && COMMANDS.includes(s);
|
|
8490
8649
|
function parseArgs(argv) {
|
|
8491
8650
|
const statePath = resolve11(process.cwd(), "glotfile.json");
|
|
@@ -8561,9 +8720,21 @@ function parseArgs(argv) {
|
|
|
8561
8720
|
else if (flag === "--batch") args.batch = true;
|
|
8562
8721
|
else if (flag === "--wait") args.wait = true;
|
|
8563
8722
|
else if (flag === "--print") args.print = true;
|
|
8723
|
+
else if (flag === "--state" && next) {
|
|
8724
|
+
args.states = next.split(",");
|
|
8725
|
+
i++;
|
|
8726
|
+
} else if (flag === "--fields" && next) {
|
|
8727
|
+
args.fields = next.split(",");
|
|
8728
|
+
i++;
|
|
8729
|
+
} else if (flag === "--keys-only") args.keysOnly = true;
|
|
8730
|
+
else if (flag === "--value" && next) {
|
|
8731
|
+
args.value = next;
|
|
8732
|
+
i++;
|
|
8733
|
+
} else if (flag === "--create") args.create = true;
|
|
8734
|
+
else if (flag === "--continue-on-error") args.continueOnError = true;
|
|
8564
8735
|
else if (args.command === "batch" && (flag === "status" || flag === "apply" || flag === "cancel")) {
|
|
8565
8736
|
args.batchAction = flag;
|
|
8566
|
-
}
|
|
8737
|
+
} else if (!flag.startsWith("-")) (args.positionals ??= []).push(flag);
|
|
8567
8738
|
}
|
|
8568
8739
|
return args;
|
|
8569
8740
|
}
|
|
@@ -8623,16 +8794,42 @@ function makeProviderOrExit(ai) {
|
|
|
8623
8794
|
return null;
|
|
8624
8795
|
}
|
|
8625
8796
|
}
|
|
8797
|
+
function parseStates(args, allowSource) {
|
|
8798
|
+
if (!args.states?.length) return void 0;
|
|
8799
|
+
const allowed = allowSource ? EFFECTIVE_STATES : EFFECTIVE_STATES.filter((s) => s !== "source");
|
|
8800
|
+
for (const s of args.states) {
|
|
8801
|
+
if (!allowed.includes(s)) {
|
|
8802
|
+
console.error(`Unknown --state '${s}'. Expected one of: ${allowed.join(", ")}.`);
|
|
8803
|
+
process.exit(1);
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
return args.states;
|
|
8807
|
+
}
|
|
8808
|
+
function translateSelection(args) {
|
|
8809
|
+
const states = parseStates(args, false);
|
|
8810
|
+
return {
|
|
8811
|
+
locales: args.locales,
|
|
8812
|
+
keyGlob: args.keyGlob,
|
|
8813
|
+
...states ? { states } : { onlyMissing: args.all ? false : args.onlyMissing ?? true }
|
|
8814
|
+
};
|
|
8815
|
+
}
|
|
8816
|
+
function readStdin() {
|
|
8817
|
+
try {
|
|
8818
|
+
return readFileSync24(0, "utf8");
|
|
8819
|
+
} catch {
|
|
8820
|
+
return "";
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
function matchKeys(state, glob) {
|
|
8824
|
+
const re = globToRegExp(glob);
|
|
8825
|
+
return Object.keys(state.keys).filter((k) => re.test(k)).sort();
|
|
8826
|
+
}
|
|
8626
8827
|
async function runTranslate(args) {
|
|
8627
8828
|
const state = loadState(args.statePath);
|
|
8628
8829
|
const projectRoot = dirname5(resolve11(args.statePath));
|
|
8629
8830
|
if (args.estimate) {
|
|
8630
8831
|
const ai = loadLocalSettings(projectRoot).ai;
|
|
8631
|
-
const est = estimateTranslation(state, ai,
|
|
8632
|
-
onlyMissing: args.all ? false : args.onlyMissing ?? true,
|
|
8633
|
-
locales: args.locales,
|
|
8634
|
-
keyGlob: args.keyGlob
|
|
8635
|
-
});
|
|
8832
|
+
const est = estimateTranslation(state, ai, translateSelection(args));
|
|
8636
8833
|
if (!est.requests) {
|
|
8637
8834
|
console.log("Nothing to translate.");
|
|
8638
8835
|
return;
|
|
@@ -8651,13 +8848,7 @@ async function runTranslate(args) {
|
|
|
8651
8848
|
}
|
|
8652
8849
|
return;
|
|
8653
8850
|
}
|
|
8654
|
-
const reqs = selectRequests(state,
|
|
8655
|
-
// Default to translating only empty values; --all forces a full re-translate
|
|
8656
|
-
// (overwriting existing translations). --only missing stays as a no-op alias.
|
|
8657
|
-
onlyMissing: args.all ? false : args.onlyMissing ?? true,
|
|
8658
|
-
locales: args.locales,
|
|
8659
|
-
keyGlob: args.keyGlob
|
|
8660
|
-
});
|
|
8851
|
+
const reqs = selectRequests(state, translateSelection(args));
|
|
8661
8852
|
const toTranslate = [...reqs];
|
|
8662
8853
|
if (args.batch) {
|
|
8663
8854
|
if (!toTranslate.length) {
|
|
@@ -9217,6 +9408,172 @@ function runSkill(args) {
|
|
|
9217
9408
|
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
9218
9409
|
console.log(`Installed the glotfile skill to ${dest}. Restart Claude Code to pick it up.`);
|
|
9219
9410
|
}
|
|
9411
|
+
function runGetCmd(args) {
|
|
9412
|
+
const state = loadState(args.statePath);
|
|
9413
|
+
const keyGlobs = [...args.positionals ?? [], ...args.keyGlob ? [args.keyGlob] : []];
|
|
9414
|
+
const out = runGet(state, {
|
|
9415
|
+
keyGlobs: keyGlobs.length ? keyGlobs : void 0,
|
|
9416
|
+
locales: args.locales,
|
|
9417
|
+
states: parseStates(args, true),
|
|
9418
|
+
fields: args.fields
|
|
9419
|
+
});
|
|
9420
|
+
if (args.keysOnly) {
|
|
9421
|
+
for (const k of out.keys) console.log(k);
|
|
9422
|
+
return;
|
|
9423
|
+
}
|
|
9424
|
+
if (args.format === "ndjson") {
|
|
9425
|
+
for (const row of out.ndjson) console.log(JSON.stringify(row));
|
|
9426
|
+
return;
|
|
9427
|
+
}
|
|
9428
|
+
console.log(JSON.stringify(out.json, null, 2));
|
|
9429
|
+
}
|
|
9430
|
+
function runStatsCmd(args) {
|
|
9431
|
+
const state = loadState(args.statePath);
|
|
9432
|
+
const stats = computeStats(state);
|
|
9433
|
+
let locales = stats.locales;
|
|
9434
|
+
if (args.locales?.length) {
|
|
9435
|
+
const want = new Set(args.locales.map(canonLocale));
|
|
9436
|
+
locales = locales.filter((l) => want.has(l.locale));
|
|
9437
|
+
}
|
|
9438
|
+
if (args.format === "text") {
|
|
9439
|
+
console.log(`${stats.totals.keys} key(s) \xB7 ${stats.totals.locales} target locale(s) \xB7 ${stats.totals.translatedPct}% translated, ${stats.totals.reviewedPct}% reviewed`);
|
|
9440
|
+
for (const l of locales) {
|
|
9441
|
+
const c = l.counts;
|
|
9442
|
+
console.log(` ${l.locale.padEnd(8)} ${String(l.translatedPct).padStart(5)}% translated (reviewed ${c.reviewed}, machine ${c.machine}, needs-review ${c.needsReview}, missing ${c.missing})`);
|
|
9443
|
+
}
|
|
9444
|
+
return;
|
|
9445
|
+
}
|
|
9446
|
+
console.log(JSON.stringify({ totals: stats.totals, locales }, null, 2));
|
|
9447
|
+
}
|
|
9448
|
+
function countNeedsReview(state, key) {
|
|
9449
|
+
const entry = state.keys[key];
|
|
9450
|
+
if (!entry) return 0;
|
|
9451
|
+
let n = 0;
|
|
9452
|
+
for (const [loc, lv] of Object.entries(entry.values)) {
|
|
9453
|
+
if (loc !== state.config.sourceLocale && lv.state === "needs-review") n++;
|
|
9454
|
+
}
|
|
9455
|
+
return n;
|
|
9456
|
+
}
|
|
9457
|
+
function runSet(args) {
|
|
9458
|
+
const pos = args.positionals ?? [];
|
|
9459
|
+
const key = pos[0];
|
|
9460
|
+
if (!key) {
|
|
9461
|
+
console.error("Usage: glotfile set <key> [value] [--locale <code>] [--state <state>] [--create]");
|
|
9462
|
+
process.exitCode = 1;
|
|
9463
|
+
return;
|
|
9464
|
+
}
|
|
9465
|
+
let value = args.value ?? pos[1];
|
|
9466
|
+
if (value === void 0) {
|
|
9467
|
+
const piped = readStdin();
|
|
9468
|
+
value = piped.length ? piped.replace(/\r?\n$/, "") : void 0;
|
|
9469
|
+
}
|
|
9470
|
+
if (value === void 0) {
|
|
9471
|
+
console.error('set requires a value (positional, --value, or piped on stdin). Use --value "" to set an empty value.');
|
|
9472
|
+
process.exitCode = 1;
|
|
9473
|
+
return;
|
|
9474
|
+
}
|
|
9475
|
+
const state = loadState(args.statePath);
|
|
9476
|
+
const sl = state.config.sourceLocale;
|
|
9477
|
+
const locale = args.locales?.[0] ? canonLocale(args.locales[0]) : sl;
|
|
9478
|
+
try {
|
|
9479
|
+
if (locale === sl) {
|
|
9480
|
+
if (args.create && !state.keys[key]) createKey(state, key, value);
|
|
9481
|
+
const before = countNeedsReview(state, key);
|
|
9482
|
+
setSourceValue(state, key, value);
|
|
9483
|
+
saveState(args.statePath, state);
|
|
9484
|
+
const flipped = countNeedsReview(state, key) - before;
|
|
9485
|
+
console.log(`set ${key} (${sl})${flipped > 0 ? ` \u2014 ${flipped} translation(s) now need re-translation (run \`glotfile translate --state needs-review\`)` : ""}`);
|
|
9486
|
+
} else {
|
|
9487
|
+
setTargetValue(state, key, locale, value);
|
|
9488
|
+
const override = args.states?.[0];
|
|
9489
|
+
if (override && override !== "reviewed") setKeyState(state, key, locale, override);
|
|
9490
|
+
saveState(args.statePath, state);
|
|
9491
|
+
console.log(`set ${key} (${locale})`);
|
|
9492
|
+
}
|
|
9493
|
+
} catch (e) {
|
|
9494
|
+
console.error(e.message);
|
|
9495
|
+
process.exitCode = 1;
|
|
9496
|
+
}
|
|
9497
|
+
}
|
|
9498
|
+
function runSetStateCmd(args) {
|
|
9499
|
+
const pos = args.positionals ?? [];
|
|
9500
|
+
const sel = args.keyGlob ?? pos[0];
|
|
9501
|
+
const stateName = args.keyGlob ? pos[0] : pos[1];
|
|
9502
|
+
if (!sel || !stateName) {
|
|
9503
|
+
console.error("Usage: glotfile set-state <key|glob> <state> [--locale <list>] (state: machine | needs-review | reviewed)");
|
|
9504
|
+
process.exitCode = 1;
|
|
9505
|
+
return;
|
|
9506
|
+
}
|
|
9507
|
+
if (!STATES.includes(stateName)) {
|
|
9508
|
+
console.error(`Unknown state '${stateName}'. Expected one of: ${STATES.join(", ")}.`);
|
|
9509
|
+
process.exitCode = 1;
|
|
9510
|
+
return;
|
|
9511
|
+
}
|
|
9512
|
+
const state = loadState(args.statePath);
|
|
9513
|
+
const sl = state.config.sourceLocale;
|
|
9514
|
+
const locales = (args.locales?.length ? args.locales : state.config.locales.filter((l) => l !== sl)).map(canonLocale);
|
|
9515
|
+
const keys = matchKeys(state, sel);
|
|
9516
|
+
let n = 0;
|
|
9517
|
+
for (const key of keys) {
|
|
9518
|
+
for (const loc of locales) {
|
|
9519
|
+
if (state.keys[key].values[loc]) {
|
|
9520
|
+
setKeyState(state, key, loc, stateName);
|
|
9521
|
+
n++;
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
saveState(args.statePath, state);
|
|
9526
|
+
console.log(`Set ${n} cell(s) to ${stateName} across ${keys.length} key(s).`);
|
|
9527
|
+
}
|
|
9528
|
+
function runClearCmd(args) {
|
|
9529
|
+
const sel = args.keyGlob ?? args.positionals?.[0];
|
|
9530
|
+
if (!sel) {
|
|
9531
|
+
console.error("Usage: glotfile clear <key|glob> --locale <list>");
|
|
9532
|
+
process.exitCode = 1;
|
|
9533
|
+
return;
|
|
9534
|
+
}
|
|
9535
|
+
if (!args.locales?.length) {
|
|
9536
|
+
console.error("clear requires --locale <list> (the locale(s) to empty).");
|
|
9537
|
+
process.exitCode = 1;
|
|
9538
|
+
return;
|
|
9539
|
+
}
|
|
9540
|
+
const state = loadState(args.statePath);
|
|
9541
|
+
const sl = state.config.sourceLocale;
|
|
9542
|
+
const locales = args.locales.map(canonLocale);
|
|
9543
|
+
if (locales.includes(sl)) {
|
|
9544
|
+
console.error(`Cannot clear the source locale (${sl}); edit it with \`glotfile set\` instead.`);
|
|
9545
|
+
process.exitCode = 1;
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
const keys = matchKeys(state, sel);
|
|
9549
|
+
let n = 0;
|
|
9550
|
+
for (const key of keys) {
|
|
9551
|
+
for (const loc of locales) {
|
|
9552
|
+
if (state.keys[key].values[loc]) {
|
|
9553
|
+
clearValue(state, key, loc);
|
|
9554
|
+
n++;
|
|
9555
|
+
}
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
saveState(args.statePath, state);
|
|
9559
|
+
console.log(`Cleared ${n} value(s) \u2192 untranslated.`);
|
|
9560
|
+
}
|
|
9561
|
+
function runApply(args) {
|
|
9562
|
+
let ops;
|
|
9563
|
+
try {
|
|
9564
|
+
ops = parseOps(readStdin());
|
|
9565
|
+
} catch (e) {
|
|
9566
|
+
console.error(e.message);
|
|
9567
|
+
process.exitCode = 1;
|
|
9568
|
+
return;
|
|
9569
|
+
}
|
|
9570
|
+
const state = loadState(args.statePath);
|
|
9571
|
+
const r = applyOps(state, ops, { continueOnError: args.continueOnError });
|
|
9572
|
+
const saved = (args.continueOnError || r.errors.length === 0) && !args.dryRun;
|
|
9573
|
+
if (saved) saveState(args.statePath, state);
|
|
9574
|
+
console.log(JSON.stringify({ applied: r.applied, keysTouched: r.keysTouched, saved, dryRun: !!args.dryRun, errors: r.errors }, null, 2));
|
|
9575
|
+
if (r.errors.length) process.exitCode = 1;
|
|
9576
|
+
}
|
|
9220
9577
|
var GLOBAL_OPTS = [
|
|
9221
9578
|
["-f, --file <path>", "State file to use (default: ./glotfile.json)"],
|
|
9222
9579
|
["-h, --help", "Show this help"]
|
|
@@ -9240,9 +9597,10 @@ var COMMAND_HELP = {
|
|
|
9240
9597
|
},
|
|
9241
9598
|
translate: {
|
|
9242
9599
|
summary: "AI-translate missing strings into your target locales (writes back to the state file).",
|
|
9243
|
-
usage: "glotfile translate [--all] [--estimate] [--locale <list>] [--key <glob>]",
|
|
9600
|
+
usage: "glotfile translate [--all] [--state <list>] [--estimate] [--locale <list>] [--key <glob>]",
|
|
9244
9601
|
options: [
|
|
9245
9602
|
["--all", "Re-translate every string, not just empty values"],
|
|
9603
|
+
["--state <list>", "Re-translate only targets in these states: missing|machine|needs-review|reviewed (e.g. needs-review = strings a source edit invalidated)"],
|
|
9246
9604
|
["--estimate", "Print batches, tokens and estimated cost without translating"],
|
|
9247
9605
|
["--locale <list>", "Comma-separated target locales (alias: --locales)"],
|
|
9248
9606
|
["--key <glob>", "Only keys matching this glob"],
|
|
@@ -9337,6 +9695,64 @@ var COMMAND_HELP = {
|
|
|
9337
9695
|
["apply", "Fetch results and write translations (auto-runs when finished)"],
|
|
9338
9696
|
["cancel", "Cancel the pending batch and discard the handle"]
|
|
9339
9697
|
]
|
|
9698
|
+
},
|
|
9699
|
+
get: {
|
|
9700
|
+
summary: "Extract values from the catalog (filtered) without loading the whole file. Prints JSON.",
|
|
9701
|
+
usage: "glotfile get [<key-glob>\u2026] [--key <glob>] [--locale <list>] [--state <list>] [--fields <list>] [--keys-only] [--format json|ndjson]",
|
|
9702
|
+
options: [
|
|
9703
|
+
["<key-glob>\u2026", "Key globs to include (e.g. auth.*); positional, repeatable. Default: all keys"],
|
|
9704
|
+
["--key <glob>", "Additional key glob (merged with positionals)"],
|
|
9705
|
+
["--locale <list>", "Locales to show (default: all configured locales, source included)"],
|
|
9706
|
+
["--state <list>", "Only keys whose shown target locales are in these states: source|missing|machine|needs-review|reviewed"],
|
|
9707
|
+
["--fields <list>", "Cell fields to project: value,state,updatedAt (default value,state); 'all' = the full key entry"],
|
|
9708
|
+
["--keys-only", "Print just the matched key names, one per line"],
|
|
9709
|
+
["--format <fmt>", "json (default, nested) or ndjson (one row per cell)"]
|
|
9710
|
+
]
|
|
9711
|
+
},
|
|
9712
|
+
stats: {
|
|
9713
|
+
summary: "Per-locale progress counts (translated / reviewed / machine / needs-review / missing).",
|
|
9714
|
+
usage: "glotfile stats [--locale <list>] [--format json|text]",
|
|
9715
|
+
options: [
|
|
9716
|
+
["--locale <list>", "Restrict to these comma-separated locales"],
|
|
9717
|
+
["--format <fmt>", "json (default) or text"]
|
|
9718
|
+
]
|
|
9719
|
+
},
|
|
9720
|
+
set: {
|
|
9721
|
+
summary: "Set one value: the source string (default \u2014 flips downstream translations to needs-review) or a target (--locale).",
|
|
9722
|
+
usage: "glotfile set <key> [value] [--locale <code>] [--state <state>] [--create]",
|
|
9723
|
+
options: [
|
|
9724
|
+
["<key> [value]", "Key, then the value (or pass --value, or pipe it on stdin)"],
|
|
9725
|
+
["--locale <code>", "Set this target locale instead of the source"],
|
|
9726
|
+
["--value <v>", "The value (alternative to the positional / stdin)"],
|
|
9727
|
+
["--state <state>", "Resulting state for a target write (default reviewed): machine|needs-review|reviewed"],
|
|
9728
|
+
["--create", "Create the key (scalar) if it does not exist yet"]
|
|
9729
|
+
]
|
|
9730
|
+
},
|
|
9731
|
+
"set-state": {
|
|
9732
|
+
summary: "Flip the review state of one key \u2014 or many via a glob \u2014 across locales.",
|
|
9733
|
+
usage: "glotfile set-state <key|glob> <state> [--locale <list>]",
|
|
9734
|
+
options: [
|
|
9735
|
+
["<key|glob> <state>", "Key/glob, then machine | needs-review | reviewed"],
|
|
9736
|
+
["--key <glob>", "Glob selecting keys (alternative to the positional key)"],
|
|
9737
|
+
["--locale <list>", "Locales to affect (default: every target locale)"]
|
|
9738
|
+
]
|
|
9739
|
+
},
|
|
9740
|
+
clear: {
|
|
9741
|
+
summary: "Empty target value(s) so they read as untranslated (and get refilled by a plain translate).",
|
|
9742
|
+
usage: "glotfile clear <key|glob> --locale <list>",
|
|
9743
|
+
options: [
|
|
9744
|
+
["<key|glob>", "Key or glob to clear"],
|
|
9745
|
+
["--key <glob>", "Glob selecting keys (alternative to the positional key)"],
|
|
9746
|
+
["--locale <list>", "Required: the locale(s) to empty (cannot be the source)"]
|
|
9747
|
+
]
|
|
9748
|
+
},
|
|
9749
|
+
apply: {
|
|
9750
|
+
summary: "Apply a JSON batch of write operations from stdin in one load \u2192 save (atomic by default).",
|
|
9751
|
+
usage: "glotfile apply [--dry-run] [--continue-on-error] < ops.json",
|
|
9752
|
+
options: [
|
|
9753
|
+
["--dry-run", "Report what would change without writing"],
|
|
9754
|
+
["--continue-on-error", "Apply the survivors past a failing op instead of stopping (and saving nothing)"]
|
|
9755
|
+
]
|
|
9340
9756
|
}
|
|
9341
9757
|
};
|
|
9342
9758
|
function formatOpts(opts) {
|
|
@@ -9395,6 +9811,12 @@ async function main(argv) {
|
|
|
9395
9811
|
if (args.command === "split") return runSplit(args);
|
|
9396
9812
|
if (args.command === "skill") return runSkill(args);
|
|
9397
9813
|
if (args.command === "batch") return runBatch(args);
|
|
9814
|
+
if (args.command === "get") return runGetCmd(args);
|
|
9815
|
+
if (args.command === "stats") return runStatsCmd(args);
|
|
9816
|
+
if (args.command === "set") return runSet(args);
|
|
9817
|
+
if (args.command === "set-state") return runSetStateCmd(args);
|
|
9818
|
+
if (args.command === "clear") return runClearCmd(args);
|
|
9819
|
+
if (args.command === "apply") return runApply(args);
|
|
9398
9820
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
9399
9821
|
const { url } = await startServer2({ statePath: args.statePath, dev: args.dev, open: !args.noOpen });
|
|
9400
9822
|
if (args.dev) console.log(`Glotfile dev API on ${url} \u2014 open the UI at the Vite "Local:" URL above`);
|