@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.
@@ -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 {spawn} = require('node:child_process')
4
- const path = require('path')
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, scriptPath, ...args] = process.argv
17
+ const [_nodeBin, _scriptPath, ...projects] = process.argv
7
18
 
8
- const proc = spawn(`${path.dirname(scriptPath)}/check-i18n-variables`, args)
19
+ if (projects.length < 1) {
20
+ console.error('Usage: check-i18n-variables <path>')
21
+ process.exit(64)
22
+ }
9
23
 
10
- proc.stdout.on('data', data => {
11
- process.stdout.write(data)
12
- })
24
+ // TODO: Pass in via ENV?
25
+ const ns = ['school', 'student', 'console', 'classroom', 'common']
13
26
 
14
- proc.stderr.on('data', data => {
15
- process.stderr.write(data)
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 {spawn} = require('node:child_process')
4
- const path = require('path')
10
+ const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
5
11
 
6
- const [_nodeBin, scriptPath, ...args] = process.argv
12
+ const ns = process.argv.slice(2)
7
13
 
8
- const proc = spawn(`${path.dirname(scriptPath)}/check-i18next-icu-parser`, args)
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
- proc.stdout.on('data', data => {
11
- process.stdout.write(data)
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
- proc.stderr.on('data', data => {
15
- process.stderr.write(data)
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()
@@ -9,7 +9,7 @@ translation_keys_in_code() {
9
9
  git ls-files "$project_path" |
10
10
  grep -E '\.(js|ts|tsx)$' |
11
11
  grep -v '\.min\.js$' |
12
- "$script_dir/translation-keys"
12
+ "$script_dir/translation-keys.js"
13
13
  }
14
14
 
15
15
  translation_keys_in_locize() {
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 interface = readline.createInterface({
17
+ const lines = readline.createInterface({
18
18
  input: process.stdin,
19
19
  output: process.stdout,
20
20
  terminal: false
21
21
  })
22
22
 
23
- interface.on('line', path => {
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": "1.1.23",
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.js"
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.332.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.8"
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 {spawn} = require('node:child_process')
4
- const path = require('path')
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, scriptPath, ...args] = process.argv
17
+ const [_nodeBin, _scriptPath, ...projects] = process.argv
7
18
 
8
- const proc = spawn(`${path.dirname(scriptPath)}/check-i18n-variables`, args)
19
+ if (projects.length < 1) {
20
+ console.error('Usage: check-i18n-variables <path>')
21
+ process.exit(64)
22
+ }
9
23
 
10
- proc.stdout.on('data', data => {
11
- process.stdout.write(data)
12
- })
24
+ // TODO: Pass in via ENV?
25
+ const ns = ['school', 'student', 'console', 'classroom', 'common']
13
26
 
14
- proc.stderr.on('data', data => {
15
- process.stderr.write(data)
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 {spawn} = require('node:child_process')
4
- const path = require('path')
10
+ const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
5
11
 
6
- const [_nodeBin, scriptPath, ...args] = process.argv
12
+ const ns = process.argv.slice(2)
7
13
 
8
- const proc = spawn(`${path.dirname(scriptPath)}/check-i18next-icu-parser`, args)
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
- proc.stdout.on('data', data => {
11
- process.stdout.write(data)
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
- proc.stderr.on('data', data => {
15
- process.stderr.write(data)
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()
@@ -9,7 +9,7 @@ translation_keys_in_code() {
9
9
  git ls-files "$project_path" |
10
10
  grep -E '\.(js|ts|tsx)$' |
11
11
  grep -v '\.min\.js$' |
12
- "$script_dir/translation-keys"
12
+ "$script_dir/translation-keys.js"
13
13
  }
14
14
 
15
15
  translation_keys_in_locize() {
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 interface = readline.createInterface({
17
+ const lines = readline.createInterface({
18
18
  input: process.stdin,
19
19
  output: process.stdout,
20
20
  terminal: false
21
21
  })
22
22
 
23
- interface.on('line', path => {
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
- })
@@ -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
- })