Tooling
A standalone CLI that round-trips your TypeScript namespaces through JSON, so a translation team can work in the format they know while you keep authoring in TypeScript. It also checks translations against the source shape, which makes a clean CI gate.
@mmstack/translate-toolsnpm
npm i -D @mmstack/translate-tools Authoring in TypeScript is what buys the compile-time safety, but a translator does not want a .ts file, and a TMS platform cannot ingest one. This tool bridges that: it exports your translations to plain JSON, and imports the translated JSON back as generated createTranslation modules, wiring each new locale into its registerNamespace call for you. Your source createNamespace is never touched.
It reads that source statically. It never runs your code and never imports @mmstack/translate, so one version of the tool works across releases (it is versioned independently, starting at 1.x, and runs in an Nx monorepo or a plain ng new app).
#The round-trip
Three commands, in order. export writes one nested JSON file per namespace per locale, keys mirroring your createNamespace tree with ICU messages preserved verbatim. A translator or TMS edits those. import reads them back, generates a createTranslation module per new locale next to the source namespace, and inserts the loader into the matching registry.
# 1. hand off source strings to translators / your TMS
npx mmtranslate export --src "src/**/*.ts" --out i18n
# 2. ...they translate i18n/<namespace>.<locale>.json...
# 3. bring the translations back as generated TypeScript
npx mmtranslate import --src "src/**/*.ts" --in i18n Export also drops a small hidden .mmtranslate-meta.json recording the source locale, so a later import knows which locale is the source without you repeating --source-locale. A new locale's file is created only if one is not already there; pass --force to overwrite. An existing locale is updated in place.
#What import validates
Before writing anything, import checks each non-source file against the source locale. Every leaf must be valid ICU and use the same placeholders as the source, so a dropped or renamed { name } is reported. It must cover every source key, so a missing translation is reported. And it must contain no unknown keys, because createTranslation is typed to the source shape and an extra key would generate code that does not compile, so the tool rejects the file rather than write it.
Rejection is per file, so one bad file never blocks the rest of the run. Files the run does not recognize (a typo'd namespace, a stray-dot name) are reported as skipped with a reason, so a mis-named file cannot silently vanish.
#Hygiene rules for CI
lint runs the hygiene rules in one discovery pass with a merged report and a single non-zero exit code, which is what a CI job or a pre-commit hook wants. dupes reports the same normalized source value under different keys (add --ignore-case to fold case), grouped by value with every key path. unused reports keys no scanned app source references.
# runs dupes + unused in one pass, single exit code for CI
npx mmtranslate lint --src "src/**/*.ts" --report json
# individual rules
npx mmtranslate dupes --src "src/**/*.ts" --ignore-case
npx mmtranslate unused --src "libs/i18n/**/*.ts" --app-src "src/**/*.ts"unused is deliberately conservative. A typed t('x.y.z') access counts as usage, but anything reached dynamically (a computed access, or a subtree passed around whole) is treated as unknown rather than unused, so the rule only reports keys it can prove are dead. This pairs with the typed API: dynamic keys are untypeable by design, which is the same property that keeps unused honest.
Silence a finding with a comment in the translation module, the same principle as eslint (rule names duplicate and unused; omit the name to disable all rules). Markers are located with the TypeScript scanner, so one written inside a translation string never suppresses anything.
/* mmtranslate-disable duplicate */ // before the first statement, whole file
export const common = createNamespace('common', {
save: 'Save',
// mmtranslate-disable-next-line duplicate
confirm: 'Save', // just this key
});#Pinning discovery
Discovery is automatic: the tool walks your source globs and resolves each registerNamespace to the namespace it points at. When you would rather pin that list than rely on the glob, or the glob is not catching something, generate-manifest writes an mmtranslate.config.ts naming the discovered namespaces, their registry files, and locales. It is a starting point you hand-edit and commit.
# write a config pinning discovered namespaces, registries, and locales
npx mmtranslate generate-manifest --src "src/**/*.ts" --out mmtranslate.config.ts The engine is also exported for scripting (discoverFromProject, planExport, runImport, and the rest), so you can drive the round-trip from your own tooling instead of the CLI.