@blax-software/i18n

Per-page, all-locale translation sources merged into vue-i18n messages, plus a CLI that builds one bundle per locale and fails CI on missing keys.
253
Install
npm install @blax-software/i18n
Latest Version:0.2.0
License:MIT
Last Updated:Sep 30, 2026
Links: GitHub  ·  npm
Maintainer: blax-software

Blax Software OSS

@blax-software/i18n

npm Node

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]
  • build writes <out>/<locale>.js, one export default {...} ESM module per locale. Sources are transpiled with esbuild, so .ts files need no bundler. --default is always emitted, on top of --locales or of every base file when --locales is omitted.
  • --packages merges translations that npm packages register (see below). auto takes every dependency in the app's package.json that registers some; a comma list names them.
  • check walks 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; --json prints the report object. Without --default the reference key set is the union of all locales; with it, that locale's keys (surplus keys elsewhere are listed as extra but 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

@i18n-micro/hmr

Translation HMR utilities for Nuxt I18n Micro (granular subpath imports).

51,795 251
@i18n-micro/node

Node.js utilities for loading translations and locale data in Nuxt I18n Micro.

4,423 251
@docx-editor.dev/i18n

Shared UI locale strings and types for the @docx-editor.dev editor adapters (Rea...

489,359 496
@i18n-micro/core

Core utilities for translations, formatting, and locale routing in Nuxt I18n Mic...

199,259 251
@i18now/nuxt

Nuxt module for loading, syncing, and managing i18now translations.

1,931

Version History

Version Released License
0.2.0 MIT
0.1.0 MIT