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.
@@ -122,6 +122,7 @@ function validate(raw) {
122
122
  if (o.indent !== void 0 && typeof o.indent !== "number") fail("config.outputs[].indent must be a number");
123
123
  if (o.finalNewline !== void 0 && typeof o.finalNewline !== "boolean") fail("config.outputs[].finalNewline must be a boolean");
124
124
  if (o.includeLocale !== void 0 && typeof o.includeLocale !== "boolean") fail("config.outputs[].includeLocale must be a boolean");
125
+ if (o.skipSourceLocale !== void 0 && typeof o.skipSourceLocale !== "boolean") fail("config.outputs[].skipSourceLocale must be a boolean");
125
126
  if (o.localeAliases !== void 0) {
126
127
  if (!isObject(o.localeAliases)) fail("config.outputs[].localeAliases must be an object");
127
128
  for (const [k, v] of Object.entries(o.localeAliases)) {
@@ -1574,27 +1575,41 @@ var init_vue_i18n_json = __esm({
1574
1575
  function xmlEscape2(s) {
1575
1576
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1576
1577
  }
1577
- function renderInterpolations(text, ids) {
1578
+ function attrEscape(s) {
1579
+ return xmlEscape2(s).replace(/"/g, "&quot;");
1580
+ }
1581
+ function angularXMeta(placeholders, name) {
1582
+ return /^[A-Z][A-Z0-9_]*$/.test(name) ? placeholders?.[name] : void 0;
1583
+ }
1584
+ function renderInterpolations(text, ids, placeholders) {
1578
1585
  let out = "";
1579
1586
  let last = 0;
1580
1587
  for (const m of text.matchAll(/\{(\w+)\}/g)) {
1581
1588
  const name = m[1];
1582
- let id = ids.get(name);
1583
- if (id === void 0) {
1584
- id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
1585
- ids.set(name, id);
1589
+ out += xmlEscape2(text.slice(last, m.index));
1590
+ const meta = angularXMeta(placeholders, name);
1591
+ if (meta) {
1592
+ const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
1593
+ const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
1594
+ out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
1595
+ } else {
1596
+ let id = ids.get(name);
1597
+ if (id === void 0) {
1598
+ id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
1599
+ ids.set(name, id);
1600
+ }
1601
+ out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
1586
1602
  }
1587
- out += xmlEscape2(text.slice(last, m.index)) + `<x id="${id}" equiv-text="{{${name}}}"/>`;
1588
1603
  last = m.index + m[0].length;
1589
1604
  }
1590
1605
  return out + xmlEscape2(text.slice(last));
1591
1606
  }
1592
- function renderPluralIcu(forms, ids) {
1607
+ function renderPluralIcu(forms, ids, placeholders) {
1593
1608
  const cats = [
1594
1609
  ...Object.keys(forms).filter((c) => c.startsWith("=")),
1595
1610
  ...PLURAL_CATEGORIES.filter((c) => forms[c] !== void 0)
1596
1611
  ];
1597
- const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids)}}`);
1612
+ const branches = cats.map((cat) => `${cat} {${renderInterpolations(forms[cat] ?? "", ids, placeholders)}}`);
1598
1613
  return `{VAR_PLURAL, plural, ${branches.join(" ")}}`;
1599
1614
  }
1600
1615
  function renderEmbeddedIcu(value) {
@@ -1604,8 +1619,8 @@ function renderEmbeddedIcu(value) {
1604
1619
  );
1605
1620
  return xmlEscape2(renamed);
1606
1621
  }
1607
- function renderScalar(value, ids) {
1608
- return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids);
1622
+ function renderScalar(value, ids, placeholders) {
1623
+ return isIcuPluralOrSelect(value) ? renderEmbeddedIcu(value) : renderInterpolations(value, ids, placeholders);
1609
1624
  }
1610
1625
  var DEFAULT_LOCALE_CASE8, angularXliff;
1611
1626
  var init_angular_xliff = __esm({
@@ -1636,6 +1651,7 @@ var init_angular_xliff = __esm({
1636
1651
  const emptyAs = resolveEmptyAs(output, "source");
1637
1652
  const keys = Object.keys(state.keys).sort();
1638
1653
  for (const locale of state.config.locales) {
1654
+ if (output.skipSourceLocale && locale === sourceLocale) continue;
1639
1655
  const token = resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE8);
1640
1656
  const units = [];
1641
1657
  for (const key of keys) {
@@ -1646,17 +1662,18 @@ var init_angular_xliff = __esm({
1646
1662
  if (entry.plural) {
1647
1663
  const targetForms = resolveForms(entry, locale, sourceLocale, emptyAs);
1648
1664
  if (targetForms === null) continue;
1649
- source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids);
1650
- target = renderPluralIcu(targetForms, ids);
1665
+ source = renderPluralIcu(entry.values[sourceLocale]?.forms ?? {}, ids, entry.placeholders);
1666
+ target = renderPluralIcu(targetForms, ids, entry.placeholders);
1651
1667
  } else {
1652
1668
  const targetValue = resolveScalar(entry, locale, sourceLocale, emptyAs);
1653
1669
  if (targetValue === null) continue;
1654
- source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids);
1655
- target = renderScalar(targetValue, ids);
1670
+ source = renderScalar(entry.values[sourceLocale]?.value ?? "", ids, entry.placeholders);
1671
+ target = renderScalar(targetValue, ids, entry.placeholders);
1656
1672
  }
1673
+ const translated = locale === sourceLocale || (entry.plural ? entry.values[locale]?.forms !== void 0 : !!entry.values[locale]?.value);
1657
1674
  units.push(` <trans-unit id="${xmlEscape2(key)}" datatype="html">`);
1658
1675
  units.push(` <source>${source}</source>`);
1659
- units.push(` <target>${target}</target>`);
1676
+ units.push(` <target${translated ? "" : ' state="new"'}>${target}</target>`);
1660
1677
  if (entry.description) {
1661
1678
  units.push(` <note priority="1" from="description">${xmlEscape2(entry.description)}</note>`);
1662
1679
  }
@@ -4032,7 +4049,7 @@ var init_accept = __esm({
4032
4049
  });
4033
4050
 
4034
4051
  // src/server/import/detect.ts
4035
- import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
4052
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
4036
4053
  import { join as join4 } from "path";
4037
4054
  function safeIsDir(p) {
4038
4055
  try {
@@ -4119,6 +4136,110 @@ function detectApple(root) {
4119
4136
  }
4120
4137
  return best;
4121
4138
  }
4139
+ function detectAngularXliff(root) {
4140
+ for (const rel of ANGULAR_DIR_CANDIDATES) {
4141
+ const localeRoot = rel === "." ? root : join4(root, rel);
4142
+ if (!safeIsDir(localeRoot)) continue;
4143
+ const files = readdirSync4(localeRoot).filter((f) => /^messages(\..+)?\.xlf$/.test(f)).sort();
4144
+ if (files.length === 0) continue;
4145
+ const locales = files.map((f) => f.match(/^messages\.(.+)\.xlf$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l));
4146
+ const attrFile = files.includes("messages.xlf") ? "messages.xlf" : files[0];
4147
+ let sourceLocale;
4148
+ try {
4149
+ sourceLocale = readFileSync11(join4(localeRoot, attrFile), "utf8").match(/source-language="([^"]+)"/)?.[1];
4150
+ } catch {
4151
+ }
4152
+ if (!sourceLocale && locales.length === 0) continue;
4153
+ sourceLocale ??= pickSource(locales, () => 0);
4154
+ if (!locales.includes(sourceLocale)) locales.unshift(sourceLocale);
4155
+ return { format: "angular-xliff", localeRoot, locales, sourceLocale };
4156
+ }
4157
+ return null;
4158
+ }
4159
+ function detectRails(root) {
4160
+ const localeRoot = join4(root, "config", "locales");
4161
+ if (!safeIsDir(localeRoot)) return null;
4162
+ const locales = [];
4163
+ for (const file of readdirSync4(localeRoot).sort()) {
4164
+ if (!/\.ya?ml$/.test(file)) continue;
4165
+ let text;
4166
+ try {
4167
+ text = readFileSync11(join4(localeRoot, file), "utf8");
4168
+ } catch {
4169
+ continue;
4170
+ }
4171
+ for (const m of text.matchAll(/^(["']?)([A-Za-z][\w-]*)\1:\s*(?:#.*)?$/gm)) {
4172
+ const token = m[2];
4173
+ if (LOCALE_RE.test(token) && !locales.includes(token)) locales.push(token);
4174
+ }
4175
+ }
4176
+ if (locales.length === 0) return null;
4177
+ return { format: "rails-yaml", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
4178
+ }
4179
+ function detectI18next(root) {
4180
+ for (const rel of I18NEXT_DIR_CANDIDATES) {
4181
+ const localeRoot = join4(root, rel);
4182
+ if (!safeIsDir(localeRoot)) continue;
4183
+ const locales = listDirs(localeRoot).filter(
4184
+ (d) => LOCALE_RE.test(d) && readdirSync4(join4(localeRoot, d)).some((f) => f.endsWith(".json"))
4185
+ );
4186
+ if (locales.length === 0) continue;
4187
+ const sourceLocale = pickSource(locales, (loc) => {
4188
+ try {
4189
+ return readdirSync4(join4(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join4(localeRoot, loc, f)).size, 0);
4190
+ } catch {
4191
+ return 0;
4192
+ }
4193
+ });
4194
+ return { format: "i18next-json", localeRoot, locales, sourceLocale };
4195
+ }
4196
+ return null;
4197
+ }
4198
+ function gettextLocales(dir) {
4199
+ const locales = [];
4200
+ for (const entry of readdirSync4(dir).sort()) {
4201
+ const flat = entry.match(/^(.+)\.po$/)?.[1];
4202
+ if (flat && LOCALE_RE.test(flat)) {
4203
+ if (!locales.includes(flat)) locales.push(flat);
4204
+ continue;
4205
+ }
4206
+ if (!LOCALE_RE.test(entry) || !safeIsDir(join4(dir, entry))) continue;
4207
+ const sub = join4(dir, entry);
4208
+ const hasPo = (d) => {
4209
+ try {
4210
+ return readdirSync4(d).some((f) => f.endsWith(".po"));
4211
+ } catch {
4212
+ return false;
4213
+ }
4214
+ };
4215
+ if (hasPo(join4(sub, "LC_MESSAGES")) || hasPo(sub)) {
4216
+ if (!locales.includes(entry)) locales.push(entry);
4217
+ }
4218
+ }
4219
+ return locales;
4220
+ }
4221
+ function detectGettext(root) {
4222
+ for (const rel of GETTEXT_DIR_CANDIDATES) {
4223
+ const localeRoot = join4(root, rel);
4224
+ if (!safeIsDir(localeRoot)) continue;
4225
+ const locales = gettextLocales(localeRoot);
4226
+ if (locales.length === 0) continue;
4227
+ return { format: "gettext-po", localeRoot, locales, sourceLocale: pickSource(locales, () => 0) };
4228
+ }
4229
+ return null;
4230
+ }
4231
+ function detectAppleStringsdict(root) {
4232
+ const candidates = [root, ...listDirs(root).map((d) => join4(root, d))];
4233
+ let best = null;
4234
+ for (const dir of candidates) {
4235
+ const locales = listDirs(dir).map((d) => d.match(/^(.+)\.lproj$/)?.[1]).filter((l) => !!l && LOCALE_RE.test(l) && existsSync10(join4(dir, `${l}.lproj`, "Localizable.stringsdict")));
4236
+ if (locales.length === 0) continue;
4237
+ if (!best || locales.length > best.locales.length) {
4238
+ best = { format: "apple-stringsdict", localeRoot: dir, locales, sourceLocale: pickSource(locales, () => 0) };
4239
+ }
4240
+ }
4241
+ return best;
4242
+ }
4122
4243
  function detect(root, formatOverride) {
4123
4244
  if (!existsSync10(root)) return null;
4124
4245
  if (formatOverride) {
@@ -4132,18 +4253,36 @@ function detect(root, formatOverride) {
4132
4253
  }
4133
4254
  return null;
4134
4255
  }
4135
- var LOCALE_RE, VUE_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
4256
+ var LOCALE_RE, VUE_DIR_CANDIDATES, ANGULAR_DIR_CANDIDATES, I18NEXT_DIR_CANDIDATES, GETTEXT_DIR_CANDIDATES, DETECTORS, BY_FORMAT;
4136
4257
  var init_detect = __esm({
4137
4258
  "src/server/import/detect.ts"() {
4138
4259
  "use strict";
4139
4260
  LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4140
4261
  VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
4141
- DETECTORS = [detectLaravel, detectVue, detectArb, detectApple];
4262
+ ANGULAR_DIR_CANDIDATES = [".", "src/locale", "src/locales", "src/i18n", "locale", "locales", "i18n", "translations"];
4263
+ I18NEXT_DIR_CANDIDATES = ["public/locales", "static/locales", "locales", "src/locales", "src/i18n/locales"];
4264
+ GETTEXT_DIR_CANDIDATES = ["locale", "locales", "po", "translations"];
4265
+ DETECTORS = [
4266
+ detectLaravel,
4267
+ detectVue,
4268
+ detectArb,
4269
+ detectApple,
4270
+ detectAngularXliff,
4271
+ detectRails,
4272
+ detectI18next,
4273
+ detectGettext,
4274
+ detectAppleStringsdict
4275
+ ];
4142
4276
  BY_FORMAT = {
4143
4277
  "laravel-php": detectLaravel,
4144
4278
  "vue-i18n-json": (root) => detectVue(root, true),
4145
4279
  "flutter-arb": detectArb,
4146
- "apple-strings": detectApple
4280
+ "apple-strings": detectApple,
4281
+ "angular-xliff": detectAngularXliff,
4282
+ "rails-yaml": detectRails,
4283
+ "i18next-json": detectI18next,
4284
+ "gettext-po": detectGettext,
4285
+ "apple-stringsdict": detectAppleStringsdict
4147
4286
  };
4148
4287
  }
4149
4288
  });
@@ -4176,7 +4315,7 @@ var init_flatten = __esm({
4176
4315
  });
4177
4316
 
4178
4317
  // src/server/import/parsers/vue-i18n-json.ts
4179
- import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
4318
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
4180
4319
  import { join as join5 } from "path";
4181
4320
  var LOCALE_RE2, vueI18nJson2;
4182
4321
  var init_vue_i18n_json2 = __esm({
@@ -4197,7 +4336,7 @@ var init_vue_i18n_json2 = __esm({
4197
4336
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4198
4337
  let data;
4199
4338
  try {
4200
- data = JSON.parse(readFileSync11(join5(localeRoot, file), "utf8"));
4339
+ data = JSON.parse(readFileSync12(join5(localeRoot, file), "utf8"));
4201
4340
  } catch (e) {
4202
4341
  warnings.push(`vue-i18n-json: failed to parse ${file}: ${e.message}`);
4203
4342
  continue;
@@ -4302,7 +4441,7 @@ var init_laravel_php2 = __esm({
4302
4441
  });
4303
4442
 
4304
4443
  // src/server/import/parsers/flutter-arb.ts
4305
- import { readdirSync as readdirSync7, readFileSync as readFileSync12 } from "fs";
4444
+ import { readdirSync as readdirSync7, readFileSync as readFileSync13 } from "fs";
4306
4445
  import { join as join7 } from "path";
4307
4446
  function localeFromArbName(file) {
4308
4447
  const m = file.match(/^(.+)\.arb$/);
@@ -4343,7 +4482,7 @@ var init_flutter_arb2 = __esm({
4343
4482
  if (opts?.locales && !opts.locales.includes(locale)) continue;
4344
4483
  let data;
4345
4484
  try {
4346
- data = JSON.parse(readFileSync12(join7(localeRoot, file), "utf8"));
4485
+ data = JSON.parse(readFileSync13(join7(localeRoot, file), "utf8"));
4347
4486
  } catch (e) {
4348
4487
  warnings.push(`flutter-arb: failed to parse ${file}: ${e.message}`);
4349
4488
  continue;
@@ -4370,7 +4509,7 @@ var init_flutter_arb2 = __esm({
4370
4509
  });
4371
4510
 
4372
4511
  // src/server/import/parsers/apple-strings.ts
4373
- import { readdirSync as readdirSync8, readFileSync as readFileSync13, statSync as statSync5 } from "fs";
4512
+ import { readdirSync as readdirSync8, readFileSync as readFileSync14, statSync as statSync5 } from "fs";
4374
4513
  import { join as join8 } from "path";
4375
4514
  function localeFromLproj(dir) {
4376
4515
  const m = dir.match(/^(.+)\.lproj$/);
@@ -4483,7 +4622,7 @@ var init_apple_strings2 = __esm({
4483
4622
  let text;
4484
4623
  try {
4485
4624
  if (!statSync5(file).isFile()) continue;
4486
- text = readFileSync13(file, "utf8");
4625
+ text = readFileSync14(file, "utf8");
4487
4626
  } catch {
4488
4627
  continue;
4489
4628
  }
@@ -4502,6 +4641,773 @@ var init_apple_strings2 = __esm({
4502
4641
  }
4503
4642
  });
4504
4643
 
4644
+ // src/server/import/parsers/angular-xliff.ts
4645
+ import { readdirSync as readdirSync9, readFileSync as readFileSync15 } from "fs";
4646
+ import { join as join9 } from "path";
4647
+ function decodeEntities(s) {
4648
+ 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, "&");
4649
+ }
4650
+ function parseAttrs(s) {
4651
+ const out = {};
4652
+ for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
4653
+ return out;
4654
+ }
4655
+ function decodeInline(raw, addMeta) {
4656
+ let out = "";
4657
+ let last = 0;
4658
+ for (const m of raw.matchAll(/<x\b([^>]*?)\/>/g)) {
4659
+ out += decodeEntities(raw.slice(last, m.index));
4660
+ const attrs = parseAttrs(m[1]);
4661
+ const id = attrs["id"] ?? "X";
4662
+ const equiv = attrs["equiv-text"];
4663
+ const simple = equiv?.match(/^\{\{\s*(\w+)\s*\}\}$/);
4664
+ if (simple) {
4665
+ out += `{${simple[1]}}`;
4666
+ } else {
4667
+ out += `{${id}}`;
4668
+ const meta = {};
4669
+ if (attrs["ctype"]) meta.type = attrs["ctype"];
4670
+ if (equiv !== void 0) meta.example = equiv;
4671
+ addMeta(id, meta);
4672
+ }
4673
+ last = m.index + m[0].length;
4674
+ }
4675
+ return out + decodeEntities(raw.slice(last));
4676
+ }
4677
+ var LOCALE_RE5, FILE_RE, angularXliff2;
4678
+ var init_angular_xliff2 = __esm({
4679
+ "src/server/import/parsers/angular-xliff.ts"() {
4680
+ "use strict";
4681
+ LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4682
+ FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
4683
+ angularXliff2 = {
4684
+ name: "angular-xliff",
4685
+ parse(localeRoot, opts) {
4686
+ const warnings = [];
4687
+ const keys = {};
4688
+ const locales = [];
4689
+ const seen = (loc) => {
4690
+ if (!locales.includes(loc)) locales.push(loc);
4691
+ };
4692
+ const files = readdirSync9(localeRoot).filter((f) => FILE_RE.test(f)).sort((a, b) => (a === "messages.xlf" ? -1 : 0) - (b === "messages.xlf" ? -1 : 0) || a.localeCompare(b));
4693
+ for (const file of files) {
4694
+ const fnameLocale = file.match(FILE_RE)[1];
4695
+ if (fnameLocale !== void 0 && !LOCALE_RE5.test(fnameLocale)) continue;
4696
+ let xml;
4697
+ try {
4698
+ xml = readFileSync15(join9(localeRoot, file), "utf8");
4699
+ } catch (e) {
4700
+ warnings.push(`angular-xliff: failed to read ${file}: ${e.message}`);
4701
+ continue;
4702
+ }
4703
+ const sourceLocale = xml.match(/source-language="([^"]+)"/)?.[1];
4704
+ if (!sourceLocale) {
4705
+ warnings.push(`angular-xliff: ${file} has no source-language attribute; skipped`);
4706
+ continue;
4707
+ }
4708
+ const targetLocale = xml.match(/target-language="([^"]+)"/)?.[1] ?? fnameLocale;
4709
+ if (opts?.locales && !opts.locales.includes(targetLocale ?? sourceLocale)) continue;
4710
+ for (const unit of xml.matchAll(/<trans-unit\b([^>]*)>([\s\S]*?)<\/trans-unit>/g)) {
4711
+ const id = parseAttrs(unit[1])["id"];
4712
+ if (!id) {
4713
+ warnings.push(`angular-xliff: ${file} has a trans-unit without an id; skipped`);
4714
+ continue;
4715
+ }
4716
+ const body = unit[2];
4717
+ const src = body.match(/<source\b[^>]*>([\s\S]*?)<\/source>/);
4718
+ let tgt = body.match(/<target\b([^>]*)>([\s\S]*?)<\/target>/);
4719
+ if (tgt && /\bstate="new"/.test(tgt[1])) tgt = null;
4720
+ const entry = keys[id] ??= { values: {} };
4721
+ const addMeta = (name, meta) => {
4722
+ (entry.placeholders ??= {})[name] ??= meta;
4723
+ };
4724
+ if (src && entry.values[sourceLocale] === void 0) {
4725
+ entry.values[sourceLocale] = decodeInline(src[1], addMeta);
4726
+ seen(sourceLocale);
4727
+ }
4728
+ if (tgt && tgt[2] !== "" && targetLocale !== void 0) {
4729
+ entry.values[targetLocale] = decodeInline(tgt[2], addMeta);
4730
+ seen(targetLocale);
4731
+ }
4732
+ }
4733
+ }
4734
+ return { locales, keys, warnings };
4735
+ }
4736
+ };
4737
+ }
4738
+ });
4739
+
4740
+ // src/server/import/parsers/gettext-po.ts
4741
+ import { readdirSync as readdirSync10, readFileSync as readFileSync16 } from "fs";
4742
+ import { join as join10 } from "path";
4743
+ function unescapePo(s) {
4744
+ return s.replace(
4745
+ /\\([\\"ntr])/g,
4746
+ (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
4747
+ );
4748
+ }
4749
+ function parseEntries(text) {
4750
+ const entries = [];
4751
+ let cur = null;
4752
+ let append = null;
4753
+ const flush = () => {
4754
+ if (cur && cur.msgid !== void 0) entries.push(cur);
4755
+ cur = null;
4756
+ append = null;
4757
+ };
4758
+ for (const line of text.split("\n")) {
4759
+ if (line.trim() === "") {
4760
+ flush();
4761
+ continue;
4762
+ }
4763
+ if (line.startsWith("#")) continue;
4764
+ const m = line.match(DIRECTIVE_RE);
4765
+ if (m) {
4766
+ const kw = m[1];
4767
+ const idx = m[2];
4768
+ const body = unescapePo(m[3]);
4769
+ if (cur && (kw === "msgctxt" || kw === "msgid" && cur.msgid !== void 0)) flush();
4770
+ cur ??= { plurals: /* @__PURE__ */ new Map() };
4771
+ const entry = cur;
4772
+ if (kw === "msgctxt") {
4773
+ entry.msgctxt = body;
4774
+ append = (c) => {
4775
+ entry.msgctxt = (entry.msgctxt ?? "") + c;
4776
+ };
4777
+ } else if (kw === "msgid") {
4778
+ entry.msgid = body;
4779
+ append = (c) => {
4780
+ entry.msgid = (entry.msgid ?? "") + c;
4781
+ };
4782
+ } else if (kw === "msgid_plural") {
4783
+ entry.msgidPlural = body;
4784
+ append = (c) => {
4785
+ entry.msgidPlural = (entry.msgidPlural ?? "") + c;
4786
+ };
4787
+ } else if (idx !== void 0) {
4788
+ const i = Number(idx);
4789
+ entry.plurals.set(i, body);
4790
+ append = (c) => {
4791
+ entry.plurals.set(i, (entry.plurals.get(i) ?? "") + c);
4792
+ };
4793
+ } else {
4794
+ entry.msgstr = body;
4795
+ append = (c) => {
4796
+ entry.msgstr = (entry.msgstr ?? "") + c;
4797
+ };
4798
+ }
4799
+ continue;
4800
+ }
4801
+ const cont = line.match(CONT_RE);
4802
+ if (cont && append) append(unescapePo(cont[1]));
4803
+ }
4804
+ flush();
4805
+ return entries;
4806
+ }
4807
+ function discoverPoFiles(root) {
4808
+ const found = [];
4809
+ const entries = readdirSync10(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
4810
+ for (const e of entries) {
4811
+ if (e.isFile() && e.name.endsWith(".po")) {
4812
+ const base = e.name.slice(0, -3);
4813
+ found.push({ path: join10(root, e.name), rel: e.name, locale: LOCALE_RE6.test(base) ? base : null });
4814
+ } else if (e.isDirectory() && LOCALE_RE6.test(e.name)) {
4815
+ for (const sub of [join10(e.name, "LC_MESSAGES"), e.name]) {
4816
+ let names;
4817
+ try {
4818
+ names = readdirSync10(join10(root, sub)).sort();
4819
+ } catch {
4820
+ continue;
4821
+ }
4822
+ for (const f of names) {
4823
+ if (f.endsWith(".po")) found.push({ path: join10(root, sub, f), rel: join10(sub, f), locale: e.name });
4824
+ }
4825
+ }
4826
+ }
4827
+ }
4828
+ return found;
4829
+ }
4830
+ var LOCALE_RE6, DIRECTIVE_RE, CONT_RE, gettextPo2;
4831
+ var init_gettext_po2 = __esm({
4832
+ "src/server/import/parsers/gettext-po.ts"() {
4833
+ "use strict";
4834
+ init_plurals();
4835
+ LOCALE_RE6 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4836
+ DIRECTIVE_RE = /^(msgctxt|msgid_plural|msgid|msgstr)(?:\[(\d+)\])?[ \t]+"(.*)"\s*$/;
4837
+ CONT_RE = /^[ \t]*"(.*)"\s*$/;
4838
+ gettextPo2 = {
4839
+ name: "gettext-po",
4840
+ parse(localeRoot, opts) {
4841
+ const warnings = [];
4842
+ const keys = {};
4843
+ const locales = [];
4844
+ for (const file of discoverPoFiles(localeRoot)) {
4845
+ let entries;
4846
+ try {
4847
+ entries = parseEntries(readFileSync16(file.path, "utf8"));
4848
+ } catch (e) {
4849
+ warnings.push(`gettext-po: failed to parse ${file.rel}: ${e.message}`);
4850
+ continue;
4851
+ }
4852
+ const header = entries.find((e) => e.msgid === "" && e.msgctxt === void 0);
4853
+ const headerLang = header?.msgstr?.match(/^Language:[ \t]*([A-Za-z0-9_-]+)/m)?.[1];
4854
+ const locale = file.locale ?? headerLang;
4855
+ if (!locale) {
4856
+ warnings.push(`gettext-po: cannot determine locale for ${file.rel}; skipped`);
4857
+ continue;
4858
+ }
4859
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4860
+ if (!locales.includes(locale)) locales.push(locale);
4861
+ const cats = categoriesFor(locale);
4862
+ for (const entry of entries) {
4863
+ if (entry === header) continue;
4864
+ const key = entry.msgctxt ?? entry.msgid;
4865
+ if (!key) continue;
4866
+ if (entry.msgidPlural !== void 0) {
4867
+ const forms = {};
4868
+ for (const [i, body] of [...entry.plurals].sort((a, b) => a[0] - b[0])) {
4869
+ if (body === "") continue;
4870
+ const cat = cats[i];
4871
+ if (!cat) {
4872
+ warnings.push(
4873
+ `gettext-po: ${file.rel} "${key}": msgstr[${i}] exceeds the ${cats.length} plural forms of "${locale}"; ignored`
4874
+ );
4875
+ continue;
4876
+ }
4877
+ forms[cat] = body.split("%d").join("{count}");
4878
+ }
4879
+ if (!forms.other) continue;
4880
+ (keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
4881
+ } else {
4882
+ if (!entry.msgstr) continue;
4883
+ (keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
4884
+ }
4885
+ }
4886
+ }
4887
+ return { locales, keys, warnings };
4888
+ }
4889
+ };
4890
+ }
4891
+ });
4892
+
4893
+ // src/server/import/parsers/i18next-json.ts
4894
+ import { readdirSync as readdirSync11, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
4895
+ import { join as join11 } from "path";
4896
+ function safeIsDir2(p) {
4897
+ try {
4898
+ return statSync6(p).isDirectory();
4899
+ } catch {
4900
+ return false;
4901
+ }
4902
+ }
4903
+ function fromI18next(value) {
4904
+ if (isIcuPluralOrSelect(value)) return value;
4905
+ return value.replace(/\{\{(\w+)\}\}/g, "{$1}");
4906
+ }
4907
+ function ingestFile(path, label, prefix, locale, keys, warnings) {
4908
+ let data;
4909
+ try {
4910
+ data = JSON.parse(readFileSync17(path, "utf8"));
4911
+ } catch (e) {
4912
+ warnings.push(`i18next-json: failed to parse ${label}: ${e.message}`);
4913
+ return false;
4914
+ }
4915
+ const fileWarnings = [];
4916
+ const flat = flattenObject(data, "", fileWarnings);
4917
+ for (const w of fileWarnings) warnings.push(`i18next-json: ${label}: ${w}`);
4918
+ const families = /* @__PURE__ */ new Set();
4919
+ for (const [k, v] of Object.entries(flat)) {
4920
+ const m = PLURAL_SUFFIX_RE.exec(k);
4921
+ if (m && m[2] === "other" && v !== "") families.add(m[1]);
4922
+ }
4923
+ const pluralForms = {};
4924
+ for (const [k, raw] of Object.entries(flat)) {
4925
+ if (raw === "") continue;
4926
+ const value = fromI18next(raw);
4927
+ const m = PLURAL_SUFFIX_RE.exec(k);
4928
+ if (m && families.has(m[1])) {
4929
+ (pluralForms[m[1]] ??= {})[m[2]] = value;
4930
+ continue;
4931
+ }
4932
+ if (families.has(k)) {
4933
+ warnings.push(
4934
+ `i18next-json: ${label}: key "${k}" collides with its own plural suffix family; the plural wins`
4935
+ );
4936
+ continue;
4937
+ }
4938
+ (keys[prefix + k] ??= { values: {} }).values[locale] = value;
4939
+ }
4940
+ for (const [base, forms] of Object.entries(pluralForms)) {
4941
+ (keys[prefix + base] ??= { values: {} }).values[locale] = formsToIcu(PLURAL_ARG, forms);
4942
+ }
4943
+ return true;
4944
+ }
4945
+ var LOCALE_RE7, PLURAL_SUFFIX_RE, PLURAL_ARG, DEFAULT_NAMESPACE, i18nextJson2;
4946
+ var init_i18next_json2 = __esm({
4947
+ "src/server/import/parsers/i18next-json.ts"() {
4948
+ "use strict";
4949
+ init_flatten();
4950
+ init_plurals();
4951
+ init_placeholders();
4952
+ LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4953
+ PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
4954
+ PLURAL_ARG = "count";
4955
+ DEFAULT_NAMESPACE = "translation";
4956
+ i18nextJson2 = {
4957
+ name: "i18next-json",
4958
+ parse(localeRoot, opts) {
4959
+ const warnings = [];
4960
+ const keys = {};
4961
+ const locales = [];
4962
+ for (const entry of readdirSync11(localeRoot).sort()) {
4963
+ const full = join11(localeRoot, entry);
4964
+ if (safeIsDir2(full)) {
4965
+ if (!LOCALE_RE7.test(entry)) continue;
4966
+ if (opts?.locales && !opts.locales.includes(entry)) continue;
4967
+ let any = false;
4968
+ for (const file of readdirSync11(full).sort()) {
4969
+ if (!file.endsWith(".json")) continue;
4970
+ const ns = file.slice(0, -".json".length);
4971
+ const prefix = ns === DEFAULT_NAMESPACE ? "" : `${ns}.`;
4972
+ if (ingestFile(join11(full, file), `${entry}/${file}`, prefix, entry, keys, warnings)) any = true;
4973
+ }
4974
+ if (any && !locales.includes(entry)) locales.push(entry);
4975
+ } else if (entry.endsWith(".json")) {
4976
+ const locale = entry.slice(0, -".json".length);
4977
+ if (!LOCALE_RE7.test(locale)) continue;
4978
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
4979
+ if (ingestFile(full, entry, "", locale, keys, warnings) && !locales.includes(locale)) {
4980
+ locales.push(locale);
4981
+ }
4982
+ }
4983
+ }
4984
+ return { locales, keys, warnings };
4985
+ }
4986
+ };
4987
+ }
4988
+ });
4989
+
4990
+ // src/server/import/parsers/rails-yaml.ts
4991
+ import { readdirSync as readdirSync12, readFileSync as readFileSync18 } from "fs";
4992
+ import { join as join12 } from "path";
4993
+ function fromRuby(value) {
4994
+ return value.replace(/%\{(\w+)\}/g, "{$1}");
4995
+ }
4996
+ function makeNode() {
4997
+ return /* @__PURE__ */ Object.create(null);
4998
+ }
4999
+ function decodeDouble(body) {
5000
+ let out = "";
5001
+ for (let i = 0; i < body.length; i++) {
5002
+ const c = body[i];
5003
+ if (c !== "\\") {
5004
+ out += c;
5005
+ continue;
5006
+ }
5007
+ const n = body[++i];
5008
+ if (n === void 0) break;
5009
+ out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
5010
+ }
5011
+ return out;
5012
+ }
5013
+ function scanQuoted(s, start) {
5014
+ const q = s[start];
5015
+ if (q === '"') {
5016
+ for (let i = start + 1; i < s.length; i++) {
5017
+ if (s[i] === "\\") i++;
5018
+ else if (s[i] === '"') return { text: decodeDouble(s.slice(start + 1, i)), end: i + 1 };
5019
+ }
5020
+ return null;
5021
+ }
5022
+ let out = "";
5023
+ for (let i = start + 1; i < s.length; i++) {
5024
+ if (s[i] === "'") {
5025
+ if (s[i + 1] === "'") {
5026
+ out += "'";
5027
+ i++;
5028
+ } else {
5029
+ return { text: out, end: i + 1 };
5030
+ }
5031
+ } else {
5032
+ out += s[i];
5033
+ }
5034
+ }
5035
+ return null;
5036
+ }
5037
+ function stripPlainComment(s) {
5038
+ const m = /(^|\s)#/.exec(s);
5039
+ return (m && m.index >= 0 ? s.slice(0, m.index) : s).trim();
5040
+ }
5041
+ function onlyTrailing(s) {
5042
+ return /^\s*(#.*)?$/.test(s);
5043
+ }
5044
+ function parseYamlSubset(text, file, warnings) {
5045
+ const roots = {};
5046
+ const lines = text.split(/\r?\n/);
5047
+ let stack = [];
5048
+ let skipDeeperThan = null;
5049
+ let lastLeafIndent = null;
5050
+ for (let n = 0; n < lines.length; n++) {
5051
+ const raw = lines[n];
5052
+ const lineNo = n + 1;
5053
+ if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
5054
+ if (raw.trim() === "---") continue;
5055
+ const indentMatch = /^[ \t]*/.exec(raw)[0];
5056
+ if (indentMatch.includes(" ")) {
5057
+ warnings.push(`rails-yaml: ${file}:${lineNo}: tab in indentation; line skipped`);
5058
+ continue;
5059
+ }
5060
+ const indent = indentMatch.length;
5061
+ if (skipDeeperThan !== null) {
5062
+ if (indent > skipDeeperThan) continue;
5063
+ skipDeeperThan = null;
5064
+ }
5065
+ const content = raw.slice(indent);
5066
+ if (content.startsWith("- ") || content === "-") {
5067
+ warnings.push(`rails-yaml: ${file}:${lineNo}: sequences are not supported; node skipped`);
5068
+ skipDeeperThan = indent;
5069
+ continue;
5070
+ }
5071
+ let key;
5072
+ let rest;
5073
+ if (content[0] === '"' || content[0] === "'") {
5074
+ const k = scanQuoted(content, 0);
5075
+ if (!k || content[k.end] !== ":") {
5076
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unparseable quoted key; line skipped`);
5077
+ skipDeeperThan = indent;
5078
+ continue;
5079
+ }
5080
+ key = k.text;
5081
+ rest = content.slice(k.end + 1);
5082
+ } else {
5083
+ const m = /^(.*?):(?=\s|$)/.exec(content);
5084
+ if (!m) {
5085
+ warnings.push(`rails-yaml: ${file}:${lineNo}: not a "key: value" mapping line; line skipped`);
5086
+ skipDeeperThan = indent;
5087
+ continue;
5088
+ }
5089
+ key = m[1].trim();
5090
+ rest = content.slice(m[0].length);
5091
+ }
5092
+ if (lastLeafIndent !== null && indent > lastLeafIndent) {
5093
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unexpected indentation under a scalar; line skipped`);
5094
+ skipDeeperThan = indent - 1;
5095
+ continue;
5096
+ }
5097
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
5098
+ const trimmed = rest.trim();
5099
+ let value;
5100
+ if (trimmed === "" || trimmed.startsWith("#")) {
5101
+ value = null;
5102
+ } else if (trimmed[0] === "&" || trimmed[0] === "*") {
5103
+ warnings.push(`rails-yaml: ${file}:${lineNo}: YAML anchors/aliases are not supported; node skipped`);
5104
+ skipDeeperThan = indent;
5105
+ continue;
5106
+ } else if (trimmed[0] === "|" || trimmed[0] === ">") {
5107
+ warnings.push(`rails-yaml: ${file}:${lineNo}: block scalars are not supported; node skipped`);
5108
+ skipDeeperThan = indent;
5109
+ continue;
5110
+ } else if (trimmed[0] === "[" || trimmed[0] === "{") {
5111
+ warnings.push(`rails-yaml: ${file}:${lineNo}: flow collections are not supported; node skipped`);
5112
+ skipDeeperThan = indent;
5113
+ continue;
5114
+ } else if (trimmed[0] === '"' || trimmed[0] === "'") {
5115
+ const v = scanQuoted(trimmed, 0);
5116
+ if (!v || !onlyTrailing(trimmed.slice(v.end))) {
5117
+ warnings.push(`rails-yaml: ${file}:${lineNo}: unterminated or trailing-garbage quoted value; line skipped`);
5118
+ continue;
5119
+ }
5120
+ value = v.text;
5121
+ } else {
5122
+ value = stripPlainComment(trimmed);
5123
+ }
5124
+ if (stack.length === 0) {
5125
+ if (value !== null) {
5126
+ warnings.push(`rails-yaml: ${file}:${lineNo}: top-level key "${key}" has a scalar value; skipped`);
5127
+ lastLeafIndent = indent;
5128
+ continue;
5129
+ }
5130
+ const root = roots[key] ??= makeNode();
5131
+ stack = [{ indent, node: root }];
5132
+ lastLeafIndent = null;
5133
+ continue;
5134
+ }
5135
+ const parent = stack[stack.length - 1].node;
5136
+ if (key in parent) {
5137
+ warnings.push(`rails-yaml: ${file}:${lineNo}: duplicate key "${key}"; later value wins`);
5138
+ }
5139
+ if (value === null) {
5140
+ const child = makeNode();
5141
+ parent[key] = child;
5142
+ stack.push({ indent, node: child });
5143
+ lastLeafIndent = null;
5144
+ } else {
5145
+ parent[key] = value;
5146
+ lastLeafIndent = indent;
5147
+ }
5148
+ }
5149
+ return { roots };
5150
+ }
5151
+ function asPluralForms(node) {
5152
+ const entries = Object.entries(node);
5153
+ if (entries.length === 0) return null;
5154
+ const forms = {};
5155
+ for (const [k, v] of entries) {
5156
+ if (!CATEGORY_SET.has(k) || typeof v !== "string") return null;
5157
+ if (v !== "") forms[k] = v;
5158
+ }
5159
+ if (!("other" in forms)) return null;
5160
+ return forms;
5161
+ }
5162
+ function synthesizeIcu(forms, file, key, warnings) {
5163
+ const parts = [];
5164
+ for (const cat of PLURAL_CATEGORIES) {
5165
+ const body = forms[cat];
5166
+ if (body === void 0) continue;
5167
+ if (body.includes("#")) {
5168
+ warnings.push(
5169
+ `rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
5170
+ );
5171
+ }
5172
+ parts.push(`${cat} {${fromRuby(body)}}`);
5173
+ }
5174
+ return `{count, plural, ${parts.join(" ")}}`;
5175
+ }
5176
+ var LOCALE_RE8, CATEGORY_SET, railsYaml2;
5177
+ var init_rails_yaml2 = __esm({
5178
+ "src/server/import/parsers/rails-yaml.ts"() {
5179
+ "use strict";
5180
+ init_schema();
5181
+ LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
5182
+ CATEGORY_SET = new Set(PLURAL_CATEGORIES);
5183
+ railsYaml2 = {
5184
+ name: "rails-yaml",
5185
+ parse(localeRoot, opts) {
5186
+ const warnings = [];
5187
+ const keys = {};
5188
+ const locales = [];
5189
+ const wanted = opts?.locales?.map((l) => l.toLowerCase());
5190
+ const addValue = (key, locale, value) => {
5191
+ (keys[key] ??= { values: {} }).values[locale] = value;
5192
+ };
5193
+ const flatten = (node, prefix, locale, file) => {
5194
+ for (const [k, v] of Object.entries(node)) {
5195
+ const key = prefix ? `${prefix}.${k}` : k;
5196
+ if (typeof v === "string") {
5197
+ if (v !== "") addValue(key, locale, fromRuby(v));
5198
+ continue;
5199
+ }
5200
+ const forms = asPluralForms(v);
5201
+ if (forms) addValue(key, locale, synthesizeIcu(forms, file, key, warnings));
5202
+ else flatten(v, key, locale, file);
5203
+ }
5204
+ };
5205
+ for (const file of readdirSync12(localeRoot).sort()) {
5206
+ if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
5207
+ let text;
5208
+ try {
5209
+ text = readFileSync18(join12(localeRoot, file), "utf8");
5210
+ } catch (e) {
5211
+ warnings.push(`rails-yaml: failed to read ${file}: ${e.message}`);
5212
+ continue;
5213
+ }
5214
+ const { roots } = parseYamlSubset(text, file, warnings);
5215
+ for (const token of Object.keys(roots).sort()) {
5216
+ if (!LOCALE_RE8.test(token)) {
5217
+ warnings.push(`rails-yaml: ${file}: top-level key "${token}" is not a locale; subtree skipped`);
5218
+ continue;
5219
+ }
5220
+ if (wanted && !wanted.includes(token.toLowerCase())) continue;
5221
+ if (!locales.includes(token)) locales.push(token);
5222
+ flatten(roots[token], "", token, file);
5223
+ }
5224
+ }
5225
+ return { locales, keys, warnings };
5226
+ }
5227
+ };
5228
+ }
5229
+ });
5230
+
5231
+ // src/server/import/parsers/apple-stringsdict.ts
5232
+ import { readdirSync as readdirSync13, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
5233
+ import { join as join13 } from "path";
5234
+ function localeFromLproj2(dir) {
5235
+ const m = dir.match(/^(.+)\.lproj$/);
5236
+ if (!m) return null;
5237
+ return LOCALE_RE9.test(m[1]) ? m[1] : null;
5238
+ }
5239
+ function decodeEntities2(s) {
5240
+ 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, "&");
5241
+ }
5242
+ function parsePlistDict(xml) {
5243
+ let i = 0;
5244
+ const n = xml.length;
5245
+ const skipTrivia = () => {
5246
+ for (; ; ) {
5247
+ while (i < n && /\s/.test(xml[i])) i++;
5248
+ if (xml.startsWith("<!--", i)) {
5249
+ const end = xml.indexOf("-->", i + 4);
5250
+ if (end === -1) throw new Error("unterminated comment");
5251
+ i = end + 3;
5252
+ continue;
5253
+ }
5254
+ if (xml.startsWith("<?", i) || xml.startsWith("<!", i) && !xml.startsWith("<!--", i)) {
5255
+ const end = xml.indexOf(">", i);
5256
+ if (end === -1) throw new Error("unterminated declaration");
5257
+ i = end + 1;
5258
+ continue;
5259
+ }
5260
+ break;
5261
+ }
5262
+ };
5263
+ const readTag = () => {
5264
+ if (xml[i] !== "<") throw new Error(`expected a tag at offset ${i}`);
5265
+ const end = xml.indexOf(">", i);
5266
+ if (end === -1) throw new Error("unterminated tag");
5267
+ let body = xml.slice(i + 1, end).trim();
5268
+ i = end + 1;
5269
+ const closing = body.startsWith("/");
5270
+ if (closing) body = body.slice(1).trim();
5271
+ const selfClosing = body.endsWith("/");
5272
+ if (selfClosing) body = body.slice(0, -1).trim();
5273
+ const name = body.split(/\s/)[0];
5274
+ if (!name) throw new Error(`empty tag at offset ${end}`);
5275
+ return { name, closing, selfClosing };
5276
+ };
5277
+ const readElementText = (name) => {
5278
+ const re = new RegExp(`</${name}\\s*>`, "g");
5279
+ re.lastIndex = i;
5280
+ const m = re.exec(xml);
5281
+ if (!m) throw new Error(`unterminated <${name}>`);
5282
+ const text = xml.slice(i, m.index);
5283
+ i = m.index + m[0].length;
5284
+ return decodeEntities2(text);
5285
+ };
5286
+ const readValue = (tag2) => {
5287
+ if (tag2.name === "dict") return tag2.selfClosing ? {} : readDict();
5288
+ if (tag2.name === "true" || tag2.name === "false") {
5289
+ if (!tag2.selfClosing) readElementText(tag2.name);
5290
+ return tag2.name;
5291
+ }
5292
+ if (["string", "integer", "real", "date", "data"].includes(tag2.name)) {
5293
+ return tag2.selfClosing ? "" : readElementText(tag2.name);
5294
+ }
5295
+ throw new Error(`unsupported plist element <${tag2.name}>`);
5296
+ };
5297
+ const readDict = () => {
5298
+ const out = {};
5299
+ for (; ; ) {
5300
+ skipTrivia();
5301
+ const tag2 = readTag();
5302
+ if (tag2.closing) {
5303
+ if (tag2.name !== "dict") throw new Error(`unexpected </${tag2.name}> inside <dict>`);
5304
+ return out;
5305
+ }
5306
+ if (tag2.name !== "key") throw new Error(`expected <key> inside <dict>, got <${tag2.name}>`);
5307
+ const key = readElementText("key");
5308
+ skipTrivia();
5309
+ const vt = readTag();
5310
+ if (vt.closing) throw new Error(`<key>${key}</key> has no value`);
5311
+ out[key] = readValue(vt);
5312
+ }
5313
+ };
5314
+ skipTrivia();
5315
+ let tag = readTag();
5316
+ if (tag.name === "plist" && !tag.closing && !tag.selfClosing) {
5317
+ skipTrivia();
5318
+ tag = readTag();
5319
+ }
5320
+ if (tag.name !== "dict" || tag.closing) throw new Error("expected a root <dict>");
5321
+ return tag.selfClosing ? {} : readDict();
5322
+ }
5323
+ function entryToIcu(key, entry, file, warnings) {
5324
+ const warn = (msg) => {
5325
+ warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
5326
+ return null;
5327
+ };
5328
+ if (typeof entry !== "object") return warn("value is not a dict; skipped");
5329
+ const fmt = entry["NSStringLocalizedFormatKey"];
5330
+ if (typeof fmt !== "string") return warn("missing NSStringLocalizedFormatKey; skipped");
5331
+ const vars = [...fmt.matchAll(VAR_RE)];
5332
+ if (vars.length !== 1) {
5333
+ return warn(`format key has ${vars.length} %#@\u2026@ variables; only exactly one is supported; skipped`);
5334
+ }
5335
+ const arg = vars[0][1];
5336
+ if (!/^\w+$/.test(arg)) return warn(`variable name "${arg}" is not a valid ICU argument; skipped`);
5337
+ const prefix = fmt.slice(0, vars[0].index);
5338
+ const suffix = fmt.slice(vars[0].index + vars[0][0].length);
5339
+ const varDict = entry[arg];
5340
+ if (typeof varDict !== "object") return warn(`variable "${arg}" has no dict; skipped`);
5341
+ const specType = varDict["NSStringFormatSpecTypeKey"];
5342
+ if (specType !== void 0 && specType !== "NSStringPluralRuleType") {
5343
+ return warn(`variable "${arg}" is not a plural rule (${String(specType)}); skipped`);
5344
+ }
5345
+ const valueType = varDict["NSStringFormatValueTypeKey"];
5346
+ const token = `%${typeof valueType === "string" && valueType ? valueType : "d"}`;
5347
+ const forms = {};
5348
+ for (const cat of PLURAL_CATEGORIES) {
5349
+ const body = varDict[cat];
5350
+ if (typeof body !== "string") continue;
5351
+ forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
5352
+ }
5353
+ if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
5354
+ return formsToIcu(arg, forms);
5355
+ }
5356
+ var LOCALE_RE9, TABLE2, VAR_RE, appleStringsdict2;
5357
+ var init_apple_stringsdict2 = __esm({
5358
+ "src/server/import/parsers/apple-stringsdict.ts"() {
5359
+ "use strict";
5360
+ init_schema();
5361
+ init_plurals();
5362
+ LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
5363
+ TABLE2 = "Localizable.stringsdict";
5364
+ VAR_RE = /%#@([^@]*)@/g;
5365
+ appleStringsdict2 = {
5366
+ name: "apple-stringsdict",
5367
+ parse(localeRoot, opts) {
5368
+ const warnings = [];
5369
+ const keys = {};
5370
+ const locales = [];
5371
+ for (const dir of readdirSync13(localeRoot).sort()) {
5372
+ const locale = localeFromLproj2(dir);
5373
+ if (!locale) continue;
5374
+ if (opts?.locales && !opts.locales.includes(locale)) continue;
5375
+ const file = join13(localeRoot, dir, TABLE2);
5376
+ let text;
5377
+ try {
5378
+ if (!statSync7(file).isFile()) continue;
5379
+ text = readFileSync19(file, "utf8");
5380
+ } catch {
5381
+ continue;
5382
+ }
5383
+ locales.push(locale);
5384
+ const others = readdirSync13(join13(localeRoot, dir)).filter(
5385
+ (f) => f.endsWith(".stringsdict") && f !== TABLE2
5386
+ );
5387
+ if (others.length) {
5388
+ warnings.push(
5389
+ `apple-stringsdict: ${dir} has other .stringsdict tables (${others.join(", ")}); only ${TABLE2} is imported`
5390
+ );
5391
+ }
5392
+ let root;
5393
+ try {
5394
+ root = parsePlistDict(text);
5395
+ } catch (e) {
5396
+ warnings.push(`apple-stringsdict: failed to parse ${file}: ${e.message}`);
5397
+ continue;
5398
+ }
5399
+ for (const key of Object.keys(root).sort()) {
5400
+ const icu = entryToIcu(key, root[key], file, warnings);
5401
+ if (icu === null) continue;
5402
+ (keys[key] ??= { values: {} }).values[locale] = icu;
5403
+ }
5404
+ }
5405
+ return { locales, keys, warnings };
5406
+ }
5407
+ };
5408
+ }
5409
+ });
5410
+
4505
5411
  // src/server/import/parsers/index.ts
4506
5412
  function getParser(name) {
4507
5413
  const p = REGISTRY[name];
@@ -4516,11 +5422,21 @@ var init_parsers = __esm({
4516
5422
  init_laravel_php2();
4517
5423
  init_flutter_arb2();
4518
5424
  init_apple_strings2();
5425
+ init_angular_xliff2();
5426
+ init_gettext_po2();
5427
+ init_i18next_json2();
5428
+ init_rails_yaml2();
5429
+ init_apple_stringsdict2();
4519
5430
  REGISTRY = {
4520
5431
  [vueI18nJson2.name]: vueI18nJson2,
4521
5432
  [laravelPhp2.name]: laravelPhp2,
4522
5433
  [flutterArb2.name]: flutterArb2,
4523
- [appleStrings2.name]: appleStrings2
5434
+ [appleStrings2.name]: appleStrings2,
5435
+ [angularXliff2.name]: angularXliff2,
5436
+ [gettextPo2.name]: gettextPo2,
5437
+ [i18nextJson2.name]: i18nextJson2,
5438
+ [railsYaml2.name]: railsYaml2,
5439
+ [appleStringsdict2.name]: appleStringsdict2
4524
5440
  };
4525
5441
  }
4526
5442
  });
@@ -4606,7 +5522,14 @@ var init_assemble = __esm({
4606
5522
  "laravel-php": { adapter: "laravel-php", path: "lang/{locale}/{namespace}.php" },
4607
5523
  "vue-i18n-json": { adapter: "vue-i18n-json", path: "src/locale/{locale}.json" },
4608
5524
  "flutter-arb": { adapter: "flutter-arb", path: "lib/l10n/app_{locale}.arb" },
4609
- "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true }
5525
+ "apple-strings": { adapter: "apple-strings", path: "{locale}.lproj/Localizable.strings", rootRelative: true },
5526
+ // skipSourceLocale: ng extract-i18n owns messages.xlf (the source file); glotfile
5527
+ // only writes the translation files back next to it.
5528
+ "angular-xliff": { adapter: "angular-xliff", path: "messages.{locale}.xlf", rootRelative: true, skipSourceLocale: true },
5529
+ "gettext-po": { adapter: "gettext-po", path: "{locale}.po", rootRelative: true },
5530
+ "i18next-json": { adapter: "i18next-json", path: "{locale}/translation.json", rootRelative: true },
5531
+ "rails-yaml": { adapter: "rails-yaml", path: "config/locales/{locale}.yml" },
5532
+ "apple-stringsdict": { adapter: "apple-stringsdict", path: "{locale}.lproj/Localizable.stringsdict", rootRelative: true }
4610
5533
  };
4611
5534
  }
4612
5535
  });
@@ -4917,12 +5840,12 @@ var init_checks = __esm({
4917
5840
  });
4918
5841
 
4919
5842
  // src/server/ui-prefs.ts
4920
- import { readFileSync as readFileSync14 } from "fs";
5843
+ import { readFileSync as readFileSync20 } from "fs";
4921
5844
  import { homedir } from "os";
4922
- import { join as join9 } from "path";
5845
+ import { join as join14 } from "path";
4923
5846
  function readJson2(path) {
4924
5847
  try {
4925
- const parsed = JSON.parse(readFileSync14(path, "utf8"));
5848
+ const parsed = JSON.parse(readFileSync20(path, "utf8"));
4926
5849
  return parsed && typeof parsed === "object" ? parsed : {};
4927
5850
  } catch {
4928
5851
  return {};
@@ -4947,7 +5870,7 @@ var init_ui_prefs = __esm({
4947
5870
  THEMES = ["system", "light", "dark"];
4948
5871
  isThemeMode = (v) => THEMES.includes(v);
4949
5872
  isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4950
- defaultUiPrefsPath = () => join9(homedir(), ".glotfile", "ui.json");
5873
+ defaultUiPrefsPath = () => join14(homedir(), ".glotfile", "ui.json");
4951
5874
  DEFAULTS = { theme: "system" };
4952
5875
  }
4953
5876
  });
@@ -4955,13 +5878,13 @@ var init_ui_prefs = __esm({
4955
5878
  // src/server/api.ts
4956
5879
  import { Hono } from "hono";
4957
5880
  import { streamSSE } from "hono/streaming";
4958
- import { readFileSync as readFileSync15, existsSync as existsSync11, readdirSync as readdirSync9, statSync as statSync6, rmSync as rmSync4 } from "fs";
5881
+ import { readFileSync as readFileSync21, existsSync as existsSync11, readdirSync as readdirSync14, statSync as statSync8, rmSync as rmSync4 } from "fs";
4959
5882
  import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
4960
5883
  function projectName(root) {
4961
5884
  const nameFile = resolve9(root, ".idea", ".name");
4962
5885
  if (existsSync11(nameFile)) {
4963
5886
  try {
4964
- const name = readFileSync15(nameFile, "utf8").trim();
5887
+ const name = readFileSync21(nameFile, "utf8").trim();
4965
5888
  if (name) return name;
4966
5889
  } catch {
4967
5890
  }
@@ -5086,7 +6009,7 @@ function createApi(deps) {
5086
6009
  if (depth > 4) return;
5087
6010
  let entries = [];
5088
6011
  try {
5089
- entries = readdirSync9(dir);
6012
+ entries = readdirSync14(dir);
5090
6013
  } catch {
5091
6014
  return;
5092
6015
  }
@@ -5100,7 +6023,7 @@ function createApi(deps) {
5100
6023
  filePath = abs;
5101
6024
  } else {
5102
6025
  try {
5103
- if (statSync6(abs).isDirectory()) walk(abs, depth + 1);
6026
+ if (statSync8(abs).isDirectory()) walk(abs, depth + 1);
5104
6027
  } catch {
5105
6028
  }
5106
6029
  continue;
@@ -5918,7 +6841,7 @@ __export(server_exports, {
5918
6841
  import { Hono as Hono2 } from "hono";
5919
6842
  import { serve } from "@hono/node-server";
5920
6843
  import { fileURLToPath } from "url";
5921
- import { dirname as dirname4, join as join10, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
6844
+ import { dirname as dirname4, join as join15, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
5922
6845
  import { readFile, stat } from "fs/promises";
5923
6846
  import { createServer } from "net";
5924
6847
  import open from "open";
@@ -5961,7 +6884,7 @@ function buildApp(opts) {
5961
6884
  const file = await readFileResponse(target);
5962
6885
  if (file) return file;
5963
6886
  }
5964
- const index = await readFileResponse(join10(root, "index.html"));
6887
+ const index = await readFileResponse(join15(root, "index.html"));
5965
6888
  if (index) return index;
5966
6889
  return c.notFound();
5967
6890
  });
@@ -6019,7 +6942,7 @@ var init_server = __esm({
6019
6942
  init_scan();
6020
6943
  init_scanner();
6021
6944
  here = dirname4(fileURLToPath(import.meta.url));
6022
- DEFAULT_UI_DIR = join10(here, "..", "ui");
6945
+ DEFAULT_UI_DIR = join15(here, "..", "ui");
6023
6946
  MIME = {
6024
6947
  ".html": "text/html; charset=utf-8",
6025
6948
  ".js": "text/javascript; charset=utf-8",
@@ -6063,8 +6986,8 @@ init_scanner();
6063
6986
  init_context();
6064
6987
  init_run2();
6065
6988
  init_outputs();
6066
- import { resolve as resolve11, dirname as dirname5, join as join11 } from "path";
6067
- import { readFileSync as readFileSync16, existsSync as existsSync12, mkdirSync as mkdirSync4, cpSync } from "fs";
6989
+ import { resolve as resolve11, dirname as dirname5, join as join16 } from "path";
6990
+ import { readFileSync as readFileSync22, existsSync as existsSync12, mkdirSync as mkdirSync4, cpSync } from "fs";
6068
6991
  import { fileURLToPath as fileURLToPath2 } from "url";
6069
6992
 
6070
6993
  // src/server/lint/locate.ts
@@ -6139,6 +7062,9 @@ function parseArgs(argv) {
6139
7062
  if (first === "help" || first === "--help" || first === "-h") {
6140
7063
  return isCommand(argv[1]) ? { command: argv[1], statePath, help: true } : { command: "help", statePath };
6141
7064
  }
7065
+ if (first === "version" || first === "--version" || first === "-v") {
7066
+ return { command: "version", statePath };
7067
+ }
6142
7068
  if (first !== void 0 && !first.startsWith("-") && !isCommand(first)) {
6143
7069
  return { command: "serve", statePath, unknownCommand: first };
6144
7070
  }
@@ -6372,7 +7298,7 @@ async function runLintCmd(args) {
6372
7298
  }
6373
7299
  return;
6374
7300
  }
6375
- const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
7301
+ const rawText = existsSync12(args.statePath) ? readFileSync22(args.statePath, "utf8") : "";
6376
7302
  const report = await runLint(state, {
6377
7303
  locales: args.locales,
6378
7304
  ruleIds: args.ruleIds,
@@ -6396,7 +7322,7 @@ async function runCheck(args) {
6396
7322
  process.exitCode = 1;
6397
7323
  return;
6398
7324
  }
6399
- const rawText = existsSync12(args.statePath) ? readFileSync16(args.statePath, "utf8") : "";
7325
+ const rawText = existsSync12(args.statePath) ? readFileSync22(args.statePath, "utf8") : "";
6400
7326
  const root = dirname5(resolve11(args.statePath));
6401
7327
  const lint = await runLint(state, {});
6402
7328
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
@@ -6557,10 +7483,10 @@ function runSplit(args) {
6557
7483
  `Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
6558
7484
  );
6559
7485
  }
6560
- var SKILL_SRC = join11(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
7486
+ var SKILL_SRC = join16(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
6561
7487
  function runSkill(args) {
6562
7488
  if (args.print) {
6563
- console.log(readFileSync16(join11(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
7489
+ console.log(readFileSync22(join16(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
6564
7490
  return;
6565
7491
  }
6566
7492
  const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
@@ -6691,12 +7617,16 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
6691
7617
  formatOpts(commands),
6692
7618
  "",
6693
7619
  "Global options:",
6694
- formatOpts(GLOBAL_OPTS),
7620
+ formatOpts([...GLOBAL_OPTS, ["-v, --version", "Print the glotfile version"]]),
6695
7621
  "",
6696
7622
  "Run `glotfile <command> --help` for a command's options."
6697
7623
  ].join("\n")
6698
7624
  );
6699
7625
  }
7626
+ function printVersion() {
7627
+ const pkgPath = join16(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
7628
+ console.log(JSON.parse(readFileSync22(pkgPath, "utf8")).version);
7629
+ }
6700
7630
  async function main(argv) {
6701
7631
  const args = parseArgs(argv);
6702
7632
  if (args.unknownCommand) {
@@ -6705,6 +7635,7 @@ async function main(argv) {
6705
7635
  return;
6706
7636
  }
6707
7637
  if (args.command === "help") return printHelp();
7638
+ if (args.command === "version") return printVersion();
6708
7639
  if (args.help) return printHelp(args.command);
6709
7640
  loadDotEnv();
6710
7641
  if (args.command === "export") return runExport(args);