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.
@@ -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
- `- Quotation marks inside a translation MUST use the target language's typographic quote characters (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes). Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply. If the source uses ASCII quotes, convert them to the target language's typographic quotes.`,
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
- throw new MalformedReplyError(text);
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
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p))).sort();
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
 
@@ -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
- return Object.keys(state.keys).filter((key) => exact.has(key) || prefixes.some((p) => key.startsWith(p))).sort();
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 globToRegExp(glob) {
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 ? globToRegExp(opts.keyGlob) : null;
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(globToRegExp2);
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
- `- Quotation marks inside a translation MUST use the target language's typographic quote characters (e.g. \u201EGerman\u201C, \xABFrench\xBB, \u201CEnglish\u201D, \u2019 for apostrophes). Never emit a raw ASCII double-quote (") inside a translated string \u2014 it corrupts the JSON reply. If the source uses ASCII quotes, convert them to the target language's typographic quotes.`,
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
- throw new MalformedReplyError(text);
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 ? globToRegExp2(opts.keyGlob) : null;
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;