glotfile 0.6.0 → 0.6.1

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.
@@ -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;
@@ -2806,7 +2806,7 @@ function buildSystemPrompt(hasPluralItems) {
2806
2806
  "- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
2807
2807
  "- 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
2808
  "- 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.`,
2809
+ '- 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
2810
  "- Match the register and capitalization conventions of the target language and of UI microcopy.",
2811
2811
  "- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
2812
2812
  ];
@@ -2898,12 +2898,66 @@ var MalformedReplyError = class extends Error {
2898
2898
  }
2899
2899
  raw;
2900
2900
  };
2901
+ function repairUnescapedQuotes(text) {
2902
+ const skipWs = (from) => {
2903
+ let i = from;
2904
+ while (i < text.length && /\s/.test(text[i])) i++;
2905
+ return i;
2906
+ };
2907
+ const stack = [];
2908
+ let out = "";
2909
+ let inString = false;
2910
+ let isKey = false;
2911
+ for (let i = 0; i < text.length; i++) {
2912
+ const ch = text[i];
2913
+ const top = stack[stack.length - 1];
2914
+ if (inString) {
2915
+ if (ch === "\\") {
2916
+ out += ch + (text[i + 1] ?? "");
2917
+ i++;
2918
+ } else if (ch !== '"') {
2919
+ out += ch;
2920
+ } else {
2921
+ const next = text[skipWs(i + 1)];
2922
+ const startsNextMember = () => {
2923
+ const after = text[skipWs(skipWs(i + 1) + 1)];
2924
+ return top?.type === "obj" ? after === '"' : after === "{" || after === "[" || after === '"';
2925
+ };
2926
+ const closes = isKey ? next === ":" : next === "}" || next === "]" || next === void 0 || next === "," && startsNextMember();
2927
+ if (closes) {
2928
+ inString = false;
2929
+ out += ch;
2930
+ } else {
2931
+ out += '\\"';
2932
+ }
2933
+ }
2934
+ continue;
2935
+ }
2936
+ out += ch;
2937
+ if (ch === '"') {
2938
+ inString = true;
2939
+ isKey = top?.type === "obj" && top.expectingKey;
2940
+ } else if (ch === "{") stack.push({ type: "obj", expectingKey: true });
2941
+ else if (ch === "[") stack.push({ type: "arr", expectingKey: false });
2942
+ else if (ch === "}" || ch === "]") stack.pop();
2943
+ else if (ch === "," && top?.type === "obj") top.expectingKey = true;
2944
+ else if (ch === ":" && top) top.expectingKey = false;
2945
+ }
2946
+ try {
2947
+ JSON.parse(out);
2948
+ return out;
2949
+ } catch {
2950
+ return void 0;
2951
+ }
2952
+ }
2901
2953
  function parseReplyItems(text) {
2902
2954
  let parsed;
2903
2955
  try {
2904
2956
  parsed = JSON.parse(text);
2905
2957
  } catch {
2906
- throw new MalformedReplyError(text);
2958
+ const repaired = repairUnescapedQuotes(text);
2959
+ if (repaired === void 0) throw new MalformedReplyError(text);
2960
+ parsed = JSON.parse(repaired);
2907
2961
  }
2908
2962
  if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
2909
2963
  return parsed.items;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glotfile",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Local-first, git-native translation management.",
5
5
  "type": "module",
6
6
  "bin": {