glotfile 0.8.6 → 0.8.7

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.
@@ -1515,17 +1515,57 @@ function extractPlaceholders(value) {
1515
1515
  function isIcuPluralOrSelect(value) {
1516
1516
  return ICU_PLURAL_SELECT.test(value);
1517
1517
  }
1518
+ function withLiterals(value, convertGap, emitLiteral) {
1519
+ let out = "";
1520
+ let gap = "";
1521
+ const flushGap = () => {
1522
+ if (gap) {
1523
+ out += convertGap(gap);
1524
+ gap = "";
1525
+ }
1526
+ };
1527
+ for (let i = 0; i < value.length; ) {
1528
+ if (value[i] === "'") {
1529
+ const next = value[i + 1];
1530
+ if (next === "'") {
1531
+ gap += "'";
1532
+ i += 2;
1533
+ continue;
1534
+ }
1535
+ if (next === "{" || next === "}" || next === "#" || next === "|") {
1536
+ flushGap();
1537
+ let j = i + 1;
1538
+ while (j < value.length && value[j] !== "'") j++;
1539
+ out += emitLiteral(value.slice(i + 1, j));
1540
+ i = j < value.length ? j + 1 : j;
1541
+ continue;
1542
+ }
1543
+ }
1544
+ gap += value[i];
1545
+ i++;
1546
+ }
1547
+ flushGap();
1548
+ return out;
1549
+ }
1550
+ function extractLiterals2(value) {
1551
+ const out = [];
1552
+ withLiterals(value, () => "", (lit) => {
1553
+ out.push(lit);
1554
+ return "";
1555
+ });
1556
+ return out;
1557
+ }
1518
1558
  function toLaravel(value) {
1519
1559
  if (isIcuPluralOrSelect(value)) return value;
1520
- return value.replace(/\{(\w+)\}/g, ":$1");
1560
+ return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
1521
1561
  }
1522
1562
  function toI18next(value) {
1523
1563
  if (isIcuPluralOrSelect(value)) return value;
1524
- return value.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}");
1564
+ return withLiterals(value, (gap) => gap.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}"), (lit) => lit);
1525
1565
  }
1526
1566
  function toRuby(value) {
1527
1567
  if (isIcuPluralOrSelect(value)) return value;
1528
- return value.replace(/(?<!%)\{(\w+)\}/g, "%{$1}");
1568
+ return withLiterals(value, (gap) => gap.replace(/(?<!%)\{(\w+)\}/g, "%{$1}"), (lit) => lit);
1529
1569
  }
1530
1570
  function placeholdersMatch(source, translation) {
1531
1571
  const a = extractPlaceholders(source).sort();
@@ -2266,6 +2306,20 @@ var laravelPhp = {
2266
2306
  message: "laravel-php cannot represent ICU plural/select; written unconverted"
2267
2307
  });
2268
2308
  }
2309
+ if (raw) {
2310
+ const names = new Set(extractPlaceholders(raw));
2311
+ for (const m of raw.matchAll(/:([a-zA-Z][a-zA-Z0-9_]*)/g)) {
2312
+ if (names.has(m[1])) {
2313
+ warnings.push({
2314
+ code: "lossy-literal",
2315
+ key,
2316
+ locale,
2317
+ message: `literal ":${m[1]}" collides with the :${m[1]} placeholder; Laravel will interpolate both`
2318
+ });
2319
+ break;
2320
+ }
2321
+ }
2322
+ }
2269
2323
  (tree[locale][namespace] ??= {})[inner] = toLaravel(raw);
2270
2324
  }
2271
2325
  }
@@ -2346,6 +2400,9 @@ var i18nextJson = {
2346
2400
  if (raw && isIcuPluralOrSelect(raw)) {
2347
2401
  warnings.push({ code: "lossy-plural", key, locale, message: "i18next-json does not yet convert ICU plural/select; written unconverted" });
2348
2402
  }
2403
+ if (raw && extractLiterals2(raw).some((lit) => /\{\{\w+\}\}/.test(lit))) {
2404
+ warnings.push({ code: "lossy-literal", key, locale, message: "i18next will interpolate a literal containing {{\u2026}}; i18next has no escape for it" });
2405
+ }
2349
2406
  if (setNested(obj, segments, toI18next(raw))) collided.add(key);
2350
2407
  }
2351
2408
  files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE3)), contents: serializeJson(obj, fmt) });
@@ -2362,7 +2419,9 @@ function poString(s) {
2362
2419
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r") + '"';
2363
2420
  }
2364
2421
  function toGettext(body, arg) {
2365
- return body.split(`{${arg}}`).join("%d");
2422
+ const gap = (text) => text.replace(/%/g, "%%").split(`{${arg}}`).join("%d");
2423
+ if (isIcuPluralOrSelect(body)) return gap(body);
2424
+ return withLiterals(body, gap, (lit) => lit.replace(/%/g, "%%"));
2366
2425
  }
2367
2426
  var DEFAULT_LOCALE_CASE4 = "lower-hyphen";
2368
2427
  var gettextPo = {
@@ -2422,8 +2481,10 @@ var gettextPo = {
2422
2481
  blocks.push(
2423
2482
  [
2424
2483
  `msgctxt ${poString(key)}`,
2425
- `msgid ${poString(src?.value ?? "")}`,
2426
- `msgstr ${poString(lv?.value ?? "")}`
2484
+ // Scalar keys carry no count arg; toGettext still strips literal
2485
+ // spans and escapes a literal % to %%.
2486
+ `msgid ${poString(toGettext(src?.value ?? "", ""))}`,
2487
+ `msgstr ${poString(toGettext(lv?.value ?? "", ""))}`
2427
2488
  ].join("\n")
2428
2489
  );
2429
2490
  }
@@ -2447,7 +2508,9 @@ function xmlEscape(s) {
2447
2508
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2448
2509
  }
2449
2510
  function toApple(body, arg) {
2450
- return body.split(`{${arg}}`).join("%d");
2511
+ const gap = (text) => text.replace(/%/g, "%%").split(`{${arg}}`).join("%d");
2512
+ if (isIcuPluralOrSelect(body)) return gap(body);
2513
+ return withLiterals(body, gap, (lit) => lit.replace(/%/g, "%%"));
2451
2514
  }
2452
2515
  var DEFAULT_LOCALE_CASE5 = "lower-hyphen";
2453
2516
  var appleStringsdict = {
@@ -2511,6 +2574,11 @@ var DEFAULT_LOCALE_CASE6 = "bcp47-hyphen";
2511
2574
  function escape(s) {
2512
2575
  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
2513
2576
  }
2577
+ function toApple2(value) {
2578
+ const gap = (text) => text.replace(/%/g, "%%");
2579
+ if (isIcuPluralOrSelect(value)) return gap(value);
2580
+ return withLiterals(value, gap, (lit) => lit.replace(/%/g, "%%"));
2581
+ }
2514
2582
  var appleStrings = {
2515
2583
  name: "apple-strings",
2516
2584
  capabilities: {
@@ -2536,7 +2604,7 @@ var appleStrings = {
2536
2604
  if (entry.plural) continue;
2537
2605
  const value = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
2538
2606
  if (value === null) continue;
2539
- lines.push(`"${escape(key)}" = "${escape(value)}";`);
2607
+ lines.push(`"${escape(key)}" = "${escape(toApple2(value))}";`);
2540
2608
  }
2541
2609
  const contents = lines.length ? lines.join("\n") + "\n" : "";
2542
2610
  files.push({
@@ -2550,6 +2618,10 @@ var appleStrings = {
2550
2618
 
2551
2619
  // src/server/adapters/vue-i18n-json.ts
2552
2620
  var DEFAULT_LOCALE_CASE7 = "lower-hyphen";
2621
+ function toVueI18n(value) {
2622
+ if (isIcuPluralOrSelect(value)) return value;
2623
+ return withLiterals(value, (gap) => gap, (content) => `{'${content}'}`);
2624
+ }
2553
2625
  var vueI18nJson = {
2554
2626
  name: "vue-i18n-json",
2555
2627
  capabilities: {
@@ -2575,7 +2647,7 @@ var vueI18nJson = {
2575
2647
  if (entry.plural) {
2576
2648
  const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
2577
2649
  if (!forms) continue;
2578
- const parts = PLURAL_CATEGORIES.map((c) => forms[c]).filter((v) => v !== void 0);
2650
+ const parts = PLURAL_CATEGORIES.map((c) => forms[c]).filter((v) => v !== void 0).map(toVueI18n);
2579
2651
  flat[key] = parts.join(" | ");
2580
2652
  } else {
2581
2653
  const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
@@ -2588,7 +2660,7 @@ var vueI18nJson = {
2588
2660
  message: "vue-i18n-json does not yet convert ICU plural/select; written unconverted"
2589
2661
  });
2590
2662
  }
2591
- flat[key] = raw;
2663
+ flat[key] = toVueI18n(raw);
2592
2664
  }
2593
2665
  }
2594
2666
  let payload = flat;
@@ -2623,27 +2695,30 @@ function angularXMeta(placeholders, name) {
2623
2695
  return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
2624
2696
  }
2625
2697
  function renderInterpolations(text, ids, placeholders) {
2626
- let out = "";
2627
- let last = 0;
2628
- for (const m of text.matchAll(/\{(\w+)\}/g)) {
2629
- const name = m[1];
2630
- out += xmlEscape2(text.slice(last, m.index));
2631
- const meta = angularXMeta(placeholders, name);
2632
- if (meta) {
2633
- const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
2634
- const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
2635
- out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
2636
- } else {
2637
- let id = ids.get(name);
2638
- if (id === void 0) {
2639
- id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
2640
- ids.set(name, id);
2698
+ const convertGap = (gap) => {
2699
+ let out = "";
2700
+ let last = 0;
2701
+ for (const m of gap.matchAll(/\{(\w+)\}/g)) {
2702
+ const name = m[1];
2703
+ out += xmlEscape2(gap.slice(last, m.index));
2704
+ const meta = angularXMeta(placeholders, name);
2705
+ if (meta) {
2706
+ const ctype = meta.type ? ` ctype="${attrEscape(meta.type)}"` : "";
2707
+ const equiv = meta.example !== void 0 ? ` equiv-text="${attrEscape(meta.example)}"` : "";
2708
+ out += `<x id="${attrEscape(name)}"${ctype}${equiv}/>`;
2709
+ } else {
2710
+ let id = ids.get(name);
2711
+ if (id === void 0) {
2712
+ id = ids.size === 0 ? "INTERPOLATION" : `INTERPOLATION_${ids.size}`;
2713
+ ids.set(name, id);
2714
+ }
2715
+ out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
2641
2716
  }
2642
- out += `<x id="${id}" equiv-text="{{${name}}}"/>`;
2717
+ last = m.index + m[0].length;
2643
2718
  }
2644
- last = m.index + m[0].length;
2645
- }
2646
- return out + xmlEscape2(text.slice(last));
2719
+ return out + xmlEscape2(gap.slice(last));
2720
+ };
2721
+ return withLiterals(text, convertGap, (lit) => xmlEscape2(`'${lit}'`));
2647
2722
  }
2648
2723
  function renderPluralIcu(forms, ids, placeholders) {
2649
2724
  const cats = [
@@ -4579,6 +4654,9 @@ function flattenObject(value, prefix, warnings) {
4579
4654
 
4580
4655
  // src/server/import/parsers/vue-i18n-json.ts
4581
4656
  var LOCALE_RE2 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
4657
+ function fromVueI18n(value) {
4658
+ return value.replace(/\{'([^']*)'\}/g, "'$1'");
4659
+ }
4582
4660
  var vueI18nJson2 = {
4583
4661
  name: "vue-i18n-json",
4584
4662
  parse(localeRoot, opts) {
@@ -4599,7 +4677,7 @@ var vueI18nJson2 = {
4599
4677
  }
4600
4678
  if (!locales.includes(locale)) locales.push(locale);
4601
4679
  for (const [key, value] of Object.entries(flattenObject(data, "", warnings))) {
4602
- (keys[key] ??= { values: {} }).values[locale] = value;
4680
+ (keys[key] ??= { values: {} }).values[locale] = fromVueI18n(value);
4603
4681
  }
4604
4682
  }
4605
4683
  return { locales, keys, warnings };
@@ -4612,8 +4690,14 @@ import { join as join8, relative as relative2 } from "path";
4612
4690
  import { execFileSync } from "child_process";
4613
4691
 
4614
4692
  // src/server/import/placeholders.ts
4693
+ function markBareBracesLiteral(value) {
4694
+ return value.replace(/(?<!%)\{(\w+)\}/g, "'{$1}'");
4695
+ }
4615
4696
  function laravelToCanonical(value) {
4616
- return value.replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, "{$1}");
4697
+ return markBareBracesLiteral(value).replace(/:([a-zA-Z][a-zA-Z0-9_]*)/g, "{$1}");
4698
+ }
4699
+ function railsToCanonical(value) {
4700
+ return markBareBracesLiteral(value).replace(/%\{(\w+)\}/g, "{$1}");
4617
4701
  }
4618
4702
 
4619
4703
  // src/server/import/parsers/laravel-php.ts
@@ -4755,6 +4839,9 @@ function localeFromLproj(dir) {
4755
4839
  if (!m) return null;
4756
4840
  return LOCALE_RE4.test(m[1]) ? m[1] : null;
4757
4841
  }
4842
+ function printfToCanonical(s) {
4843
+ return s.replace(/%%/g, "%");
4844
+ }
4758
4845
  function unescape(body) {
4759
4846
  return body.replace(/\\(U[0-9a-fA-F]{4}|u[0-9a-fA-F]{4}|.)/g, (_m, esc) => {
4760
4847
  const c = esc[0];
@@ -4874,7 +4961,7 @@ var appleStrings2 = {
4874
4961
  warnings.push(`apple-strings: ${dir} has other .strings tables (${others.join(", ")}); only ${TABLE} is imported`);
4875
4962
  }
4876
4963
  for (const { key, value } of parseStrings(text, file, warnings)) {
4877
- (keys[key] ??= { values: {} }).values[locale] = value;
4964
+ (keys[key] ??= { values: {} }).values[locale] = printfToCanonical(value);
4878
4965
  }
4879
4966
  }
4880
4967
  return { locales, keys, warnings };
@@ -5006,6 +5093,24 @@ function unescapePo(s) {
5006
5093
  (_, c) => c === "n" ? "\n" : c === "t" ? " " : c === "r" ? "\r" : c
5007
5094
  );
5008
5095
  }
5096
+ function printfToCanonical2(s, arg) {
5097
+ let out = "";
5098
+ for (let i = 0; i < s.length; ) {
5099
+ if (s[i] === "%" && s[i + 1] === "%") {
5100
+ out += "%";
5101
+ i += 2;
5102
+ continue;
5103
+ }
5104
+ if (arg && s[i] === "%" && s[i + 1] === "d") {
5105
+ out += `{${arg}}`;
5106
+ i += 2;
5107
+ continue;
5108
+ }
5109
+ out += s[i];
5110
+ i++;
5111
+ }
5112
+ return out;
5113
+ }
5009
5114
  function parseEntries(text) {
5010
5115
  const entries = [];
5011
5116
  let cur = null;
@@ -5126,13 +5231,13 @@ var gettextPo2 = {
5126
5231
  );
5127
5232
  continue;
5128
5233
  }
5129
- forms[cat] = body.split("%d").join("{count}");
5234
+ forms[cat] = printfToCanonical2(body, "count");
5130
5235
  }
5131
5236
  if (!forms.other) continue;
5132
5237
  (keys[key] ??= { values: {} }).values[locale] = formsToIcu("count", forms);
5133
5238
  } else {
5134
5239
  if (!entry.msgstr) continue;
5135
- (keys[key] ??= { values: {} }).values[locale] = entry.msgstr;
5240
+ (keys[key] ??= { values: {} }).values[locale] = printfToCanonical2(entry.msgstr, "");
5136
5241
  }
5137
5242
  }
5138
5243
  }
@@ -5233,9 +5338,6 @@ import { readdirSync as readdirSync11, readFileSync as readFileSync18 } from "fs
5233
5338
  import { join as join14 } from "path";
5234
5339
  var LOCALE_RE8 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/i;
5235
5340
  var CATEGORY_SET = new Set(PLURAL_CATEGORIES);
5236
- function fromRuby(value) {
5237
- return value.replace(/%\{(\w+)\}/g, "{$1}");
5238
- }
5239
5341
  function makeNode() {
5240
5342
  return /* @__PURE__ */ Object.create(null);
5241
5343
  }
@@ -5421,7 +5523,7 @@ function synthesizeIcu(forms, file, key, warnings) {
5421
5523
  `rails-yaml: ${file}: plural "${key}" form "${cat}" contains "#", which ICU reads as the count placeholder`
5422
5524
  );
5423
5525
  }
5424
- parts.push(`${cat} {${fromRuby(body)}}`);
5526
+ parts.push(`${cat} {${railsToCanonical(body)}}`);
5425
5527
  }
5426
5528
  return `{count, plural, ${parts.join(" ")}}`;
5427
5529
  }
@@ -5439,7 +5541,7 @@ var railsYaml2 = {
5439
5541
  for (const [k, v] of Object.entries(node)) {
5440
5542
  const key = prefix ? `${prefix}.${k}` : k;
5441
5543
  if (typeof v === "string") {
5442
- if (v !== "") addValue(key, locale, fromRuby(v));
5544
+ if (v !== "") addValue(key, locale, railsToCanonical(v));
5443
5545
  continue;
5444
5546
  }
5445
5547
  const forms = asPluralForms(v);
@@ -5566,6 +5668,24 @@ function parsePlistDict(xml) {
5566
5668
  return tag.selfClosing ? {} : readDict();
5567
5669
  }
5568
5670
  var VAR_RE = /%#@([^@]*)@/g;
5671
+ function printfToCanonical3(body, token, arg) {
5672
+ let out = "";
5673
+ for (let i = 0; i < body.length; ) {
5674
+ if (body[i] === "%" && body[i + 1] === "%") {
5675
+ out += "%";
5676
+ i += 2;
5677
+ continue;
5678
+ }
5679
+ if (body.startsWith(token, i)) {
5680
+ out += `{${arg}}`;
5681
+ i += token.length;
5682
+ continue;
5683
+ }
5684
+ out += body[i];
5685
+ i++;
5686
+ }
5687
+ return out;
5688
+ }
5569
5689
  function entryToIcu(key, entry, file, warnings) {
5570
5690
  const warn = (msg) => {
5571
5691
  warnings.push(`apple-stringsdict: ${file}: key "${key}": ${msg}`);
@@ -5594,7 +5714,7 @@ function entryToIcu(key, entry, file, warnings) {
5594
5714
  for (const cat of PLURAL_CATEGORIES) {
5595
5715
  const body = varDict[cat];
5596
5716
  if (typeof body !== "string") continue;
5597
- forms[cat] = prefix + body.split(token).join(`{${arg}}`) + suffix;
5717
+ forms[cat] = prefix + printfToCanonical3(body, token, arg) + suffix;
5598
5718
  }
5599
5719
  if (forms.other === void 0) return warn(`variable "${arg}" has no "other" form; skipped`);
5600
5720
  return formsToIcu(arg, forms);