@shipi18n/core 2.6.1 → 2.7.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.
- package/CHANGELOG.md +6 -0
- package/package.json +3 -2
- package/src/tree.js +40 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @shipi18n/core
|
|
2
2
|
|
|
3
|
+
## 2.7.0
|
|
4
|
+
|
|
5
|
+
- New: **YAML locale files** (`.yaml` / `.yml`) are checked alongside JSON — flat and nested trees,
|
|
6
|
+
every existing check (placeholders, plurals, missing/orphan keys, glossary). The check logic was
|
|
7
|
+
already format-agnostic; this adds parsing + file discovery. Adds one dependency (`yaml`).
|
|
8
|
+
|
|
3
9
|
## 2.6.1
|
|
4
10
|
|
|
5
11
|
- Fix: the OpenAI adapter sent `max_tokens`, which current OpenAI models (gpt-5.x) reject with a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipi18n/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
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",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"jest": "^29.7.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"chalk": "^5.3.0"
|
|
61
|
+
"chalk": "^5.3.0",
|
|
62
|
+
"yaml": "^2.5.0"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
package/src/tree.js
CHANGED
|
@@ -11,8 +11,17 @@
|
|
|
11
11
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
|
|
12
12
|
import { join, basename, resolve, relative, dirname } from 'node:path'
|
|
13
13
|
import { checkTranslations } from './check.js'
|
|
14
|
+
import { parse as parseYaml } from 'yaml'
|
|
14
15
|
import { parseArbBundle } from './formats/arb.js'
|
|
15
16
|
import { parseXcstrings } from './formats/xcstrings.js'
|
|
17
|
+
|
|
18
|
+
// Locale files are JSON or YAML. The check logic is format-agnostic once the
|
|
19
|
+
// file is parsed to an object, so support is entirely a parse + discovery
|
|
20
|
+
// concern. `.json` is matched first so it stays the default when both exist.
|
|
21
|
+
const LOCALE_EXT = /\.(json|ya?ml)$/i
|
|
22
|
+
const stripExt = (name) => name.replace(LOCALE_EXT, '')
|
|
23
|
+
const readData = (file) =>
|
|
24
|
+
/\.ya?ml$/i.test(file) ? parseYaml(readFileSync(file, 'utf8')) : JSON.parse(readFileSync(file, 'utf8'))
|
|
16
25
|
import { flatten } from './translate.js'
|
|
17
26
|
import { lockId, lockFinding } from './locks.js'
|
|
18
27
|
import { reviewTranslations } from './review.js'
|
|
@@ -37,19 +46,19 @@ export function discoverLayout(inputPath, sourceLang) {
|
|
|
37
46
|
|
|
38
47
|
if (statSync(path).isFile()) {
|
|
39
48
|
const dir = resolve(path, '..')
|
|
40
|
-
const lang = basename(path)
|
|
49
|
+
const lang = stripExt(basename(path))
|
|
41
50
|
return flatLayout(dir, lang)
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
const entries = readdirSync(path, { withFileTypes: true })
|
|
45
|
-
if (entries.some((e) => e.isFile() && e.name ===
|
|
54
|
+
if (entries.some((e) => e.isFile() && LOCALE_EXT.test(e.name) && stripExt(e.name) === sourceLang)) {
|
|
46
55
|
return flatLayout(path, sourceLang)
|
|
47
56
|
}
|
|
48
57
|
if (entries.some((e) => e.isDirectory() && e.name === sourceLang)) {
|
|
49
58
|
return nestedLayout(path, sourceLang)
|
|
50
59
|
}
|
|
51
60
|
throw new Error(
|
|
52
|
-
`no source locale found: expected ${join(inputPath, sourceLang + '.json')} or ${join(inputPath, sourceLang)}/`
|
|
61
|
+
`no source locale found: expected ${join(inputPath, sourceLang + '.{json,yaml}')} or ${join(inputPath, sourceLang)}/`
|
|
53
62
|
)
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -63,18 +72,26 @@ export function discoverLayout(inputPath, sourceLang) {
|
|
|
63
72
|
const LOCALE_NAME = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/
|
|
64
73
|
|
|
65
74
|
export function flatLayout(dir, sourceLang) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// lang → actual file path (extension resolved, since a tree may be .json or
|
|
76
|
+
// .yaml — .json wins if both somehow exist for the same lang).
|
|
77
|
+
const pathByLang = {}
|
|
78
|
+
for (const f of readdirSync(dir).sort()) {
|
|
79
|
+
if (f.startsWith('.') || !LOCALE_EXT.test(f)) continue
|
|
80
|
+
const name = stripExt(f)
|
|
81
|
+
if (!(LOCALE_NAME.test(name) || name === sourceLang)) continue
|
|
82
|
+
if (!pathByLang[name] || f.endsWith('.json')) pathByLang[name] = join(dir, f)
|
|
83
|
+
}
|
|
84
|
+
const langs = Object.keys(pathByLang)
|
|
85
|
+
if (!langs.includes(sourceLang))
|
|
86
|
+
throw new Error(`source file not found: ${join(dir, sourceLang + '.{json,yaml}')}`)
|
|
72
87
|
return {
|
|
73
88
|
layout: 'flat',
|
|
74
89
|
dir,
|
|
75
90
|
sourceLang,
|
|
76
|
-
source:
|
|
77
|
-
targets: langs
|
|
91
|
+
source: { translation: pathByLang[sourceLang] },
|
|
92
|
+
targets: langs
|
|
93
|
+
.filter((l) => l !== sourceLang)
|
|
94
|
+
.map((lang) => ({ lang, files: { translation: pathByLang[lang] } })),
|
|
78
95
|
}
|
|
79
96
|
}
|
|
80
97
|
|
|
@@ -91,12 +108,15 @@ export function nestedLayout(dir, sourceLang) {
|
|
|
91
108
|
(LOCALE_NAME.test(e.name) || e.name === sourceLang)
|
|
92
109
|
)
|
|
93
110
|
.map((e) => e.name)
|
|
94
|
-
const nsFiles = (lang) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
const nsFiles = (lang) => {
|
|
112
|
+
const out = {}
|
|
113
|
+
for (const f of readdirSync(join(dir, lang)).sort()) {
|
|
114
|
+
if (!LOCALE_EXT.test(f)) continue
|
|
115
|
+
const ns = stripExt(f)
|
|
116
|
+
if (!out[ns] || f.endsWith('.json')) out[ns] = join(dir, lang, f)
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
100
120
|
return {
|
|
101
121
|
layout: 'nested',
|
|
102
122
|
dir,
|
|
@@ -182,7 +202,7 @@ export function jsonMode({ input, source, isIgnored, glossary, locks }) {
|
|
|
182
202
|
const layout = discoverLayout(input, source)
|
|
183
203
|
|
|
184
204
|
const sourceData = {}
|
|
185
|
-
for (const [ns, file] of Object.entries(layout.source)) sourceData[ns] =
|
|
205
|
+
for (const [ns, file] of Object.entries(layout.source)) sourceData[ns] = readData(file) // broken source = usage error
|
|
186
206
|
|
|
187
207
|
const perLang = {}
|
|
188
208
|
const languages = []
|
|
@@ -200,9 +220,9 @@ export function jsonMode({ input, source, isIgnored, glossary, locks }) {
|
|
|
200
220
|
}
|
|
201
221
|
let data
|
|
202
222
|
try {
|
|
203
|
-
data =
|
|
223
|
+
data = readData(file)
|
|
204
224
|
} catch (err) {
|
|
205
|
-
const findings = [{ type: 'invalid-json', severity: 'error', path: ns, message: `
|
|
225
|
+
const findings = [{ type: 'invalid-json', severity: 'error', path: ns, message: `could not parse ${rel(file)}: ${err.message}` }]
|
|
206
226
|
namespaces.push({ ns, file: rel(file), findings, stats: statsFrom(findings, srcKeys, 0) })
|
|
207
227
|
continue
|
|
208
228
|
}
|