@shipi18n/core 2.11.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipi18n/core",
3
- "version": "2.11.0",
3
+ "version": "2.11.1",
4
4
  "description": "Translation QA for i18n locale files: placeholder and plural validation, key parity, coverage, and an LLM-as-judge semantic review. Also a structure-preserving translation engine — bring your own OpenAI or Anthropic key.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -42,8 +42,29 @@ export function extractPlaceholders(str) {
42
42
  return found.sort()
43
43
  }
44
44
 
45
+ // An ICU argument used with a function: {count, plural, …}, {gender, select, …},
46
+ // {n, selectordinal, …}, {v, number}, {d, date}, … The bare `{count,` form does
47
+ // not match the single-brace placeholder pattern, so a translation that upgrades
48
+ // a plain {count} to an ICU plural would otherwise read as "dropped {count}".
49
+ const ICU_ARG =
50
+ /\{\s*([a-zA-Z0-9_]+)\s*,\s*(?:plural|selectordinal|select|number|date|time|spellout|ordinal|duration)\b/g
51
+ const SIMPLE_BRACE = /^\{([a-zA-Z0-9_.]+)\}$/ // a react-intl/ICU {name} placeholder token
52
+
53
+ /** Names used as ICU function arguments in a string. */
54
+ function icuArgNames(str) {
55
+ const set = new Set()
56
+ if (typeof str !== 'string') return set
57
+ ICU_ARG.lastIndex = 0
58
+ let m
59
+ while ((m = ICU_ARG.exec(str)) !== null) set.add(m[1])
60
+ return set
61
+ }
62
+
45
63
  /**
46
64
  * Does the translation preserve exactly the placeholders of the source?
65
+ * A source `{x}` is considered present when the translation uses `x` as an ICU
66
+ * argument (`{x, plural|select|…}`) and vice-versa — upgrading a plain variable
67
+ * to an ICU plural is correct, not a dropped placeholder.
47
68
  * @param {string} source
48
69
  * @param {string} translation
49
70
  * @returns {{ ok: boolean, missing: string[], added: string[] }}
@@ -53,14 +74,20 @@ export function validatePlaceholders(source, translation) {
53
74
  const out = extractPlaceholders(translation)
54
75
  const outCounts = tally(out)
55
76
  const srcCounts = tally(src)
77
+ const srcIcu = icuArgNames(source)
78
+ const outIcu = icuArgNames(translation)
56
79
  const missing = []
57
80
  const added = []
58
81
  for (const [ph, n] of Object.entries(srcCounts)) {
59
- const diff = n - (outCounts[ph] || 0)
82
+ let diff = n - (outCounts[ph] || 0)
83
+ const b = SIMPLE_BRACE.exec(ph)
84
+ if (diff > 0 && b && outIcu.has(b[1])) diff = 0 // used as an ICU arg in the translation
60
85
  for (let i = 0; i < diff; i++) missing.push(ph)
61
86
  }
62
87
  for (const [ph, n] of Object.entries(outCounts)) {
63
- const diff = n - (srcCounts[ph] || 0)
88
+ let diff = n - (srcCounts[ph] || 0)
89
+ const b = SIMPLE_BRACE.exec(ph)
90
+ if (diff > 0 && b && srcIcu.has(b[1])) diff = 0 // source used it as an ICU arg
64
91
  for (let i = 0; i < diff; i++) added.push(ph)
65
92
  }
66
93
  return { ok: missing.length === 0 && added.length === 0, missing, added }
package/src/reporters.js CHANGED
@@ -76,6 +76,12 @@ export const RULE_META = {
76
76
  'semantic-mistranslation': 'LLM judge (majority vote): the translation states something different from the source.',
77
77
  'semantic-omission': 'LLM judge (majority vote): meaningful source content is missing from the translation.',
78
78
  'semantic-addition': 'LLM judge (majority vote): the translation contains claims the source does not make.',
79
+ 'jed-drift': 'A WordPress JED .json string is out of sync with the .po (re-run wp i18n make-json).',
80
+ 'jed-orphan': 'A WordPress JED .json carries a string the .po no longer has.',
81
+ 'secret-detected': 'A locale string contains a secret or PII (API key, private key, card, email).',
82
+ 'secret-preflight': 'A string was withheld from the LLM because it held a secret or PII.',
83
+ 'android-unescaped-apostrophe': "An Android strings.xml apostrophe is not escaped (\\') or double-quote-wrapped.",
84
+ 'android-unbalanced-quote': 'An Android strings.xml has an unbalanced or unescaped double-quote.',
79
85
  }
80
86
 
81
87
  /** SARIF 2.1.0 — one run, one rule per finding type, one result per finding. */