@freckle/i18n-scripts 1.1.23 → 2.0.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/dist/check-i18n-variables.js +125 -10
- package/dist/check-i18next-icu-parser.js +56 -10
- package/dist/check-missing-translations +1 -1
- package/dist/translation-extraction.js +0 -0
- package/{src/translation-keys → dist/translation-keys.js} +4 -6
- package/package.json +4 -4
- package/src/check-i18n-variables.js +125 -10
- package/src/check-i18next-icu-parser.js +56 -10
- package/src/check-missing-translations +1 -1
- package/src/translation-extraction.js +0 -0
- package/{dist/translation-keys → src/translation-keys.js} +4 -6
- package/dist/check-i18n-variables +0 -133
- package/dist/check-i18next-icu-parser +0 -64
- package/dist/check-missing-translations.js +0 -16
- package/src/check-i18n-variables +0 -133
- package/src/check-i18next-icu-parser +0 -64
- package/src/check-missing-translations.js +0 -16
|
@@ -1,16 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Checks all locize keys (in the codebase) with known variables against key
|
|
4
|
+
// values in locize ensuring that variables in locize refer to known vars.
|
|
5
|
+
//
|
|
6
|
+
const i18next = require('i18next')
|
|
7
|
+
const ICU = require('i18next-icu')
|
|
8
|
+
const HttpBackend = require('i18next-http-backend')
|
|
9
|
+
const {IntlMessageFormat} = require('intl-messageformat')
|
|
2
10
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
11
|
+
const {
|
|
12
|
+
extractTranslationKeysAndVariables,
|
|
13
|
+
KNOWN_VARIABLES_TAG
|
|
14
|
+
} = require('./translation-extraction.js')
|
|
15
|
+
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
5
16
|
|
|
6
|
-
const [_nodeBin,
|
|
17
|
+
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
7
18
|
|
|
8
|
-
|
|
19
|
+
if (projects.length < 1) {
|
|
20
|
+
console.error('Usage: check-i18n-variables <path>')
|
|
21
|
+
process.exit(64)
|
|
22
|
+
}
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
})
|
|
24
|
+
// TODO: Pass in via ENV?
|
|
25
|
+
const ns = ['school', 'student', 'console', 'classroom', 'common']
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
const translationKeysWithKnownVariablesInProject = async project => {
|
|
28
|
+
const files = await getFiles(project)
|
|
29
|
+
const jsFiles = files.filter(filterJsFiles)
|
|
30
|
+
const results = []
|
|
31
|
+
|
|
32
|
+
jsFiles.forEach(({path}) => {
|
|
33
|
+
extractTranslationKeysAndVariables(path)
|
|
34
|
+
.filter(({tag}) => tag === KNOWN_VARIABLES_TAG)
|
|
35
|
+
.forEach(keyWithVars => results.push(keyWithVars))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return results
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The AST is undocumented, however, its type definitions are here
|
|
42
|
+
// https://github.com/formatjs/formatjs/blob/82a2e5d07f9a999624ffb6387a58464f9a5f7399/packages/icu-messageformat-parser/types.ts#L122-L131
|
|
43
|
+
const extractVariablesFromICUAst = ast => {
|
|
44
|
+
// Some AST field values are `null`
|
|
45
|
+
if (!ast) {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ast.constructor === Array) {
|
|
50
|
+
return ast.flatMap(extractVariablesFromICUAst)
|
|
51
|
+
}
|
|
52
|
+
if (ast.constructor === Object) {
|
|
53
|
+
const subVariables = Object.values(ast).flatMap(extractVariablesFromICUAst)
|
|
54
|
+
// `type`s that aren't 0 (string literals) are variables when `.value` exists
|
|
55
|
+
const hasVariable = ast.type && typeof ast.type === 'number' && ast.type !== 0 && ast.value
|
|
56
|
+
|
|
57
|
+
return (hasVariable ? [ast.value] : []).concat(subVariables)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const main = async () => {
|
|
64
|
+
await i18next
|
|
65
|
+
.use(ICU)
|
|
66
|
+
.use(HttpBackend)
|
|
67
|
+
.init({
|
|
68
|
+
supportedLngs,
|
|
69
|
+
allowMultiLoading: true,
|
|
70
|
+
keySeparator: false,
|
|
71
|
+
ns,
|
|
72
|
+
backend: {
|
|
73
|
+
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
let anyErrors = false
|
|
78
|
+
|
|
79
|
+
// Project key extraction is not cheap so cache per project
|
|
80
|
+
const keysPerProject = {}
|
|
81
|
+
|
|
82
|
+
await Promise.all(
|
|
83
|
+
projects.map(
|
|
84
|
+
async project =>
|
|
85
|
+
(keysPerProject[project] = await translationKeysWithKnownVariablesInProject(project))
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for (const lang of supportedLngs) {
|
|
90
|
+
await i18next.changeLanguage(lang)
|
|
91
|
+
const translations = i18next.getDataByLanguage(lang)
|
|
92
|
+
|
|
93
|
+
for (const [project, keys] of Object.entries(keysPerProject)) {
|
|
94
|
+
for (const {i18nKey, variables: codeVariables} of keys) {
|
|
95
|
+
const keyValue =
|
|
96
|
+
(translations[project] && translations[project][i18nKey]) || translations.common[i18nKey]
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (keyValue) {
|
|
100
|
+
const locizeVariables = extractVariablesFromICUAst(
|
|
101
|
+
new IntlMessageFormat(keyValue, lang, {}, {ignoreTag: true}).getAst()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for (const locizeVariable of locizeVariables) {
|
|
105
|
+
if (!codeVariables.includes(locizeVariable)) {
|
|
106
|
+
anyErrors = true
|
|
107
|
+
console.error(
|
|
108
|
+
`${project}:${lang}:${i18nKey} does not pass the variable ${locizeVariable}`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// We have another script that checks that `IntlMessageFormat` can
|
|
115
|
+
// successfully parse the ICU text so let's not fail for that reason.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (anyErrors) {
|
|
122
|
+
console.error(
|
|
123
|
+
'The above keys have variables not passed by the code and need to be fixed in Locize.'
|
|
124
|
+
)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
} else {
|
|
127
|
+
process.exit(0)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main()
|
|
@@ -1,16 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Checks all keys in Locize to make sure that they can be parsed by the `i18next` `t` function
|
|
4
|
+
//
|
|
5
|
+
const i18next = require('i18next')
|
|
6
|
+
const ICU = require('i18next-icu')
|
|
7
|
+
const HttpBackend = require('i18next-http-backend')
|
|
8
|
+
const {supportedLngs} = require('./helpers.js')
|
|
2
9
|
|
|
3
|
-
const
|
|
4
|
-
const path = require('path')
|
|
10
|
+
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
5
11
|
|
|
6
|
-
const
|
|
12
|
+
const ns = process.argv.slice(2)
|
|
7
13
|
|
|
8
|
-
const
|
|
14
|
+
for (const arg of ns) {
|
|
15
|
+
if (!VALID_NAMESPACES.some(ns => ns === arg)) {
|
|
16
|
+
throw new Error(`Invalid arguments: ${arg} is not a namespace`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const main = async () => {
|
|
21
|
+
const t = await i18next
|
|
22
|
+
.use(ICU)
|
|
23
|
+
.use(HttpBackend)
|
|
24
|
+
.init({
|
|
25
|
+
supportedLngs,
|
|
26
|
+
allowMultiLoading: true,
|
|
27
|
+
keySeparator: false,
|
|
28
|
+
ns,
|
|
29
|
+
backend: {
|
|
30
|
+
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
let anyErrors = false
|
|
35
|
+
|
|
36
|
+
for (const l of supportedLngs) {
|
|
37
|
+
await i18next.changeLanguage(l)
|
|
38
|
+
for (const n of ns) {
|
|
39
|
+
for (const key of Object.keys(i18next.getDataByLanguage(l)[n])) {
|
|
40
|
+
try {
|
|
41
|
+
t(`${n}:${key}`)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isInterpolationError(error)) {
|
|
44
|
+
anyErrors = true
|
|
45
|
+
console.error(`${n}:${l}:${key}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (anyErrors) {
|
|
53
|
+
console.error('The above keys cannot be parsed as ICU and need to be fixed in Locize.')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
} else {
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isInterpolationError = error => /^The intl string context variable/.test(error.message)
|
|
61
|
+
|
|
62
|
+
main()
|
|
File without changes
|
|
@@ -7,21 +7,19 @@
|
|
|
7
7
|
// git ls-files |
|
|
8
8
|
// grep '\.js$' |
|
|
9
9
|
// grep -v '\.min\.js$' |
|
|
10
|
-
// ./scripts/translation-keys
|
|
10
|
+
// ./scripts/translation-keys.js
|
|
11
11
|
//
|
|
12
12
|
// We stream out the keys as seen. We do no sorting or de-duplication.
|
|
13
13
|
//
|
|
14
14
|
const {extractTranslationKeysAndVariables} = require('./translation-extraction.js')
|
|
15
15
|
const readline = require('readline')
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const lines = readline.createInterface({
|
|
18
18
|
input: process.stdin,
|
|
19
19
|
output: process.stdout,
|
|
20
20
|
terminal: false
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
extractTranslationKeysAndVariables(path).forEach(({i18nKey}) =>
|
|
25
|
-
console.log(i18nKey)
|
|
26
|
-
)
|
|
23
|
+
lines.on('line', path => {
|
|
24
|
+
extractTranslationKeysAndVariables(path).forEach(({i18nKey}) => console.log(i18nKey))
|
|
27
25
|
})
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freckle/i18n-scripts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Scripts for i18n",
|
|
5
5
|
"author": "Renaissance Learning Inc",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"check-i18n-variables": "./dist/check-i18n-variables.js",
|
|
9
9
|
"check-i18next-icu-parser": "./dist/check-i18next-icu-parser.js",
|
|
10
|
-
"check-missing-translations": "./dist/check-missing-translations
|
|
10
|
+
"check-missing-translations": "./dist/check-missing-translations"
|
|
11
11
|
},
|
|
12
12
|
"main": "./dist/index.js",
|
|
13
13
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"esprima-walk": "0.1.0",
|
|
25
|
-
"flow-parser": "0.
|
|
25
|
+
"flow-parser": "0.333.0",
|
|
26
26
|
"i18next": "26.4.2",
|
|
27
27
|
"i18next-http-backend": "^4.0.2",
|
|
28
28
|
"i18next-icu": "^2.4.4",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@freckle/eslint-config": "^1.1.0",
|
|
33
33
|
"eslint": "^10.11.0",
|
|
34
34
|
"globals": "^17.12.0",
|
|
35
|
-
"prettier": "^3.9.
|
|
35
|
+
"prettier": "^3.9.9"
|
|
36
36
|
},
|
|
37
37
|
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec"
|
|
38
38
|
}
|
|
@@ -1,16 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Checks all locize keys (in the codebase) with known variables against key
|
|
4
|
+
// values in locize ensuring that variables in locize refer to known vars.
|
|
5
|
+
//
|
|
6
|
+
const i18next = require('i18next')
|
|
7
|
+
const ICU = require('i18next-icu')
|
|
8
|
+
const HttpBackend = require('i18next-http-backend')
|
|
9
|
+
const {IntlMessageFormat} = require('intl-messageformat')
|
|
2
10
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
11
|
+
const {
|
|
12
|
+
extractTranslationKeysAndVariables,
|
|
13
|
+
KNOWN_VARIABLES_TAG
|
|
14
|
+
} = require('./translation-extraction.js')
|
|
15
|
+
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
5
16
|
|
|
6
|
-
const [_nodeBin,
|
|
17
|
+
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
7
18
|
|
|
8
|
-
|
|
19
|
+
if (projects.length < 1) {
|
|
20
|
+
console.error('Usage: check-i18n-variables <path>')
|
|
21
|
+
process.exit(64)
|
|
22
|
+
}
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
})
|
|
24
|
+
// TODO: Pass in via ENV?
|
|
25
|
+
const ns = ['school', 'student', 'console', 'classroom', 'common']
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
const translationKeysWithKnownVariablesInProject = async project => {
|
|
28
|
+
const files = await getFiles(project)
|
|
29
|
+
const jsFiles = files.filter(filterJsFiles)
|
|
30
|
+
const results = []
|
|
31
|
+
|
|
32
|
+
jsFiles.forEach(({path}) => {
|
|
33
|
+
extractTranslationKeysAndVariables(path)
|
|
34
|
+
.filter(({tag}) => tag === KNOWN_VARIABLES_TAG)
|
|
35
|
+
.forEach(keyWithVars => results.push(keyWithVars))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return results
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The AST is undocumented, however, its type definitions are here
|
|
42
|
+
// https://github.com/formatjs/formatjs/blob/82a2e5d07f9a999624ffb6387a58464f9a5f7399/packages/icu-messageformat-parser/types.ts#L122-L131
|
|
43
|
+
const extractVariablesFromICUAst = ast => {
|
|
44
|
+
// Some AST field values are `null`
|
|
45
|
+
if (!ast) {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ast.constructor === Array) {
|
|
50
|
+
return ast.flatMap(extractVariablesFromICUAst)
|
|
51
|
+
}
|
|
52
|
+
if (ast.constructor === Object) {
|
|
53
|
+
const subVariables = Object.values(ast).flatMap(extractVariablesFromICUAst)
|
|
54
|
+
// `type`s that aren't 0 (string literals) are variables when `.value` exists
|
|
55
|
+
const hasVariable = ast.type && typeof ast.type === 'number' && ast.type !== 0 && ast.value
|
|
56
|
+
|
|
57
|
+
return (hasVariable ? [ast.value] : []).concat(subVariables)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const main = async () => {
|
|
64
|
+
await i18next
|
|
65
|
+
.use(ICU)
|
|
66
|
+
.use(HttpBackend)
|
|
67
|
+
.init({
|
|
68
|
+
supportedLngs,
|
|
69
|
+
allowMultiLoading: true,
|
|
70
|
+
keySeparator: false,
|
|
71
|
+
ns,
|
|
72
|
+
backend: {
|
|
73
|
+
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
let anyErrors = false
|
|
78
|
+
|
|
79
|
+
// Project key extraction is not cheap so cache per project
|
|
80
|
+
const keysPerProject = {}
|
|
81
|
+
|
|
82
|
+
await Promise.all(
|
|
83
|
+
projects.map(
|
|
84
|
+
async project =>
|
|
85
|
+
(keysPerProject[project] = await translationKeysWithKnownVariablesInProject(project))
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for (const lang of supportedLngs) {
|
|
90
|
+
await i18next.changeLanguage(lang)
|
|
91
|
+
const translations = i18next.getDataByLanguage(lang)
|
|
92
|
+
|
|
93
|
+
for (const [project, keys] of Object.entries(keysPerProject)) {
|
|
94
|
+
for (const {i18nKey, variables: codeVariables} of keys) {
|
|
95
|
+
const keyValue =
|
|
96
|
+
(translations[project] && translations[project][i18nKey]) || translations.common[i18nKey]
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (keyValue) {
|
|
100
|
+
const locizeVariables = extractVariablesFromICUAst(
|
|
101
|
+
new IntlMessageFormat(keyValue, lang, {}, {ignoreTag: true}).getAst()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for (const locizeVariable of locizeVariables) {
|
|
105
|
+
if (!codeVariables.includes(locizeVariable)) {
|
|
106
|
+
anyErrors = true
|
|
107
|
+
console.error(
|
|
108
|
+
`${project}:${lang}:${i18nKey} does not pass the variable ${locizeVariable}`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// We have another script that checks that `IntlMessageFormat` can
|
|
115
|
+
// successfully parse the ICU text so let's not fail for that reason.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (anyErrors) {
|
|
122
|
+
console.error(
|
|
123
|
+
'The above keys have variables not passed by the code and need to be fixed in Locize.'
|
|
124
|
+
)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
} else {
|
|
127
|
+
process.exit(0)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main()
|
|
@@ -1,16 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Checks all keys in Locize to make sure that they can be parsed by the `i18next` `t` function
|
|
4
|
+
//
|
|
5
|
+
const i18next = require('i18next')
|
|
6
|
+
const ICU = require('i18next-icu')
|
|
7
|
+
const HttpBackend = require('i18next-http-backend')
|
|
8
|
+
const {supportedLngs} = require('./helpers.js')
|
|
2
9
|
|
|
3
|
-
const
|
|
4
|
-
const path = require('path')
|
|
10
|
+
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
5
11
|
|
|
6
|
-
const
|
|
12
|
+
const ns = process.argv.slice(2)
|
|
7
13
|
|
|
8
|
-
const
|
|
14
|
+
for (const arg of ns) {
|
|
15
|
+
if (!VALID_NAMESPACES.some(ns => ns === arg)) {
|
|
16
|
+
throw new Error(`Invalid arguments: ${arg} is not a namespace`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const main = async () => {
|
|
21
|
+
const t = await i18next
|
|
22
|
+
.use(ICU)
|
|
23
|
+
.use(HttpBackend)
|
|
24
|
+
.init({
|
|
25
|
+
supportedLngs,
|
|
26
|
+
allowMultiLoading: true,
|
|
27
|
+
keySeparator: false,
|
|
28
|
+
ns,
|
|
29
|
+
backend: {
|
|
30
|
+
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
let anyErrors = false
|
|
35
|
+
|
|
36
|
+
for (const l of supportedLngs) {
|
|
37
|
+
await i18next.changeLanguage(l)
|
|
38
|
+
for (const n of ns) {
|
|
39
|
+
for (const key of Object.keys(i18next.getDataByLanguage(l)[n])) {
|
|
40
|
+
try {
|
|
41
|
+
t(`${n}:${key}`)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isInterpolationError(error)) {
|
|
44
|
+
anyErrors = true
|
|
45
|
+
console.error(`${n}:${l}:${key}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (anyErrors) {
|
|
53
|
+
console.error('The above keys cannot be parsed as ICU and need to be fixed in Locize.')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
} else {
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isInterpolationError = error => /^The intl string context variable/.test(error.message)
|
|
61
|
+
|
|
62
|
+
main()
|
|
File without changes
|
|
@@ -7,21 +7,19 @@
|
|
|
7
7
|
// git ls-files |
|
|
8
8
|
// grep '\.js$' |
|
|
9
9
|
// grep -v '\.min\.js$' |
|
|
10
|
-
// ./scripts/translation-keys
|
|
10
|
+
// ./scripts/translation-keys.js
|
|
11
11
|
//
|
|
12
12
|
// We stream out the keys as seen. We do no sorting or de-duplication.
|
|
13
13
|
//
|
|
14
14
|
const {extractTranslationKeysAndVariables} = require('./translation-extraction.js')
|
|
15
15
|
const readline = require('readline')
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const lines = readline.createInterface({
|
|
18
18
|
input: process.stdin,
|
|
19
19
|
output: process.stdout,
|
|
20
20
|
terminal: false
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
extractTranslationKeysAndVariables(path).forEach(({i18nKey}) =>
|
|
25
|
-
console.log(i18nKey)
|
|
26
|
-
)
|
|
23
|
+
lines.on('line', path => {
|
|
24
|
+
extractTranslationKeysAndVariables(path).forEach(({i18nKey}) => console.log(i18nKey))
|
|
27
25
|
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// Checks all locize keys (in the codebase) with known variables against key
|
|
4
|
-
// values in locize ensuring that variables in locize refer to known vars.
|
|
5
|
-
//
|
|
6
|
-
const {promises: fs} = require('fs')
|
|
7
|
-
const p = require('path')
|
|
8
|
-
const i18next = require('i18next')
|
|
9
|
-
const ICU = require('i18next-icu')
|
|
10
|
-
const HttpBackend = require('i18next-http-backend')
|
|
11
|
-
const {IntlMessageFormat} = require('intl-messageformat')
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
extractTranslationKeysAndVariables,
|
|
15
|
-
KNOWN_VARIABLES_TAG
|
|
16
|
-
} = require('./translation-extraction.js')
|
|
17
|
-
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
18
|
-
|
|
19
|
-
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
20
|
-
|
|
21
|
-
if (projects.length < 1) {
|
|
22
|
-
console.log('Usage: check-i18n-variables <path>')
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// TODO: Pass in via ENV?
|
|
27
|
-
const ns = ['school', 'student', 'console', 'classroom', 'common']
|
|
28
|
-
|
|
29
|
-
const translationKeysWithKnownVariablesInProject = async project => {
|
|
30
|
-
const files = await getFiles(project)
|
|
31
|
-
const jsFiles = files.filter(filterJsFiles)
|
|
32
|
-
const results = []
|
|
33
|
-
|
|
34
|
-
jsFiles.forEach(({path}) => {
|
|
35
|
-
extractTranslationKeysAndVariables(path)
|
|
36
|
-
.filter(({tag}) => tag === KNOWN_VARIABLES_TAG)
|
|
37
|
-
.forEach(keyWithVars => results.push(keyWithVars))
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return results
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// The AST is undocumented, however, its type definitions are here
|
|
44
|
-
// https://github.com/formatjs/formatjs/blob/82a2e5d07f9a999624ffb6387a58464f9a5f7399/packages/icu-messageformat-parser/types.ts#L122-L131
|
|
45
|
-
const extractVariablesFromICUAst = ast => {
|
|
46
|
-
// Some AST field values are `null`
|
|
47
|
-
if (!ast) {
|
|
48
|
-
return []
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (ast.constructor === Array) {
|
|
52
|
-
return ast.flatMap(extractVariablesFromICUAst)
|
|
53
|
-
}
|
|
54
|
-
if (ast.constructor === Object) {
|
|
55
|
-
const subVariables = Object.values(ast).flatMap(extractVariablesFromICUAst)
|
|
56
|
-
// `type`s that aren't 0 (string literals) are variables when `.value` exists
|
|
57
|
-
const hasVariable = ast.type && typeof ast.type === 'number' && ast.type !== 0 && ast.value
|
|
58
|
-
|
|
59
|
-
return (hasVariable ? [ast.value] : []).concat(subVariables)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return []
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const main = async () => {
|
|
66
|
-
const t = await i18next
|
|
67
|
-
.use(ICU)
|
|
68
|
-
.use(HttpBackend)
|
|
69
|
-
.init({
|
|
70
|
-
supportedLngs,
|
|
71
|
-
allowMultiLoading: true,
|
|
72
|
-
keySeparator: false,
|
|
73
|
-
ns,
|
|
74
|
-
backend: {
|
|
75
|
-
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
let anyErrors = false
|
|
80
|
-
|
|
81
|
-
// Project key extraction is not cheap so cache per project
|
|
82
|
-
const keysPerProject = {}
|
|
83
|
-
|
|
84
|
-
await Promise.all(
|
|
85
|
-
projects.map(
|
|
86
|
-
async project =>
|
|
87
|
-
(keysPerProject[project] = await translationKeysWithKnownVariablesInProject(project))
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
for (const lang of supportedLngs) {
|
|
92
|
-
await i18next.changeLanguage(lang)
|
|
93
|
-
const translations = i18next.getDataByLanguage(lang)
|
|
94
|
-
|
|
95
|
-
for (const [project, keys] of Object.entries(keysPerProject)) {
|
|
96
|
-
for (const {i18nKey, variables: codeVariables} of keys) {
|
|
97
|
-
const keyValue =
|
|
98
|
-
(translations[project] && translations[project][i18nKey]) || translations.common[i18nKey]
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
if (keyValue) {
|
|
102
|
-
const locizeVariables = extractVariablesFromICUAst(
|
|
103
|
-
new IntlMessageFormat(keyValue, lang, {}, {ignoreTag: true}).getAst()
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
for (const locizeVariable of locizeVariables) {
|
|
107
|
-
if (!codeVariables.includes(locizeVariable)) {
|
|
108
|
-
anyErrors = true
|
|
109
|
-
console.error(
|
|
110
|
-
`${project}:${lang}:${i18nKey} does not pass the variable ${locizeVariable}`
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch (e) {
|
|
116
|
-
// We have another script that checks that `IntlMessageFormat` can
|
|
117
|
-
// successfully parse the ICU text so let's not fail for that reason.
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (anyErrors) {
|
|
124
|
-
console.error(
|
|
125
|
-
'The above keys have variables not passed by the code and need to be fixed in Locize.'
|
|
126
|
-
)
|
|
127
|
-
process.exit(1)
|
|
128
|
-
} else {
|
|
129
|
-
process.exit(0)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
main()
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @flow
|
|
3
|
-
//
|
|
4
|
-
// Checks all keys in Locize to make sure that they can be parsed by the `i18next` `t` function
|
|
5
|
-
//
|
|
6
|
-
const i18next = require('i18next')
|
|
7
|
-
const ICU = require('i18next-icu')
|
|
8
|
-
const HttpBackend = require('i18next-http-backend')
|
|
9
|
-
const {supportedLngs} = require('./helpers.js')
|
|
10
|
-
|
|
11
|
-
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
12
|
-
|
|
13
|
-
const ns = process.argv.slice(2)
|
|
14
|
-
|
|
15
|
-
for (const arg of ns) {
|
|
16
|
-
if (!VALID_NAMESPACES.some(ns => ns === arg)) {
|
|
17
|
-
throw new Error(`Invalid arguments: ${arg} is not a namespace`)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const main = async () => {
|
|
23
|
-
const t = await i18next
|
|
24
|
-
.use(ICU)
|
|
25
|
-
.use(HttpBackend)
|
|
26
|
-
.init({
|
|
27
|
-
supportedLngs,
|
|
28
|
-
allowMultiLoading: true,
|
|
29
|
-
keySeparator: false,
|
|
30
|
-
ns,
|
|
31
|
-
backend: {
|
|
32
|
-
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
let anyErrors = false
|
|
37
|
-
|
|
38
|
-
for (const l of supportedLngs) {
|
|
39
|
-
await i18next.changeLanguage(l)
|
|
40
|
-
for (const n of ns) {
|
|
41
|
-
for (const key of Object.keys(i18next.getDataByLanguage(l)[n])) {
|
|
42
|
-
try {
|
|
43
|
-
t(`${n}:${key}`)
|
|
44
|
-
} catch (error) {
|
|
45
|
-
if (!isInterpolationError(error)) {
|
|
46
|
-
anyErrors = true
|
|
47
|
-
console.error(`${n}:${l}:${key}`)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (anyErrors) {
|
|
55
|
-
console.error('The above keys cannot be parsed as ICU and need to be fixed in Locize.')
|
|
56
|
-
process.exit(1)
|
|
57
|
-
} else {
|
|
58
|
-
process.exit(0)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const isInterpolationError = error => /^The intl string context variable/.test(error.message)
|
|
63
|
-
|
|
64
|
-
main()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const {spawn} = require('node:child_process')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
|
|
6
|
-
const [_nodeBin, scriptPath, ...args] = process.argv
|
|
7
|
-
|
|
8
|
-
const proc = spawn(`${path.dirname(scriptPath)}/check-missing-translations`, args)
|
|
9
|
-
|
|
10
|
-
proc.stdout.on('data', data => {
|
|
11
|
-
process.stdout.write(data)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
proc.stderr.on('data', data => {
|
|
15
|
-
process.stderr.write(data)
|
|
16
|
-
})
|
package/src/check-i18n-variables
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// Checks all locize keys (in the codebase) with known variables against key
|
|
4
|
-
// values in locize ensuring that variables in locize refer to known vars.
|
|
5
|
-
//
|
|
6
|
-
const {promises: fs} = require('fs')
|
|
7
|
-
const p = require('path')
|
|
8
|
-
const i18next = require('i18next')
|
|
9
|
-
const ICU = require('i18next-icu')
|
|
10
|
-
const HttpBackend = require('i18next-http-backend')
|
|
11
|
-
const {IntlMessageFormat} = require('intl-messageformat')
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
extractTranslationKeysAndVariables,
|
|
15
|
-
KNOWN_VARIABLES_TAG
|
|
16
|
-
} = require('./translation-extraction.js')
|
|
17
|
-
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
18
|
-
|
|
19
|
-
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
20
|
-
|
|
21
|
-
if (projects.length < 1) {
|
|
22
|
-
console.log('Usage: check-i18n-variables <path>')
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// TODO: Pass in via ENV?
|
|
27
|
-
const ns = ['school', 'student', 'console', 'classroom', 'common']
|
|
28
|
-
|
|
29
|
-
const translationKeysWithKnownVariablesInProject = async project => {
|
|
30
|
-
const files = await getFiles(project)
|
|
31
|
-
const jsFiles = files.filter(filterJsFiles)
|
|
32
|
-
const results = []
|
|
33
|
-
|
|
34
|
-
jsFiles.forEach(({path}) => {
|
|
35
|
-
extractTranslationKeysAndVariables(path)
|
|
36
|
-
.filter(({tag}) => tag === KNOWN_VARIABLES_TAG)
|
|
37
|
-
.forEach(keyWithVars => results.push(keyWithVars))
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return results
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// The AST is undocumented, however, its type definitions are here
|
|
44
|
-
// https://github.com/formatjs/formatjs/blob/82a2e5d07f9a999624ffb6387a58464f9a5f7399/packages/icu-messageformat-parser/types.ts#L122-L131
|
|
45
|
-
const extractVariablesFromICUAst = ast => {
|
|
46
|
-
// Some AST field values are `null`
|
|
47
|
-
if (!ast) {
|
|
48
|
-
return []
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (ast.constructor === Array) {
|
|
52
|
-
return ast.flatMap(extractVariablesFromICUAst)
|
|
53
|
-
}
|
|
54
|
-
if (ast.constructor === Object) {
|
|
55
|
-
const subVariables = Object.values(ast).flatMap(extractVariablesFromICUAst)
|
|
56
|
-
// `type`s that aren't 0 (string literals) are variables when `.value` exists
|
|
57
|
-
const hasVariable = ast.type && typeof ast.type === 'number' && ast.type !== 0 && ast.value
|
|
58
|
-
|
|
59
|
-
return (hasVariable ? [ast.value] : []).concat(subVariables)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return []
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const main = async () => {
|
|
66
|
-
const t = await i18next
|
|
67
|
-
.use(ICU)
|
|
68
|
-
.use(HttpBackend)
|
|
69
|
-
.init({
|
|
70
|
-
supportedLngs,
|
|
71
|
-
allowMultiLoading: true,
|
|
72
|
-
keySeparator: false,
|
|
73
|
-
ns,
|
|
74
|
-
backend: {
|
|
75
|
-
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
let anyErrors = false
|
|
80
|
-
|
|
81
|
-
// Project key extraction is not cheap so cache per project
|
|
82
|
-
const keysPerProject = {}
|
|
83
|
-
|
|
84
|
-
await Promise.all(
|
|
85
|
-
projects.map(
|
|
86
|
-
async project =>
|
|
87
|
-
(keysPerProject[project] = await translationKeysWithKnownVariablesInProject(project))
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
for (const lang of supportedLngs) {
|
|
92
|
-
await i18next.changeLanguage(lang)
|
|
93
|
-
const translations = i18next.getDataByLanguage(lang)
|
|
94
|
-
|
|
95
|
-
for (const [project, keys] of Object.entries(keysPerProject)) {
|
|
96
|
-
for (const {i18nKey, variables: codeVariables} of keys) {
|
|
97
|
-
const keyValue =
|
|
98
|
-
(translations[project] && translations[project][i18nKey]) || translations.common[i18nKey]
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
if (keyValue) {
|
|
102
|
-
const locizeVariables = extractVariablesFromICUAst(
|
|
103
|
-
new IntlMessageFormat(keyValue, lang, {}, {ignoreTag: true}).getAst()
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
for (const locizeVariable of locizeVariables) {
|
|
107
|
-
if (!codeVariables.includes(locizeVariable)) {
|
|
108
|
-
anyErrors = true
|
|
109
|
-
console.error(
|
|
110
|
-
`${project}:${lang}:${i18nKey} does not pass the variable ${locizeVariable}`
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch (e) {
|
|
116
|
-
// We have another script that checks that `IntlMessageFormat` can
|
|
117
|
-
// successfully parse the ICU text so let's not fail for that reason.
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (anyErrors) {
|
|
124
|
-
console.error(
|
|
125
|
-
'The above keys have variables not passed by the code and need to be fixed in Locize.'
|
|
126
|
-
)
|
|
127
|
-
process.exit(1)
|
|
128
|
-
} else {
|
|
129
|
-
process.exit(0)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
main()
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @flow
|
|
3
|
-
//
|
|
4
|
-
// Checks all keys in Locize to make sure that they can be parsed by the `i18next` `t` function
|
|
5
|
-
//
|
|
6
|
-
const i18next = require('i18next')
|
|
7
|
-
const ICU = require('i18next-icu')
|
|
8
|
-
const HttpBackend = require('i18next-http-backend')
|
|
9
|
-
const {supportedLngs} = require('./helpers.js')
|
|
10
|
-
|
|
11
|
-
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
12
|
-
|
|
13
|
-
const ns = process.argv.slice(2)
|
|
14
|
-
|
|
15
|
-
for (const arg of ns) {
|
|
16
|
-
if (!VALID_NAMESPACES.some(ns => ns === arg)) {
|
|
17
|
-
throw new Error(`Invalid arguments: ${arg} is not a namespace`)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const main = async () => {
|
|
23
|
-
const t = await i18next
|
|
24
|
-
.use(ICU)
|
|
25
|
-
.use(HttpBackend)
|
|
26
|
-
.init({
|
|
27
|
-
supportedLngs,
|
|
28
|
-
allowMultiLoading: true,
|
|
29
|
-
keySeparator: false,
|
|
30
|
-
ns,
|
|
31
|
-
backend: {
|
|
32
|
-
loadPath: 'https://translations.freckle.com/latest/{{lng}}/{{ns}}'
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
let anyErrors = false
|
|
37
|
-
|
|
38
|
-
for (const l of supportedLngs) {
|
|
39
|
-
await i18next.changeLanguage(l)
|
|
40
|
-
for (const n of ns) {
|
|
41
|
-
for (const key of Object.keys(i18next.getDataByLanguage(l)[n])) {
|
|
42
|
-
try {
|
|
43
|
-
t(`${n}:${key}`)
|
|
44
|
-
} catch (error) {
|
|
45
|
-
if (!isInterpolationError(error)) {
|
|
46
|
-
anyErrors = true
|
|
47
|
-
console.error(`${n}:${l}:${key}`)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (anyErrors) {
|
|
55
|
-
console.error('The above keys cannot be parsed as ICU and need to be fixed in Locize.')
|
|
56
|
-
process.exit(1)
|
|
57
|
-
} else {
|
|
58
|
-
process.exit(0)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const isInterpolationError = error => /^The intl string context variable/.test(error.message)
|
|
63
|
-
|
|
64
|
-
main()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const {spawn} = require('node:child_process')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
|
|
6
|
-
const [_nodeBin, scriptPath, ...args] = process.argv
|
|
7
|
-
|
|
8
|
-
const proc = spawn(`${path.dirname(scriptPath)}/check-missing-translations`, args)
|
|
9
|
-
|
|
10
|
-
proc.stdout.on('data', data => {
|
|
11
|
-
process.stdout.write(data)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
proc.stderr.on('data', data => {
|
|
15
|
-
process.stderr.write(data)
|
|
16
|
-
})
|