glotfile 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server/cli.js
CHANGED
|
@@ -187,7 +187,7 @@ function validate(raw) {
|
|
|
187
187
|
if (config.scan !== void 0) {
|
|
188
188
|
const scan = config.scan;
|
|
189
189
|
if (!isObject(scan)) fail("config.scan must be an object");
|
|
190
|
-
for (const f of ["include", "exclude", "accessors", "patterns"]) {
|
|
190
|
+
for (const f of ["include", "exclude", "accessors", "patterns", "keep"]) {
|
|
191
191
|
const v = scan[f];
|
|
192
192
|
if (v !== void 0 && (!Array.isArray(v) || !v.every((x) => typeof x === "string"))) {
|
|
193
193
|
fail(`config.scan.${f} must be an array of strings`);
|
|
@@ -1981,7 +1981,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
1981
1981
|
"- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
|
|
1982
1982
|
"- 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.",
|
|
1983
1983
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
1984
|
-
|
|
1984
|
+
'- Quotation marks and apostrophes: punctuate exactly as a professional native translator instinctively would for the target language \u2014 its typographic conventions (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes), applied with judgment about what is quoted prose versus a literal that must stay untouched. Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply.',
|
|
1985
1985
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
1986
1986
|
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
1987
1987
|
];
|
|
@@ -2071,12 +2071,66 @@ var init_provider = __esm({
|
|
|
2071
2071
|
});
|
|
2072
2072
|
|
|
2073
2073
|
// src/server/ai/batch.ts
|
|
2074
|
+
function repairUnescapedQuotes(text) {
|
|
2075
|
+
const skipWs = (from) => {
|
|
2076
|
+
let i = from;
|
|
2077
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2078
|
+
return i;
|
|
2079
|
+
};
|
|
2080
|
+
const stack = [];
|
|
2081
|
+
let out = "";
|
|
2082
|
+
let inString = false;
|
|
2083
|
+
let isKey = false;
|
|
2084
|
+
for (let i = 0; i < text.length; i++) {
|
|
2085
|
+
const ch = text[i];
|
|
2086
|
+
const top = stack[stack.length - 1];
|
|
2087
|
+
if (inString) {
|
|
2088
|
+
if (ch === "\\") {
|
|
2089
|
+
out += ch + (text[i + 1] ?? "");
|
|
2090
|
+
i++;
|
|
2091
|
+
} else if (ch !== '"') {
|
|
2092
|
+
out += ch;
|
|
2093
|
+
} else {
|
|
2094
|
+
const next = text[skipWs(i + 1)];
|
|
2095
|
+
const startsNextMember = () => {
|
|
2096
|
+
const after = text[skipWs(skipWs(i + 1) + 1)];
|
|
2097
|
+
return top?.type === "obj" ? after === '"' : after === "{" || after === "[" || after === '"';
|
|
2098
|
+
};
|
|
2099
|
+
const closes = isKey ? next === ":" : next === "}" || next === "]" || next === void 0 || next === "," && startsNextMember();
|
|
2100
|
+
if (closes) {
|
|
2101
|
+
inString = false;
|
|
2102
|
+
out += ch;
|
|
2103
|
+
} else {
|
|
2104
|
+
out += '\\"';
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
out += ch;
|
|
2110
|
+
if (ch === '"') {
|
|
2111
|
+
inString = true;
|
|
2112
|
+
isKey = top?.type === "obj" && top.expectingKey;
|
|
2113
|
+
} else if (ch === "{") stack.push({ type: "obj", expectingKey: true });
|
|
2114
|
+
else if (ch === "[") stack.push({ type: "arr", expectingKey: false });
|
|
2115
|
+
else if (ch === "}" || ch === "]") stack.pop();
|
|
2116
|
+
else if (ch === "," && top?.type === "obj") top.expectingKey = true;
|
|
2117
|
+
else if (ch === ":" && top) top.expectingKey = false;
|
|
2118
|
+
}
|
|
2119
|
+
try {
|
|
2120
|
+
JSON.parse(out);
|
|
2121
|
+
return out;
|
|
2122
|
+
} catch {
|
|
2123
|
+
return void 0;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2074
2126
|
function parseReplyItems(text) {
|
|
2075
2127
|
let parsed;
|
|
2076
2128
|
try {
|
|
2077
2129
|
parsed = JSON.parse(text);
|
|
2078
2130
|
} catch {
|
|
2079
|
-
|
|
2131
|
+
const repaired = repairUnescapedQuotes(text);
|
|
2132
|
+
if (repaired === void 0) throw new MalformedReplyError(text);
|
|
2133
|
+
parsed = JSON.parse(repaired);
|
|
2080
2134
|
}
|
|
2081
2135
|
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2082
2136
|
return parsed.items;
|
|
@@ -3171,13 +3225,15 @@ function computeUsedKeys(state, cache2) {
|
|
|
3171
3225
|
if (p.prefix) prefixes.push(p.prefix);
|
|
3172
3226
|
}
|
|
3173
3227
|
}
|
|
3174
|
-
|
|
3228
|
+
const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
|
|
3229
|
+
return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || keep.some((re) => re.test(key))).sort();
|
|
3175
3230
|
}
|
|
3176
3231
|
var init_scan = __esm({
|
|
3177
3232
|
"src/server/scan.ts"() {
|
|
3178
3233
|
"use strict";
|
|
3179
3234
|
init_atomic_write();
|
|
3180
3235
|
init_glotfile_dir();
|
|
3236
|
+
init_glob();
|
|
3181
3237
|
}
|
|
3182
3238
|
});
|
|
3183
3239
|
|
package/dist/server/server.js
CHANGED
|
@@ -186,7 +186,7 @@ function validate(raw) {
|
|
|
186
186
|
if (config.scan !== void 0) {
|
|
187
187
|
const scan = config.scan;
|
|
188
188
|
if (!isObject(scan)) fail("config.scan must be an object");
|
|
189
|
-
for (const f of ["include", "exclude", "accessors", "patterns"]) {
|
|
189
|
+
for (const f of ["include", "exclude", "accessors", "patterns", "keep"]) {
|
|
190
190
|
const v = scan[f];
|
|
191
191
|
if (v !== void 0 && (!Array.isArray(v) || !v.every((x) => typeof x === "string"))) {
|
|
192
192
|
fail(`config.scan.${f} must be an array of strings`);
|
|
@@ -817,6 +817,12 @@ function ensureGlotfileDir(projectRoot) {
|
|
|
817
817
|
return dir;
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
+
// src/server/glob.ts
|
|
821
|
+
function globToRegExp(glob) {
|
|
822
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
823
|
+
return new RegExp(`^${escaped}$`);
|
|
824
|
+
}
|
|
825
|
+
|
|
820
826
|
// src/server/scan.ts
|
|
821
827
|
function loadUsageCache(projectRoot) {
|
|
822
828
|
const path = resolve2(projectRoot, ".glotfile", "usage.json");
|
|
@@ -854,7 +860,8 @@ function computeUsedKeys(state, cache2) {
|
|
|
854
860
|
if (p.prefix) prefixes.push(p.prefix);
|
|
855
861
|
}
|
|
856
862
|
}
|
|
857
|
-
|
|
863
|
+
const keep = (state.config.scan?.keep ?? []).map(globToRegExp);
|
|
864
|
+
return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p)) || keep.some((re) => re.test(key))).sort();
|
|
858
865
|
}
|
|
859
866
|
|
|
860
867
|
// src/server/scanner.ts
|
|
@@ -1139,7 +1146,7 @@ var MAX_CONTEXT_LENGTH = 500;
|
|
|
1139
1146
|
var SNIPPET_WINDOW = 15;
|
|
1140
1147
|
var MAX_SNIPPETS = 3;
|
|
1141
1148
|
var EXCLUDED_DIRS = ["node_modules/", "vendor/", "dist/", ".git/", ".glotfile/"];
|
|
1142
|
-
function
|
|
1149
|
+
function globToRegExp2(glob) {
|
|
1143
1150
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1144
1151
|
return new RegExp(`^${escaped}$`);
|
|
1145
1152
|
}
|
|
@@ -1182,7 +1189,7 @@ function buildUsageIndex(cache2) {
|
|
|
1182
1189
|
}
|
|
1183
1190
|
function selectContextTargets(state, opts, cache2, lastRunAt) {
|
|
1184
1191
|
const cutoff = opts.all ? void 0 : opts.since ?? lastRunAt;
|
|
1185
|
-
const keyRe = opts.keyGlob ?
|
|
1192
|
+
const keyRe = opts.keyGlob ? globToRegExp2(opts.keyGlob) : null;
|
|
1186
1193
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
1187
1194
|
const usageIndex = buildUsageIndex(cache2);
|
|
1188
1195
|
let candidates = [];
|
|
@@ -1678,12 +1685,6 @@ function runChecks(state, opts = {}) {
|
|
|
1678
1685
|
return { issues: visible, spellPending };
|
|
1679
1686
|
}
|
|
1680
1687
|
|
|
1681
|
-
// src/server/glob.ts
|
|
1682
|
-
function globToRegExp2(glob) {
|
|
1683
|
-
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1684
|
-
return new RegExp(`^${escaped}$`);
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
1688
|
// src/server/lint/spelling.ts
|
|
1688
1689
|
var spellingRule = {
|
|
1689
1690
|
id: "spelling",
|
|
@@ -1933,7 +1934,7 @@ async function runLint(state, options = {}) {
|
|
|
1933
1934
|
spellers,
|
|
1934
1935
|
allowWords
|
|
1935
1936
|
};
|
|
1936
|
-
const ignoreRes = (config.ignore ?? []).map(
|
|
1937
|
+
const ignoreRes = (config.ignore ?? []).map(globToRegExp);
|
|
1937
1938
|
const localeFilter = options.locales ? new Set(options.locales) : null;
|
|
1938
1939
|
const findings = [];
|
|
1939
1940
|
let suppressed = 0;
|
|
@@ -2806,7 +2807,7 @@ function buildSystemPrompt(hasPluralItems) {
|
|
|
2806
2807
|
"- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
|
|
2807
2808
|
"- 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.",
|
|
2808
2809
|
"- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
|
|
2809
|
-
|
|
2810
|
+
'- Quotation marks and apostrophes: punctuate exactly as a professional native translator instinctively would for the target language \u2014 its typographic conventions (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes), applied with judgment about what is quoted prose versus a literal that must stay untouched. Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply.',
|
|
2810
2811
|
"- Match the register and capitalization conventions of the target language and of UI microcopy.",
|
|
2811
2812
|
"- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
|
|
2812
2813
|
];
|
|
@@ -2898,12 +2899,66 @@ var MalformedReplyError = class extends Error {
|
|
|
2898
2899
|
}
|
|
2899
2900
|
raw;
|
|
2900
2901
|
};
|
|
2902
|
+
function repairUnescapedQuotes(text) {
|
|
2903
|
+
const skipWs = (from) => {
|
|
2904
|
+
let i = from;
|
|
2905
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2906
|
+
return i;
|
|
2907
|
+
};
|
|
2908
|
+
const stack = [];
|
|
2909
|
+
let out = "";
|
|
2910
|
+
let inString = false;
|
|
2911
|
+
let isKey = false;
|
|
2912
|
+
for (let i = 0; i < text.length; i++) {
|
|
2913
|
+
const ch = text[i];
|
|
2914
|
+
const top = stack[stack.length - 1];
|
|
2915
|
+
if (inString) {
|
|
2916
|
+
if (ch === "\\") {
|
|
2917
|
+
out += ch + (text[i + 1] ?? "");
|
|
2918
|
+
i++;
|
|
2919
|
+
} else if (ch !== '"') {
|
|
2920
|
+
out += ch;
|
|
2921
|
+
} else {
|
|
2922
|
+
const next = text[skipWs(i + 1)];
|
|
2923
|
+
const startsNextMember = () => {
|
|
2924
|
+
const after = text[skipWs(skipWs(i + 1) + 1)];
|
|
2925
|
+
return top?.type === "obj" ? after === '"' : after === "{" || after === "[" || after === '"';
|
|
2926
|
+
};
|
|
2927
|
+
const closes = isKey ? next === ":" : next === "}" || next === "]" || next === void 0 || next === "," && startsNextMember();
|
|
2928
|
+
if (closes) {
|
|
2929
|
+
inString = false;
|
|
2930
|
+
out += ch;
|
|
2931
|
+
} else {
|
|
2932
|
+
out += '\\"';
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
out += ch;
|
|
2938
|
+
if (ch === '"') {
|
|
2939
|
+
inString = true;
|
|
2940
|
+
isKey = top?.type === "obj" && top.expectingKey;
|
|
2941
|
+
} else if (ch === "{") stack.push({ type: "obj", expectingKey: true });
|
|
2942
|
+
else if (ch === "[") stack.push({ type: "arr", expectingKey: false });
|
|
2943
|
+
else if (ch === "}" || ch === "]") stack.pop();
|
|
2944
|
+
else if (ch === "," && top?.type === "obj") top.expectingKey = true;
|
|
2945
|
+
else if (ch === ":" && top) top.expectingKey = false;
|
|
2946
|
+
}
|
|
2947
|
+
try {
|
|
2948
|
+
JSON.parse(out);
|
|
2949
|
+
return out;
|
|
2950
|
+
} catch {
|
|
2951
|
+
return void 0;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2901
2954
|
function parseReplyItems(text) {
|
|
2902
2955
|
let parsed;
|
|
2903
2956
|
try {
|
|
2904
2957
|
parsed = JSON.parse(text);
|
|
2905
2958
|
} catch {
|
|
2906
|
-
|
|
2959
|
+
const repaired = repairUnescapedQuotes(text);
|
|
2960
|
+
if (repaired === void 0) throw new MalformedReplyError(text);
|
|
2961
|
+
parsed = JSON.parse(repaired);
|
|
2907
2962
|
}
|
|
2908
2963
|
if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
|
|
2909
2964
|
return parsed.items;
|
|
@@ -3407,7 +3462,7 @@ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
|
3407
3462
|
import { resolve as resolve5, extname as extname2 } from "path";
|
|
3408
3463
|
function selectRequests(state, opts) {
|
|
3409
3464
|
const targets = (opts.locales ?? state.config.locales).filter((l) => l !== state.config.sourceLocale);
|
|
3410
|
-
const keyRe = opts.keyGlob ?
|
|
3465
|
+
const keyRe = opts.keyGlob ? globToRegExp(opts.keyGlob) : null;
|
|
3411
3466
|
const keySet = opts.keys ? new Set(opts.keys) : null;
|
|
3412
3467
|
const reqs = [];
|
|
3413
3468
|
let id = 0;
|