Changelog
Every release of chki18n, newest first. Each package versions on its own, so a release on one side is not a release on the others.
Released as chki18n on npm.
1.2.0 (2026-09-05)
A scan can be pointed at an application's root rather than at a folder of locales. It skips the configuration and lock files that root is full of, an exclude can name a path rather than a bare directory name, and it reports which interpolation delimiters the files it read appear to use.
Added
excludeFilesnames the files a scan never reads, as patterns where*stands for any run of characters and case is ignored. It defaults to the configuration and lock files no project keeps translations in —package.json,tsconfig.json,tsconfig.*.json,eslintrc.json,*-lock.json,*-config.jsonand*.config.json— which a scan of an application root used to read and parse in full on every run. The default list is exported asDEFAULT_EXCLUDE_FILES, and--exclude-filesis the flagdetectInterpolationDelimitersguesses which delimiters a text writes its interpolation keys with:before{name}, and[[ ]],(( ))and<< >>after them.Chki18nScanResult.detectedInterpolationreports what a whole scan saw, whichloadTranslationscarries assession.detectedInterpolation. It is a suggestion for a project being set up, never what the run compared with — that staysinterpolationPrefix. The pairs it knows are exported in order asINTERPOLATION_DELIMITERScreatePathExcluderandcreateFileExcluderbuild the two tests a scan applies, so an application that shows a user which folders are excluded can ask the same question the scan asks
Changed
excludeaccepts a path as well as a name. One segment still names a directory at any depth, sonode_modulesmeans everynode_modulesthere is; an entry with a separator names a path from the scanned root, sosrc/legacyexcludes that folder and everything under it without touching alegacybelonging to something else- Both exclude lists apply to the source tree
sourcenames as well as to the translation directory
1.1.0 (2026-09-04)
The target language is checked too. Everything else is compared against it, so it sat outside every check, and a mistake typed into the source language stayed there however often the files were checked.
Changed
- The target language is now checked by everything that reads one value on its own:
EMPTY_VALUE,SURROUNDING_WHITESPACE,INVISIBLE_CHARACTER,INVALID_VALUE_TYPEandUNTRANSLATED_SCRIPT. It was skipped outright before, so an empty string, a trailing space or a zero width character inen.jsonwas never reported. The comparison checks still say nothing about it, since it is what they compare against. None of the five reports aterror, so a run that passed still passes; switch one off withignoreChecksthe way you would for any other language - An issue about the target language's own value carries no
targetValue. What a report quotes beside a finding is the value it would be compared to, and for the target language that is the value already shown
Fixed
- The
githubreporter writes the annotation'sfile=path with forward slashes on every platform. On a Windows runner it wrote the platform's own separator, which GitHub matches against nothing, so the annotation silently attached to no file
1.0.0 (2026-09-03)
The first stable release. The checks no longer depend on the file system, so they run against translations an application already holds in memory as well as against a folder of files; the report was rebuilt around what a reader is looking for, and can be written as JSON, Markdown or GitHub annotations; and the comparison grew to twenty-five checks.
Breaking changes
- A project that passed on
0.3.0can fail on this release. Four of the checks added report aterror—NO_LOCALE,DUPLICATE_KEY,EXTRA_INTERPOLATION_KEYandINTERPOLATION_COUNT— andEMPTY_VALUEandDUMMY_KEYwere fixed to report at all. Everything else added reports atwarnorinfo, which never fails a run. Switch off what a project does not want withignoreChecks, or--ignore-checkson the command line - The CLI report was rebuilt around what the reader is looking for: a heading block naming what was scanned, one section per language with its own tally, each check's meaning printed once above its findings, and a summary that counts the axis the sections did not use. Columns are laid out by display width, so a Korean, Japanese or Chinese value no longer pushes the ones beside it out of line. Nothing should read the text of that report — anything that used to parse standard output wants
--reporter json --no-infodrops the heading block and the summary rather than only the progress lines, and--debugwrites to standard error so a report piped out of standard output stays parseable- The CLI moved to its own entry point (
dist/cli.js), which thechki18nbinary points at. Importing the module has no side effect and never writes to the console or exits the process, so an import that ran the CLI has to callcheckTranslationFilesinstead - The result now carries every issue, whether the run passed or failed, as a flat
issueslist plusissuesByCodeand asummarywith per-level, per-code, per-locale and per-group counts. Each issue has its ownlevel,messageand originatingfile. Code that read the old shape has to move to these fields
Added
analyzeTranslationscompares translations passed in directly, with no file system workcreateAnalyzerreturns a reusable analyzer whosecheckEntryre-checks a single keyloadTranslationsreads a directory once and returns a session that holds the parsed translations:analyze,checkKey,get,set,remove,keys,translationsandreloadall work on what is already in memory.createSessionis the same for translations passed in directlyEXTRA_INTERPOLATION_KEY,SURROUNDING_WHITESPACE,MISSING_NUMBERandINVALID_VALUE_TYPEchecksNO_LOCALE, for a group of files that holds nothing for a language the other groups have. Until now a translation file nobody created dropped out of the comparison entirely and the run passedINTERPOLATION_COUNT, for a placeholder used a different number of times than the target language uses it. The two interpolation checks compare which placeholders a value has, so{name} invited {name}translated with one{name}passed bothTAG_MISMATCH, for markup a translation dropped, added or left unbalanced. Tags are counted rather than looked for, and read case-insensitivelyUNTRANSLATED_SCRIPT, for a value holding no character of the script its language is written in.NOT_TRANSLATED_VALUEonly catches a translation identical to the original, soHellobecomingHello!passed itINVISIBLE_CHARACTER, for a zero width space, a byte order mark, a bidirectional control or a non-breaking spaceNUMBER_MISMATCH, for a number the translation changed rather than dropped.MISSING_NUMBERonly asks whether any digits survivedINCONSISTENT_VALUE, for two keys sharing one target language string that a locale translates two different ways.DUPLICATE_VALUEasks whether one locale repeats itself; this asks the oppositeKEY_NAMINGandKEY_DEPTH, for the shape of a key rather than what it translates to. Both wait forkeyCaseandmaxKeyDepthto say what the project wants, and are judged once per key rather than once per localekeyCaseandmaxKeyDepth, the options those two compare against.keyCaseaccepts the plural and context suffixes an i18n library appends, whatever case the project usesUNDEFINED_KEY, the reverse ofUNUSED_KEYand the more serious of the two: the scanned source calls for a key and no language file defines it. Reads the calls it finds undersource, and lets through a key built at run time, one reached through a bound prefix, and a plural key asked for by its basetranslateFunctions, the names a translation call goes by. The default covers i18next, react-i18next and vue-i18nNO_PLURAL_FORM, for a plural key missing a form its language needs. Which forms a language needs is a fact about the language rather than about the original: English writes two, Russian four, Korean one. The older i18next pairing of a bare key with_pluralis left as ordinary keys, and a language the table does not cover is never judgedSUSPICIOUS_LENGTH, reported atinfo, for a value far longer or shorter than the one it translates. Waits forlengthRatio, and measures columns rather than characters so a Korean or Japanese value is not short by defaultlengthRatio, the option it compares againstcheckKeyShapeanddisplayWidthare exported, so an editor can judge a key the user is still typing and lay a value out the way the report doesextractTags,extractNumbers,findInvisibleCharacter,scriptOfLocaleandhasTranslatableTextare exported, so an editor can run the same measurements on a value it is holdingDUPLICATE_KEY, which catches a key defined twice — both the literal kind ({"a": 1, "a": 2}, whichJSON.parseresolves silently) and the kind where a nested key and a dotted one flatten onto each otherUNUSED_KEY, reported atinfo, for keys nothing in asourcedirectory appears to reference. An application that has already worked this out can pass its own answer asunusedKeysinsteadsource, the directory the unused-key scan searches- Support for the folder-per-locale (
en/common.json) and single-file-per-project ({ "en": ... }) layouts, alongside the existing one-file-per-locale layout. Files holding the same keys are compared as a group, so several translation files no longer share one pile of keys Chki18nInputacceptsissuesandfileFormat, so whatever produced the input can report its own problems into the same resultchki18n/core, a subpath that exports the comparison engine on its own. It imports no Node built-in, so it bundles for a browser or an editor's renderer processchecks,ignoreChecks,levels,format,excludeand the interpolation delimiter options, available both as CLI flags and as JavaScript options--helpand--versionreporter, which decides the shape of the report:prettyfor a terminal,listfor one line per issue,jsonfor another program to read,markdownfor a table, andgithubfor the workflow commands GitHub Actions turns into annotations on the translation files themselves. Everything butprettyprints the report alone, with no banner, so it can be piped straight into something elsegroupBy, the axis the report groups its issues by:locale(the default),code,group,fileornoneoutput, a file the report is written to as well as the terminal. The extension picks the format, an explicitreporteroverrides it, missing directories are created, and a write that fails is reported as an error and fails the runcolor, and--no-colorwith it. A file written byoutputis never colouredwidth, the column count the report is laid out to. Without it the terminal's own width is used, thenCOLUMNS, which is where a CI runner reports its log widthformatResultandgroupIssuesare exported, so an application can render a result the way the CLI does, along withdisplayWidth,padToandtruncatefor laying out columns of its own
Changed
CHECK_METAdescribes each check's severity and wording, so a user interface does not have to hard-code them- A description that does not fit the width wraps onto the next line instead of being cut short, so a narrow terminal loses no wording
NO_KEYandDUMMY_KEYno longer ask every language for every plural form. Korean needs onlyitem_other, soitem_onebeing absent from it was reported as missing and is not; Russian needs anitem_fewthat English never writes, and that was reported as a stray key. Applies only to keys ending in a named plural category, and only to the languages the plural table coversUNUSED_KEYsearches for a plural key by its base. No source file writesitem_one, so every plural key in a project was reported as unused- The package now publishes
types, so TypeScript consumers get the result shape - Analysis is linear in the number of keys rather than quadratic: comparing 5,000 keys across 5 locales went from about 10.8s to about 26ms
- The twelve checks added since cost about a third as much again as the original twelve over the same 5,000 keys, and a check that is switched off costs nothing at all.
lengthRatiois the exception worth knowing: measuring every value in display columns more than doubles the run, which is part of why it stays off until it is asked for
Fixed
--no-warnhid the heading of a warning but still printed the lines under it. A suppressed issue is now left out entirely, and the report says how many it hidEMPTY_VALUEandDUMMY_KEYwere written so their conditions could never be true, and never reported anything- A missing target language file threw instead of being reported
- A relative path was joined as if it were absolute, so a scan rooted at one found nothing. Paths now resolve against the working directory
- An explicit
pathoption was overridden by the (absent) first argument, which is how the CLI passes it
0.1.0 ~ 0.3.0 (2026-04-14 ~ 2026-04-17) (Beta)
This is for the Beta release. It can be used in production, but some features are still under development.
- Cleanup and refactoring (0.1.0)
- Improved error handling (0.1.0)
- Fix and cleanup codes (0.2.0)
- Add
NO_INTERPOLATION_KEYcheck (0.2.0) - Base JS function support (0.3.0)
0.0.1 ~ 0.0.5 (2026-04-14) (Alpha)
This is for the Alpha release and is not recommended for use
- Initial release
Released as chki18n on pub.dev.
1.2.0 (2026-09-05)
A scan can be pointed at an application's root rather than at a folder of locales. It skips the configuration and lock files that root is full of, an exclude can name a path rather than a bare directory name, and it reports which interpolation delimiters the files it read appear to use.
Added
excludeFilesnames the files a scan never reads, as patterns where*stands for any run of characters and case is ignored. It defaults to the configuration and lock files no project keeps translations in —package.json,tsconfig.json,tsconfig.*.json,eslintrc.json,*-lock.json,*-config.jsonand*.config.json— which a scan of an application root used to read and parse in full on every run. The default list is exported asdefaultExcludeFiles, and--exclude-filesis the flagdetectInterpolationDelimitersguesses which delimiters a text writes its interpolation keys with:before{name}, and[[ ]],(( ))and<< >>after them.Chki18nScanResult.detectedInterpolationreports what a whole scan saw, whichloadTranslationscarries assession.detectedInterpolation. It is a suggestion for a project being set up, never what the run compared with — that staysinterpolationPrefix. The pairs it knows are exported in order asinterpolationDelimiterscreatePathExcluderandcreateFileExcluderbuild the two tests a scan applies, so an application that shows a user which folders are excluded can ask the same question the scan asks
Changed
excludeaccepts a path as well as a name. One segment still names a directory at any depth, sonode_modulesmeans everynode_modulesthere is; an entry with a separator names a path from the scanned root, sosrc/legacyexcludes that folder and everything under it without touching alegacybelonging to something else- Both exclude lists apply to the source tree
sourcenames as well as to the translation directory
1.1.0 (2026-09-04)
The target language is checked too. Everything else is compared against it, so it sat outside every check, and a mistake typed into the source language stayed there however often the files were checked.
Changed
- The target language is now checked by everything that reads one value on its own:
EMPTY_VALUE,SURROUNDING_WHITESPACE,INVISIBLE_CHARACTER,INVALID_VALUE_TYPEandUNTRANSLATED_SCRIPT. It was skipped outright before, so an empty string, a trailing space or a zero width character inen.jsonwas never reported. The comparison checks still say nothing about it, since it is what they compare against. None of the five reports aterror, so a run that passed still passes; switch one off withignoreChecksthe way you would for any other language - An issue about the target language's own value carries no
targetValue. What a report quotes beside a finding is the value it would be compared to, and for the target language that is the value already shown
Fixed
- The
githubreporter writes the annotation'sfile=path with forward slashes on every platform. On a Windows runner it wrote the platform's own separator, which GitHub matches against nothing, so the annotation silently attached to no file
1.0.0 (2026-09-03)
The first release. A port of the JavaScript package at the same version: the same twenty-five checks, the same option names, the same report, and the same exit code from the command line.
Added
checkTranslationFilesreads a directory of translation files and compares every language against the target language, in one callanalyzeTranslationscompares translations passed in directly, with no file system workcreateAnalyzerreturns a reusable analyzer whosecheckEntryre-checks a single keyloadTranslationsreads a directory once and returns a session that holds the parsed translations:analyze,checkKey,get,set,remove,keys,translationsandreloadall work on what is already in memory.createSessionis the same for translations passed in directly- Twenty-five checks.
NO_KEY,NO_LOCALE,DUPLICATE_KEY,NO_INTERPOLATION_KEY,EXTRA_INTERPOLATION_KEYandINTERPOLATION_COUNTreport aterrorand fail a run;DUMMY_KEY,EMPTY_VALUE,INVALID_VALUE_TYPE,TAG_MISMATCH,NOT_TRANSLATED_VALUE,UNTRANSLATED_SCRIPT,DUPLICATE_VALUE,INCONSISTENT_VALUE,SURROUNDING_WHITESPACE,INVISIBLE_CHARACTER,MISSING_NUMBER,NUMBER_MISMATCH,NO_PLURAL_FORM,KEY_NAMING,KEY_DEPTHandUNDEFINED_KEYatwarn;UNUSED_KEYandSUSPICIOUS_LENGTHatinfo - Every on-disk layout: one file per locale (
en.json), one folder per locale (en/common.json), and one file holding every locale ({"en": ...}). Files holding the same keys are compared as a group - Five reporters —
pretty,list,json,markdownandgithub— and five grouping axes:locale,code,group,fileandnone.outputwrites a copy to a file, in the shape its extension implies - The
chki18ncommand, which takes the same options as the library and exits with1when anerrorlevel issue was found. Install it withdart pub global activate chki18n package:chki18n/core.dart, the comparison on its own. It imports nodart:io, so it runs in a Flutter web build or anywhere else the file system is not there
Notes on the port
- Options are one object with named parameters —
Chki18nOptions(target: 'en')— rather than the twenty-four named parameters each entry point would otherwise carry. Every field is optional and every one has the JavaScript default - Enums replace the string unions:
Chki18nCheckCode.noKey,Chki18nLevel.error,Chki18nFileFormat.single. Each carries the spelling the CLI and the JSON reporter share, soChki18nCheckCode.noKey.codeisNO_KEYin every package - The loose text forms a command line writes —
'NO_KEY,EMPTY_VALUE','EMPTY_VALUE=error'— live onChki18nTextOptionsrather than being a second accepted type on every field.resolveOptionsreads both, and a typed field wins over the text one - A translation file whose JSON is not an object is reported as
INVALID_FILErather than read as an empty one - No dependencies. The argument parser, the ANSI colours and the path helpers are written out, because
args,chalkandpackage:pathwould be the only three
Released as chki18n on PyPI.
1.2.0 (2026-09-05)
A scan can be pointed at an application's root rather than at a folder of locales. It skips the configuration and lock files that root is full of, an exclude can name a path rather than a bare directory name, and it reports which interpolation delimiters the files it read appear to use.
Added
exclude_filesnames the files a scan never reads, as patterns where*stands for any run of characters and case is ignored. It defaults to the configuration and lock files no project keeps translations in —package.json,tsconfig.json,tsconfig.*.json,eslintrc.json,*-lock.json,*-config.jsonand*.config.json— which a scan of an application root used to read and parse in full on every run. The default list is exported asDEFAULT_EXCLUDE_FILES, and--exclude-filesis the flagdetect_interpolation_delimitersguesses which delimiters a text writes its interpolation keys with:before{name}, and[[ ]],(( ))and<< >>after them.ScanResult.detected_interpolationreports what a whole scan saw, whichload_translationscarries assession.detected_interpolation. It is a suggestion for a project being set up, never what the run compared with — that staysinterpolation_prefix. The pairs it knows are exported in order asINTERPOLATION_DELIMITERScreate_path_excluderandcreate_file_excluderbuild the two tests a scan applies, so an application that shows a user which folders are excluded can ask the same question the scan asks
Changed
excludeaccepts a path as well as a name. One segment still names a directory at any depth, sonode_modulesmeans everynode_modulesthere is; an entry with a separator names a path from the scanned root, sosrc/legacyexcludes that folder and everything under it without touching alegacybelonging to something else- Both exclude lists apply to the source tree
sourcenames as well as to the translation directory
1.1.0 (2026-09-04)
The target language is checked too. Everything else is compared against it, so it sat outside every check, and a mistake typed into the source language stayed there however often the files were checked.
Changed
- The target language is now checked by everything that reads one value on its own:
EMPTY_VALUE,SURROUNDING_WHITESPACE,INVISIBLE_CHARACTER,INVALID_VALUE_TYPEandUNTRANSLATED_SCRIPT. It was skipped outright before, so an empty string, a trailing space or a zero width character inen.jsonwas never reported. The comparison checks still say nothing about it, since it is what they compare against. None of the five reports aterror, so a run that passed still passes; switch one off withignore_checksthe way you would for any other language - An issue about the target language's own value carries no
target_value. What a report quotes beside a finding is the value it would be compared to, and for the target language that is the value already shown
Fixed
- The
githubreporter writes the annotation'sfile=path with forward slashes on every platform. On a Windows runner it wrote the platform's own separator, which GitHub matches against nothing, so the annotation silently attached to no file - The command writes UTF-8 to the console whatever its code page is. On Windows it printed the banner and then died with a
UnicodeEncodeErrorhalf a report in, exiting1on a directory with nothing wrong
1.0.0 (2026-09-03)
The first release. A port of the JavaScript package at the same version: the same twenty-five checks, the same option names, the same report, and the same exit code from the command line.
Added
check_translation_filesreads a directory of translation files and compares every language against the target language, in one callanalyze_translationscompares translations passed in directly, with no file system workcreate_analyzerreturns a reusable analyzer whosecheck_entryre-checks a single keyload_translationsreads a directory once and returns a session that holds the parsed translations:analyze,check_key,get,set,remove,keys,translationsandreloadall work on what is already in memory.create_sessionis the same for translations passed in directly- Twenty-five checks.
NO_KEY,NO_LOCALE,DUPLICATE_KEY,NO_INTERPOLATION_KEY,EXTRA_INTERPOLATION_KEYandINTERPOLATION_COUNTreport aterrorand fail a run;DUMMY_KEY,EMPTY_VALUE,INVALID_VALUE_TYPE,TAG_MISMATCH,NOT_TRANSLATED_VALUE,UNTRANSLATED_SCRIPT,DUPLICATE_VALUE,INCONSISTENT_VALUE,SURROUNDING_WHITESPACE,INVISIBLE_CHARACTER,MISSING_NUMBER,NUMBER_MISMATCH,NO_PLURAL_FORM,KEY_NAMING,KEY_DEPTHandUNDEFINED_KEYatwarn;UNUSED_KEYandSUSPICIOUS_LENGTHatinfo - Every on-disk layout: one file per locale (
en.json), one folder per locale (en/common.json), and one file holding every locale ({"en": ...}). Files holding the same keys are compared as a group - Five reporters —
pretty,list,json,markdownandgithub— and five grouping axes:locale,code,group,fileandnone.outputwrites a copy to a file, in the shape its extension implies - The
chki18ncommand, which takes the same options as the library and exits with1when anerrorlevel issue was found chki18n.core, the comparison on its own. It reaches no file system, so an application that must not touch the disk can import the checks without the scanner
Notes on the port
- Options are one keyword-only frozen dataclass —
Options(target="en")— rather than the twenty-four keyword arguments each entry point would otherwise carry. Every field is optional and every one has the JavaScript default - Check codes, severities and the option choices are the same strings the JavaScript package uses, typed as
Literal, so"NO_KEY"and"error"need no translating between the two.checksaccepts a list or the comma separated text a flag gives it, andlevelsa mapping or the CLI'sCODE=levellist - The entry points are synchronous. The JavaScript package is asynchronous because Node's file system is; Python's is not, and an
asyncsurface would be a promise this package cannot keep anything with - A translation file whose JSON is not an object is reported as
INVALID_FILErather than read as an empty one - No dependencies. The argument parser, the ANSI colours and the JSON key scanner are written out, because
argparsecannot express--no-xas the negation ofxand the other two would be a dependency each