glotfile 0.5.4 → 0.6.0

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
@@ -3675,7 +3692,7 @@ function readLog(projectRoot, limit = 100) {
3675
3692
  import { relative as relative3 } from "path";
3676
3693
 
3677
3694
  // src/server/import/detect.ts
3678
- import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
3695
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
3679
3696
  import { join as join4 } from "path";
3680
3697
  var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3681
3698
  var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
@@ -3764,12 +3781,134 @@ function detectApple(root) {
3764
3781
  }
3765
3782
  return best;
3766
3783
  }
3767
- var DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
3784
+ var ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
3785
+ function detectAngularXliff(root) {
3786
+ for (const rel of ANGULAR_DIR_CANDIDATES) {
3787
+ const localeRoot = rel === "." ? root : join4(root, rel);
3788
+ if (!safeIsDir(localeRoot)) continue;
3789
+ const files = readdirSync3(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
3790
+ if (files.length === 0) continue;
3791
+ const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
3792
+ const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
3793
+ let sourceLocale;
3794
+ try {
3795
+ sourceLocale = readFileSync9(join4(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
3796
+ } catch {
3797
+ }
3798
+ if (!sourceLocale && locales.length === 0) continue;
3799
+ sourceLocale ??= pickSource(locales, () => 0);
3800
+ if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
3801
+ return { format: "angular-xliff", localeRoot, locales, sourceLocale };
3802
+ }
3803
+ return null;
3804
+ }
3805
+ function detectRails(root) {
3806
+ const localeRoot = join4(root, "config", "locales");
3807
+ if (!safeIsDir(localeRoot)) return null;
3808
+ const locales = [];
3809
+ for (const file of readdirSync3(localeRoot).sort()) {
3810
+ if (!/\.ya?ml$/.test(file)) continue;
3811
+ let text;
3812
+ try {
3813
+ text = readFileSync9(join4(localeRoot, file), "utf8");
3814
+ } catch {
3815
+ continue;
3816
+ }
3817
+ for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
3818
+ const token = m[2];
3819
+ if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
3820
+ }
3821
+ }
3822
+ if (locales.length === 0) return null;
3823
+ return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
3824
+ }
3825
+ var I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
3826
+ function detectI18next(root) {
3827
+ for (const rel of I18NEXT_DIR_CANDIDATES) {
3828
+ const localeRoot = join4(root, rel);
3829
+ if (!safeIsDir(localeRoot)) continue;
3830
+ const locales = listDirs(localeRoot).filter(
3831
+ (d) => LOCALE_RE.test(d) && readdirSync3(join4(localeRoot, d)).some((f) => f.endsWith(".json"))
3832
+ );
3833
+ if (locales.length === 0) continue;
3834
+ const sourceLocale = pickSource(locales, (loc) => {
3835
+ try {
3836
+ return readdirSync3(join4(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync2(join4(localeRoot, loc, f)).size, 0);
3837
+ } catch {
3838
+ return 0;
3839
+ }
3840
+ });
3841
+ return { format: "i18next-json", localeRoot, locales, sourceLocale };
3842
+ }
3843
+ return null;
3844
+ }
3845
+ function gettextLocales(dir) {
3846
+ const locales = [];
3847
+ for (const entry of readdirSync3(dir).sort()) {
3848
+ const flat = entry.match(/^(.+)\.po$/)?.[1];
3849
+ if (flat && LOCALE_RE.test(flat)) {
3850
+ if (!locales.includes(flat)) locales.push(flat);
3851
+ continue;
3852
+ }
3853
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join4(dir, entry))) continue;
3854
+ const sub = join4(dir, entry);
3855
+ const hasPo = (d) => {
3856
+ try {
3857
+ return readdirSync3(d).some((f) => f.endsWith(".po"));
3858
+ } catch {
3859
+ return false;
3860
+ }
3861
+ };
3862
+ if (hasPo(join4(sub, "LC_MESSAGES")) || hasPo(sub)) {
3863
+ if (!locales.includes(entry)) locales.push(entry);
3864
+ }
3865
+ }
3866
+ return locales;
3867
+ }
3868
+ var GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
3869
+ function detectGettext(root) {
3870
+ for (const rel of GETTEXT_DIR_CANDIDATES) {
3871
+ const localeRoot = join4(root, rel);
3872
+ if (!safeIsDir(localeRoot)) continue;
3873
+ const locales = gettextLocales(localeRoot);
3874
+ if (locales.length === 0) continue;
3875
+ return { format: "gettext-po", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
3876
+ }
3877
+ return null;
3878
+ }
3879
+ function detectAppleStringsdict(root) {
3880
+ const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
3881
+ let best = null;
3882
+ for (const dir of candidates) {
3883
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync9(join4(dir, `${l}.lproj`, "Localizable.stringsdict")));
3884
+ if (locales.length === 0) continue;
3885
+ if (!best || locales.length > best.locales.length) {
3886
+ best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
3887
+ }
3888
+ }
3889
+ return best;
3890
+ }
3891
+ var DETECTORS = [
3892
+ detectLaravel,
3893
+ detectVue,
3894
+ detectArb,
3895
+ detectApple,
3896
+ detectAngularXliff,
3897
+ detectRails,
3898
+ detectI18next,
3899
+ detectGettext,
3900
+ detectAppleStringsdict
3901
+ ];
3768
3902
  var BY_FORMAT = {
3769
3903
  "laravel-php": detectLaravel,
3770
3904
  "vue-i18n-json": (root) => detectVue(root, true),
3771
3905
  "flutter-arb": detectArb,
3772
- "apple-strings": detectApple
3906
+ "apple-strings": detectApple,
3907
+ "angular-xliff": detectAngularXliff,
3908
+ "rails-yaml": detectRails,
3909
+ "i18next-json": detectI18next,
3910
+ "gettext-po": detectGettext,
3911
+ "apple-stringsdict": detectAppleStringsdict
3773
3912
  };
3774
3913
  function detect(root, formatOverride) {
3775
3914
  if (!existsSync9(root)) return null;
@@ -3786,7 +3925,7 @@ function detect(root, formatOverride) {
3786
3925
  }
3787
3926
 
3788
3927
  // src/server/import/parsers/vue-i18n-json.ts
3789
- import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
3928
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
3790
3929
  import { join as join5 } from "path";
3791
3930
 
3792
3931
  // src/server/import/flatten.ts
@@ -3826,7 +3965,7 @@ var vueI18nJson2 = {
3826
3965
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3827
3966
  let data;
3828
3967
  try {
3829
- data = JSON.parse(readFileSync9(join5(localeRoot, file), "utf8"));
3968
+ data = JSON.parse(readFileSync10(join5(localeRoot, file), "utf8"));
3830
3969
  } catch (e) {
3831
3970
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
3832
3971
  continue;
@@ -3918,7 +4057,7 @@ var laravelPhp2 = {
3918
4057
  };
3919
4058
 
3920
4059
  // src/server/import/parsers/flutter-arb.ts
3921
- import { readdirSync as readdirSync6, readFileSync as readFileSync10 } from "fs";
4060
+ import { readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
3922
4061
  import { join as join7 } from "path";
3923
4062
  var LOCALE_RE3 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3924
4063
  function localeFromArbName(file) {
@@ -3955,7 +4094,7 @@ var flutterArb2 = {
3955
4094
  if (opts?.locales && !opts.locales.includes(locale)) continue;
3956
4095
  let data;
3957
4096
  try {
3958
- data = JSON.parse(readFileSync10(join7(localeRoot, file), "utf8"));
4097
+ data = JSON.parse(readFileSync11(join7(localeRoot, file), "utf8"));
3959
4098
  } catch (e) {
3960
4099
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
3961
4100
  continue;
@@ -3980,7 +4119,7 @@ var flutterArb2 = {
3980
4119
  };
3981
4120
 
3982
4121
  // src/server/import/parsers/apple-strings.ts
3983
- import { readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4 } from "fs";
4122
+ import { readdirSync as readdirSync7, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
3984
4123
  import { join as join8 } from "path";
3985
4124
  var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
3986
4125
  var TABLE = "Localizable.strings";
@@ -4089,7 +4228,7 @@ var appleStrings2 = {
4089
4228
  let text;
4090
4229
  try {
4091
4230
  if (!statSync4(file).isFile()) continue;
4092
- text = readFileSync11(file, "utf8");
4231
+ text = readFileSync12(file, "utf8");
4093
4232
  } catch {
4094
4233
  continue;
4095
4234
  }
@@ -4106,12 +4245,747 @@ var appleStrings2 = {
4106
4245
  }
4107
4246
  };
4108
4247
 
4248
+ // src/server/import/parsers/angular-xliff.ts
4249
+ import { readdirSync as readdirSync8, readFileSync as readFileSync13 } from "fs";
4250
+ import { join as join9 } from "path";
4251
+ var LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4252
+ var FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
4253
+ function decodeEntities(s) {
4254
+ 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, "&");
4255
+ }
4256
+ function parseAttrs(s) {
4257
+ const out = {};
4258
+ for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
4259
+ return out;
4260
+ }
4261
+ function decodeInline(raw, addMeta) {
4262
+ let out = "";
4263
+ let last = 0;
4264
+ for (const m of raw.matchAll(/<x\b([^>]*?)\/>/g)) {
4265
+ out += decodeEntities(raw.slice(last, m.index));
4266
+ const attrs = parseAttrs(m[1]);
4267
+ const id = attrs["id"] ?? "X";
4268
+ const equiv = attrs["equiv-text"];
4269
+ const simple = equiv?.match(/^\{\{\s*(\w+)\s*\}\}$/);
4270
+ if (simple) {
4271
+ out += `{${simple[1]}}`;
4272
+ } else {
4273
+ out += `{${id}}`;
4274
+ const meta = {};
4275
+ if (attrs["ctype"]) meta.type = attrs["ctype"];
4276
+ if (equiv !== void 0) meta.example = equiv;
4277
+ addMeta(id, meta);
4278
+ }
4279
+ last = m.index + m[0].length;
4280
+ }
4281
+ return out + decodeEntities(raw.slice(last));
4282
+ }
4283
+ var angularXliff2 = {
4284
+ name: "angular-xliff",
4285
+ parse(localeRoot, opts) {
4286
+ const warnings = [];
4287
+ const keys = {};
4288
+ const locales = [];
4289
+ const seen = (loc) => {
4290
+ if (!locales.includes(loc)) locales.push(loc);
4291
+ };
4292
+ 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));
4293
+ for (const file of files) {
4294
+ const fnameLocale = file.match(FILE_RE)[1];
4295
+ if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
4296
+ let xml;
4297
+ try {
4298
+ xml = readFileSync13(join9(localeRoot, file), "utf8");
4299
+ } catch (e) {
4300
+ warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
4301
+ continue;
4302
+ }
4303
+ const sourceLocale = xml.match(/source-language="([^"]+)"/)?.[1];
4304
+ if (!sourceLocale) {
4305
+ warnings.push(`angular-xliff: ${file} has no source-language attribute; skipped`);
4306
+ continue;
4307
+ }
4308
+ const targetLocale = xml.match(/target-language="([^"]+)"/)?.[1] ?? fnameLocale;
4309
+ if (opts?.locales && !opts.locales.includes(targetLocale ?? sourceLocale)) continue;
4310
+ for (const unit of xml.matchAll(/<trans-unit\b([^>]*)>([\s\S]*?)<\/trans-unit>/g)) {
4311
+ const id = parseAttrs(unit[1])["id"];
4312
+ if (!id) {
4313
+ warnings.push(`angular-xliff: ${file} has a trans-unit without an id; skipped`);
4314
+ continue;
4315
+ }
4316
+ const body = unit[2];
4317
+ const src = body.match(/<source\b[^>]*>([\s\S]*?)<\/source>/);
4318
+ let tgt = body.match(/<target\b([^>]*)>([\s\S]*?)<\/target>/);
4319
+ if (tgt && /\bstate="new"/.test(tgt[1])) tgt = null;
4320
+ const entry = keys[id] ??= { values: {} };
4321
+ const addMeta = (name, meta) => {
4322
+ (entry.placeholders ??= {})[name] ??= meta;
4323
+ };
4324
+ if (src && entry.values[sourceLocale] === void 0) {
4325
+ entry.values[sourceLocale] = decodeInline(src[1], addMeta);
4326
+ seen(sourceLocale);
4327
+ }
4328
+ if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
4329
+ entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
4330
+ seen(targetLocale);
4331
+ }
4332
+ }
4333
+ }
4334
+ return { locales, keys, warnings };
4335
+ }
4336
+ };
4337
+
4338
+ // src/server/import/parsers/gettext-po.ts
4339
+ import { readdirSync as readdirSync9, readFileSync as readFileSync14 } from "fs";
4340
+ import { join as join10 } from "path";
4341
+ var LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4342
+ var DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
4343
+ var CONT_RE = /^[ \t]*"(.*)"\s*$/;
4344
+ function unescapePo(s) {
4345
+ return s.replace(
4346
+ /\\([\\"ntr])/g,
4347
+ (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
4348
+ );
4349
+ }
4350
+ function parseEntries(text) {
4351
+ const entries = [];
4352
+ let cur = null;
4353
+ let append = null;
4354
+ const flush = () => {
4355
+ if (cur && cur.msgid !== void 0) entries.push(cur);
4356
+ cur = null;
4357
+ append = null;
4358
+ };
4359
+ for (const line of text.split("\n")) {
4360
+ if (line.trim() === "") {
4361
+ flush();
4362
+ continue;
4363
+ }
4364
+ if (line.startsWith("#")) continue;
4365
+ const m = line.match(DIRECTIVE_RE);
4366
+ if (m) {
4367
+ const kw = m[1];
4368
+ const idx = m[2];
4369
+ const body = unescapePo(m[3]);
4370
+ if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
4371
+ cur ??= { plurals: /* @__PURE__ */ new Map() };
4372
+ const entry = cur;
4373
+ if (kw === "msgctxt") {
4374
+ entry.msgctxt = body;
4375
+ append = (c) => {
4376
+ entry.msgctxt = (entry.msgctxt ?? "") + c;
4377
+ };
4378
+ } else if (kw === "msgid") {
4379
+ entry.msgid = body;
4380
+ append = (c) => {
4381
+ entry.msgid = (entry.msgid ?? "") + c;
4382
+ };
4383
+ } else if (kw === "msgid_plural") {
4384
+ entry.msgidPlural = body;
4385
+ append = (c) => {
4386
+ entry.msgidPlural = (entry.msgidPlural ?? "") + c;
4387
+ };
4388
+ } else if (idx !== void 0) {
4389
+ const i = Number(idx);
4390
+ entry.plurals.set(i, body);
4391
+ append = (c) => {
4392
+ entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
4393
+ };
4394
+ } else {
4395
+ entry.msgstr = body;
4396
+ append = (c) => {
4397
+ entry.msgstr = (entry.msgstr ?? "") + c;
4398
+ };
4399
+ }
4400
+ continue;
4401
+ }
4402
+ const cont = line.match(CONT_RE);
4403
+ if (cont && append) append(unescapePo(cont[1]));
4404
+ }
4405
+ flush();
4406
+ return entries;
4407
+ }
4408
+ function discoverPoFiles(root) {
4409
+ const found = [];
4410
+ const entries = readdirSync9(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
4411
+ for (const e of entries) {
4412
+ if (e.isFile() && e.name.endsWith(".po")) {
4413
+ const base = e.name.slice(0, -3);
4414
+ found.push({ path: join10(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
4415
+ } else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
4416
+ for (const sub of [join10(e.name, "LC_MESSAGES"), e.name]) {
4417
+ let names;
4418
+ try {
4419
+ names = readdirSync9(join10(root, sub)).sort();
4420
+ } catch {
4421
+ continue;
4422
+ }
4423
+ for (const f of names) {
4424
+ if (f.endsWith(".po")) found.push({ path: join10(root, sub, f), rel: join10(sub, f), locale: e.name });
4425
+ }
4426
+ }
4427
+ }
4428
+ }
4429
+ return found;
4430
+ }
4431
+ var gettextPo2 = {
4432
+ name: "gettext-po",
4433
+ parse(localeRoot, opts) {
4434
+ const warnings = [];
4435
+ const keys = {};
4436
+ const locales = [];
4437
+ for (const file of discoverPoFiles(localeRoot)) {
4438
+ let entries;
4439
+ try {
4440
+ entries = parseEntries(readFileSync14(file.path, "utf8"));
4441
+ } catch (e) {
4442
+ warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
4443
+ continue;
4444
+ }
4445
+ const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
4446
+ const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
4447
+ const locale = file.locale ?? headerLang;
4448
+ if (!locale) {
4449
+ warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
4450
+ continue;
4451
+ }
4452
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4453
+ if (!locales.includes(locale)) locales.push(locale);
4454
+ const cats = categoriesFor(locale);
4455
+ for (const entry of entries) {
4456
+ if (entry === header) continue;
4457
+ const key = entry.msgctxt ?? entry.msgid;
4458
+ if (!key) continue;
4459
+ if (entry.msgidPlural !== void 0) {
4460
+ const forms = {};
4461
+ for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
4462
+ if (body === "") continue;
4463
+ const cat = cats[i];
4464
+ if (!cat) {
4465
+ warnings.push(
4466
+ `gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
4467
+ );
4468
+ continue;
4469
+ }
4470
+ forms[cat] = body.split("%d").join("{count}");
4471
+ }
4472
+ if (!forms.other) continue;
4473
+ (keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
4474
+ } else {
4475
+ if (!entry.msgstr) continue;
4476
+ (keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
4477
+ }
4478
+ }
4479
+ }
4480
+ return { locales, keys, warnings };
4481
+ }
4482
+ };
4483
+
4484
+ // src/server/import/parsers/i18next-json.ts
4485
+ import { readdirSync as readdirSync10, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
4486
+ import { join as join11 } from "path";
4487
+ var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4488
+ var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
4489
+ var PLURAL_ARG = "count";
4490
+ var DEFAULT_NAMESPACE = "translation";
4491
+ function safeIsDir2(p) {
4492
+ try {
4493
+ return statSync5(p).isDirectory();
4494
+ } catch {
4495
+ return false;
4496
+ }
4497
+ }
4498
+ function fromI18next(value) {
4499
+ if (isIcuPluralOrSelect(value)) return value;
4500
+ return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
4501
+ }
4502
+ function ingestFile(path, label, prefix, locale, keys, warnings) {
4503
+ let data;
4504
+ try {
4505
+ data = JSON.parse(readFileSync15(path, "utf8"));
4506
+ } catch (e) {
4507
+ warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
4508
+ return false;
4509
+ }
4510
+ const fileWarnings = [];
4511
+ const flat = flattenObject(data, "", fileWarnings);
4512
+ for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
4513
+ const families = /* @__PURE__ */ new Set();
4514
+ for (const [k, v] of Object.entries(flat)) {
4515
+ const m = PLURAL_SUFFIX_RE.exec(k);
4516
+ if (m && m[2] === "other" && v !== "") families.add(m[1]);
4517
+ }
4518
+ const pluralForms = {};
4519
+ for (const [k, raw] of Object.entries(flat)) {
4520
+ if (raw === "") continue;
4521
+ const value = fromI18next(raw);
4522
+ const m = PLURAL_SUFFIX_RE.exec(k);
4523
+ if (m && families.has(m[1])) {
4524
+ (pluralForms[m[1]] ??= {})[m[2]] = value;
4525
+ continue;
4526
+ }
4527
+ if (families.has(k)) {
4528
+ warnings.push(
4529
+ `i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
4530
+ );
4531
+ continue;
4532
+ }
4533
+ (keys[prefix + k] ??= { values: {} }).values[locale] = value;
4534
+ }
4535
+ for (const [base, forms] of Object.entries(pluralForms)) {
4536
+ (keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
4537
+ }
4538
+ return true;
4539
+ }
4540
+ var i18nextJson2 = {
4541
+ name: "i18next-json",
4542
+ parse(localeRoot, opts) {
4543
+ const warnings = [];
4544
+ const keys = {};
4545
+ const locales = [];
4546
+ for (const entry of readdirSync10(localeRoot).sort()) {
4547
+ const full = join11(localeRoot, entry);
4548
+ if (safeIsDir2(full)) {
4549
+ if (!LOCALE_RE7.test(entry)) continue;
4550
+ if (opts?.locales && !opts.locales.includes(entry)) continue;
4551
+ let any = false;
4552
+ for (const file of readdirSync10(full).sort()) {
4553
+ if (!file.endsWith(".json")) continue;
4554
+ const ns = file.slice(0, -".json".length);
4555
+ const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
4556
+ if (ingestFile(join11(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
4557
+ }
4558
+ if (any && !locales.includes(entry)) locales.push(entry);
4559
+ } else if (entry.endsWith(".json")) {
4560
+ const locale = entry.slice(0, -".json".length);
4561
+ if (!LOCALE_RE7.test(locale)) continue;
4562
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4563
+ if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
4564
+ locales.push(locale);
4565
+ }
4566
+ }
4567
+ }
4568
+ return { locales, keys, warnings };
4569
+ }
4570
+ };
4571
+
4572
+ // src/server/import/parsers/rails-yaml.ts
4573
+ import { readdirSync as readdirSync11, readFileSync as readFileSync16 } from "fs";
4574
+ import { join as join12 } from "path";
4575
+ var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
4576
+ var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
4577
+ function fromRuby(value) {
4578
+ return value.replace(/%\{(\w+)\}/g, "{$1}");
4579
+ }
4580
+ function makeNode() {
4581
+ return /* @__PURE__ */ Object.create(null);
4582
+ }
4583
+ function decodeDouble(body) {
4584
+ let out = "";
4585
+ for (let i = 0; i < body.length; i++) {
4586
+ const c = body[i];
4587
+ if (c !== "\\") {
4588
+ out += c;
4589
+ continue;
4590
+ }
4591
+ const n = body[++i];
4592
+ if (n === void 0) break;
4593
+ out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
4594
+ }
4595
+ return out;
4596
+ }
4597
+ function scanQuoted(s, start) {
4598
+ const q = s[start];
4599
+ if (q === '"') {
4600
+ for (let i = start + 1; i < s.length; i++) {
4601
+ if (s[i] === "\\") i++;
4602
+ else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
4603
+ }
4604
+ return null;
4605
+ }
4606
+ let out = "";
4607
+ for (let i = start + 1; i < s.length; i++) {
4608
+ if (s[i] === "'") {
4609
+ if (s[i + 1] === "'") {
4610
+ out += "'";
4611
+ i++;
4612
+ } else {
4613
+ return { text: out, end: i + 1 };
4614
+ }
4615
+ } else {
4616
+ out += s[i];
4617
+ }
4618
+ }
4619
+ return null;
4620
+ }
4621
+ function stripPlainComment(s) {
4622
+ const m = /(^|\s)#/.exec(s);
4623
+ return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
4624
+ }
4625
+ function onlyTrailing(s) {
4626
+ return /^\s*(#.*)?$/.test(s);
4627
+ }
4628
+ function parseYamlSubset(text, file, warnings) {
4629
+ const roots = {};
4630
+ const lines = text.split(/\r?\n/);
4631
+ let stack = [];
4632
+ let skipDeeperThan = null;
4633
+ let lastLeafIndent = null;
4634
+ for (let n = 0; n < lines.length; n++) {
4635
+ const raw = lines[n];
4636
+ const lineNo = n + 1;
4637
+ if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
4638
+ if (raw.trim() === "---") continue;
4639
+ const indentMatch = /^[ \t]*/.exec(raw)[0];
4640
+ if (indentMatch.includes(" ")) {
4641
+ warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
4642
+ continue;
4643
+ }
4644
+ const indent = indentMatch.length;
4645
+ if (skipDeeperThan !== null) {
4646
+ if (indent > skipDeeperThan) continue;
4647
+ skipDeeperThan = null;
4648
+ }
4649
+ const content = raw.slice(indent);
4650
+ if (content.startsWith("- ") || content === "-") {
4651
+ warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
4652
+ skipDeeperThan = indent;
4653
+ continue;
4654
+ }
4655
+ let key;
4656
+ let rest;
4657
+ if (content[0] === '"' || content[0] === "'") {
4658
+ const k = scanQuoted(content, 0);
4659
+ if (!k || content[k.end] !== ":") {
4660
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
4661
+ skipDeeperThan = indent;
4662
+ continue;
4663
+ }
4664
+ key = k.text;
4665
+ rest = content.slice(k.end + 1);
4666
+ } else {
4667
+ const m = /^(.*?):(?=\s|$)/.exec(content);
4668
+ if (!m) {
4669
+ warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
4670
+ skipDeeperThan = indent;
4671
+ continue;
4672
+ }
4673
+ key = m[1].trim();
4674
+ rest = content.slice(m[0].length);
4675
+ }
4676
+ if (lastLeafIndent !== null && indent > lastLeafIndent) {
4677
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
4678
+ skipDeeperThan = indent - 1;
4679
+ continue;
4680
+ }
4681
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
4682
+ const trimmed = rest.trim();
4683
+ let value;
4684
+ if (trimmed === "" || trimmed.startsWith("#")) {
4685
+ value = null;
4686
+ } else if (trimmed[0] === "&" || trimmed[0] === "*") {
4687
+ warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
4688
+ skipDeeperThan = indent;
4689
+ continue;
4690
+ } else if (trimmed[0] === "|" || trimmed[0] === ">") {
4691
+ warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
4692
+ skipDeeperThan = indent;
4693
+ continue;
4694
+ } else if (trimmed[0] === "[" || trimmed[0] === "{") {
4695
+ warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
4696
+ skipDeeperThan = indent;
4697
+ continue;
4698
+ } else if (trimmed[0] === '"' || trimmed[0] === "'") {
4699
+ const v = scanQuoted(trimmed, 0);
4700
+ if (!v || !onlyTrailing(trimmed.slice(v.end))) {
4701
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
4702
+ continue;
4703
+ }
4704
+ value = v.text;
4705
+ } else {
4706
+ value = stripPlainComment(trimmed);
4707
+ }
4708
+ if (stack.length === 0) {
4709
+ if (value !== null) {
4710
+ warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
4711
+ lastLeafIndent = indent;
4712
+ continue;
4713
+ }
4714
+ const root = roots[key] ??= makeNode();
4715
+ stack = [{ indent, node: root }];
4716
+ lastLeafIndent = null;
4717
+ continue;
4718
+ }
4719
+ const parent = stack[stack.length - 1].node;
4720
+ if (key in parent) {
4721
+ warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
4722
+ }
4723
+ if (value === null) {
4724
+ const child = makeNode();
4725
+ parent[key] = child;
4726
+ stack.push({ indent, node: child });
4727
+ lastLeafIndent = null;
4728
+ } else {
4729
+ parent[key] = value;
4730
+ lastLeafIndent = indent;
4731
+ }
4732
+ }
4733
+ return { roots };
4734
+ }
4735
+ function asPluralForms(node) {
4736
+ const entries = Object.entries(node);
4737
+ if (entries.length === 0) return null;
4738
+ const forms = {};
4739
+ for (const [k, v] of entries) {
4740
+ if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
4741
+ if (v !== "") forms[k] = v;
4742
+ }
4743
+ if (!("other" in forms)) return null;
4744
+ return forms;
4745
+ }
4746
+ function synthesizeIcu(forms, file, key, warnings) {
4747
+ const parts = [];
4748
+ for (const cat of PLURAL_CATEGORIES) {
4749
+ const body = forms[cat];
4750
+ if (body === void 0) continue;
4751
+ if (body.includes("#")) {
4752
+ warnings.push(
4753
+ `rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
4754
+ );
4755
+ }
4756
+ parts.push(`${cat} {${fromRuby(body)}}`);
4757
+ }
4758
+ return `{count, plural, ${parts.join(" ")}}`;
4759
+ }
4760
+ var railsYaml2 = {
4761
+ name: "rails-yaml",
4762
+ parse(localeRoot, opts) {
4763
+ const warnings = [];
4764
+ const keys = {};
4765
+ const locales = [];
4766
+ const wanted = opts?.locales?.map((l) => l.toLowerCase());
4767
+ const addValue = (key, locale, value) => {
4768
+ (keys[key] ??= { values: {} }).values[locale] = value;
4769
+ };
4770
+ const flatten = (node, prefix, locale, file) => {
4771
+ for (const [k, v] of Object.entries(node)) {
4772
+ const key = prefix ? `${prefix}.${k}` : k;
4773
+ if (typeof v === "string") {
4774
+ if (v !== "") addValue(key, locale, fromRuby(v));
4775
+ continue;
4776
+ }
4777
+ const forms = asPluralForms(v);
4778
+ if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
4779
+ else flatten(v, key, locale, file);
4780
+ }
4781
+ };
4782
+ for (const file of readdirSync11(localeRoot).sort()) {
4783
+ if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
4784
+ let text;
4785
+ try {
4786
+ text = readFileSync16(join12(localeRoot, file), "utf8");
4787
+ } catch (e) {
4788
+ warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
4789
+ continue;
4790
+ }
4791
+ const { roots } = parseYamlSubset(text, file, warnings);
4792
+ for (const token of Object.keys(roots).sort()) {
4793
+ if (!LOCALE_RE8.test(token)) {
4794
+ warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
4795
+ continue;
4796
+ }
4797
+ if (wanted && !wanted.includes(token.toLowerCase())) continue;
4798
+ if (!locales.includes(token)) locales.push(token);
4799
+ flatten(roots[token], "", token, file);
4800
+ }
4801
+ }
4802
+ return { locales, keys, warnings };
4803
+ }
4804
+ };
4805
+
4806
+ // src/server/import/parsers/apple-stringsdict.ts
4807
+ import { readdirSync as readdirSync12, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
4808
+ import { join as join13 } from "path";
4809
+ var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4810
+ var TABLE2 = "Localizable.stringsdict";
4811
+ function localeFromLproj2(dir) {
4812
+ const m = dir.match(/^(.+)\.lproj$/);
4813
+ if (!m) return null;
4814
+ return LOCALE_RE9.test(m[1]) ? m[1] : null;
4815
+ }
4816
+ function decodeEntities2(s) {
4817
+ 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, "&");
4818
+ }
4819
+ function parsePlistDict(xml) {
4820
+ let i = 0;
4821
+ const n = xml.length;
4822
+ const skipTrivia = () => {
4823
+ for (; ; ) {
4824
+ while (i < n && /\s/.test(xml[i])) i++;
4825
+ if (xml.startsWith("<!--", i)) {
4826
+ const end = xml.indexOf("-->", i + 4);
4827
+ if (end === -1) throw new Error("unterminated comment");
4828
+ i = end + 3;
4829
+ continue;
4830
+ }
4831
+ if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
4832
+ const end = xml.indexOf(">", i);
4833
+ if (end === -1) throw new Error("unterminated declaration");
4834
+ i = end + 1;
4835
+ continue;
4836
+ }
4837
+ break;
4838
+ }
4839
+ };
4840
+ const readTag = () => {
4841
+ if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
4842
+ const end = xml.indexOf(">", i);
4843
+ if (end === -1) throw new Error("unterminated tag");
4844
+ let body = xml.slice(i + 1, end).trim();
4845
+ i = end + 1;
4846
+ const closing = body.startsWith("/");
4847
+ if (closing) body = body.slice(1).trim();
4848
+ const selfClosing = body.endsWith("/");
4849
+ if (selfClosing) body = body.slice(0, -1).trim();
4850
+ const name = body.split(/\s/)[0];
4851
+ if (!name) throw new Error(`empty tag at offset ${end}`);
4852
+ return { name, closing, selfClosing };
4853
+ };
4854
+ const readElementText = (name) => {
4855
+ const re = new RegExp(`</${name}\\s*>`, "g");
4856
+ re.lastIndex = i;
4857
+ const m = re.exec(xml);
4858
+ if (!m) throw new Error(`unterminated <${name}>`);
4859
+ const text = xml.slice(i, m.index);
4860
+ i = m.index + m[0].length;
4861
+ return decodeEntities2(text);
4862
+ };
4863
+ const readValue = (tag2) => {
4864
+ if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
4865
+ if (tag2.name === "true" || tag2.name === "false") {
4866
+ if (!tag2.selfClosing) readElementText(tag2.name);
4867
+ return tag2.name;
4868
+ }
4869
+ if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
4870
+ return tag2.selfClosing ? "" : readElementText(tag2.name);
4871
+ }
4872
+ throw new Error(`unsupported plist element <${tag2.name}>`);
4873
+ };
4874
+ const readDict = () => {
4875
+ const out = {};
4876
+ for (; ; ) {
4877
+ skipTrivia();
4878
+ const tag2 = readTag();
4879
+ if (tag2.closing) {
4880
+ if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
4881
+ return out;
4882
+ }
4883
+ if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
4884
+ const key = readElementText("key");
4885
+ skipTrivia();
4886
+ const vt = readTag();
4887
+ if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
4888
+ out[key] = readValue(vt);
4889
+ }
4890
+ };
4891
+ skipTrivia();
4892
+ let tag = readTag();
4893
+ if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
4894
+ skipTrivia();
4895
+ tag = readTag();
4896
+ }
4897
+ if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
4898
+ return tag.selfClosing ? {} : readDict();
4899
+ }
4900
+ var VAR_RE = /%#@([^@]*)@/g;
4901
+ function entryToIcu(key, entry, file, warnings) {
4902
+ const warn = (msg) => {
4903
+ warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
4904
+ return null;
4905
+ };
4906
+ if (typeof entry !== "object") return warn("value is not a dict; skipped");
4907
+ const fmt = entry["NSStringLocalizedFormatKey"];
4908
+ if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
4909
+ const vars = [...fmt.matchAll(VAR_RE)];
4910
+ if (vars.length !== 1) {
4911
+ return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
4912
+ }
4913
+ const arg = vars[0][1];
4914
+ if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
4915
+ const prefix = fmt.slice(0, vars[0].index);
4916
+ const suffix = fmt.slice(vars[0].index + vars[0][0].length);
4917
+ const varDict = entry[arg];
4918
+ if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
4919
+ const specType = varDict["NSStringFormatSpecTypeKey"];
4920
+ if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
4921
+ return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
4922
+ }
4923
+ const valueType = varDict["NSStringFormatValueTypeKey"];
4924
+ const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
4925
+ const forms = {};
4926
+ for (const cat of PLURAL_CATEGORIES) {
4927
+ const body = varDict[cat];
4928
+ if (typeof body !== "string") continue;
4929
+ forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
4930
+ }
4931
+ if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
4932
+ return formsToIcu(arg, forms);
4933
+ }
4934
+ var appleStringsdict2 = {
4935
+ name: "apple-stringsdict",
4936
+ parse(localeRoot, opts) {
4937
+ const warnings = [];
4938
+ const keys = {};
4939
+ const locales = [];
4940
+ for (const dir of readdirSync12(localeRoot).sort()) {
4941
+ const locale = localeFromLproj2(dir);
4942
+ if (!locale) continue;
4943
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4944
+ const file = join13(localeRoot, dir, TABLE2);
4945
+ let text;
4946
+ try {
4947
+ if (!statSync6(file).isFile()) continue;
4948
+ text = readFileSync17(file, "utf8");
4949
+ } catch {
4950
+ continue;
4951
+ }
4952
+ locales.push(locale);
4953
+ const others = readdirSync12(join13(localeRoot, dir)).filter(
4954
+ (f) => f.endsWith(".stringsdict") && f !== TABLE2
4955
+ );
4956
+ if (others.length) {
4957
+ warnings.push(
4958
+ `apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
4959
+ );
4960
+ }
4961
+ let root;
4962
+ try {
4963
+ root = parsePlistDict(text);
4964
+ } catch (e) {
4965
+ warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
4966
+ continue;
4967
+ }
4968
+ for (const key of Object.keys(root).sort()) {
4969
+ const icu = entryToIcu(key, root[key], file, warnings);
4970
+ if (icu === null) continue;
4971
+ (keys[key] ??= { values: {} }).values[locale] = icu;
4972
+ }
4973
+ }
4974
+ return { locales, keys, warnings };
4975
+ }
4976
+ };
4977
+
4109
4978
  // src/server/import/parsers/index.ts
4110
4979
  var REGISTRY = {
4111
4980
  [vueI18nJson2.name]: vueI18nJson2,
4112
4981
  [laravelPhp2.name]: laravelPhp2,
4113
4982
  [flutterArb2.name]: flutterArb2,
4114
- [appleStrings2.name]: appleStrings2
4983
+ [appleStrings2.name]: appleStrings2,
4984
+ [angularXliff2.name]: angularXliff2,
4985
+ [gettextPo2.name]: gettextPo2,
4986
+ [i18nextJson2.name]: i18nextJson2,
4987
+ [railsYaml2.name]: railsYaml2,
4988
+ [appleStringsdict2.name]: appleStringsdict2
4115
4989
  };
4116
4990
  function getParser(name) {
4117
4991
  const p = REGISTRY[name];
@@ -4124,7 +4998,14 @@ var OUTPUT_BY_FORMAT = {
4124
4998
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
4125
4999
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
4126
5000
  "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
4127
- "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
5001
+ "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
5002
+ // skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
5003
+ // only writes the translation files back next to it.
5004
+ "angular-xliff": { adapter: "angular-xliff", path: "messages.{locale}.xlf", rootRelative: true, skipSourceLocale: true },
5005
+ "gettext-po": { adapter: "gettext-po", path: "{locale}.po", rootRelative: true },
5006
+ "i18next-json": { adapter: "i18next-json", path: "{locale}/translation.json", rootRelative: true },
5007
+ "rails-yaml": { adapter: "rails-yaml", path: "config/locales/{locale}.yml" },
5008
+ "apple-stringsdict": { adapter: "apple-stringsdict", path: "{locale}.lproj/Localizable.stringsdict", rootRelative: true }
4128
5009
  };
4129
5010
  function assemble2(parsed, opts) {
4130
5011
  const warnings = [...parsed.warnings];
@@ -4242,7 +5123,7 @@ function runImport(opts) {
4242
5123
  }
4243
5124
 
4244
5125
  // src/server/export-run.ts
4245
- import { existsSync as existsSync10, readFileSync as readFileSync12, readdirSync as readdirSync8, rmdirSync, statSync as statSync5, unlinkSync } from "fs";
5126
+ import { existsSync as existsSync10, readFileSync as readFileSync18, readdirSync as readdirSync13, rmdirSync, statSync as statSync7, unlinkSync } from "fs";
4246
5127
  import { dirname as dirname2, resolve as resolve7, sep } from "path";
4247
5128
  function effectiveLocales(config) {
4248
5129
  const limit = config.exportLocales;
@@ -4285,7 +5166,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4285
5166
  if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
4286
5167
  const next = resolve7(dir, segment);
4287
5168
  if (isLast) {
4288
- if (stale(locale) && existsSync10(next) && statSync5(next).isFile()) {
5169
+ if (stale(locale) && existsSync10(next) && statSync7(next).isFile()) {
4289
5170
  unlinkSync(next);
4290
5171
  deleted++;
4291
5172
  removeEmptyDirs(dir, root);
@@ -4298,7 +5179,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
4298
5179
  const re = segmentRegExp(segment);
4299
5180
  let entries;
4300
5181
  try {
4301
- entries = readdirSync8(dir, { withFileTypes: true });
5182
+ entries = readdirSync13(dir, { withFileTypes: true });
4302
5183
  } catch {
4303
5184
  return;
4304
5185
  }
@@ -4341,7 +5222,7 @@ function exportToDisk(state, projectRoot, opts) {
4341
5222
  writtenPaths.add(abs);
4342
5223
  let current = null;
4343
5224
  try {
4344
- current = readFileSync12(abs, "utf8");
5225
+ current = readFileSync18(abs, "utf8");
4345
5226
  } catch {
4346
5227
  }
4347
5228
  if (current === f.contents) {
@@ -4358,17 +5239,17 @@ function exportToDisk(state, projectRoot, opts) {
4358
5239
  }
4359
5240
 
4360
5241
  // src/server/ui-prefs.ts
4361
- import { readFileSync as readFileSync13 } from "fs";
5242
+ import { readFileSync as readFileSync19 } from "fs";
4362
5243
  import { homedir } from "os";
4363
- import { join as join9 } from "path";
5244
+ import { join as join14 } from "path";
4364
5245
  var THEMES = ["system", "light", "dark"];
4365
5246
  var isThemeMode = (v) => THEMES.includes(v);
4366
5247
  var isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4367
- var defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
5248
+ var defaultUiPrefsPath = () => join14(homedir(), ".glotfile", "ui.json");
4368
5249
  var DEFAULTS = { theme: "system" };
4369
5250
  function readJson(path) {
4370
5251
  try {
4371
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
5252
+ const parsed = JSON.parse(readFileSync19(path, "utf8"));
4372
5253
  return parsed && typeof parsed === "object" ? parsed : {};
4373
5254
  } catch {
4374
5255
  return {};
@@ -4387,7 +5268,7 @@ function saveUiPrefs(path, prefs) {
4387
5268
  }
4388
5269
 
4389
5270
  // src/server/local-settings.ts
4390
- import { readFileSync as readFileSync14 } from "fs";
5271
+ import { readFileSync as readFileSync20 } from "fs";
4391
5272
  import { resolve as resolve8 } from "path";
4392
5273
  var EDITOR_IDS = ["vscode", "zed", "phpstorm"];
4393
5274
  var isEditorId = (v) => EDITOR_IDS.includes(v);
@@ -4402,7 +5283,7 @@ var DEFAULT_EDITOR = "vscode";
4402
5283
  var settingsPath = (projectRoot) => resolve8(projectRoot, ".glotfile", "settings.json");
4403
5284
  function readJson2(path) {
4404
5285
  try {
4405
- const parsed = JSON.parse(readFileSync14(path, "utf8"));
5286
+ const parsed = JSON.parse(readFileSync20(path, "utf8"));
4406
5287
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4407
5288
  } catch {
4408
5289
  return {};
@@ -4475,7 +5356,7 @@ function projectName(root) {
4475
5356
  const nameFile = resolve9(root, ".idea", ".name");
4476
5357
  if (existsSync11(nameFile)) {
4477
5358
  try {
4478
- const name = readFileSync15(nameFile, "utf8").trim();
5359
+ const name = readFileSync21(nameFile, "utf8").trim();
4479
5360
  if (name) return name;
4480
5361
  } catch {
4481
5362
  }
@@ -4600,7 +5481,7 @@ function createApi(deps) {
4600
5481
  if (depth > 4) return;
4601
5482
  let entries = [];
4602
5483
  try {
4603
- entries = readdirSync9(dir);
5484
+ entries = readdirSync14(dir);
4604
5485
  } catch {
4605
5486
  return;
4606
5487
  }
@@ -4614,7 +5495,7 @@ function createApi(deps) {
4614
5495
  filePath = abs;
4615
5496
  } else {
4616
5497
  try {
4617
- if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
5498
+ if (statSync8(abs).isDirectory()) walk(abs, depth + 1);
4618
5499
  } catch {
4619
5500
  }
4620
5501
  continue;
@@ -5395,7 +6276,7 @@ function createApi(deps) {
5395
6276
 
5396
6277
  // src/server/server.ts
5397
6278
  var here = dirname4(fileURLToPath(import.meta.url));
5398
- var DEFAULT_UI_DIR = join10(here, "..", "ui");
6279
+ var DEFAULT_UI_DIR = join15(here, "..", "ui");
5399
6280
  var MIME = {
5400
6281
  ".html": "text/html; charset=utf-8",
5401
6282
  ".js": "text/javascript; charset=utf-8",
@@ -5449,7 +6330,7 @@ function buildApp(opts) {
5449
6330
  const file = await readFileResponse(target);
5450
6331
  if (file) return file;
5451
6332
  }
5452
- const index = await readFileResponse(join10(root, "index.html"));
6333
+ const index = await readFileResponse(join15(root, "index.html"));
5453
6334
  if (index) return index;
5454
6335
  return c.notFound();
5455
6336
  });