├── .github └── workflows │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets ├── i18n-check-screenshot-full.png ├── i18n-check-screenshot-summary.png └── i18n-check-workflow-example.png ├── eslint.config.mjs ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── bin │ ├── index.test.ts │ └── index.ts ├── errorReporters.test.ts ├── errorReporters.ts ├── index.ts ├── types.ts └── utils │ ├── findInvalidTranslations.test.ts │ ├── findInvalidTranslations.ts │ ├── findInvalidi18nTranslations.test.ts │ ├── findInvalidi18nTranslations.ts │ ├── findMissingKeys.test.ts │ ├── findMissingKeys.ts │ ├── flattenTranslations.test.ts │ ├── flattenTranslations.ts │ ├── i18NextParser.test.ts │ ├── i18NextParser.ts │ ├── nextIntlSrcParser.test.ts │ └── nextIntlSrcParser.ts ├── translations ├── codeExamples │ ├── next-intl │ │ ├── locales │ │ │ ├── en │ │ │ │ └── translation.json │ │ │ └── fr │ │ │ │ └── translation.json │ │ └── src │ │ │ ├── AdvancedExample.tsx │ │ │ ├── AsyncExample.tsx │ │ │ ├── Basic.tsx │ │ │ ├── ClientCounter.tsx │ │ │ ├── Counter.tsx │ │ │ ├── DynamicKeysExample.tsx │ │ │ ├── NestedExample.tsx │ │ │ └── StrictTypesExample.tsx │ ├── react-intl │ │ ├── locales │ │ │ ├── de-DE │ │ │ │ ├── one.json │ │ │ │ ├── three.json │ │ │ │ └── two.json │ │ │ └── en-US │ │ │ │ ├── one.json │ │ │ │ ├── three.json │ │ │ │ └── two.json │ │ └── src │ │ │ ├── App.tsx │ │ │ └── Content.tsx │ └── reacti18next │ │ ├── locales │ │ ├── en │ │ │ ├── more.json │ │ │ └── translation.json │ │ └── fr │ │ │ ├── more.json │ │ │ └── translation.json │ │ ├── secondSrcFolder │ │ └── Main.tsx │ │ └── src │ │ ├── App.tsx │ │ ├── Content.tsx │ │ └── basic.ts ├── de-de.json ├── en-us.json ├── flattenExamples │ ├── de-de.json │ ├── en-us.json │ └── fr-fr.json ├── folderExample │ ├── de-DE │ │ └── index.json │ └── en-US │ │ └── index.json ├── i18NextMessageExamples │ ├── de-de.json │ └── en-us.json ├── largeFileExamples │ ├── de-de.json │ ├── en-us.json │ └── fr-fr.json ├── messageExamples │ ├── de-de.json │ └── en-us.json ├── multipleFilesFolderExample │ ├── de-DE │ │ ├── one.json │ │ ├── three.json │ │ └── two.json │ └── en-US │ │ ├── one.json │ │ ├── three.json │ │ └── two.json ├── multipleFoldersExample │ ├── spaceOne │ │ └── locales │ │ │ ├── de-DE │ │ │ ├── one.json │ │ │ ├── three.json │ │ │ └── two.json │ │ │ └── en-US │ │ │ ├── one.json │ │ │ ├── three.json │ │ │ └── two.json │ └── spaceTwo │ │ └── locales │ │ ├── de-DE │ │ ├── one.json │ │ ├── three.json │ │ └── two.json │ │ └── en-US │ │ ├── one.json │ │ ├── three.json │ │ └── two.json └── yaml │ ├── basic │ ├── de-de.yaml │ └── en-us.yaml │ ├── flattenExamples │ ├── de-de.yaml │ ├── en-us.yaml │ └── fr-fr.yaml │ ├── folderExample │ ├── de-DE │ │ └── index.yaml │ └── en-US │ │ └── index.yaml │ ├── messageExamples │ ├── de-de.yaml │ └── en-us.yaml │ ├── multipleFilesFolderExample │ ├── de-DE │ │ ├── one.yaml │ │ ├── three.yaml │ │ └── two.yaml │ └── en-US │ │ ├── one.yaml │ │ ├── three.yaml │ │ └── two.yaml │ └── multipleFoldersExample │ ├── spaceOne │ └── locales │ │ ├── de-DE │ │ ├── one.yaml │ │ ├── three.yaml │ │ └── two.yaml │ │ └── en-US │ │ ├── one.yaml │ │ ├── three.yaml │ │ └── two.yaml │ └── spaceTwo │ └── locales │ ├── de-DE │ ├── one.yaml │ ├── three.yaml │ └── two.yaml │ └── en-US │ ├── one.yaml │ ├── three.yaml │ └── two.yaml └── tsconfig.json /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: pnpm/action-setup@v4 13 | 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '20.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | cache: 'pnpm' 19 | - name: Install dependencies and build 20 | run: pnpm install --frozen-lockfile && pnpm run build 21 | - name: Publish package to NPM 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | run: npm publish --access public 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20.x' 19 | cache: 'pnpm' 20 | - name: pnpm install & lint 21 | run: | 22 | pnpm install --frozen-lockfile 23 | pnpm lint 24 | 25 | test: 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest, windows-latest] 29 | 30 | runs-on: ${{ matrix.os }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: pnpm/action-setup@v4 35 | 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: '20.x' 39 | cache: 'pnpm' 40 | - name: pnpm install & test 41 | run: | 42 | pnpm install --frozen-lockfile 43 | pnpm test 44 | 45 | test-cli: 46 | strategy: 47 | matrix: 48 | os: [ubuntu-latest, windows-latest] 49 | 50 | runs-on: ${{ matrix.os }} 51 | 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: pnpm/action-setup@v4 55 | 56 | - uses: actions/setup-node@v4 57 | with: 58 | node-version: '20.x' 59 | cache: 'pnpm' 60 | - name: pnpm install & test 61 | run: | 62 | pnpm install --frozen-lockfile 63 | pnpm test:cli 64 | 65 | test-build: 66 | strategy: 67 | matrix: 68 | os: [ubuntu-latest, windows-latest] 69 | 70 | runs-on: ${{ matrix.os }} 71 | 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: pnpm/action-setup@v4 75 | 76 | - uses: actions/setup-node@v4 77 | with: 78 | node-version: '20.x' 79 | cache: 'pnpm' 80 | 81 | - name: pnpm install & build 82 | run: | 83 | pnpm install --frozen-lockfile 84 | pnpm build 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | yarn.lock 9 | 10 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 lingual 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lingual i18n-check 2 | 3 | **i18n-check** validates your [ICU](https://github.com/unicode-org/icu) and [i18next](https://www.i18next.com/) translation files and checks for missing and broken translations. 4 | It compares the defined source language with all target translation files and finds inconsistencies between source and target files. 5 | You can run these checks as a pre-commit hook or on the CI depending on your use-case and setup. 6 | 7 | ![example 1](./assets/i18n-check-screenshot-full.png) 8 | 9 | ![example 2](./assets/i18n-check-screenshot-summary.png) 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [General Usage](#general-usage) 15 | - [CLI Options](#options) 16 | - [Examples](#examples) 17 | - [Single folder](#single-folder) 18 | - [Folder per locale](#folder-per-locale) 19 | - [Folder per locale with multiple files](#folder-per-locale-with-multiple-files) 20 | - [Github Action](#as-github-action) 21 | - [API](#api) 22 | - [Development](#development) 23 | - [Links](#links) 24 | 25 | ## Installation 26 | 27 | Using **yarn**: 28 | 29 | ```bash 30 | yarn add --dev @lingual/i18n-check 31 | ``` 32 | 33 | Using **npm**: 34 | 35 | ```bash 36 | npm install --save-dev @lingual/i18n-check 37 | ``` 38 | 39 | Using **pnpm**: 40 | 41 | ```bash 42 | pnpm add --save-dev @lingual/i18n-check 43 | ``` 44 | 45 | After the installation, i18n-check can either be accessed via defining a command in the `package.json` file or directly in the CLI. 46 | 47 | Update your `package.json` and add a new command: 48 | 49 | ```bash 50 | "scripts": { 51 | // ...other commands, 52 | "i18n:check": "i18n-check" 53 | } 54 | ``` 55 | 56 | Now you can run the `i18n:check` command directly from the command-line, i.e. `yarn i18n:check`. 57 | 58 | Alternatively you can also access the library directly: 59 | 60 | ```bash 61 | node_modules/.bin/i18n-check 62 | ``` 63 | 64 | ## General Usage 65 | 66 | For i18n-check to work you need to provide it at a minimum the source locale (`--source, -s`) for the primary language and the path to the locale translation files (`--locales, -l`). Currently supported file formats are JSON and YAML. 67 | 68 | Example: 69 | 70 | ```bash 71 | yarn i18n:check -s en-US --locales translations/ 72 | ``` 73 | 74 | Instead of a single source file you can also pass a directory: 75 | 76 | ```bash 77 | yarn i18n:check -s en-US --locales translations/ 78 | ``` 79 | 80 | See the [examples](#examples) for more details. 81 | 82 | ## Options 83 | 84 | ### --locales, -l 85 | 86 | With the `-l` or `--locales` option you define which folder or multiple folders you want to run the i18n checks against. It is a **required** option. i18n-check will try to find all target locale files and compare these files against the defined source file(s). 87 | Check the [example](#examples) to see how different locale translation files are organised and how they can be addressed. 88 | 89 | ```bash 90 | yarn i18n:check --locales translations/messageExamples -s en-US 91 | ``` 92 | 93 | ### --source, -s 94 | 95 | With the `-s` or `--source` option you define the source locale to compare all target files against. It is a **required** option. i18n-check will try to find all target locale files and compare these files against the applicable source file(s). 96 | Check the [examples](#examples) to see how different locale translation files are organised and how they can be addressed. 97 | 98 | ```bash 99 | yarn i18n:check --locales translations/messageExamples -s en-US 100 | ``` 101 | 102 | ### --format, -f 103 | 104 | By default i18n-check will validate against any [ICU](https://github.com/unicode-org/icu) compliant translations. 105 | Additionally the `i18next` format is supported and can be set via the `-f` or `--format` option. 106 | 107 | There are i18n libraries that have their own specific format, which might not be based on ICU and therefore can not be validated against currently. On a side-note: there might be future support for more specific formats. 108 | 109 | Hint: If you want to use the `--unused` flag, you should provide `react-intl` or `i18-next` as the format. Also see the [`unused` section](#--unused) for more details. 110 | 111 | ```bash 112 | yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next 113 | ``` 114 | 115 | ### --only, -o 116 | 117 | By default i18n-check will perform a validation against any **missing** and/or **invalid** keys, additionally **unused** and **undefined** checks if the `--unused` option is set. There are situations where only a specific check should run. By using the `-o` or `--only` option you can specify a specific check to run. 118 | 119 | The available options are: 120 | 121 | - `missingKeys`: will check against any missing keys in the target files. 122 | - `invalidKeys`: will check for invalid keys, where the target translations has a different type then the one defined in the source file. 123 | - `unused`: will check for any locale keys that do not exist in the codebase. 124 | - `undefined`: will check for any keys that exist in the codebase but not in the source locale files. 125 | 126 | Check for missing keys only: 127 | 128 | ```bash 129 | yarn i18n:check --locales translations/messageExamples -s en-US -o missingKeys 130 | ``` 131 | 132 | Check for invalid keys only: 133 | 134 | ```bash 135 | yarn i18n:check --locales translations/messageExamples -s en-US -o invalidKeys 136 | ``` 137 | 138 | Check for unused key only: 139 | 140 | ```bash 141 | yarn i18n:check --locales translations/messageExamples -s en-US -o unused 142 | ``` 143 | 144 | Check for undefined keys only: 145 | 146 | ```bash 147 | yarn i18n:check --locales translations/messageExamples -s en-US -o undefined 148 | ``` 149 | 150 | Check for missing and invalid keys (which is the default): 151 | 152 | ```bash 153 | yarn i18n:check --locales translations/messageExamples -s en-US -o missingKeys invalidKeys 154 | ``` 155 | 156 | Check for unused and undefined keys only: 157 | 158 | ```bash 159 | yarn i18n:check --locales translations/messageExamples -s en-US -o unused undefined 160 | ``` 161 | 162 | ### --unused, -u 163 | 164 | This feature is currently only supported for `react-intl` and `i18next` as well as `next-intl` (experimental at the moment) based React applications and is useful when you need to know which keys exist in your translation files but not in your codebase. Additionally an inverse check is run to find any keys that exist in the codebase but not in the translation files. 165 | 166 | Via the `-u` or `--unused` option you provide a source path to the code, which will be parsed to find all unused as well as undefined keys in the primary target language. 167 | 168 | It is important to note that you must also provide the `-f` or `--format` option with `react-intl`, `i18next` or `next-intl` as value. See the [`format` section](#--format) for more information. 169 | 170 | ```bash 171 | yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f react-intl 172 | ``` 173 | 174 | or 175 | 176 | ```bash 177 | yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f i18next 178 | ``` 179 | 180 | or 181 | 182 | ```bash 183 | yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f next-intl 184 | ``` 185 | 186 | ### --reporter, -r 187 | 188 | The standard reporting prints out all the missing or invalid keys. 189 | Using the `-r` or `--reporter` option enables to override the standard error reporting. Passing the `summary` option will print a summary of the missing or invalid keys. 190 | 191 | ```bash 192 | yarn i18n:check --locales translations/messageExamples -s en-US -r summary 193 | ``` 194 | 195 | ### --exclude, -e 196 | 197 | There are situations where we want to exclude a single or multiple files or a single folder or a group of folders. A typical scenario would be that some keys are missing in a specific folder, as they are being work in progress for example. To exclude this or these files/folders you can use the `-e` or `--exclude` option. It expects one or more files and/or folders. 198 | 199 | To exclude a single file: 200 | 201 | ```bash 202 | yarn i18n:check --locales translations/messageExamples -s en-US -e translations/messageExamples/fr-fr.json 203 | ``` 204 | 205 | To exclude multiple files provide all files: 206 | 207 | ```bash 208 | yarn i18n:check --locales translations/messageExamples -s en-US -e translations/messageExamples/fr-fr.json translations/messageExamples/de-at.json 209 | ``` 210 | 211 | To exclude a single folder: 212 | 213 | ```bash 214 | yarn i18n:check --locales translations/folderExamples -s en-US -e translations/folderExamples/fr/* 215 | ``` 216 | 217 | Alternatively you can exclude multiple folders by providing the folders to be excluded: 218 | 219 | ```bash 220 | yarn i18n:check --locales translations/folderExamples -s en-US -e translations/folderExamples/fr/* translations/folderExample/it/* 221 | ``` 222 | 223 | The `--exclude` option also accepts a mix of files and folders, which follows the same pattern as above, i.e. 224 | `-e translations/folderExamples/fr/* translations/messageExamples/it.json` 225 | 226 | ### --ignore, -i 227 | 228 | There can be situations where we only want to translate a feature for a specific region and therefore need to ignore any missing key checks against non supported locales. Another scenario is that we know of the missing keys and want to be able to skip these missing keys when running checks. For these aforementioned scenarios, by using the `--ignore` or `-i` option you can specify which keys to ignore, additionally also being able to define ignoring all keys inside a defined path, i.e. `some.keys.path.*`. 229 | 230 | To ignore regular keys: 231 | 232 | ```bash 233 | yarn i18n:check --locales translations/folderExamples -s en-US -i some.key.to.ignore other.key.to.ignore 234 | ``` 235 | 236 | To ignore all keys within a provided path: 237 | 238 | ```bash 239 | yarn i18n:check --locales translations/folderExamples -s en-US -i "some.path.to.keys.*" 240 | ``` 241 | 242 | A mix of regular keys and paths: 243 | 244 | ```bash 245 | yarn i18n:check --locales translations/folderExamples -s en-US -i "some.path.to.keys.*" some.key.to.ignore other.key.to.ignore 246 | ``` 247 | 248 | ### --parser-component-functions 249 | 250 | When using the `--unused` option, there will be situations where the i18next-parser will not be able to find components that wrap a `Trans` component.The component names for i18next-parser to match should be provided via the `--parser-component-functions` option. This option should onlybe used to define additional names for matching, a by default `Trans` will always be matched. 251 | 252 | ```bash 253 | yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next 254 | -u src --parser-component-functions WrappedTransComponent 255 | ``` 256 | 257 | ```bash 258 | yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next 259 | -u src --parser-component-functions WrappedTransComponent AnotherWrappedTransComponent 260 | ``` 261 | 262 | ## Examples 263 | 264 | i18n-check is able to load and validate against different locale folder structures. Depending on how the locale files are organized, there are different configuration options. 265 | 266 | ### Single folder 267 | 268 | If all the locales are organized in a **single folder**: 269 | 270 | ``` 271 | locales/ 272 | en-en.json 273 | de-de.json 274 | ``` 275 | 276 | Use the `-l` or `--locales` option to define the directory that should be checked for target files. With the `s` or `source` option you can specify the base/reference file to compare the target files against. 277 | 278 | ```bash 279 | yarn i18n:check --locales locales -s locales/en-us.json 280 | ``` 281 | 282 | ### Folder per locale 283 | 284 | If the locales are **organised as folders** containing a single JSON/YAML file: 285 | 286 | ``` 287 | locales/ 288 | en-US/ 289 | index.json 290 | de-DE/ 291 | index.json 292 | ``` 293 | 294 | Define the `locales` folder as the directory to look for target files. 295 | 296 | ```bash 297 | yarn i18n:check --locales locales -s en-US 298 | ``` 299 | 300 | ### Folder per locale with multiple files 301 | 302 | If the locales are **organised as folders** containing multiple JSON/YAML files: 303 | 304 | ``` 305 | locales/ 306 | en-US/ 307 | one.json 308 | two.json 309 | three.json 310 | de-DE/ 311 | one.json 312 | two.json 313 | three.json 314 | ``` 315 | 316 | Define the `locales` folder as the directory to look for target files and pass `locales/en-US/` as the `source` option. i18n-check will try to collect all the files in the provided source directory and compare each one against the corresponding files in the target locales. 317 | 318 | ```bash 319 | yarn i18n:check --locales locales -s en-US 320 | ``` 321 | 322 | #### Multiple folders containing locales 323 | 324 | If the locales are **organised as folders** containing multiple JSON/YAML files: 325 | 326 | ``` 327 | - spaceOne 328 | - locales/ 329 | - en-US/ 330 | - one.json 331 | - two.json 332 | - three.json 333 | - de-DE/ 334 | - one.json 335 | - two.json 336 | - three.json 337 | - spaceTwo 338 | - locales/ 339 | - en-US/ 340 | - one.json 341 | - two.json 342 | - three.json 343 | - de-DE/ 344 | - one.json 345 | - two.json 346 | - three.json 347 | ``` 348 | 349 | Define the `locales` folder as the directory to look for target files and pass `en-US` as the `source` option. i18n-check will try to collect all the files in the provided source directory and compare each one against the corresponding files in the target locales. 350 | 351 | ```bash 352 | yarn i18n:check -l spaceOne spaceTwo -s en-US 353 | ``` 354 | 355 | ## As Github Action 356 | 357 | We currently do not offer an explicit **Github Action** you can use out of the box, but if you have i18n-check already installed, you can define your own **YAML** file. The following example can be seen as a starting point that you can adapt to your current setup: 358 | 359 | ```yml 360 | name: i18n Check 361 | on: 362 | pull_request: 363 | branches: 364 | - main 365 | push: 366 | branches: 367 | - main 368 | 369 | jobs: 370 | i18n-check: 371 | runs-on: ubuntu-latest 372 | 373 | steps: 374 | - uses: actions/checkout@master 375 | 376 | - name: yarn install & build 377 | run: | 378 | yarn install 379 | yarn build 380 | 381 | - name: yarn i18n-check 382 | run: | 383 | yarn i18n-check --locales translations/messageExamples --source en-US 384 | ``` 385 | 386 | The above workflow will return any missing or invalid keys and the action would fail if missing/invalid keys are found: 387 | 388 | ![i18n-check Github workflow example out](./assets/i18n-check-workflow-example.png) 389 | 390 | ## API 391 | 392 | Aside from using the CLI, i18n-check also exposes a set of check functions that can be accessed programmatically. 393 | Start by importing i18n-check: 394 | 395 | ```ts 396 | import * as i18nCheck from '@lingual/i18n-check'; 397 | ``` 398 | 399 | ### `i18nCheck.checkTranslations(source, targets [, options])` 400 | 401 | `checkTranslations` expects the base and comparison or target files and returns an object containing the missing and invalid keys. The optional `options` objects can be provided as a third argument to define the format style via the `format` property, this is useful if you want to validate `i18next` specific translations. 402 | 403 | ```ts 404 | import { checkTranslations } from '@lingual/i18n-check'; 405 | 406 | const options = { 407 | format: 'i18next', 408 | }; 409 | 410 | const { invalidKeys, missingKeys } = checkTranslations( 411 | source, 412 | targets, 413 | options 414 | ); 415 | ``` 416 | 417 | Additionally the `options` object enables to also define which checks should run via the `checks` property, f.e. if you only want to check for missing or invalid keys only. 418 | 419 | ```ts 420 | import { checkTranslations } from '@lingual/i18n-check'; 421 | 422 | const options = { 423 | format: 'icu', 424 | checks: ['invalidKeys'], 425 | }; 426 | 427 | const { invalidKeys } = checkTranslations(source, targets, options); 428 | ``` 429 | 430 | Calling `checkTranslation` will return the following shape: 431 | 432 | ```ts 433 | export type CheckResult = Record; 434 | 435 | type Result = { 436 | missingKeys: CheckResult | undefined; 437 | invalidKeys: CheckResult | undefined; 438 | }; 439 | ``` 440 | 441 | The result for `missingKeys` as well as `invalidKeys` is an object containing the provided locales and their corresponding affected keys as an array 442 | 443 | ```ts 444 | { 445 | missingKeys: 446 | { 447 | "de-de": ["missing_example_key", "some_other_key"], 448 | "fr-fr": [], 449 | } 450 | }; 451 | ``` 452 | 453 | ### `i18nCheck.checkMissingTranslations(source, targets)` 454 | 455 | `checkMissingTranslations` checks for any missing keys in the target files. All files are compared against the source file. 456 | 457 | ```ts 458 | import { checkMissingTranslations } from '@lingual/i18n-check'; 459 | 460 | const result = checkMissingTranslations(source, targets); 461 | 462 | // { 463 | // "de-de": ["missing_translation_key", "some_other_missing_translation_key"], 464 | // "fr-fr": [], 465 | // }; 466 | ``` 467 | 468 | The result is an object containing the provided locales and their corresponding missing keys as an array. 469 | 470 | ### `i18nCheck.checkInvalidTranslations(source, targets [, options])` 471 | 472 | `checkInvalidTranslations` checks if there are any invalid keys in the target files. All files are compared against the source file. 473 | 474 | ```ts 475 | import { checkInvalidTranslations } from '@lingual/i18n-check'; 476 | 477 | const options = { 478 | format: 'i18next', 479 | }; 480 | 481 | const result = checkInvalidTranslations(source, targets, options); 482 | 483 | // { 484 | // "de-de": ["invalid_translation_key", "some_other_invalid_translation_key"], 485 | // "fr-fr": [], 486 | // }; 487 | ``` 488 | 489 | The result is an object containing the provided locales and their corresponding invalid keys as an array. 490 | 491 | ## Development 492 | 493 | If you want to checkout and run the code, you need to run the `build` command first. 494 | 495 | Run `pnpm run build` and then depending on the scenario one of the following commands. 496 | 497 | Basic icu translation example: 498 | 499 | ```bash 500 | node dist/bin/index.js --locales translations/messageExamples -s en-US 501 | ``` 502 | 503 | Flatted translation keys example: 504 | 505 | ```bash 506 | node dist/bin/index.js --locales translations/flattenExamples -s en-US 507 | ``` 508 | 509 | i18next translation example: 510 | 511 | ```bash 512 | node dist/bin/index.js --locales translations/i18NextMessageExamples -s en-US -f i18next 513 | ``` 514 | 515 | Single file translation example: 516 | 517 | ```bash 518 | node dist/bin/index.js --locales translations/folderExample -s en-US 519 | ``` 520 | 521 | Multiple files per folder translation example: 522 | 523 | ```bash 524 | node dist/bin/index.js --locales translations/multipleFilesFolderExample/ -s en-US 525 | ``` 526 | 527 | Multiple folders containing locales translation example: 528 | 529 | ```bash 530 | node dist/bin/index.js --locales translations/folderExample,translations/messageExamples -s en-US 531 | ``` 532 | 533 | ### Tests 534 | 535 | To run the tests use one of the following commands: 536 | 537 | ```bash 538 | pnpm test 539 | ``` 540 | 541 | ## Links 542 | 543 | - [Introducing i18n-check](https://lingual.dev/blog/introducing-i18n-check/) 544 | - [Twitter](https://twitter.com/lingualdev) 545 | -------------------------------------------------------------------------------- /assets/i18n-check-screenshot-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingualdev/i18n-check/5c2e5591dc5f5a3fc6655ee85327b2cc9d499369/assets/i18n-check-screenshot-full.png -------------------------------------------------------------------------------- /assets/i18n-check-screenshot-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingualdev/i18n-check/5c2e5591dc5f5a3fc6655ee85327b2cc9d499369/assets/i18n-check-screenshot-summary.png -------------------------------------------------------------------------------- /assets/i18n-check-workflow-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingualdev/i18n-check/5c2e5591dc5f5a3fc6655ee85327b2cc9d499369/assets/i18n-check-workflow-example.png -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import tseslint from 'typescript-eslint'; 4 | import { globalIgnores, defineConfig } from 'eslint/config'; 5 | 6 | export default defineConfig([ 7 | { 8 | files: ['**/*.{js,mjs,cjs,ts}'], 9 | plugins: { js }, 10 | extends: ['js/recommended'], 11 | rules: { 12 | '@typescript-eslint/no-require-imports': [ 13 | 'error', 14 | { allow: ['\\.json$'] }, 15 | ], 16 | '@typescript-eslint/no-unused-vars': [ 17 | 'error', 18 | { 19 | argsIgnorePattern: '^_', 20 | varsIgnorePattern: '^_', 21 | caughtErrorsIgnorePattern: '^_', 22 | }, 23 | ], 24 | }, 25 | }, 26 | { 27 | files: ['**/*.{js,mjs,cjs,ts}'], 28 | languageOptions: { globals: globals.browser }, 29 | }, 30 | globalIgnores(['translations/**/*']), 31 | tseslint.configs.recommended, 32 | ]); 33 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testPathIgnorePatterns: ['dist'], 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lingual/i18n-check", 3 | "version": "0.8.4", 4 | "description": "i18n translation messages check", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.js", 8 | "bin": { 9 | "i18n-check": "dist/bin/index.js" 10 | }, 11 | "types": "dist/index.d.ts", 12 | "scripts": { 13 | "build": "tsc", 14 | "format": "prettier --write .", 15 | "lint": "eslint src", 16 | "lint:fix": "eslint src --fix ", 17 | "check-format": "prettier --check './{src,translations}/**/*.{js,jsx,ts,tsx,json,html,css}'", 18 | "test": "jest src/ --testPathIgnorePatterns src/bin/*", 19 | "test:cli": "tsc && jest src/bin/index.test.ts" 20 | }, 21 | "files": [ 22 | "dist/" 23 | ], 24 | "dependencies": { 25 | "@formatjs/cli-lib": "^6.6.6", 26 | "@formatjs/icu-messageformat-parser": "^2.11.2", 27 | "chalk": "^4.1.2", 28 | "commander": "^12.1.0", 29 | "glob": "^11.0.2", 30 | "i18next-parser": "^9.3.0", 31 | "js-yaml": "^4.1.0", 32 | "typescript": "^5.8.3" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.26.0", 36 | "@types/jest": "^29.5.14", 37 | "@types/js-yaml": "^4.0.9", 38 | "@types/node": "^22.14.1", 39 | "@types/vinyl": "^2.0.12", 40 | "braces": "^3.0.3", 41 | "eslint": "^9.26.0", 42 | "globals": "^16.0.0", 43 | "jest": "^29.7.0", 44 | "prettier": "^3.5.3", 45 | "ts-jest": "^29.3.2", 46 | "typescript-eslint": "^8.31.1" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/lingualdev/i18n-check.git" 51 | }, 52 | "engines": { 53 | "node": ">=20" 54 | }, 55 | "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" 56 | } 57 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | overrides: 2 | "glob": "11.0.2" 3 | "rimraf": "6.0.1" 4 | -------------------------------------------------------------------------------- /src/bin/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import fs from 'node:fs'; 4 | import { exit } from 'node:process'; 5 | import chalk from 'chalk'; 6 | import { program } from 'commander'; 7 | import { glob, globSync } from 'glob'; 8 | import yaml from 'js-yaml'; 9 | import { checkTranslations, checkUndefinedKeys, checkUnusedKeys } from '..'; 10 | import { 11 | CheckOptions, 12 | Context, 13 | formatCheckResultTable, 14 | formatInvalidTranslationsResultTable, 15 | formatSummaryTable, 16 | } from '../errorReporters'; 17 | import { 18 | CheckResult, 19 | FileInfo, 20 | InvalidTranslationsResult, 21 | TranslationFile, 22 | } from '../types'; 23 | import { flattenTranslations } from '../utils/flattenTranslations'; 24 | import path from 'node:path'; 25 | 26 | const version = require('../../package.json').version; 27 | 28 | program 29 | .version(version) 30 | .option( 31 | '-l, --locales ', 32 | 'name of the directory containing the locales to validate' 33 | ) 34 | .option('-s, --source ', 'the source locale to validate against') 35 | .option( 36 | '-f, --format ', 37 | 'define the specific format: i18next, react-intl or next-intl' 38 | ) 39 | .option( 40 | '-c, --check ', 41 | 'this option is deprecated - use -o or --only instead' 42 | ) 43 | .option( 44 | '-o, --only ', 45 | 'define the specific checks you want to run: invalidKeys, missingKeys, unused, undefined. By default the check will validate against missing and invalid keys, i.e. --only invalidKeys,missingKeys' 46 | ) 47 | .option( 48 | '-r, --reporter