@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 CHANGED
@@ -10,7 +10,27 @@ pnpm add @freckle/i18n-scripts
10
10
 
11
11
  ## Usage
12
12
 
13
- TODO
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
- 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')
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
- const i18next = require('i18next')
6
- const ICU = require('i18next-icu')
7
- const HttpBackend = require('i18next-http-backend')
8
- const {supportedLngs} = require('./helpers.js')
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
- #!/usr/bin/env node
2
- const {promises: fs} = require('fs')
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
- module.exports.getFiles = getFiles
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
- module.exports.filterJsFiles = filterJsFiles
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
- const fs = require('fs')
2
- const parser = require('flow-parser')
3
- const walk = require('esprima-walk')
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
- module.exports.extractTranslationKeysAndVariables = path => {
67
+ export const extractTranslationKeysAndVariables = path => {
71
68
  const content = fs.readFileSync(path, {encoding: 'utf8'})
72
69
  const ast = parser.parse(content)
73
70
 
@@ -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
- const {extractTranslationKeysAndVariables} = require('./translation-extraction.js')
15
- const readline = require('readline')
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": "2.0.0",
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
- "main": "./dist/index.js",
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 -r dist && cp -r src dist",
19
- "format": "prettier --write 'src/**/*.js'",
20
- "format-check": "prettier --check 'src/**/*.js'",
21
- "lint": "eslint"
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.333.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.1.0",
33
- "eslint": "^10.11.0",
34
- "globals": "^17.12.0",
35
- "prettier": "^3.9.9"
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.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec"
47
+ "packageManager": "pnpm@12.8.2+sha512.a5941679663d952c5f0ecc38ba98af98b4dc01b95780354f6894f2f873973cef2f7e2989d7db3ee5393938ae21f62fe06bcdf685d475ddad829a62095d2b8b11",
48
+ "prettier": "@freckle/prettier-config"
38
49
  }
@@ -1 +0,0 @@
1
- * @freckle/team-student-activities
@@ -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
@@ -1,12 +0,0 @@
1
- restylers:
2
- - clang-format:
3
- enabled: false
4
- - jq:
5
- enabled: false # prefer prettier-json
6
- - prettier:
7
- include:
8
- - 'src/**/*.js'
9
- - whitespace:
10
- include:
11
- - '**/*'
12
- - '!dist/*'
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,6 +0,0 @@
1
- {
2
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": [
4
- "local>freckle/renovate-config"
5
- ]
6
- }
@@ -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
- }
@@ -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
- })