glotfile 0.5.4 → 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.
@@ -2,7 +2,7 @@
2
2
  import { Hono as Hono2 } from "hono";
3
3
  import { serve } from "@hono/node-server";
4
4
  import { fileURLToPath } from "url";
5
- import { dirname as dirname4, join as join10, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5
+ import { dirname as dirname4, join as join15, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
6
6
  import { readFile, stat } from "fs/promises";
7
7
  import { createServer } from "net";
8
8
  import open from "open";
@@ -121,6 +121,7 @@ function validate(raw) {
121
121
  if (o.indent !== void 0 && typeof o.indent !== "number") fail("config.outputs[].indent must be a number");
122
122
  if (o.finalNewline !== void 0 && typeof o.finalNewline !== "boolean") fail("config.outputs[].finalNewline must be a boolean");
123
123
  if (o.includeLocale !== void 0 && typeof o.includeLocale !== "boolean") fail("config.outputs[].includeLocale must be a boolean");
124
+ if (o.skipSourceLocale !== void 0 && typeof o.skipSourceLocale !== "boolean") fail("config.outputs[].skipSourceLocale must be a boolean");
124
125
  if (o.localeAliases !== void 0) {
125
126
  if (!isObject(o.localeAliases)) fail("config.outputs[].localeAliases must be an object");
126
127
  for (const [k, v] of Object.entries(o.localeAliases)) {
@@ -2535,27 +2536,41 @@ var vueI18nJson = {
2535
2536
  function xmlEscape2(s) {
2536
2537
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2537
2538
  }
2538
- function renderInterpolations(text, ids) {
2539
+ function attrEscape(s) {
2540
+ return xmlEscape2(s).replace(/"/g, "&quot;");
2541
+ }
2542
+ function angularXMeta(placeholders, name) {
2543
+ return /^[A-Z][A-Z0-9_]*$/.test(name) ? placeholders?.[name] : void 0;
2544
+ }
2545
+ function renderInterpolations(text, ids, placeholders) {
2539
2546
  let out = "";
2540
2547
  let last = 0;
2541
2548
  for (const m of text.matchAll(/\{(\w+)\}/g)) {
2542
2549
  const name = m[1];
2543
- let id = ids.get(name);
2544
- if (id === void 0) {
2545
- id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
2546
- ids.set(name, id);
2550
+ out += xmlEscape2(text.slice(last, m.index));
2551
+ const meta = angularXMeta(placeholders, name);
2552
+ if (meta) {
2553
+ const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
2554
+ const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
2555
+ out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
2556
+ } else {
2557
+ let id = ids.get(name);
2558
+ if (id === void 0) {
2559
+ id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
2560
+ ids.set(name, id);
2561
+ }
2562
+ out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
2547
2563
  }
2548
- out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
2549
2564
  last = m.index + m[0].length;
2550
2565
  }
2551
2566
  return out + xmlEscape2(text.slice(last));
2552
2567
  }
2553
- function renderPluralIcu(forms, ids) {
2568
+ function renderPluralIcu(forms, ids, placeholders) {
2554
2569
  const cats = [
2555
2570
  ...Object.keys(forms).filter((c) => c.startsWith("=")),
2556
2571
  ...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
2557
2572
  ];
2558
- const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
2573
+ const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids, placeholders)}}`);
2559
2574
  return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
2560
2575
  }
2561
2576
  function renderEmbeddedIcu(value) {
@@ -2565,8 +2580,8 @@ function renderEmbeddedIcu(value) {
2565
2580
  );
2566
2581
  return xmlEscape2(renamed);
2567
2582
  }
2568
- function renderScalar(value, ids) {
2569
- return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
2583
+ function renderScalar(value, ids, placeholders) {
2584
+ return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
2570
2585
  }
2571
2586
  var DEFAULT_LOCALE_CASE8 = "bcp47-hyphen";
2572
2587
  var angularXliff = {
@@ -2589,6 +2604,7 @@ var angularXliff = {
2589
2604
  const emptyAs = resolveEmptyAs(output, "source");
2590
2605
  const keys = Object.keys(state.keys).sort();
2591
2606
  for (const locale of state.config.locales) {
2607
+ if (output.skipSourceLocale && locale === sourceLocale) continue;
2592
2608
  const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
2593
2609
  const units = [];
2594
2610
  for (const key of keys) {
@@ -2599,17 +2615,18 @@ var angularXliff = {
2599
2615
  if (entry.plural) {
2600
2616
  const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
2601
2617
  if (targetForms === null) continue;
2602
- source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
2603
- target = renderPluralIcu(targetForms, ids);
2618
+ source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids, entry.placeholders);
2619
+ target = renderPluralIcu(targetForms, ids, entry.placeholders);
2604
2620
  } else {
2605
2621
  const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
2606
2622
  if (targetValue === null) continue;
2607
- source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
2608
- target = renderScalar(targetValue, ids);
2623
+ source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids, entry.placeholders);
2624
+ target = renderScalar(targetValue, ids, entry.placeholders);
2609
2625
  }
2626
+ const translated = locale === sourceLocale || (entry.plural ? entry.values[locale]?.forms !== void 0 : !!entry.values[locale]?.value);
2610
2627
  units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
2611
2628
  units.push(` <source>${source}</source>`);
2612
- units.push(` <target>${target}</target>`);
2629
+ units.push(` <target${translated ? "" : ' state="new"'}>${target}</target>`);
2613
2630
  if (entry.description) {
2614
2631
  units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
2615
2632
  }
@@ -2770,7 +2787,7 @@ function checkOutputs(state, root) {
2770
2787
  }
2771
2788
 
2772
2789
  // src/server/api.ts
2773
- import { readFileSync as readFileSync15, existsSync as existsSync11, readdirSync as readdirSync9, statSync as statSync6, rmSync as rmSync4 } from "fs";
2790
+ import { readFileSync as readFileSync21, existsSync as existsSync11, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync4 } from "fs";
2774
2791
  import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
2775
2792
 
2776
2793
  // src/server/ai/anthropic.ts
@@ -2789,7 +2806,7 @@ function buildSystemPrompt(hasPluralItems) {
2789
2806
  "- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
2790
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.",
2791
2808
  "- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
2792
- `- 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.',
2793
2810
  "- Match the register and capitalization conventions of the target language and of UI microcopy.",
2794
2811
  "- Return ONLY the translated string for each item \u2014 no quotes, notes, or explanations."
2795
2812
  ];
@@ -2881,12 +2898,66 @@ var MalformedReplyError = class extends Error {
2881
2898
  }
2882
2899
  raw;
2883
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
+ }
2884
2953
  function parseReplyItems(text) {
2885
2954
  let parsed;
2886
2955
  try {
2887
2956
  parsed = JSON.parse(text);
2888
2957
  } catch {
2889
- throw new MalformedReplyError(text);
2958
+ const repaired = repairUnescapedQuotes(text);
2959
+ if (repaired === void 0) throw new MalformedReplyError(text);
2960
+ parsed = JSON.parse(repaired);
2890
2961
  }
2891
2962
  if (!Array.isArray(parsed.items)) throw new MalformedReplyError(text);
2892
2963
  return parsed.items;
@@ -3675,7 +3746,7 @@ function readLog(projectRoot, limit = 100) {
3675
3746
  import { relative as relative3 } from "path";
3676
3747
 
3677
3748
  // src/server/import/detect.ts
3678
- import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
3749
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
3679
3750
  import { join as join4 } from "path";
3680
3751
  var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3681
3752
  var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
@@ -3764,12 +3835,134 @@ function detectApple(root) {
3764
3835
  }
3765
3836
  return best;
3766
3837
  }
3767
- var DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
3838
+ var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
3839
+ function detectAngularXliff(root) {
3840
+ for (const rel of ANGULAR_DIR_CANDIDATES) {
3841
+ const localeRoot = rel === "." ? root : join4(root, rel);
3842
+ if (!safeIsDir(localeRoot)) continue;
3843
+ const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
3844
+ if (files.length === 0) continue;
3845
+ const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
3846
+ const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
3847
+ let sourceLocale;
3848
+ try {
3849
+ sourceLocale = readFileSync9(join4(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
3850
+ } catch {
3851
+ }
3852
+ if (!sourceLocale && locales.length === 0) continue;
3853
+ sourceLocale ??= pickSource(locales, () => 0);
3854
+ if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
3855
+ return { format: "angular-xliff", localeRoot, locales, sourceLocale };
3856
+ }
3857
+ return null;
3858
+ }
3859
+ function detectRails(root) {
3860
+ const localeRoot = join4(root, "config", "locales");
3861
+ if (!safeIsDir(localeRoot)) return null;
3862
+ const locales = [];
3863
+ for (const file of readdirSync3(localeRoot).sort()) {
3864
+ if (!/\.ya?ml$/.test(file)) continue;
3865
+ let text;
3866
+ try {
3867
+ text = readFileSync9(join4(localeRoot, file), "utf8");
3868
+ } catch {
3869
+ continue;
3870
+ }
3871
+ for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
3872
+ const token = m[2];
3873
+ if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
3874
+ }
3875
+ }
3876
+ if (locales.length === 0) return null;
3877
+ return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
3878
+ }
3879
+ var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
3880
+ function detectI18next(root) {
3881
+ for (const rel of I18NEXT_DIR_CANDIDATES) {
3882
+ const localeRoot = join4(root, rel);
3883
+ if (!safeIsDir(localeRoot)) continue;
3884
+ const locales = listDirs(localeRoot).filter(
3885
+ (d) => LOCALE_RE.test(d) && readdirSync3(join4(localeRoot, d)).some((f) => f.endsWith(".json"))
3886
+ );
3887
+ if (locales.length === 0) continue;
3888
+ const sourceLocale = pickSource(locales, (loc) => {
3889
+ try {
3890
+ return readdirSync3(join4(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join4(localeRoot, loc, f)).size, 0);
3891
+ } catch {
3892
+ return 0;
3893
+ }
3894
+ });
3895
+ return { format: "i18next-json", localeRoot, locales, sourceLocale };
3896
+ }
3897
+ return null;
3898
+ }
3899
+ function gettextLocales(dir) {
3900
+ const locales = [];
3901
+ for (const entry of readdirSync3(dir).sort()) {
3902
+ const flat = entry.match(/^(.+)\.po$/)?.[1];
3903
+ if (flat && LOCALE_RE.test(flat)) {
3904
+ if (!locales.includes(flat)) locales.push(flat);
3905
+ continue;
3906
+ }
3907
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join4(dir, entry))) continue;
3908
+ const sub = join4(dir, entry);
3909
+ const hasPo = (d) => {
3910
+ try {
3911
+ return readdirSync3(d).some((f) => f.endsWith(".po"));
3912
+ } catch {
3913
+ return false;
3914
+ }
3915
+ };
3916
+ if (hasPo(join4(sub, "LC_MESSAGES")) || hasPo(sub)) {
3917
+ if (!locales.includes(entry)) locales.push(entry);
3918
+ }
3919
+ }
3920
+ return locales;
3921
+ }
3922
+ var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
3923
+ function detectGettext(root) {
3924
+ for (const rel of GETTEXT_DIR_CANDIDATES) {
3925
+ const localeRoot = join4(root, rel);
3926
+ if (!safeIsDir(localeRoot)) continue;
3927
+ const locales = gettextLocales(localeRoot);
3928
+ if (locales.length === 0) continue;
3929
+ return { format: "gettext-po", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
3930
+ }
3931
+ return null;
3932
+ }
3933
+ function detectAppleStringsdict(root) {
3934
+ const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
3935
+ let best = null;
3936
+ for (const dir of candidates) {
3937
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync9(join4(dir, `${l}.lproj`, "Localizable.stringsdict")));
3938
+ if (locales.length === 0) continue;
3939
+ if (!best || locales.length > best.locales.length) {
3940
+ best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
3941
+ }
3942
+ }
3943
+ return best;
3944
+ }
3945
+ var DETECTORS = [
3946
+ detectLaravel,
3947
+ detectVue,
3948
+ detectArb,
3949
+ detectApple,
3950
+ detectAngularXliff,
3951
+ detectRails,
3952
+ detectI18next,
3953
+ detectGettext,
3954
+ detectAppleStringsdict
3955
+ ];
3768
3956
  var BY_FORMAT = {
3769
3957
  "laravel-php": detectLaravel,
3770
3958
  "vue-i18n-json": (root) => detectVue(root, true),
3771
3959
  "flutter-arb": detectArb,
3772
- "apple-strings": detectApple
3960
+ "apple-strings": detectApple,
3961
+ "angular-xliff": detectAngularXliff,
3962
+ "rails-yaml": detectRails,
3963
+ "i18next-json": detectI18next,
3964
+ "gettext-po": detectGettext,
3965
+ "apple-stringsdict": detectAppleStringsdict
3773
3966
  };
3774
3967
  function detect(root, formatOverride) {
3775
3968
  if (!existsSync9(root)) return null;
@@ -3786,7 +3979,7 @@ function detect(root, formatOverride) {
3786
3979
  }
3787
3980
 
3788
3981
  // src/server/import/parsers/vue-i18n-json.ts
3789
- import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
3982
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
3790
3983
  import { join as join5 } from "path";
3791
3984
 
3792
3985
  // src/server/import/flatten.ts
@@ -3826,7 +4019,7 @@ var vueI18nJson2 = {
3826
4019
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3827
4020
  let data;
3828
4021
  try {
3829
- data = JSON.parse(readFileSync9(join5(localeRoot, file), "utf8"));
4022
+ data = JSON.parse(readFileSync10(join5(localeRoot, file), "utf8"));
3830
4023
  } catch (e) {
3831
4024
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
3832
4025
  continue;
@@ -3918,7 +4111,7 @@ var laravelPhp2 = {
3918
4111
  };
3919
4112
 
3920
4113
  // src/server/import/parsers/flutter-arb.ts
3921
- import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
4114
+ import { readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
3922
4115
  import { join as join7 } from "path";
3923
4116
  var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3924
4117
  function localeFromArbName(file) {
@@ -3955,7 +4148,7 @@ var flutterArb2 = {
3955
4148
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3956
4149
  let data;
3957
4150
  try {
3958
- data = JSON.parse(readFileSync10(join7(localeRoot, file), "utf8"));
4151
+ data = JSON.parse(readFileSync11(join7(localeRoot, file), "utf8"));
3959
4152
  } catch (e) {
3960
4153
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
3961
4154
  continue;
@@ -3980,7 +4173,7 @@ var flutterArb2 = {
3980
4173
  };
3981
4174
 
3982
4175
  // src/server/import/parsers/apple-strings.ts
3983
- import { readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
4176
+ import { readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
3984
4177
  import { join as join8 } from "path";
3985
4178
  var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3986
4179
  var TABLE = "Localizable.strings";
@@ -4089,7 +4282,7 @@ var appleStrings2 = {
4089
4282
  let text;
4090
4283
  try {
4091
4284
  if (!statSync4(file).isFile()) continue;
4092
- text = readFileSync11(file, "utf8");
4285
+ text = readFileSync12(file, "utf8");
4093
4286
  } catch {
4094
4287
  continue;
4095
4288
  }
@@ -4106,12 +4299,747 @@ var appleStrings2 = {
4106
4299
  }
4107
4300
  };
4108
4301
 
4302
+ // src/server/import/parsers/angular-xliff.ts
4303
+ import { readdirSync as readdirSync8, readFileSync as readFileSync13 } from "fs";
4304
+ import { join as join9 } from "path";
4305
+ var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4306
+ var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
4307
+ function decodeEntities(s) {
4308
+ return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
4309
+ }
4310
+ function parseAttrs(s) {
4311
+ const out = {};
4312
+ for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
4313
+ return out;
4314
+ }
4315
+ function decodeInline(raw, addMeta) {
4316
+ let out = "";
4317
+ let last = 0;
4318
+ for (const m of raw.matchAll(/<x\b([^>]*?)\/>/g)) {
4319
+ out += decodeEntities(raw.slice(last, m.index));
4320
+ const attrs = parseAttrs(m[1]);
4321
+ const id = attrs["id"] ?? "X";
4322
+ const equiv = attrs["equiv-text"];
4323
+ const simple = equiv?.match(/^\{\{\s*(\w+)\s*\}\}$/);
4324
+ if (simple) {
4325
+ out += `{${simple[1]}}`;
4326
+ } else {
4327
+ out += `{${id}}`;
4328
+ const meta = {};
4329
+ if (attrs["ctype"]) meta.type = attrs["ctype"];
4330
+ if (equiv !== void 0) meta.example = equiv;
4331
+ addMeta(id, meta);
4332
+ }
4333
+ last = m.index + m[0].length;
4334
+ }
4335
+ return out + decodeEntities(raw.slice(last));
4336
+ }
4337
+ var angularXliff2 = {
4338
+ name: "angular-xliff",
4339
+ parse(localeRoot, opts) {
4340
+ const warnings = [];
4341
+ const keys = {};
4342
+ const locales = [];
4343
+ const seen = (loc) => {
4344
+ if (!locales.includes(loc)) locales.push(loc);
4345
+ };
4346
+ const files = readdirSync8(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
4347
+ for (const file of files) {
4348
+ const fnameLocale = file.match(FILE_RE)[1];
4349
+ if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
4350
+ let xml;
4351
+ try {
4352
+ xml = readFileSync13(join9(localeRoot, file), "utf8");
4353
+ } catch (e) {
4354
+ warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
4355
+ continue;
4356
+ }
4357
+ const sourceLocale = xml.match(/source-language="([^"]+)"/)?.[1];
4358
+ if (!sourceLocale) {
4359
+ warnings.push(`angular-xliff: ${file} has no source-language attribute; skipped`);
4360
+ continue;
4361
+ }
4362
+ const targetLocale = xml.match(/target-language="([^"]+)"/)?.[1] ?? fnameLocale;
4363
+ if (opts?.locales && !opts.locales.includes(targetLocale ?? sourceLocale)) continue;
4364
+ for (const unit of xml.matchAll(/<trans-unit\b([^>]*)>([\s\S]*?)<\/trans-unit>/g)) {
4365
+ const id = parseAttrs(unit[1])["id"];
4366
+ if (!id) {
4367
+ warnings.push(`angular-xliff: ${file} has a trans-unit without an id; skipped`);
4368
+ continue;
4369
+ }
4370
+ const body = unit[2];
4371
+ const src = body.match(/<source\b[^>]*>([\s\S]*?)<\/source>/);
4372
+ let tgt = body.match(/<target\b([^>]*)>([\s\S]*?)<\/target>/);
4373
+ if (tgt && /\bstate="new"/.test(tgt[1])) tgt = null;
4374
+ const entry = keys[id] ??= { values: {} };
4375
+ const addMeta = (name, meta) => {
4376
+ (entry.placeholders ??= {})[name] ??= meta;
4377
+ };
4378
+ if (src && entry.values[sourceLocale] === void 0) {
4379
+ entry.values[sourceLocale] = decodeInline(src[1], addMeta);
4380
+ seen(sourceLocale);
4381
+ }
4382
+ if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
4383
+ entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
4384
+ seen(targetLocale);
4385
+ }
4386
+ }
4387
+ }
4388
+ return { locales, keys, warnings };
4389
+ }
4390
+ };
4391
+
4392
+ // src/server/import/parsers/gettext-po.ts
4393
+ import { readdirSync as readdirSync9, readFileSync as readFileSync14 } from "fs";
4394
+ import { join as join10 } from "path";
4395
+ var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4396
+ var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
4397
+ var CONT_RE = /^[ \t]*"(.*)"\s*$/;
4398
+ function unescapePo(s) {
4399
+ return s.replace(
4400
+ /\\([\\"ntr])/g,
4401
+ (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
4402
+ );
4403
+ }
4404
+ function parseEntries(text) {
4405
+ const entries = [];
4406
+ let cur = null;
4407
+ let append = null;
4408
+ const flush = () => {
4409
+ if (cur && cur.msgid !== void 0) entries.push(cur);
4410
+ cur = null;
4411
+ append = null;
4412
+ };
4413
+ for (const line of text.split("\n")) {
4414
+ if (line.trim() === "") {
4415
+ flush();
4416
+ continue;
4417
+ }
4418
+ if (line.startsWith("#")) continue;
4419
+ const m = line.match(DIRECTIVE_RE);
4420
+ if (m) {
4421
+ const kw = m[1];
4422
+ const idx = m[2];
4423
+ const body = unescapePo(m[3]);
4424
+ if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
4425
+ cur ??= { plurals: /* @__PURE__ */ new Map() };
4426
+ const entry = cur;
4427
+ if (kw === "msgctxt") {
4428
+ entry.msgctxt = body;
4429
+ append = (c) => {
4430
+ entry.msgctxt = (entry.msgctxt ?? "") + c;
4431
+ };
4432
+ } else if (kw === "msgid") {
4433
+ entry.msgid = body;
4434
+ append = (c) => {
4435
+ entry.msgid = (entry.msgid ?? "") + c;
4436
+ };
4437
+ } else if (kw === "msgid_plural") {
4438
+ entry.msgidPlural = body;
4439
+ append = (c) => {
4440
+ entry.msgidPlural = (entry.msgidPlural ?? "") + c;
4441
+ };
4442
+ } else if (idx !== void 0) {
4443
+ const i = Number(idx);
4444
+ entry.plurals.set(i, body);
4445
+ append = (c) => {
4446
+ entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
4447
+ };
4448
+ } else {
4449
+ entry.msgstr = body;
4450
+ append = (c) => {
4451
+ entry.msgstr = (entry.msgstr ?? "") + c;
4452
+ };
4453
+ }
4454
+ continue;
4455
+ }
4456
+ const cont = line.match(CONT_RE);
4457
+ if (cont && append) append(unescapePo(cont[1]));
4458
+ }
4459
+ flush();
4460
+ return entries;
4461
+ }
4462
+ function discoverPoFiles(root) {
4463
+ const found = [];
4464
+ const entries = readdirSync9(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
4465
+ for (const e of entries) {
4466
+ if (e.isFile() && e.name.endsWith(".po")) {
4467
+ const base = e.name.slice(0, -3);
4468
+ found.push({ path: join10(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
4469
+ } else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
4470
+ for (const sub of [join10(e.name, "LC_MESSAGES"), e.name]) {
4471
+ let names;
4472
+ try {
4473
+ names = readdirSync9(join10(root, sub)).sort();
4474
+ } catch {
4475
+ continue;
4476
+ }
4477
+ for (const f of names) {
4478
+ if (f.endsWith(".po")) found.push({ path: join10(root, sub, f), rel: join10(sub, f), locale: e.name });
4479
+ }
4480
+ }
4481
+ }
4482
+ }
4483
+ return found;
4484
+ }
4485
+ var gettextPo2 = {
4486
+ name: "gettext-po",
4487
+ parse(localeRoot, opts) {
4488
+ const warnings = [];
4489
+ const keys = {};
4490
+ const locales = [];
4491
+ for (const file of discoverPoFiles(localeRoot)) {
4492
+ let entries;
4493
+ try {
4494
+ entries = parseEntries(readFileSync14(file.path, "utf8"));
4495
+ } catch (e) {
4496
+ warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
4497
+ continue;
4498
+ }
4499
+ const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
4500
+ const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
4501
+ const locale = file.locale ?? headerLang;
4502
+ if (!locale) {
4503
+ warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
4504
+ continue;
4505
+ }
4506
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4507
+ if (!locales.includes(locale)) locales.push(locale);
4508
+ const cats = categoriesFor(locale);
4509
+ for (const entry of entries) {
4510
+ if (entry === header) continue;
4511
+ const key = entry.msgctxt ?? entry.msgid;
4512
+ if (!key) continue;
4513
+ if (entry.msgidPlural !== void 0) {
4514
+ const forms = {};
4515
+ for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
4516
+ if (body === "") continue;
4517
+ const cat = cats[i];
4518
+ if (!cat) {
4519
+ warnings.push(
4520
+ `gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
4521
+ );
4522
+ continue;
4523
+ }
4524
+ forms[cat] = body.split("%d").join("{count}");
4525
+ }
4526
+ if (!forms.other) continue;
4527
+ (keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
4528
+ } else {
4529
+ if (!entry.msgstr) continue;
4530
+ (keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
4531
+ }
4532
+ }
4533
+ }
4534
+ return { locales, keys, warnings };
4535
+ }
4536
+ };
4537
+
4538
+ // src/server/import/parsers/i18next-json.ts
4539
+ import { readdirSync as readdirSync10, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
4540
+ import { join as join11 } from "path";
4541
+ var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4542
+ var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
4543
+ var PLURAL_ARG = "count";
4544
+ var DEFAULT_NAMESPACE = "translation";
4545
+ function safeIsDir2(p) {
4546
+ try {
4547
+ return statSync5(p).isDirectory();
4548
+ } catch {
4549
+ return false;
4550
+ }
4551
+ }
4552
+ function fromI18next(value) {
4553
+ if (isIcuPluralOrSelect(value)) return value;
4554
+ return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
4555
+ }
4556
+ function ingestFile(path, label, prefix, locale, keys, warnings) {
4557
+ let data;
4558
+ try {
4559
+ data = JSON.parse(readFileSync15(path, "utf8"));
4560
+ } catch (e) {
4561
+ warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
4562
+ return false;
4563
+ }
4564
+ const fileWarnings = [];
4565
+ const flat = flattenObject(data, "", fileWarnings);
4566
+ for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
4567
+ const families = /* @__PURE__ */ new Set();
4568
+ for (const [k, v] of Object.entries(flat)) {
4569
+ const m = PLURAL_SUFFIX_RE.exec(k);
4570
+ if (m && m[2] === "other" && v !== "") families.add(m[1]);
4571
+ }
4572
+ const pluralForms = {};
4573
+ for (const [k, raw] of Object.entries(flat)) {
4574
+ if (raw === "") continue;
4575
+ const value = fromI18next(raw);
4576
+ const m = PLURAL_SUFFIX_RE.exec(k);
4577
+ if (m && families.has(m[1])) {
4578
+ (pluralForms[m[1]] ??= {})[m[2]] = value;
4579
+ continue;
4580
+ }
4581
+ if (families.has(k)) {
4582
+ warnings.push(
4583
+ `i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
4584
+ );
4585
+ continue;
4586
+ }
4587
+ (keys[prefix + k] ??= { values: {} }).values[locale] = value;
4588
+ }
4589
+ for (const [base, forms] of Object.entries(pluralForms)) {
4590
+ (keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
4591
+ }
4592
+ return true;
4593
+ }
4594
+ var i18nextJson2 = {
4595
+ name: "i18next-json",
4596
+ parse(localeRoot, opts) {
4597
+ const warnings = [];
4598
+ const keys = {};
4599
+ const locales = [];
4600
+ for (const entry of readdirSync10(localeRoot).sort()) {
4601
+ const full = join11(localeRoot, entry);
4602
+ if (safeIsDir2(full)) {
4603
+ if (!LOCALE_RE7.test(entry)) continue;
4604
+ if (opts?.locales && !opts.locales.includes(entry)) continue;
4605
+ let any = false;
4606
+ for (const file of readdirSync10(full).sort()) {
4607
+ if (!file.endsWith(".json")) continue;
4608
+ const ns = file.slice(0, -".json".length);
4609
+ const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
4610
+ if (ingestFile(join11(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
4611
+ }
4612
+ if (any && !locales.includes(entry)) locales.push(entry);
4613
+ } else if (entry.endsWith(".json")) {
4614
+ const locale = entry.slice(0, -".json".length);
4615
+ if (!LOCALE_RE7.test(locale)) continue;
4616
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4617
+ if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
4618
+ locales.push(locale);
4619
+ }
4620
+ }
4621
+ }
4622
+ return { locales, keys, warnings };
4623
+ }
4624
+ };
4625
+
4626
+ // src/server/import/parsers/rails-yaml.ts
4627
+ import { readdirSync as readdirSync11, readFileSync as readFileSync16 } from "fs";
4628
+ import { join as join12 } from "path";
4629
+ var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
4630
+ var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
4631
+ function fromRuby(value) {
4632
+ return value.replace(/%\{(\w+)\}/g, "{$1}");
4633
+ }
4634
+ function makeNode() {
4635
+ return /* @__PURE__ */ Object.create(null);
4636
+ }
4637
+ function decodeDouble(body) {
4638
+ let out = "";
4639
+ for (let i = 0; i < body.length; i++) {
4640
+ const c = body[i];
4641
+ if (c !== "\\") {
4642
+ out += c;
4643
+ continue;
4644
+ }
4645
+ const n = body[++i];
4646
+ if (n === void 0) break;
4647
+ out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
4648
+ }
4649
+ return out;
4650
+ }
4651
+ function scanQuoted(s, start) {
4652
+ const q = s[start];
4653
+ if (q === '"') {
4654
+ for (let i = start + 1; i < s.length; i++) {
4655
+ if (s[i] === "\\") i++;
4656
+ else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
4657
+ }
4658
+ return null;
4659
+ }
4660
+ let out = "";
4661
+ for (let i = start + 1; i < s.length; i++) {
4662
+ if (s[i] === "'") {
4663
+ if (s[i + 1] === "'") {
4664
+ out += "'";
4665
+ i++;
4666
+ } else {
4667
+ return { text: out, end: i + 1 };
4668
+ }
4669
+ } else {
4670
+ out += s[i];
4671
+ }
4672
+ }
4673
+ return null;
4674
+ }
4675
+ function stripPlainComment(s) {
4676
+ const m = /(^|\s)#/.exec(s);
4677
+ return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
4678
+ }
4679
+ function onlyTrailing(s) {
4680
+ return /^\s*(#.*)?$/.test(s);
4681
+ }
4682
+ function parseYamlSubset(text, file, warnings) {
4683
+ const roots = {};
4684
+ const lines = text.split(/\r?\n/);
4685
+ let stack = [];
4686
+ let skipDeeperThan = null;
4687
+ let lastLeafIndent = null;
4688
+ for (let n = 0; n < lines.length; n++) {
4689
+ const raw = lines[n];
4690
+ const lineNo = n + 1;
4691
+ if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
4692
+ if (raw.trim() === "---") continue;
4693
+ const indentMatch = /^[ \t]*/.exec(raw)[0];
4694
+ if (indentMatch.includes(" ")) {
4695
+ warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
4696
+ continue;
4697
+ }
4698
+ const indent = indentMatch.length;
4699
+ if (skipDeeperThan !== null) {
4700
+ if (indent > skipDeeperThan) continue;
4701
+ skipDeeperThan = null;
4702
+ }
4703
+ const content = raw.slice(indent);
4704
+ if (content.startsWith("- ") || content === "-") {
4705
+ warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
4706
+ skipDeeperThan = indent;
4707
+ continue;
4708
+ }
4709
+ let key;
4710
+ let rest;
4711
+ if (content[0] === '"' || content[0] === "'") {
4712
+ const k = scanQuoted(content, 0);
4713
+ if (!k || content[k.end] !== ":") {
4714
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
4715
+ skipDeeperThan = indent;
4716
+ continue;
4717
+ }
4718
+ key = k.text;
4719
+ rest = content.slice(k.end + 1);
4720
+ } else {
4721
+ const m = /^(.*?):(?=\s|$)/.exec(content);
4722
+ if (!m) {
4723
+ warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
4724
+ skipDeeperThan = indent;
4725
+ continue;
4726
+ }
4727
+ key = m[1].trim();
4728
+ rest = content.slice(m[0].length);
4729
+ }
4730
+ if (lastLeafIndent !== null && indent > lastLeafIndent) {
4731
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
4732
+ skipDeeperThan = indent - 1;
4733
+ continue;
4734
+ }
4735
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
4736
+ const trimmed = rest.trim();
4737
+ let value;
4738
+ if (trimmed === "" || trimmed.startsWith("#")) {
4739
+ value = null;
4740
+ } else if (trimmed[0] === "&" || trimmed[0] === "*") {
4741
+ warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
4742
+ skipDeeperThan = indent;
4743
+ continue;
4744
+ } else if (trimmed[0] === "|" || trimmed[0] === ">") {
4745
+ warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
4746
+ skipDeeperThan = indent;
4747
+ continue;
4748
+ } else if (trimmed[0] === "[" || trimmed[0] === "{") {
4749
+ warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
4750
+ skipDeeperThan = indent;
4751
+ continue;
4752
+ } else if (trimmed[0] === '"' || trimmed[0] === "'") {
4753
+ const v = scanQuoted(trimmed, 0);
4754
+ if (!v || !onlyTrailing(trimmed.slice(v.end))) {
4755
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
4756
+ continue;
4757
+ }
4758
+ value = v.text;
4759
+ } else {
4760
+ value = stripPlainComment(trimmed);
4761
+ }
4762
+ if (stack.length === 0) {
4763
+ if (value !== null) {
4764
+ warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
4765
+ lastLeafIndent = indent;
4766
+ continue;
4767
+ }
4768
+ const root = roots[key] ??= makeNode();
4769
+ stack = [{ indent, node: root }];
4770
+ lastLeafIndent = null;
4771
+ continue;
4772
+ }
4773
+ const parent = stack[stack.length - 1].node;
4774
+ if (key in parent) {
4775
+ warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
4776
+ }
4777
+ if (value === null) {
4778
+ const child = makeNode();
4779
+ parent[key] = child;
4780
+ stack.push({ indent, node: child });
4781
+ lastLeafIndent = null;
4782
+ } else {
4783
+ parent[key] = value;
4784
+ lastLeafIndent = indent;
4785
+ }
4786
+ }
4787
+ return { roots };
4788
+ }
4789
+ function asPluralForms(node) {
4790
+ const entries = Object.entries(node);
4791
+ if (entries.length === 0) return null;
4792
+ const forms = {};
4793
+ for (const [k, v] of entries) {
4794
+ if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
4795
+ if (v !== "") forms[k] = v;
4796
+ }
4797
+ if (!("other" in forms)) return null;
4798
+ return forms;
4799
+ }
4800
+ function synthesizeIcu(forms, file, key, warnings) {
4801
+ const parts = [];
4802
+ for (const cat of PLURAL_CATEGORIES) {
4803
+ const body = forms[cat];
4804
+ if (body === void 0) continue;
4805
+ if (body.includes("#")) {
4806
+ warnings.push(
4807
+ `rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
4808
+ );
4809
+ }
4810
+ parts.push(`${cat} {${fromRuby(body)}}`);
4811
+ }
4812
+ return `{count, plural, ${parts.join(" ")}}`;
4813
+ }
4814
+ var railsYaml2 = {
4815
+ name: "rails-yaml",
4816
+ parse(localeRoot, opts) {
4817
+ const warnings = [];
4818
+ const keys = {};
4819
+ const locales = [];
4820
+ const wanted = opts?.locales?.map((l) => l.toLowerCase());
4821
+ const addValue = (key, locale, value) => {
4822
+ (keys[key] ??= { values: {} }).values[locale] = value;
4823
+ };
4824
+ const flatten = (node, prefix, locale, file) => {
4825
+ for (const [k, v] of Object.entries(node)) {
4826
+ const key = prefix ? `${prefix}.${k}` : k;
4827
+ if (typeof v === "string") {
4828
+ if (v !== "") addValue(key, locale, fromRuby(v));
4829
+ continue;
4830
+ }
4831
+ const forms = asPluralForms(v);
4832
+ if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
4833
+ else flatten(v, key, locale, file);
4834
+ }
4835
+ };
4836
+ for (const file of readdirSync11(localeRoot).sort()) {
4837
+ if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
4838
+ let text;
4839
+ try {
4840
+ text = readFileSync16(join12(localeRoot, file), "utf8");
4841
+ } catch (e) {
4842
+ warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
4843
+ continue;
4844
+ }
4845
+ const { roots } = parseYamlSubset(text, file, warnings);
4846
+ for (const token of Object.keys(roots).sort()) {
4847
+ if (!LOCALE_RE8.test(token)) {
4848
+ warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
4849
+ continue;
4850
+ }
4851
+ if (wanted && !wanted.includes(token.toLowerCase())) continue;
4852
+ if (!locales.includes(token)) locales.push(token);
4853
+ flatten(roots[token], "", token, file);
4854
+ }
4855
+ }
4856
+ return { locales, keys, warnings };
4857
+ }
4858
+ };
4859
+
4860
+ // src/server/import/parsers/apple-stringsdict.ts
4861
+ import { readdirSync as readdirSync12, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
4862
+ import { join as join13 } from "path";
4863
+ var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4864
+ var TABLE2 = "Localizable.stringsdict";
4865
+ function localeFromLproj2(dir) {
4866
+ const m = dir.match(/^(.+)\.lproj$/);
4867
+ if (!m) return null;
4868
+ return LOCALE_RE9.test(m[1]) ? m[1] : null;
4869
+ }
4870
+ function decodeEntities2(s) {
4871
+ return s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
4872
+ }
4873
+ function parsePlistDict(xml) {
4874
+ let i = 0;
4875
+ const n = xml.length;
4876
+ const skipTrivia = () => {
4877
+ for (; ; ) {
4878
+ while (i < n && /\s/.test(xml[i])) i++;
4879
+ if (xml.startsWith("<!--", i)) {
4880
+ const end = xml.indexOf("-->", i + 4);
4881
+ if (end === -1) throw new Error("unterminated comment");
4882
+ i = end + 3;
4883
+ continue;
4884
+ }
4885
+ if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
4886
+ const end = xml.indexOf(">", i);
4887
+ if (end === -1) throw new Error("unterminated declaration");
4888
+ i = end + 1;
4889
+ continue;
4890
+ }
4891
+ break;
4892
+ }
4893
+ };
4894
+ const readTag = () => {
4895
+ if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
4896
+ const end = xml.indexOf(">", i);
4897
+ if (end === -1) throw new Error("unterminated tag");
4898
+ let body = xml.slice(i + 1, end).trim();
4899
+ i = end + 1;
4900
+ const closing = body.startsWith("/");
4901
+ if (closing) body = body.slice(1).trim();
4902
+ const selfClosing = body.endsWith("/");
4903
+ if (selfClosing) body = body.slice(0, -1).trim();
4904
+ const name = body.split(/\s/)[0];
4905
+ if (!name) throw new Error(`empty tag at offset ${end}`);
4906
+ return { name, closing, selfClosing };
4907
+ };
4908
+ const readElementText = (name) => {
4909
+ const re = new RegExp(`</${name}\\s*>`, "g");
4910
+ re.lastIndex = i;
4911
+ const m = re.exec(xml);
4912
+ if (!m) throw new Error(`unterminated <${name}>`);
4913
+ const text = xml.slice(i, m.index);
4914
+ i = m.index + m[0].length;
4915
+ return decodeEntities2(text);
4916
+ };
4917
+ const readValue = (tag2) => {
4918
+ if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
4919
+ if (tag2.name === "true" || tag2.name === "false") {
4920
+ if (!tag2.selfClosing) readElementText(tag2.name);
4921
+ return tag2.name;
4922
+ }
4923
+ if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
4924
+ return tag2.selfClosing ? "" : readElementText(tag2.name);
4925
+ }
4926
+ throw new Error(`unsupported plist element <${tag2.name}>`);
4927
+ };
4928
+ const readDict = () => {
4929
+ const out = {};
4930
+ for (; ; ) {
4931
+ skipTrivia();
4932
+ const tag2 = readTag();
4933
+ if (tag2.closing) {
4934
+ if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
4935
+ return out;
4936
+ }
4937
+ if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
4938
+ const key = readElementText("key");
4939
+ skipTrivia();
4940
+ const vt = readTag();
4941
+ if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
4942
+ out[key] = readValue(vt);
4943
+ }
4944
+ };
4945
+ skipTrivia();
4946
+ let tag = readTag();
4947
+ if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
4948
+ skipTrivia();
4949
+ tag = readTag();
4950
+ }
4951
+ if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
4952
+ return tag.selfClosing ? {} : readDict();
4953
+ }
4954
+ var VAR_RE = /%#@([^@]*)@/g;
4955
+ function entryToIcu(key, entry, file, warnings) {
4956
+ const warn = (msg) => {
4957
+ warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
4958
+ return null;
4959
+ };
4960
+ if (typeof entry !== "object") return warn("value is not a dict; skipped");
4961
+ const fmt = entry["NSStringLocalizedFormatKey"];
4962
+ if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
4963
+ const vars = [...fmt.matchAll(VAR_RE)];
4964
+ if (vars.length !== 1) {
4965
+ return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
4966
+ }
4967
+ const arg = vars[0][1];
4968
+ if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
4969
+ const prefix = fmt.slice(0, vars[0].index);
4970
+ const suffix = fmt.slice(vars[0].index + vars[0][0].length);
4971
+ const varDict = entry[arg];
4972
+ if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
4973
+ const specType = varDict["NSStringFormatSpecTypeKey"];
4974
+ if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
4975
+ return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
4976
+ }
4977
+ const valueType = varDict["NSStringFormatValueTypeKey"];
4978
+ const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
4979
+ const forms = {};
4980
+ for (const cat of PLURAL_CATEGORIES) {
4981
+ const body = varDict[cat];
4982
+ if (typeof body !== "string") continue;
4983
+ forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
4984
+ }
4985
+ if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
4986
+ return formsToIcu(arg, forms);
4987
+ }
4988
+ var appleStringsdict2 = {
4989
+ name: "apple-stringsdict",
4990
+ parse(localeRoot, opts) {
4991
+ const warnings = [];
4992
+ const keys = {};
4993
+ const locales = [];
4994
+ for (const dir of readdirSync12(localeRoot).sort()) {
4995
+ const locale = localeFromLproj2(dir);
4996
+ if (!locale) continue;
4997
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4998
+ const file = join13(localeRoot, dir, TABLE2);
4999
+ let text;
5000
+ try {
5001
+ if (!statSync6(file).isFile()) continue;
5002
+ text = readFileSync17(file, "utf8");
5003
+ } catch {
5004
+ continue;
5005
+ }
5006
+ locales.push(locale);
5007
+ const others = readdirSync12(join13(localeRoot, dir)).filter(
5008
+ (f) => f.endsWith(".stringsdict") && f !== TABLE2
5009
+ );
5010
+ if (others.length) {
5011
+ warnings.push(
5012
+ `apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
5013
+ );
5014
+ }
5015
+ let root;
5016
+ try {
5017
+ root = parsePlistDict(text);
5018
+ } catch (e) {
5019
+ warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
5020
+ continue;
5021
+ }
5022
+ for (const key of Object.keys(root).sort()) {
5023
+ const icu = entryToIcu(key, root[key], file, warnings);
5024
+ if (icu === null) continue;
5025
+ (keys[key] ??= { values: {} }).values[locale] = icu;
5026
+ }
5027
+ }
5028
+ return { locales, keys, warnings };
5029
+ }
5030
+ };
5031
+
4109
5032
  // src/server/import/parsers/index.ts
4110
5033
  var REGISTRY = {
4111
5034
  [vueI18nJson2.name]: vueI18nJson2,
4112
5035
  [laravelPhp2.name]: laravelPhp2,
4113
5036
  [flutterArb2.name]: flutterArb2,
4114
- [appleStrings2.name]: appleStrings2
5037
+ [appleStrings2.name]: appleStrings2,
5038
+ [angularXliff2.name]: angularXliff2,
5039
+ [gettextPo2.name]: gettextPo2,
5040
+ [i18nextJson2.name]: i18nextJson2,
5041
+ [railsYaml2.name]: railsYaml2,
5042
+ [appleStringsdict2.name]: appleStringsdict2
4115
5043
  };
4116
5044
  function getParser(name) {
4117
5045
  const p = REGISTRY[name];
@@ -4124,7 +5052,14 @@ var OUTPUT_BY_FORMAT = {
4124
5052
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
4125
5053
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
4126
5054
  "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
4127
- "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
5055
+ "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
5056
+ // skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
5057
+ // only writes the translation files back next to it.
5058
+ "angular-xliff": { adapter: "angular-xliff", path: "messages.{locale}.xlf", rootRelative: true, skipSourceLocale: true },
5059
+ "gettext-po": { adapter: "gettext-po", path: "{locale}.po", rootRelative: true },
5060
+ "i18next-json": { adapter: "i18next-json", path: "{locale}/translation.json", rootRelative: true },
5061
+ "rails-yaml": { adapter: "rails-yaml", path: "config/locales/{locale}.yml" },
5062
+ "apple-stringsdict": { adapter: "apple-stringsdict", path: "{locale}.lproj/Localizable.stringsdict", rootRelative: true }
4128
5063
  };
4129
5064
  function assemble2(parsed, opts) {
4130
5065
  const warnings = [...parsed.warnings];
@@ -4242,7 +5177,7 @@ function runImport(opts) {
4242
5177
  }
4243
5178
 
4244
5179
  // src/server/export-run.ts
4245
- import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync8, rmdirSync, statSync as statSync5, unlinkSync } from "fs";
5180
+ import { existsSync as existsSync10, readFileSync as readFileSync18, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
4246
5181
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
4247
5182
  function effectiveLocales(config) {
4248
5183
  const limit = config.exportLocales;
@@ -4285,7 +5220,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4285
5220
  if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
4286
5221
  const next = resolve7(dir, segment);
4287
5222
  if (isLast) {
4288
- if (stale(locale) && existsSync10(next) && statSync5(next).isFile()) {
5223
+ if (stale(locale) && existsSync10(next) && statSync7(next).isFile()) {
4289
5224
  unlinkSync(next);
4290
5225
  deleted++;
4291
5226
  removeEmptyDirs(dir, root);
@@ -4298,7 +5233,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4298
5233
  const re = segmentRegExp(segment);
4299
5234
  let entries;
4300
5235
  try {
4301
- entries = readdirSync8(dir, { withFileTypes: true });
5236
+ entries = readdirSync13(dir, { withFileTypes: true });
4302
5237
  } catch {
4303
5238
  return;
4304
5239
  }
@@ -4341,7 +5276,7 @@ function exportToDisk(state, projectRoot, opts) {
4341
5276
  writtenPaths.add(abs);
4342
5277
  let current = null;
4343
5278
  try {
4344
- current = readFileSync12(abs, "utf8");
5279
+ current = readFileSync18(abs, "utf8");
4345
5280
  } catch {
4346
5281
  }
4347
5282
  if (current === f.contents) {
@@ -4358,17 +5293,17 @@ function exportToDisk(state, projectRoot, opts) {
4358
5293
  }
4359
5294
 
4360
5295
  // src/server/ui-prefs.ts
4361
- import { readFileSync as readFileSync13 } from "fs";
5296
+ import { readFileSync as readFileSync19 } from "fs";
4362
5297
  import { homedir } from "os";
4363
- import { join as join9 } from "path";
5298
+ import { join as join14 } from "path";
4364
5299
  var THEMES = ["system", "light", "dark"];
4365
5300
  var isThemeMode = (v) => THEMES.includes(v);
4366
5301
  var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4367
- var defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
5302
+ var defaultUiPrefsPath = () => join14(homedir(), ".glotfile", "ui.json");
4368
5303
  var DEFAULTS = { theme: "system" };
4369
5304
  function readJson(path) {
4370
5305
  try {
4371
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
5306
+ const parsed = JSON.parse(readFileSync19(path, "utf8"));
4372
5307
  return parsed && typeof parsed === "object" ? parsed : {};
4373
5308
  } catch {
4374
5309
  return {};
@@ -4387,7 +5322,7 @@ function saveUiPrefs(path, prefs) {
4387
5322
  }
4388
5323
 
4389
5324
  // src/server/local-settings.ts
4390
- import { readFileSync as readFileSync14 } from "fs";
5325
+ import { readFileSync as readFileSync20 } from "fs";
4391
5326
  import { resolve as resolve8 } from "path";
4392
5327
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
4393
5328
  var isEditorId = (v) => EDITOR_IDS.includes(v);
@@ -4402,7 +5337,7 @@ var DEFAULT_EDITOR = "vscode";
4402
5337
  var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
4403
5338
  function readJson2(path) {
4404
5339
  try {
4405
- const parsed = JSON.parse(readFileSync14(path, "utf8"));
5340
+ const parsed = JSON.parse(readFileSync20(path, "utf8"));
4406
5341
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4407
5342
  } catch {
4408
5343
  return {};
@@ -4475,7 +5410,7 @@ function projectName(root) {
4475
5410
  const nameFile = resolve9(root, ".idea", ".name");
4476
5411
  if (existsSync11(nameFile)) {
4477
5412
  try {
4478
- const name = readFileSync15(nameFile, "utf8").trim();
5413
+ const name = readFileSync21(nameFile, "utf8").trim();
4479
5414
  if (name) return name;
4480
5415
  } catch {
4481
5416
  }
@@ -4600,7 +5535,7 @@ function createApi(deps) {
4600
5535
  if (depth > 4) return;
4601
5536
  let entries = [];
4602
5537
  try {
4603
- entries = readdirSync9(dir);
5538
+ entries = readdirSync14(dir);
4604
5539
  } catch {
4605
5540
  return;
4606
5541
  }
@@ -4614,7 +5549,7 @@ function createApi(deps) {
4614
5549
  filePath = abs;
4615
5550
  } else {
4616
5551
  try {
4617
- if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
5552
+ if (statSync8(abs).isDirectory()) walk(abs, depth + 1);
4618
5553
  } catch {
4619
5554
  }
4620
5555
  continue;
@@ -5395,7 +6330,7 @@ function createApi(deps) {
5395
6330
 
5396
6331
  // src/server/server.ts
5397
6332
  var here = dirname4(fileURLToPath(import.meta.url));
5398
- var DEFAULT_UI_DIR = join10(here, "..", "ui");
6333
+ var DEFAULT_UI_DIR = join15(here, "..", "ui");
5399
6334
  var MIME = {
5400
6335
  ".html": "text/html; charset=utf-8",
5401
6336
  ".js": "text/javascript; charset=utf-8",
@@ -5449,7 +6384,7 @@ function buildApp(opts) {
5449
6384
  const file = await readFileResponse(target);
5450
6385
  if (file) return file;
5451
6386
  }
5452
- const index = await readFileResponse(join10(root, "index.html"));
6387
+ const index = await readFileResponse(join15(root, "index.html"));
5453
6388
  if (index) return index;
5454
6389
  return c.notFound();
5455
6390
  });