Skip to content

Options

The command line and the API take the same options. Every CLI flag is an API option with the same name, resolved from one definition, so the two cannot drift apart and what passes in CI passes in your build script.

Option names are written here in their JavaScript spelling. Dart uses the same one, and Python is snake_case throughout, so ignoreChecks is ignore_checks and maxKeyDepth is max_key_depth. Getting started states that mapping once.

The full set

OptionCLI flagTypeDefault
path--pathstring
target--targetstring'en'
format--format'auto' | 'single' | 'folder' | 'nested''auto'
checks--checksstring[] or a comma separated stringall
ignoreChecks--ignore-checksstring[] or a comma separated stringnone
levels--levelsRecord<code, level> or CODE=level pairsnone
interpolationPrefix--interpolation-prefixstring'{'
interpolationSuffix--interpolation-suffixstring'}'
exclude--excludestring[] or a comma separated stringsee below
excludeFiles--exclude-filesstring[] or a comma separated stringsee below
source--sourcestring
translateFunctions--translate-functionsstring[] or a comma separated stringsee below
keyCase--key-case'kebab' | 'camel' | 'snake'
maxKeyDepth--max-key-depthnumber
lengthRatio--length-rationumber
reporter--reporter'pretty' | 'list' | 'json' | 'markdown' | 'github''pretty'
groupBy--group-by'locale' | 'code' | 'group' | 'file' | 'none''locale'
output--outputstring
color--no-colorbooleantrue
width--widthnumberthe terminal's
info--no-infobooleantrue
warn--no-warnbooleantrue
debug--debugbooleanfalse
flattenedbooleanfalse
verbosebooleanfalse

The last two are API-only: the CLI always prints, so verbose is set for you, and flattened describes data you pass in rather than a directory.

Dart takes all of them as one Chki18nOptions object built with named parameters, and the closed value sets are enums: Chki18nFileFormat.folder for 'folder', Chki18nCheckCode.noKey for 'NO_KEY'. The text forms a flag writes ('NO_KEY,EMPTY_VALUE', 'EMPTY_VALUE=error') live on Chki18nTextOptions, which Chki18nOptions.text carries, so no field has to accept two types.

Python takes all of them as one keyword-only Options object, and the closed value sets stay the strings they are everywhere else: format="folder", checks=["NO_KEY"], levels={"EMPTY_VALUE": "error"}. A list option also accepts the comma separated text a flag gives it.

Path and target

path

The directory holding the translation files. On the CLI it is also the bare positional argument, so these are the same:

bash
chki18n ./locales
chki18n --path ./locales

A relative path resolves against the current working directory. From code it is the first argument, and the path option is accepted too. The option wins when both are given, which is how the CLI passes its positional argument through.

target

The language every other language is compared against, which is the one you write first. It defaults to en, and a run that falls back to the default says so at info level instead of failing.

bash
chki18n ./locales --target ko

If the target language is not among the scanned files there is nothing to compare against, and that is an error rather than a silent pass.

Layout

format

Which on-disk layout to read. auto decides from the paths and is right nearly always. Force it when the detection guesses wrong, or when you want a mismatch to fail outright.

bash
chki18n ./locales --format folder

See File layouts for what each value means.

exclude

Directories to skip while scanning. Replaces the default list rather than adding to it:

text
node_modules  dist  build  out  coverage
.git  .next  .nuxt  .svelte-kit  .turbo  .cache
bash
chki18n . --exclude node_modules,dist,fixtures

The default list is exported as DEFAULT_EXCLUDE_DIRS if you would rather extend it than replace it:

javascript
import { DEFAULT_EXCLUDE_DIRS } from 'chki18n';

await checkTranslationFiles('.', { exclude: [...DEFAULT_EXCLUDE_DIRS, 'fixtures'] });
dart
import 'package:chki18n/chki18n.dart';

await checkTranslationFiles(
  path: '.',
  options: const Chki18nOptions(exclude: [...defaultExcludeDirs, 'fixtures']),
);
python
from chki18n import DEFAULT_EXCLUDE_DIRS, Options, check_translation_files

check_translation_files(".", Options(exclude=[*DEFAULT_EXCLUDE_DIRS, "fixtures"]))

An entry of one segment names a directory wherever it appears, so node_modules means every node_modules in the tree. An entry with a separator names a path from the scanned root, matching that directory and everything under it, so a project can drop its own src/legacy without dropping a legacy belonging to something else:

bash
chki18n . --exclude node_modules,src/legacy

Hidden entries, meaning anything starting with ., are always skipped whatever this is set to.

excludeFiles

File names never read as translations, as patterns where * stands for any run of characters and case is ignored. Replaces the default list rather than adding to it:

text
package.json  tsconfig.json  tsconfig.*.json  eslintrc.json
*-lock.json   *-config.json  *.config.json

These are the configuration and lock files an application root is full of. Skipping them is what makes it practical to point chki18n at that root rather than at a folder of locales: reading and parsing every one of them costs more than the comparison itself.

bash
chki18n . --exclude-files '*-lock.json,*.config.json,messages.json'

The default list is exported as DEFAULT_EXCLUDE_FILESdefaultExcludeFilesDEFAULT_EXCLUDE_FILES if you would rather extend it than replace it:

javascript
import { DEFAULT_EXCLUDE_FILES } from 'chki18n';

await checkTranslationFiles('.', { excludeFiles: [...DEFAULT_EXCLUDE_FILES, 'messages.json'] });
dart
import 'package:chki18n/chki18n.dart';

await checkTranslationFiles(
  path: '.',
  options: const Chki18nOptions(excludeFiles: [...defaultExcludeFiles, 'messages.json']),
);
python
from chki18n import DEFAULT_EXCLUDE_FILES, Options, check_translation_files

check_translation_files(".", Options(exclude_files=[*DEFAULT_EXCLUDE_FILES, "messages.json"]))

source

A directory of source files to search for key usages, which is what the UNUSED_KEY check needs. Without it that check reports nothing.

bash
chki18n ./locales --target en --source ./src

Only text files are read, anything over 5MB is skipped, and both exclude and excludeFiles apply here as well. The project's own translation files are never searched.

UNDEFINED_KEY uses the same directory for the opposite question: which keys the source calls for that no language file defines.

translateFunctions

The names a translation call goes by, which is how UNDEFINED_KEY finds the keys the source asks for. It replaces the default list rather than adding to it:

text
t  $t  translate

Those three cover i18next, react-i18next and vue-i18n between them, including i18n.t and a t bound by useTranslation, since a call is matched wherever its name ends. The i18nKey attribute a <Trans> component takes is always read.

bash
chki18n ./locales --source ./src --translate-functions t,trans,__

The default list is exported as TRANSLATION_FUNCTIONS if you would rather extend it than replace it.

Key and value limits

The three options below exist only to give a check something to compare against. Each one is off until it is set, because none of them has a right answer of its own, only the one your project chose.

keyCase

The case every segment of a key has to be written in, which is what KEY_NAMING compares against: kebab, camel or snake.

bash
chki18n ./locales --key-case kebab

The plural and context suffixes an i18n library appends, such as item-count_one and greeting_male, are accepted whatever case you chose.

maxKeyDepth

How many levels a key may be nested, for KEY_DEPTH. 2 allows attr.folder and reports attr.folder.name.

bash
chki18n ./locales --max-key-depth 2

lengthRatio

How many times longer or shorter than its original a value may be before SUSPICIOUS_LENGTH reports it. 4 allows a quarter to four times.

bash
chki18n ./locales --length-ratio 4

Lengths are counted in columns rather than characters, so a Korean or Japanese value is not short by default, and originals under eight columns are skipped.

Choosing checks

checks

Run only these. Accepts an array or a comma separated string, case-insensitively:

bash
chki18n ./locales --checks NO_KEY,NO_INTERPOLATION_KEY
javascript
await checkTranslationFiles('./locales', { checks: ['NO_KEY', 'NO_INTERPOLATION_KEY'] });
dart
await checkTranslationFiles(
  path: './locales',
  options: const Chki18nOptions(
    checks: [Chki18nCheckCode.noKey, Chki18nCheckCode.noInterpolationKey],
  ),
);
python
check_translation_files("./locales", Options(checks=["NO_KEY", "NO_INTERPOLATION_KEY"]))

ignoreChecks

Run everything except these:

bash
chki18n ./locales --ignore-checks DUPLICATE_VALUE

Combining the two is not allowed: checks wins, and an INVALID_OPTIONS issue says ignoreChecks was ignored. An unknown code is reported the same way and skipped instead of failing the run, so a typo in one flag does not stop the rest of the scan.

levels

Report a check at another severity. Accepts an object or CODE=level pairs:

bash
chki18n ./locales --levels EMPTY_VALUE=error,DUPLICATE_VALUE=info
javascript
await checkTranslationFiles('./locales', { levels: { EMPTY_VALUE: 'error' } });
dart
await checkTranslationFiles(
  path: './locales',
  options: const Chki18nOptions(
    levels: {Chki18nCheckCode.emptyValue: Chki18nLevel.error},
  ),
);
python
check_translation_files("./locales", Options(levels={"EMPTY_VALUE": "error"}))

Only comparison checks can be re-graded; INVALID_FILE and INVALID_OPTIONS report how the run itself went and keep their level.

Interpolation

interpolationPrefix / interpolationSuffix

The delimiters that mark a placeholder. The defaults are { and }, and , [[ ]] and %{ } are all common in real projects.

bash
chki18n ./locales --interpolation-prefix "{{" --interpolation-suffix "}}"

Getting this wrong leaves the placeholders unrecognised, so both interpolation checks find nothing and pass without a word.

Output

reporter

The shape of the report: pretty for a terminal, list for one line per issue, json for another program, markdown for a table, and github for the workflow commands GitHub Actions turns into annotations on the files themselves. Every reporter carries the same issues in the same order.

bash
chki18n ./locales --reporter json > report.json
javascript
import { formatResult, resolveOptions } from 'chki18n';

const { options } = resolveOptions({ target: 'en', reporter: 'markdown' });

formatResult(result, options); // the report as a string
dart
import 'package:chki18n/chki18n.dart';

final resolved = resolveOptions(
  const Chki18nOptions(target: 'en', reporter: Chki18nReporter.markdown),
);

formatResult(result, resolved.options); // the report as a string
python
from chki18n import Options, format_result, resolve_options

options, _ = resolve_options(Options(target="en", reporter="markdown"))

format_result(result, options)  # the report as a string

Anything other than pretty prints the report on its own, with no banner and no progress lines, so it can be piped straight into another program. An unknown name is reported as an INVALID_OPTIONS issue and falls back to pretty.

groupBy

What a section of the report is: locale (the default), code, group, file or none. Grouping by language matches how a translator works, and grouping by check matches how a maintainer fixes things.

bash
chki18n ./locales --group-by code

Sections with an error come first, then those with only warnings. The order is fixed for a given set of files, so two reports of the same translations can be compared line by line.

groupIssuesgroupIssuesgroup_issues is exported if you would rather do the grouping yourself:
javascript
import { groupIssues } from 'chki18n';

groupIssues(result.issues, 'locale'); // [{ id, label, issues, counts }, ...]
dart
import 'package:chki18n/chki18n.dart';

groupIssues(result.issues, Chki18nGroupBy.locale); // [Chki18nIssueGroup, ...]
python
from chki18n import group_issues

group_issues(result.issues, "locale")  # [IssueGroup(id=…, label=…, issues=…, counts=…), …]

output

A file to write the report to, in addition to the terminal. The extension picks the reporter, with .json and .md having one of their own and anything else written as plain text. reporter overrides it when both are given.

bash
chki18n ./locales --output report.md

Missing directories are created, colour codes are never written, and the layout uses a fixed width rather than the terminal's, so the same run produces the same file anywhere. A write that fails is reported as an error and fails the run.

color

Whether to colour the terminal report. On by default where the terminal supports it, and --no-color turns it off. A file written by output is never coloured, whatever this says.

width

Columns to lay the report out to. Without it the terminal's own width is used, then COLUMNS, then 96. A measured width is capped at 120, since past that the counts stop reading as part of the same line as their label. What width asks for is not capped.

bash
chki18n ./locales --width 72

Descriptions wrap instead of being cut short, so a narrow report loses no wording. A file written by output ignores the terminal and uses the fixed default unless width says otherwise, so the same run produces the same file anywhere.

info, warn, debug

What the CLI prints. --no-info drops the heading block and the summary, --no-warn drops warning level issues, and --debug adds the resolved options, the detected layout and every file that was skipped.

bash
chki18n ./locales --no-info

These affect printing only. A suppressed warning is still in result.issues, still counted in result.summary, and still present in the json report, which is the whole result rather than a rendering of it. When something is hidden, the report says how many.

--debug writes to standard error, so it never lands in a report piped out of standard output.

verbose

API-only. The library prints nothing unless this is set, so importing it cannot pollute a host application's output. The CLI sets it for you.

javascript
await checkTranslationFiles('./locales', { verbose: true });
dart
await checkTranslationFiles(
  path: './locales',
  options: const Chki18nOptions(verbose: true),
);
python
check_translation_files("./locales", Options(verbose=True))

Data

flattened

API-only. Says the translations you are passing in already use flat keys ('desc.hello') rather than nested objects, so the flatten pass is skipped and the objects you passed are read directly:

javascript
analyzeTranslations({ locales: { en, ko } }, { target: 'en', flattened: true });
dart
analyzeTranslations(
  Chki18nInput(locales: {'en': en, 'ko': ko}),
  options: const Chki18nOptions(target: 'en', flattened: true),
);
python
analyze_translations(Input(locales={"en": en, "ko": ko}), Options(target="en", flattened=True))

Setting it when the data is actually nested does not error. It compares the top-level keys and finds very little.

Resolving options yourself

resolveOptionsresolveOptionsresolve_options applies the defaults, normalises the loose forms, and reports what it could not use instead of raising:
javascript
import { argsToOptions, resolveOptions } from 'chki18n';

const { options, issues } = resolveOptions({ target: 'ko', ignoreChecks: 'NO_KEY' });

options.enabledChecks; // Set of the codes that will run
issues; // anything unusable, as INVALID_OPTIONS issues

// The CLI form resolves to exactly the same thing
resolveOptions(argsToOptions({ _: [], target: 'ko', 'ignore-checks': 'NO_KEY' }));
dart
import 'package:chki18n/chki18n.dart';

final resolved = resolveOptions(
  const Chki18nOptions(target: 'ko', ignoreChecks: [Chki18nCheckCode.noKey]),
);

resolved.options.enabledChecks; // Set of the codes that will run
resolved.issues; // anything unusable, as INVALID_OPTIONS issues

// The CLI form resolves to exactly the same thing
resolveOptions(optionsFromArgs({'_': <String>[], 'target': 'ko', 'ignore-checks': 'NO_KEY'}));
python
from chki18n import Options, options_from_args, resolve_options

options, issues = resolve_options(Options(target="ko", ignore_checks="NO_KEY"))

options.enabled_checks  # frozenset of the codes that will run
issues  # anything unusable, as INVALID_OPTIONS issues

# The CLI form resolves to exactly the same thing
resolve_options(options_from_args({"_": [], "target": "ko", "ignore-checks": "NO_KEY"}))
OPTION_DEFINITIONSoptionDefinitionsOPTION_DEFINITIONS is the table both sides are built from, if you are generating a UI or a help text of your own.

Released under the MIT License