@freckle/i18n-scripts 2.0.0 → 3.0.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/README.md +21 -1
- package/dist/check-i18n-variables.js +7 -10
- package/dist/check-i18next-icu-parser.js +5 -4
- package/dist/helpers.js +5 -12
- package/dist/translation-extraction.js +6 -9
- package/dist/translation-keys.js +3 -2
- package/package.json +23 -12
- package/.github/CODEOWNERS +0 -1
- package/.github/workflows/ci.yml +0 -27
- package/.github/workflows/mergeabot.yml +0 -19
- package/.github/workflows/release.yml +0 -26
- package/.nvmrc +0 -1
- package/.prettierrc +0 -18
- package/.releaserc.yaml +0 -15
- package/.restyled.yaml +0 -12
- package/RELEASE.md +0 -14
- package/eslint.config.mjs +0 -22
- package/renovate.json +0 -6
- package/src/check-i18n-variables.js +0 -131
- package/src/check-i18next-icu-parser.js +0 -62
- package/src/check-missing-translations +0 -69
- package/src/helpers.js +0 -34
- package/src/translation-extraction.js +0 -141
- package/src/translation-keys.js +0 -25
package/README.md
CHANGED
|
@@ -10,7 +10,27 @@ pnpm add @freckle/i18n-scripts
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The package installs three bins:
|
|
14
|
+
|
|
15
|
+
- `check-i18n-variables <project-path>...` — extracts translation keys and the variables the
|
|
16
|
+
code passes to them, then reports keys whose Locize value references a variable the code
|
|
17
|
+
does not pass.
|
|
18
|
+
- `check-i18next-icu-parser <namespace>...` — reports Locize keys that `i18next` cannot parse
|
|
19
|
+
as ICU.
|
|
20
|
+
- `check-missing-translations <namespace> [project-path]` — reports keys used in code but
|
|
21
|
+
missing from Locize, and keys present in Locize but untranslated. Needs `git`, `curl` and
|
|
22
|
+
`jq`.
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
- **Package manager**: pnpm (Node version pinned in `.nvmrc`)
|
|
27
|
+
- `pnpm build` — copies `src/` to `dist/` minus the tests; `dist/` is committed
|
|
28
|
+
- `pnpm test` — Vitest
|
|
29
|
+
- `pnpm coverage` — Vitest with coverage, gated at 70% (lines/branches/functions/statements)
|
|
30
|
+
- `pnpm lint` — ESLint
|
|
31
|
+
- `pnpm format` / `pnpm format-check` — Prettier
|
|
32
|
+
- `pnpm knip` — unused files, dependencies and exports
|
|
33
|
+
- CI runs all of the above on every PR, plus a check that `dist/` is up to date
|
|
14
34
|
|
|
15
35
|
## Release
|
|
16
36
|
|
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
// Checks all locize keys (in the codebase) with known variables against key
|
|
4
4
|
// values in locize ensuring that variables in locize refer to known vars.
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
KNOWN_VARIABLES_TAG
|
|
14
|
-
} = require('./translation-extraction.js')
|
|
15
|
-
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
6
|
+
import HttpBackend from 'i18next-http-backend'
|
|
7
|
+
import ICU from 'i18next-icu'
|
|
8
|
+
import i18next from 'i18next'
|
|
9
|
+
import {IntlMessageFormat} from 'intl-messageformat'
|
|
10
|
+
|
|
11
|
+
import {getFiles, filterJsFiles, supportedLngs} from './helpers.js'
|
|
12
|
+
import {extractTranslationKeysAndVariables, KNOWN_VARIABLES_TAG} from './translation-extraction.js'
|
|
16
13
|
|
|
17
14
|
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
18
15
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Checks all keys in Locize to make sure that they can be parsed by the `i18next` `t` function
|
|
4
4
|
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import HttpBackend from 'i18next-http-backend'
|
|
6
|
+
import ICU from 'i18next-icu'
|
|
7
|
+
import i18next from 'i18next'
|
|
8
|
+
|
|
9
|
+
import {supportedLngs} from './helpers.js'
|
|
9
10
|
|
|
10
11
|
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
11
12
|
|
package/dist/helpers.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const p = require('path')
|
|
1
|
+
import {promises as fs} from 'node:fs'
|
|
2
|
+
import p from 'node:path'
|
|
4
3
|
|
|
5
4
|
// Modified from https://dev.to/leonard/get-files-recursive-with-the-node-js-file-system-fs-2n7o
|
|
6
|
-
async function getFiles(path) {
|
|
5
|
+
export async function getFiles(path) {
|
|
7
6
|
const allEntries = await fs.readdir(path, {withFileTypes: true})
|
|
8
7
|
|
|
9
8
|
const entries = allEntries.filter(file => file.name !== 'node_modules' && file.name !== '.cache')
|
|
@@ -21,14 +20,8 @@ async function getFiles(path) {
|
|
|
21
20
|
return files
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const filterJsFiles = f =>
|
|
23
|
+
export const filterJsFiles = f =>
|
|
27
24
|
(f.name.endsWith('.js') || f.name.endsWith('.ts') || f.name.endsWith('.tsx')) &&
|
|
28
25
|
!f.name.endsWith('min.js')
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const supportedLngs = ['es', 'en', 'en-GB']
|
|
33
|
-
|
|
34
|
-
module.exports.supportedLngs = supportedLngs
|
|
27
|
+
export const supportedLngs = ['es', 'en', 'en-GB']
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import parser from 'flow-parser'
|
|
3
|
+
import walk from 'esprima-walk'
|
|
4
4
|
|
|
5
5
|
const isIdentifierT = node => node && node.type === 'Identifier' && node.name === 't'
|
|
6
6
|
|
|
@@ -24,8 +24,8 @@ const isObjectWithSimpleProperties = node =>
|
|
|
24
24
|
node.type === 'ObjectExpression' &&
|
|
25
25
|
node.properties.every(p => p.type === 'Property' && p.key.type === 'Identifier')
|
|
26
26
|
|
|
27
|
-
const KNOWN_VARIABLES_TAG = 'i18n-key-with-known-variables'
|
|
28
|
-
const UNKNOWN_VARIABLES_TAG = 'i18n-key-with-unknown-variables'
|
|
27
|
+
export const KNOWN_VARIABLES_TAG = 'i18n-key-with-known-variables'
|
|
28
|
+
export const UNKNOWN_VARIABLES_TAG = 'i18n-key-with-unknown-variables'
|
|
29
29
|
|
|
30
30
|
const knownVariables = (source, i18nKey, objectWithSimpleProperties) => ({
|
|
31
31
|
tag: KNOWN_VARIABLES_TAG,
|
|
@@ -47,9 +47,6 @@ const unknownVariables = (source, i18nKey) => ({
|
|
|
47
47
|
i18nKey
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
module.exports.KNOWN_VARIABLES_TAG = KNOWN_VARIABLES_TAG
|
|
51
|
-
module.exports.UNKNOWN_VARIABLES_TAG = UNKNOWN_VARIABLES_TAG
|
|
52
|
-
|
|
53
50
|
// Attempt to find all translations keys and their provided variables in use in JS files.
|
|
54
51
|
//
|
|
55
52
|
// Methodology:
|
|
@@ -67,7 +64,7 @@ module.exports.UNKNOWN_VARIABLES_TAG = UNKNOWN_VARIABLES_TAG
|
|
|
67
64
|
// are not known
|
|
68
65
|
//
|
|
69
66
|
// Yields keys with variables as encountered without sorting or de-duplication.
|
|
70
|
-
|
|
67
|
+
export const extractTranslationKeysAndVariables = path => {
|
|
71
68
|
const content = fs.readFileSync(path, {encoding: 'utf8'})
|
|
72
69
|
const ast = parser.parse(content)
|
|
73
70
|
|
package/dist/translation-keys.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
//
|
|
12
12
|
// We stream out the keys as seen. We do no sorting or de-duplication.
|
|
13
13
|
//
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import readline from 'node:readline'
|
|
15
|
+
|
|
16
|
+
import {extractTranslationKeysAndVariables} from './translation-extraction.js'
|
|
16
17
|
|
|
17
18
|
const lines = readline.createInterface({
|
|
18
19
|
input: process.stdin,
|
package/package.json
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freckle/i18n-scripts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Scripts for i18n",
|
|
5
5
|
"author": "Renaissance Learning Inc",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"bin": {
|
|
8
9
|
"check-i18n-variables": "./dist/check-i18n-variables.js",
|
|
9
10
|
"check-i18next-icu-parser": "./dist/check-i18next-icu-parser.js",
|
|
10
11
|
"check-missing-translations": "./dist/check-missing-translations"
|
|
11
12
|
},
|
|
12
|
-
"
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
18
|
"url": "git+https://github.com/freckle/i18n-scripts-js.git"
|
|
16
19
|
},
|
|
17
20
|
"scripts": {
|
|
18
|
-
"build": "rm -
|
|
19
|
-
"
|
|
20
|
-
"format
|
|
21
|
-
"
|
|
21
|
+
"build": "rm -rf dist && cp -r src dist && rm -f dist/*.test.js",
|
|
22
|
+
"coverage": "vitest run --coverage",
|
|
23
|
+
"format": "prettier --write 'src/**/*.js' '*.{js,mjs}'",
|
|
24
|
+
"format-check": "prettier --check 'src/**/*.js' '*.{js,mjs}'",
|
|
25
|
+
"knip": "knip",
|
|
26
|
+
"lint": "eslint",
|
|
27
|
+
"test": "vitest run"
|
|
22
28
|
},
|
|
23
29
|
"dependencies": {
|
|
24
30
|
"esprima-walk": "0.1.0",
|
|
25
|
-
"flow-parser": "0.
|
|
31
|
+
"flow-parser": "0.334.0",
|
|
26
32
|
"i18next": "26.4.2",
|
|
27
33
|
"i18next-http-backend": "^4.0.2",
|
|
28
34
|
"i18next-icu": "^2.4.4",
|
|
29
35
|
"intl-messageformat": "^12.1.2"
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
32
|
-
"@freckle/eslint-config": "^1.
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
38
|
+
"@freckle/eslint-config": "^1.3.0",
|
|
39
|
+
"@freckle/prettier-config": "^1.3.0",
|
|
40
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
41
|
+
"eslint": "^10.12.0",
|
|
42
|
+
"globals": "^17.13.0",
|
|
43
|
+
"knip": "^6.39.0",
|
|
44
|
+
"prettier": "^3.9.9",
|
|
45
|
+
"vitest": "^5.0.3"
|
|
36
46
|
},
|
|
37
|
-
"packageManager": "pnpm@12.
|
|
47
|
+
"packageManager": "pnpm@12.8.2+sha512.a5941679663d952c5f0ecc38ba98af98b4dc01b95780354f6894f2f873973cef2f7e2989d7db3ee5393938ae21f62fe06bcdf685d475ddad829a62095d2b8b11",
|
|
48
|
+
"prettier": "@freckle/prettier-config"
|
|
38
49
|
}
|
package/.github/CODEOWNERS
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* @freckle/team-student-activities
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
push:
|
|
6
|
-
branches: main
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v7
|
|
16
|
-
- uses: pnpm/action-setup@v6
|
|
17
|
-
- uses: actions/setup-node@v7
|
|
18
|
-
with:
|
|
19
|
-
node-version-file: .nvmrc
|
|
20
|
-
cache: pnpm
|
|
21
|
-
- run: pnpm install --frozen-lockfile
|
|
22
|
-
- run: pnpm format-check
|
|
23
|
-
- run: pnpm lint
|
|
24
|
-
- run: pnpm build
|
|
25
|
-
- uses: freckle/check-git-clean-action@v1
|
|
26
|
-
with:
|
|
27
|
-
hint: "Run pnpm build to update your dist/"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: Mergeabot
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
schedule:
|
|
5
|
-
- cron: "0 0 * * *"
|
|
6
|
-
|
|
7
|
-
pull_request:
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
pull-requests: write
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
mergeabot:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
steps:
|
|
17
|
-
- uses: freckle/mergeabot-action@v3
|
|
18
|
-
with:
|
|
19
|
-
quarantine-days: -1
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
- rc/*
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
release:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v7
|
|
17
|
-
- uses: cycjimmy/semantic-release-action@v6.0.0
|
|
18
|
-
# conventional-changelog-conventionalcommits is held at 9.x: v10 needs
|
|
19
|
-
# conventional-changelog-writer@9, which semantic-release@25 does not ship.
|
|
20
|
-
with:
|
|
21
|
-
extra_plugins: |
|
|
22
|
-
conventional-changelog-conventionalcommits@9
|
|
23
|
-
env:
|
|
24
|
-
FORCE_COLOR: 1
|
|
25
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
26
|
-
GITHUB_TOKEN: ${{ github.token }}
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
24.21.0
|
package/.prettierrc
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"printWidth": 100,
|
|
3
|
-
"tabWidth": 2,
|
|
4
|
-
"useTabs": false,
|
|
5
|
-
"semi": false,
|
|
6
|
-
"singleQuote": true,
|
|
7
|
-
"quoteProps": "as-needed",
|
|
8
|
-
"jsxSingleQuote": false,
|
|
9
|
-
"trailingComma": "none",
|
|
10
|
-
"bracketSpacing": false,
|
|
11
|
-
"bracketSameLine": false,
|
|
12
|
-
"arrowParens": "avoid",
|
|
13
|
-
"requirePragma": false,
|
|
14
|
-
"insertPragma": false,
|
|
15
|
-
"proseWrap": "preserve",
|
|
16
|
-
"htmlWhitespaceSensitivity": "css",
|
|
17
|
-
"endOfLine": "auto"
|
|
18
|
-
}
|
package/.releaserc.yaml
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# .releaserc.yaml
|
|
2
|
-
plugins:
|
|
3
|
-
- - '@semantic-release/commit-analyzer'
|
|
4
|
-
- preset: 'conventionalcommits'
|
|
5
|
-
- - '@semantic-release/release-notes-generator'
|
|
6
|
-
- preset: 'conventionalcommits'
|
|
7
|
-
- - '@semantic-release/github'
|
|
8
|
-
- successCommentCondition: false
|
|
9
|
-
|
|
10
|
-
- '@semantic-release/npm'
|
|
11
|
-
|
|
12
|
-
branches:
|
|
13
|
-
- main
|
|
14
|
-
- name: rc/*
|
|
15
|
-
prerelease: '${name.replace(/^rc\//, "rc-")}'
|
package/.restyled.yaml
DELETED
package/RELEASE.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Release
|
|
2
|
-
|
|
3
|
-
This project uses [semantic-release](https://github.com/semantic-release/semantic-release)
|
|
4
|
-
with [conventional commits](https://www.conventionalcommits.org/) to trigger releases.
|
|
5
|
-
|
|
6
|
-
To trigger a release in this project, merge a commit to `main` prefixed with:
|
|
7
|
-
|
|
8
|
-
1. `fix:` to trigger a patch release,
|
|
9
|
-
1. `feat:` to trigger minor, or
|
|
10
|
-
1. Use `<type>!:` or the `BREAKING CHANGES: <change>` footer to trigger major
|
|
11
|
-
|
|
12
|
-
Pre-releases can be made by pushing to an `rc/*` branch.
|
|
13
|
-
|
|
14
|
-
For more details, see the [Semantic Release](https://illuminate.atlassian.net/wiki/spaces/PENG/pages/17952735277/Semantic+Release) documentation.
|
package/eslint.config.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import freckle from '@freckle/eslint-config'
|
|
2
|
-
import globals from 'globals'
|
|
3
|
-
|
|
4
|
-
export default [
|
|
5
|
-
// The shared config scopes its rules to `**/*.ts`; this package is plain
|
|
6
|
-
// JavaScript, so without retargeting it would lint nothing.
|
|
7
|
-
...freckle.map(config => (config.files ? {...config, files: ['**/*.js']} : config)),
|
|
8
|
-
{
|
|
9
|
-
files: ['**/*.js'],
|
|
10
|
-
languageOptions: {
|
|
11
|
-
globals: globals.node
|
|
12
|
-
},
|
|
13
|
-
rules: {
|
|
14
|
-
// typescript-eslint turns this off because tsc reports undefined names;
|
|
15
|
-
// nothing type-checks this package, so it has to come back on.
|
|
16
|
-
'no-undef': 'error',
|
|
17
|
-
// The sources are CommonJS until the ESM conversion in #97, which drops
|
|
18
|
-
// this waiver.
|
|
19
|
-
'@typescript-eslint/no-require-imports': 'off'
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
]
|
package/renovate.json
DELETED
|
@@ -1,131 +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 i18next = require('i18next')
|
|
7
|
-
const ICU = require('i18next-icu')
|
|
8
|
-
const HttpBackend = require('i18next-http-backend')
|
|
9
|
-
const {IntlMessageFormat} = require('intl-messageformat')
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
extractTranslationKeysAndVariables,
|
|
13
|
-
KNOWN_VARIABLES_TAG
|
|
14
|
-
} = require('./translation-extraction.js')
|
|
15
|
-
const {getFiles, filterJsFiles, supportedLngs} = require('./helpers.js')
|
|
16
|
-
|
|
17
|
-
const [_nodeBin, _scriptPath, ...projects] = process.argv
|
|
18
|
-
|
|
19
|
-
if (projects.length < 1) {
|
|
20
|
-
console.error('Usage: check-i18n-variables <path>')
|
|
21
|
-
process.exit(64)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// TODO: Pass in via ENV?
|
|
25
|
-
const ns = ['school', 'student', 'console', 'classroom', 'common']
|
|
26
|
-
|
|
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,62 +0,0 @@
|
|
|
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')
|
|
9
|
-
|
|
10
|
-
const VALID_NAMESPACES = ['school', 'student', 'console', 'classroom', 'common']
|
|
11
|
-
|
|
12
|
-
const ns = process.argv.slice(2)
|
|
13
|
-
|
|
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
|
-
}
|
|
19
|
-
|
|
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
|
-
})
|
|
33
|
-
|
|
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()
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -eu
|
|
3
|
-
|
|
4
|
-
script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
|
|
6
|
-
translation_keys_in_code() {
|
|
7
|
-
local project_path=$1
|
|
8
|
-
|
|
9
|
-
git ls-files "$project_path" |
|
|
10
|
-
grep -E '\.(js|ts|tsx)$' |
|
|
11
|
-
grep -v '\.min\.js$' |
|
|
12
|
-
"$script_dir/translation-keys.js"
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
translation_keys_in_locize() {
|
|
16
|
-
local namespace=$1
|
|
17
|
-
|
|
18
|
-
curl --silent --fail --show-error \
|
|
19
|
-
"https://translations.freckle.com/latest/en/$namespace" |
|
|
20
|
-
jq --raw-output 'keys[]'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
empty_translation_keys_in_locize() {
|
|
24
|
-
local namespace=$1
|
|
25
|
-
|
|
26
|
-
curl --silent --fail --show-error \
|
|
27
|
-
"https://translations.freckle.com/latest/en/$namespace" |
|
|
28
|
-
jq --raw-output '[to_entries | .[] | select(.key | test("^[A-Z_]+$")) | select(.key == .value)] | .[] | .key' |
|
|
29
|
-
grep -xv 'OK' | grep -xv 'ELA' # Whitelist some common translation keys
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
translation_keys_only_in_code() {
|
|
33
|
-
local project_path=$1 namespace=$2
|
|
34
|
-
|
|
35
|
-
comm -23 \
|
|
36
|
-
<(translation_keys_in_code "$project_path" | sort -u) \
|
|
37
|
-
<(translation_keys_in_locize "$namespace" | sort -u)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
empty_translation_keys_used_in_locize() {
|
|
41
|
-
local project_path=$1 namespace=$2
|
|
42
|
-
|
|
43
|
-
comm -12 \
|
|
44
|
-
<(translation_keys_in_code "$project_path" | sort -u) \
|
|
45
|
-
<(empty_translation_keys_in_locize "$namespace" | sort -u)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (($# < 1)); then
|
|
49
|
-
echo "Usage: check-missing-translations namespace [project/path]" >&2
|
|
50
|
-
exit 64
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
namespace=${1}
|
|
54
|
-
project_path=${2-.}
|
|
55
|
-
|
|
56
|
-
tmp=$(mktemp)
|
|
57
|
-
trap 'rm -f -- "$tmp"' EXIT
|
|
58
|
-
|
|
59
|
-
# Check which keys are present in the frontend but not Locize
|
|
60
|
-
translation_keys_only_in_code "$project_path" "$namespace" | sed 's/$/ not present/' | tee "$tmp"
|
|
61
|
-
|
|
62
|
-
# Check which keys are present in both the frontend and Locize but are untranslated
|
|
63
|
-
empty_translation_keys_used_in_locize "$project_path" "$namespace" | sed 's/$/ not translated/' | tee -a "$tmp"
|
|
64
|
-
|
|
65
|
-
if [ -s "$tmp" ]; then
|
|
66
|
-
echo
|
|
67
|
-
echo "The above keys either need to be added to Locize or translated" >&2
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
package/src/helpers.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const {promises: fs} = require('fs')
|
|
3
|
-
const p = require('path')
|
|
4
|
-
|
|
5
|
-
// Modified from https://dev.to/leonard/get-files-recursive-with-the-node-js-file-system-fs-2n7o
|
|
6
|
-
async function getFiles(path) {
|
|
7
|
-
const allEntries = await fs.readdir(path, {withFileTypes: true})
|
|
8
|
-
|
|
9
|
-
const entries = allEntries.filter(file => file.name !== 'node_modules' && file.name !== '.cache')
|
|
10
|
-
|
|
11
|
-
const files = entries
|
|
12
|
-
.filter(file => !file.isDirectory())
|
|
13
|
-
.map(file => ({...file, path: p.join(path, file.name)}))
|
|
14
|
-
|
|
15
|
-
const folders = entries.filter(folder => folder.isDirectory())
|
|
16
|
-
|
|
17
|
-
for (const folder of folders) {
|
|
18
|
-
files.push(...(await getFiles(p.join(path, folder.name))))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return files
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
module.exports.getFiles = getFiles
|
|
25
|
-
|
|
26
|
-
const filterJsFiles = f =>
|
|
27
|
-
(f.name.endsWith('.js') || f.name.endsWith('.ts') || f.name.endsWith('.tsx')) &&
|
|
28
|
-
!f.name.endsWith('min.js')
|
|
29
|
-
|
|
30
|
-
module.exports.filterJsFiles = filterJsFiles
|
|
31
|
-
|
|
32
|
-
const supportedLngs = ['es', 'en', 'en-GB']
|
|
33
|
-
|
|
34
|
-
module.exports.supportedLngs = supportedLngs
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const parser = require('flow-parser')
|
|
3
|
-
const walk = require('esprima-walk')
|
|
4
|
-
|
|
5
|
-
const isIdentifierT = node => node && node.type === 'Identifier' && node.name === 't'
|
|
6
|
-
|
|
7
|
-
const isTransTag = node =>
|
|
8
|
-
node.type === 'JSXElement' &&
|
|
9
|
-
node.openingElement.type === 'JSXOpeningElement' &&
|
|
10
|
-
node.openingElement.name.name === 'Trans'
|
|
11
|
-
|
|
12
|
-
const isJsxAttribute = attr => node =>
|
|
13
|
-
node && node.type === 'JSXAttribute' && node.name.name === attr
|
|
14
|
-
|
|
15
|
-
const naiveLooksLikeApplication = (content, node, previousNode) =>
|
|
16
|
-
content.slice(previousNode.range[1], node.range[0]).trim() === '('
|
|
17
|
-
|
|
18
|
-
const looksLikeTApplication = (content, node, previousNode) =>
|
|
19
|
-
node.type === 'Literal' &&
|
|
20
|
-
isIdentifierT(previousNode) &&
|
|
21
|
-
naiveLooksLikeApplication(content, node, previousNode)
|
|
22
|
-
|
|
23
|
-
const isObjectWithSimpleProperties = node =>
|
|
24
|
-
node.type === 'ObjectExpression' &&
|
|
25
|
-
node.properties.every(p => p.type === 'Property' && p.key.type === 'Identifier')
|
|
26
|
-
|
|
27
|
-
const KNOWN_VARIABLES_TAG = 'i18n-key-with-known-variables'
|
|
28
|
-
const UNKNOWN_VARIABLES_TAG = 'i18n-key-with-unknown-variables'
|
|
29
|
-
|
|
30
|
-
const knownVariables = (source, i18nKey, objectWithSimpleProperties) => ({
|
|
31
|
-
tag: KNOWN_VARIABLES_TAG,
|
|
32
|
-
source,
|
|
33
|
-
i18nKey,
|
|
34
|
-
variables: objectWithSimpleProperties.properties.map(p => p.key.name)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const knownNoVariables = (source, i18nKey) => ({
|
|
38
|
-
tag: KNOWN_VARIABLES_TAG,
|
|
39
|
-
source,
|
|
40
|
-
i18nKey,
|
|
41
|
-
variables: []
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const unknownVariables = (source, i18nKey) => ({
|
|
45
|
-
tag: UNKNOWN_VARIABLES_TAG,
|
|
46
|
-
source,
|
|
47
|
-
i18nKey
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
module.exports.KNOWN_VARIABLES_TAG = KNOWN_VARIABLES_TAG
|
|
51
|
-
module.exports.UNKNOWN_VARIABLES_TAG = UNKNOWN_VARIABLES_TAG
|
|
52
|
-
|
|
53
|
-
// Attempt to find all translations keys and their provided variables in use in JS files.
|
|
54
|
-
//
|
|
55
|
-
// Methodology:
|
|
56
|
-
//
|
|
57
|
-
// Locate any
|
|
58
|
-
// - Literal nodes that immediately follow an Identifier node named t, and
|
|
59
|
-
// output their value, or
|
|
60
|
-
// - Trans component instances and output their i18nKey values
|
|
61
|
-
//
|
|
62
|
-
// If possible, output any variables (if possible) used by the keys. This is
|
|
63
|
-
// done with no further analysis than looking for a variable object and
|
|
64
|
-
// - if one's not found, report no variables
|
|
65
|
-
// - if one's found and is a simple object, report its properties
|
|
66
|
-
// - if one's found and is a more complex object, report that the variables
|
|
67
|
-
// are not known
|
|
68
|
-
//
|
|
69
|
-
// Yields keys with variables as encountered without sorting or de-duplication.
|
|
70
|
-
module.exports.extractTranslationKeysAndVariables = path => {
|
|
71
|
-
const content = fs.readFileSync(path, {encoding: 'utf8'})
|
|
72
|
-
const ast = parser.parse(content)
|
|
73
|
-
|
|
74
|
-
const results = []
|
|
75
|
-
let previousNode = null
|
|
76
|
-
|
|
77
|
-
// Possible `tag` values are
|
|
78
|
-
// - 'finding-i18n-key' which means we're trying to find an i18n key
|
|
79
|
-
// - 'found-t-application' which means we found a key like `t('KEY'` and we
|
|
80
|
-
// ought to check the next token to see if it's a simple object literal
|
|
81
|
-
let state = {tag: 'finding-i18n-key'}
|
|
82
|
-
|
|
83
|
-
walk(ast, node => {
|
|
84
|
-
switch (state.tag) {
|
|
85
|
-
case 'finding-i18n-key': {
|
|
86
|
-
if (looksLikeTApplication(content, node, previousNode)) {
|
|
87
|
-
state = {tag: 'found-t-application', key: node.value}
|
|
88
|
-
} else if (isTransTag(node)) {
|
|
89
|
-
const i18nKey = node.openingElement.attributes.find(isJsxAttribute('i18nKey'))
|
|
90
|
-
const values = node.openingElement.attributes.find(isJsxAttribute('values'))
|
|
91
|
-
const hasCount = node.openingElement.attributes.some(isJsxAttribute('count'))
|
|
92
|
-
|
|
93
|
-
// `Trans` keys can have a `count` property which ends up being a
|
|
94
|
-
// variable. If we have known variables we've gotta account for it...
|
|
95
|
-
const accountForCount = i18nKeyVithVariables => ({
|
|
96
|
-
...i18nKeyVithVariables,
|
|
97
|
-
variables: i18nKeyVithVariables.variables.concat(hasCount ? ['count'] : [])
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
i18nKey.value.value &&
|
|
102
|
-
values &&
|
|
103
|
-
values.value.type === 'JSXExpressionContainer' &&
|
|
104
|
-
isObjectWithSimpleProperties(values.value.expression)
|
|
105
|
-
) {
|
|
106
|
-
results.push(
|
|
107
|
-
accountForCount(
|
|
108
|
-
knownVariables('trans-component', i18nKey.value.value, values.value.expression)
|
|
109
|
-
)
|
|
110
|
-
)
|
|
111
|
-
} else if (i18nKey.value.value && !values) {
|
|
112
|
-
results.push(accountForCount(knownNoVariables('trans-component', i18nKey.value.value)))
|
|
113
|
-
} else if (i18nKey.value.value) {
|
|
114
|
-
results.push(unknownVariables('trans-component', i18nKey.value.value))
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
break
|
|
118
|
-
}
|
|
119
|
-
case 'found-t-application': {
|
|
120
|
-
if (isObjectWithSimpleProperties(node)) {
|
|
121
|
-
results.push(knownVariables('t-application', state.key, node))
|
|
122
|
-
} else if (typeof node === 'number') {
|
|
123
|
-
// No object after the key means no variables. For some reason this is an integer
|
|
124
|
-
results.push(knownNoVariables('t-application', state.key))
|
|
125
|
-
} else {
|
|
126
|
-
results.push(unknownVariables('t-application', state.key))
|
|
127
|
-
}
|
|
128
|
-
state = {tag: 'finding-i18n-key'}
|
|
129
|
-
break
|
|
130
|
-
}
|
|
131
|
-
case 'found-trans-component': {
|
|
132
|
-
state = {tag: 'finding-i18n-key'}
|
|
133
|
-
break
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
previousNode = node
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
return results
|
|
141
|
-
}
|
package/src/translation-keys.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// Attempt to find all translations keys in use in JS files passed on stdin.
|
|
4
|
-
//
|
|
5
|
-
// Usage:
|
|
6
|
-
//
|
|
7
|
-
// git ls-files |
|
|
8
|
-
// grep '\.js$' |
|
|
9
|
-
// grep -v '\.min\.js$' |
|
|
10
|
-
// ./scripts/translation-keys.js
|
|
11
|
-
//
|
|
12
|
-
// We stream out the keys as seen. We do no sorting or de-duplication.
|
|
13
|
-
//
|
|
14
|
-
const {extractTranslationKeysAndVariables} = require('./translation-extraction.js')
|
|
15
|
-
const readline = require('readline')
|
|
16
|
-
|
|
17
|
-
const lines = readline.createInterface({
|
|
18
|
-
input: process.stdin,
|
|
19
|
-
output: process.stdout,
|
|
20
|
-
terminal: false
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
lines.on('line', path => {
|
|
24
|
-
extractTranslationKeysAndVariables(path).forEach(({i18nKey}) => console.log(i18nKey))
|
|
25
|
-
})
|