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
| Option | CLI flag | Type | Default |
|---|---|---|---|
path | --path | string | — |
target | --target | string | 'en' |
format | --format | 'auto' | 'single' | 'folder' | 'nested' | 'auto' |
checks | --checks | string[] or a comma separated string | all |
ignoreChecks | --ignore-checks | string[] or a comma separated string | none |
levels | --levels | Record<code, level> or CODE=level pairs | none |
interpolationPrefix | --interpolation-prefix | string | '{' |
interpolationSuffix | --interpolation-suffix | string | '}' |
exclude | --exclude | string[] or a comma separated string | see below |
excludeFiles | --exclude-files | string[] or a comma separated string | see below |
source | --source | string | — |
translateFunctions | --translate-functions | string[] or a comma separated string | see below |
keyCase | --key-case | 'kebab' | 'camel' | 'snake' | — |
maxKeyDepth | --max-key-depth | number | — |
lengthRatio | --length-ratio | number | — |
reporter | --reporter | 'pretty' | 'list' | 'json' | 'markdown' | 'github' | 'pretty' |
groupBy | --group-by | 'locale' | 'code' | 'group' | 'file' | 'none' | 'locale' |
output | --output | string | — |
color | --no-color | boolean | true |
width | --width | number | the terminal's |
info | --no-info | boolean | true |
warn | --no-warn | boolean | true |
debug | --debug | boolean | false |
flattened | — | boolean | false |
verbose | — | boolean | false |
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:
chki18n ./locales
chki18n --path ./localesA 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.
chki18n ./locales --target koIf 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.
chki18n ./locales --format folderSee File layouts for what each value means.
exclude
Directories to skip while scanning. Replaces the default list rather than adding to it:
node_modules dist build out coverage
.git .next .nuxt .svelte-kit .turbo .cachechki18n . --exclude node_modules,dist,fixturesThe default list is exported as DEFAULT_EXCLUDE_DIRS if you would rather extend it than replace it:
import { DEFAULT_EXCLUDE_DIRS } from 'chki18n';
await checkTranslationFiles('.', { exclude: [...DEFAULT_EXCLUDE_DIRS, 'fixtures'] });import 'package:chki18n/chki18n.dart';
await checkTranslationFiles(
path: '.',
options: const Chki18nOptions(exclude: [...defaultExcludeDirs, 'fixtures']),
);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:
chki18n . --exclude node_modules,src/legacyHidden 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:
package.json tsconfig.json tsconfig.*.json eslintrc.json
*-lock.json *-config.json *.config.jsonThese 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.
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:
import { DEFAULT_EXCLUDE_FILES } from 'chki18n';
await checkTranslationFiles('.', { excludeFiles: [...DEFAULT_EXCLUDE_FILES, 'messages.json'] });import 'package:chki18n/chki18n.dart';
await checkTranslationFiles(
path: '.',
options: const Chki18nOptions(excludeFiles: [...defaultExcludeFiles, 'messages.json']),
);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.
chki18n ./locales --target en --source ./srcOnly 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:
t $t translateThose 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.
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.
chki18n ./locales --key-case kebabThe 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.
chki18n ./locales --max-key-depth 2lengthRatio
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.
chki18n ./locales --length-ratio 4Lengths 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:
chki18n ./locales --checks NO_KEY,NO_INTERPOLATION_KEYawait checkTranslationFiles('./locales', { checks: ['NO_KEY', 'NO_INTERPOLATION_KEY'] });await checkTranslationFiles(
path: './locales',
options: const Chki18nOptions(
checks: [Chki18nCheckCode.noKey, Chki18nCheckCode.noInterpolationKey],
),
);check_translation_files("./locales", Options(checks=["NO_KEY", "NO_INTERPOLATION_KEY"]))ignoreChecks
Run everything except these:
chki18n ./locales --ignore-checks DUPLICATE_VALUECombining 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:
chki18n ./locales --levels EMPTY_VALUE=error,DUPLICATE_VALUE=infoawait checkTranslationFiles('./locales', { levels: { EMPTY_VALUE: 'error' } });await checkTranslationFiles(
path: './locales',
options: const Chki18nOptions(
levels: {Chki18nCheckCode.emptyValue: Chki18nLevel.error},
),
);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.
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.
chki18n ./locales --reporter json > report.jsonimport { formatResult, resolveOptions } from 'chki18n';
const { options } = resolveOptions({ target: 'en', reporter: 'markdown' });
formatResult(result, options); // the report as a stringimport 'package:chki18n/chki18n.dart';
final resolved = resolveOptions(
const Chki18nOptions(target: 'en', reporter: Chki18nReporter.markdown),
);
formatResult(result, resolved.options); // the report as a stringfrom chki18n import Options, format_result, resolve_options
options, _ = resolve_options(Options(target="en", reporter="markdown"))
format_result(result, options) # the report as a stringAnything 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.
chki18n ./locales --group-by codeSections 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:import { groupIssues } from 'chki18n';
groupIssues(result.issues, 'locale'); // [{ id, label, issues, counts }, ...]import 'package:chki18n/chki18n.dart';
groupIssues(result.issues, Chki18nGroupBy.locale); // [Chki18nIssueGroup, ...]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.
chki18n ./locales --output report.mdMissing 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.
chki18n ./locales --width 72Descriptions 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.
chki18n ./locales --no-infoThese 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.
await checkTranslationFiles('./locales', { verbose: true });await checkTranslationFiles(
path: './locales',
options: const Chki18nOptions(verbose: true),
);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:
analyzeTranslations({ locales: { en, ko } }, { target: 'en', flattened: true });analyzeTranslations(
Chki18nInput(locales: {'en': en, 'ko': ko}),
options: const Chki18nOptions(target: 'en', flattened: true),
);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: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' }));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'}));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.