@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 +1 -1
- package/src/placeholders.js +29 -2
- package/src/reporters.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipi18n/core",
|
|
3
|
-
"version": "2.11.
|
|
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",
|
package/src/placeholders.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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. */
|