@blax-software/i18n
| Install | |
|---|---|
npm install @blax-software/i18n |
|
| Latest Version: | 0.2.0 |
| License: | MIT |
| Last Updated: | Sep 30, 2026 |
| Links: | GitHub · npm |
@blax-software/i18n
Per-page, all-locale translation sources for vue-i18n / Nuxt i18n, plus a CLI that builds one message bundle per locale and fails CI when a key is missing.
The source layout it works on
i18n/
locales/en.ts export default { common: { save: 'Save' } }
locales/de.ts export default { common: { save: 'Speichern' } }
locales/pl.ts
pages/cart.ts export default { en: {...}, de: {...}, pl: {...} } satisfies TranslationFile
pages/checkout.ts
.messages/ generated by `blax-i18n build` (gitignore it)
Base files hold one locale each. Page files hold every locale side by side, so a
translator sees en/de/pl of the same screen in one place. The merged result
is locales seeded first, then every page deep-merged in path order (last wins).
Install
npm install @blax-software/i18n
Runtime API (@blax-software/i18n)
Framework-agnostic, no dependencies, safe in the browser.
import {
TranslationMissing, type TranslationFile,
mergeMessages, fallbackChain, createLazyLoaders,
} from '@blax-software/i18n'
| Export | What it does |
|---|---|
TranslationMissing |
The string '[missing]'. Put it where a translation is still open; blax-i18n check reports every occurrence. |
TranslationFile |
{ [locale: string]: Record<string, any> } — the page-module shape. Use satisfies TranslationFile. |
mergeMessages(locales, pages, { packages? }) |
{ [locale]: messages } — seed from locales, deep-merge each page's [locale] slice in order. Inputs are not mutated. Only locales present in locales are emitted. packages (page-shaped objects) merge first, so the app overrides them. |
fallbackChain(locales, defaultLocale = 'en') |
vue-i18n fallbackLocale map: { de: ['en'], pl: ['en'], default: ['en'] }. |
createLazyLoaders(loaders) |
{ loadLocaleMessages(locale), hasLocaleBundle(locale) } over static import() calls of the built bundles. |
checkTranslations(sources, { locales, reference? }) |
The check logic on already-loaded data (what the CLI runs). formatReport(report) renders it. |
Eager merge (Vite / Nuxt)
// i18n/messageloader.ts
import { mergeMessages } from '@blax-software/i18n'
const localeMods = import.meta.glob('./locales/*.ts', { eager: true, import: 'default' })
const pageMods = import.meta.glob('./pages/**/*.ts', { eager: true, import: 'default' })
const byPath = (mods: Record<string, any>) => Object.keys(mods).sort().map((p) => mods[p])
const locales = Object.fromEntries(
Object.keys(localeMods).sort().map((p) => [p.replace(/^.*\/([^/.]+)\.ts$/, '$1'), localeMods[p]]),
)
export default mergeMessages(locales, byPath(pageMods))
// i18n.config.ts
import { fallbackChain } from '@blax-software/i18n'
import messages from './i18n/messageloader'
export default {
fallbackLocale: fallbackChain(['en', 'de', 'pl']),
messages,
}
Lazy per-locale bundles
Run blax-i18n build (below), then load only the active locale:
// i18n/lazy-loader.ts
import { createLazyLoaders } from '@blax-software/i18n'
export const { loadLocaleMessages, hasLocaleBundle } = createLazyLoaders({
de: () => import('./.messages/de.js'),
pl: () => import('./.messages/pl.js'),
})
Leave the default locale out of the map and import it eagerly, so there is always a complete fallback.
Marking open translations
import { TranslationMissing, type TranslationFile } from '@blax-software/i18n'
export default {
en: { cart: { title: 'Your cart' } },
de: { cart: { title: 'Dein Warenkorb' } },
pl: { cart: { title: TranslationMissing } },
} satisfies TranslationFile
CLI (blax-i18n)
blax-i18n build [--src i18n] [--out <src>/.messages] [--locales en,de,pl] [--default en] [--packages auto|a,b]
blax-i18n check [--src i18n] [--locales en,de,pl] [--default en] [--packages auto|a,b] [--json]
buildwrites<out>/<locale>.js, oneexport default {...}ESM module per locale. Sources are transpiled with esbuild, so.tsfiles need no bundler.--defaultis always emitted, on top of--localesor of every base file when--localesis omitted.--packagesmerges translations that npm packages register (see below).autotakes every dependency in the app's package.json that registers some; a comma list names them.checkwalks every key across the listed locales and reports, per locale, keys that are missing and values that still hold[missing]. Exit code 1 when anything is missing;--jsonprints the report object. Without--defaultthe reference key set is the union of all locales; with it, that locale's keys (surplus keys elsewhere are listed asextrabut do not fail the run).
{
"scripts": {
"i18n:build": "blax-i18n build --src i18n --out i18n/.messages --locales en,de,pl",
"i18n:check": "blax-i18n check --src i18n --locales en,de,pl"
}
}
$ blax-i18n check --src i18n --locales en,de,pl
en: ok (312 keys)
de: ok (312 keys)
pl: 1 missing, 2 [missing] (311 keys)
pages/account.ts account.logout missing
locales/* common.cancel [missing]
pages/cart.ts cart.empty [missing]
i18n check: 3 problems
Translation files are loaded as pure data: relative imports between them resolve,
import { TranslationMissing } from '@blax-software/i18n' resolves, any other import
yields {}.
Translations shipped by packages
A library that shows text of its own (a connection toast, a validation error) can ship
those strings instead of making every app redefine them. It points a blax-i18n field in
its package.json at a page-shaped module:
{
"name": "@blax-software/networking",
"blax-i18n": "./dist/i18n.js",
"exports": { "./i18n": { "import": "./dist/i18n.js", "require": "./dist/i18n.cjs" } }
}
// src/i18n.ts in the library
export default {
en: { websocket: { connectionlost: 'Connection lost. Reconnecting…' } },
de: { websocket: { connectionlost: 'Verbindung unterbrochen. Verbinde neu…' } },
}
The app opts in with --packages auto (or packages: 'auto' in the Node API). Package
strings merge first, so any base file or page can override one. check then also verifies
every package key in every checked locale, and accepts a locale the package lacks when the
app supplies it. Apps that merge at runtime import the module themselves:
import networking from '@blax-software/networking/i18n'
export default mergeMessages(locales, pages, { packages: [networking] })
Keep keys under one namespace per package (websocket.*) so they cannot collide with app keys.
Node API (@blax-software/i18n/node)
readSourceTree, buildMessages, writeMessages, checkSourceTree, readPackageSources
and loadModule are what the CLI calls; use them from a build script (for example a Nuxt
hook that regenerates bundles on change). Every one of them takes packages and root.
Development
npm install
npm run build # tsup → dist/ (ESM + CJS + d.ts, bin/cli.js)
npm run typecheck
npm test # vitest
License
MIT
Related Packages
Translation HMR utilities for Nuxt I18n Micro (granular subpath imports).
Node.js utilities for loading translations and locale data in Nuxt I18n Micro.
Shared UI locale strings and types for the @docx-editor.dev editor adapters (Rea...
Core utilities for translations, formatting, and locale routing in Nuxt I18n Mic...
Version History
| Version | Released | License |
|---|---|---|
| 0.2.0 | MIT | |
| 0.1.0 | MIT |