npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@veloxdevworks/i18n

v0.3.0

Published

Velox i18n — i18nliner-compatible static analyzer and JavaScript runtime

Readme

@veloxdevworks/i18n

Velox i18n — i18nliner-compatible static analyzer (velox-analyzer), platform sync CLI (velox-i18n sync), and JavaScript runtime (t(), React, extension mode).

Documentation: veloxdevworks.com/i18n


Install (npm)

npm install @veloxdevworks/i18n --save-dev
npx velox-analyzer --help
npx velox-i18n sync --help

The package ships both the static analyzer CLI and the TypeScript runtime (dist/). Use import { t } from '@veloxdevworks/i18n' or the React bindings at @veloxdevworks/i18n/react.

Analyzer binaries: the main package is small (JS only). A single host-matched native binary is installed automatically via optionalDependencies (same pattern as esbuild/SWC/Biome) — about one ~15–25 MB platform package, not all seven. The velox-analyzer command is a small Node launcher that runs that binary (including Alpine/musl). velox-i18n is a Node CLI that posts an analyzer catalog to the Velox Platform key-sync API.

Platform packages: @veloxdevworks/i18n-darwin-arm64, …-darwin-x64, …-linux-x64-gnu, …-linux-arm64-gnu, …-linux-x64-musl, …-linux-arm64-musl, …-win32-x64

Do not install with --omit=optional if you need the analyzer; that skips the native binary.

i18nliner migration (keep your locale scripts):

{
  "devDependencies": {
    "@veloxdevworks/i18n": "^0.1.0"
  },
  "scripts": {
    "check": "velox-analyzer --check -c velox.config.json app packages",
    "export-text": "velox-analyzer -o ./default.json -c velox.config.json app packages && node ./scripts/validateTranslations/index.js ./default.json"
  }
}

Copy velox.config.json.example to your repo root. Locale gap files (needsTranslation.json), validateTranslations, and import-translations stay your scripts — see Scope: analyzer vs locale validation.

Publish (maintainers):

  • Local: cross-build every target, publish 7 platform packages then the main package. One-time setup: brew install zig && cargo install cargo-zigbuild (every crate dep is pure Rust, so Zig is the only cross-linker needed). Then, from the repo root:

    npm login                                   # @veloxdevworks scope, once
    pnpm --filter @veloxdevworks/i18n run release:local
    # = build:ts → build:platforms → publish-all (platform pkgs then main)

    publish-all packs each native/<key>/ binary into @veloxdevworks/i18n-<key> and publishes it at the same version as the main package; then publishes @veloxdevworks/i18n with exact-version optionalDependencies. Re-runnable with --skip-existing if a platform package already exists on the registry. Windows ships the x86_64-pc-windows-gnu build (runs on stock Windows; no MSVC runtime needed). Musl targets (linux-*-musl) are required for Alpine / node:*-alpine CI.

    To build binaries without publishing: pnpm --filter @veloxdevworks/i18n run build:platforms (or a subset: bash scripts/build-analyzer-platforms.sh darwin-arm64 linux-x64-musl). Stage packages with pnpm --filter @veloxdevworks/i18n run pack:platforms.

  • CI: push a tag v*; .github/workflows/release-analyzer.yml builds each target (musl via cargo-zigbuild) and publishes platform packages + main with provenance (needs NPM_TOKEN secret). Local publish uses windows-gnu; CI Windows job still builds msvc into the same win32-x64/ key.

Minimum requirements: Node.js ≥ 18, React ≥ 16.8 (peer dependency, optional)


Table of Contents

  1. Overview
  2. Runtime — Shipped Features
  3. Runtime — Planned / Not Yet Shipped
  4. Static Analyzer

Overview

@veloxdevworks/i18n is Velox's i18n package on npm:

  • Static Analyzervelox-analyzer scans TS/JS, derives i18nliner-compatible keys, exports a catalog JSON.
  • Platform syncvelox-i18n sync posts that catalog to Velox Platform (POST …/keys/sync) for CI drift checks and apply.
  • Runtimet(), translate, I18n.t, React hooks, applyExtension() for i18n-js apps.

See veloxdevworks.com/i18n for full documentation.

The design is heavily inspired by i18nliner: by default, the source string itself is the key (auto-generated from the string's content), so developers never have to manually maintain a key catalog. Custom resource keys are also fully supported when more control is needed.


Runtime — Shipped Features

Installation & Setup

npm install @veloxdevworks/i18n

Vanilla JS / Node.js

import { I18n, configure } from '@veloxdevworks/i18n';

// Optional: call before new I18n() / I18nProvider so new instances inherit it.
// Must stay in sync with velox.config.json (see Key config parity below).
configure({ inferredKeyFormat: 'underscored_crc32' }); // default

const translations = await fetch('/api/v1/translations/en').then(r => r.json());

const i18n = new I18n('en', translations);
console.log(i18n.t('Hello, world!'));

React

import { configure } from '@veloxdevworks/i18n';
import { I18nProvider } from '@veloxdevworks/i18n/react';

configure({ inferredKeyFormat: 'underscored_crc32' }); // before first provider mount

const translations = await fetch('/api/v1/translations/en').then(r => r.json());

root.render(
  <I18nProvider locale="en" translations={translations}>
    <App />
  </I18nProvider>
);
// Or pass config={{ ... }} on the provider / constructor 4th argument.

How configure() applies

| Path | Receives module configure()? | |------|--------------------------------| | Module t() / default singleton | Yes (always) | | new I18n(...) created after configure() | Yes (baseline; constructor arg wins) | | <I18nProvider> without config prop, mounted after configure() | Yes | | Instance i18n.configure(...) | That instance only | | configure() after an instance was created | Does not retroactively update that instance |

Precedence for a new instance: defaults < module configure() < constructor / provider config prop.

Key Strategy: Auto-generated vs. Custom Keys

Auto-generated keys (default)

Inspired by i18nliner, you write the default English string inline. The key is automatically derived from the string by normalizing whitespace and computing a stable slug + CRC32 checksum:

t('Welcome to Velox!')
// key derived → "welcome_to_velox_<crc32>" (i18nliner-compatible)

This means:

  • No key management overhead for developers.
  • The English source string is the fallback value when a translation is missing.
  • Refactoring a string changes its key — the analyzer detects this and marks the old key as unused.

Key formats supported:

| inferredKeyFormat | Example output | |---|---| | underscored_crc32 (default) | "welcome_to_velox_d2a1f2c4" | | underscored | "welcome_to_velox" | | literal | "Welcome to Velox!" | | hash | "d2a1f2c4" (CRC only — smaller catalogs; Velox extension) |

Custom resource keys (opt-in)

When you need stable keys that survive content changes (e.g., legal copy, marketing text), pass a key option:

t('Please read our terms of service.', { key: 'legal.terms_intro' })

Or use the i18nliner positional-key signature:

t('legal.terms_intro', 'Please read our terms of service.')

Basic Usage

import { useTranslation } from '@veloxdevworks/i18n/react';

function Greeting() {
  const { t } = useTranslation();
  return <p>{t('Hello, world!')}</p>;
}

With a namespace scope:

const { t } = useTranslation('checkout');
t('Place your order') // looks up key in "checkout" namespace first, falls back to root

Supported call signatures

// 1. Inferred key from source string
t('Hello, world!')

// 2. Explicit dotted key + positional default (i18nliner style)
t('nav.home', 'Home')

// 3. Explicit key via options (Velox style)
t('Home', { key: 'nav.home' })

// 4. Plural forms object
t({ one: '{{count}} item', other: '{{count}} items' }, { count: n })

// 5. Auto-plural shorthand (English words only)
t('person', { count: n })   // → "1 person" / "N people"

// 6. Context suffix (friend_male → friend with { context: 'male' })
t('Friend', { key: 'friend', context: 'male' })

// 7. Key-array fallback — first catalog match wins
t(['nav.new_home', 'nav.home'])

String Interpolation

Velox accepts three placeholder conventions. All work in the same string:

| Syntax | Convention | |---|---| | %{variable} | i18nliner | | {{variable}} | Velox / mustache | | {variable} | ICU MessageFormat |

t('Hello, {{name}}!', { values: { name: 'Alice' } })
// → "Hello, Alice!"

t('Hello, %{name}!', { name: 'Alice' })
// → "Hello, Alice!" (i18nliner-style inline values)

t('Welcome back, {name}!', { name: 'Alex' })
// → "Welcome back, Alex!" (ICU-style)

t('Dear %{name}, you have {{count}} messages.', { name: 'Bob', values: { count: 3 } })
// → "Dear Bob, you have 3 messages."

Unmatched placeholders are left as literal text (e.g. {unknown} stays unchanged when unknown is not passed).

HTML-safe interpolation (i18nliner compatible)

Values are HTML-escaped when wrappers are used, when a %h{var} hint appears in the string, or when a HtmlSafeString value is present:

import { HtmlSafeString } from '@velox/i18n-js';

// Wrappers trigger HTML-escaping of all non-safe values
t('Click *here* to continue.', {
  wrappers: ['<a href="/foo">$1</a>'],
})
// → "Click <a href="/foo">here</a> to continue."

// HtmlSafeString bypasses escaping; plain strings are escaped
t('%{unsafe} and %{safe}', {
  unsafe: '<script>',
  safe: new HtmlSafeString('<em>ok</em>'),
})
// → "&lt;script&gt; and <em>ok</em>"

Pluralization (CLDR via Intl.PluralRules)

Shipped. Plural form selection uses the browser/Node built-in Intl.PluralRules for the active locale, covering all six CLDR categories: zero, one, two, few, many, other.

// English — one / other
t({ one: '{{count}} result', other: '{{count}} results' }, { count: 1 })  // "1 result"
t({ one: '{{count}} result', other: '{{count}} results' }, { count: 42 }) // "42 results"

// Russian — one / few / many
const i18n = new I18n('ru', {});
i18n.t({ one: '{{count}} рез.', few: '{{count}} рез. (few)', many: '{{count}} рез. (many)', other: '{{count}} рез.' }, { count: 5 })
// → "5 рез. (many)"

// Arabic — zero / one / two / few / many / other
const ar = new I18n('ar', {});
ar.t({ zero: 'صفر', one: 'واحد', two: 'اثنان', few: '{{count}} قلة', many: '{{count}} كثير', other: '{{count}} آخر' }, { count: 3 })
// → "3 قلة"

A missing category falls back to other. The zero form, when provided, is always used for count === 0 regardless of locale rules (i18nliner compatibility).

Context suffixes

When context is set, Velox looks up key_context first (e.g. friend_male), then falls back to the base key:

t('Friend', { key: 'friend', context: 'male' })

Plural suffix catalogs

Sibling keys with CLDR suffixes (item_one, item_other, …) can be grouped at import time:

import { groupPluralSuffixes } from '@veloxdevworks/i18n/catalog';

groupPluralSuffixes({ item_one: '1 item', item_other: '{{count}} items' })
// → { item: { one: '1 item', other: '{{count}} items' } }

Auto-plural shorthand (English only)

t('person', { count: 1 })  // → "1 person"
t('person', { count: 3 })  // → "3 people"
t('child', { count: 5 })   // → "5 children"

Only triggers for single-word strings matching /^[\w-]+$/.

ICU plural strings (react-intl compatible subset)

When count is provided, Velox can parse a single top-level ICU plural message in catalog values or inline defaults:

t('{count, plural, one {# item} other {# items}}', { count: 1 })  // → "1 item"
t('{count, plural, one {# item} other {# items}}', { count: 5 })  // → "5 items"

Supports =N exact matches, offset:N, and all CLDR plural categories via Intl.PluralRules. Prefer an explicit key when storing ICU strings in catalogs (derived keys from full ICU text are opaque).

ICU select / gender strings

When the select variable is provided in options, Velox resolves {var, select, …} messages:

t('{gender, select, female {she} male {he} other {they}}', { gender: 'female' })  // → "she"

Unknown values fall back to the other branch.

ICU selectordinal strings

When the ordinal variable is provided in options, Velox resolves {var, selectordinal, …} with Intl.PluralRules({ type: 'ordinal' }):

t(
  '{place, selectordinal, one {#st place} two {#nd place} few {#rd place} other {#th place}}',
  { place: 21 },
)  // → "21st place"

Same branch grammar as ICU plural (=N, offset:N, CLDR categories). The named variable (e.g. place) is read from options; missing or non-numeric values leave the message unchanged. # is replaced with the locale-formatted number (minus any offset).


Date & Number Formatting

Shipped. Locale-aware wrappers around Intl.DateTimeFormat, Intl.NumberFormat, and Intl.RelativeTimeFormat, cached per locale + options:

const i18n = new I18n('fr');
i18n.formatDate(new Date('2024-06-15'), { dateStyle: 'long' });
i18n.formatNumber(1234.5, { style: 'currency', currency: 'EUR' });
i18n.formatRelativeTime(-1, 'day');  // → "yesterday" (en)

import { selectRelativeTimeUnit } from '@veloxdevworks/i18n';
const [value, unit] = selectRelativeTimeUnit(targetDate.getTime() - Date.now());
i18n.formatRelativeTime(value, unit);

const { formatDate, formatNumber, formatRelativeTime } = useTranslation();
formatDate(order.createdAt, { timeZone: 'UTC' });

Namespaces & Dot-notation

Namespaces map to Velox project namespaces. They allow large applications to load translation bundles incrementally.

// Load only the "dashboard" namespace
const { t } = useTranslation('dashboard');
t('Overview') // looks up key in dashboard namespace first, falls back to root ''

Dot-notation namespaces are supported with parent-chain fallback:

const { t } = useTranslation('settings.account');
t('Change password')

Lookup order for namespace settings.account:

  1. translations['settings.account'][key] — exact namespace match
  2. translations['settings'][key] — parent namespace
  3. translations[''][key] — root namespace (always last)

Three-level namespaces work the same way: a.b.ca.ba → root.


Catalog interoperability

Shipped. Shape-based helpers in @veloxdevworks/i18n/catalog normalize foreign JSON catalogs into Velox TranslationsMap form. They are tool-agnostic — no per-library adapters.

import {
  flattenNested,
  groupPluralSuffixes,
  toTranslationsMap,
  unwrapDescriptors,
} from '@veloxdevworks/i18n/catalog';

// Descriptor-shaped catalogs: { id: { defaultMessage, ... } }
const flat = unwrapDescriptors(extractedCatalog);

// Nested JSON: { app: { welcome: "Hi" } }
const namespaces = flattenNested(nestedCatalog, { namespaceDepth: 1 });

// Plural suffix siblings: { item_one: "...", item_other: "..." }
const grouped = groupPluralSuffixes(suffixedCatalog);

// Load into the runtime
const translations = toTranslationsMap([
  { namespace: '', catalog: flat },
  { namespace: '', catalog: grouped },
  ...namespaces,
]);

flattenNested warns (does not throw) when two nested paths collapse to the same key.


React Integration

The React integration is in @velox/i18n-js/react and exposes:

| Export | Description | |---|---| | <I18nProvider> | Context provider; accepts locale, translations, and optional onMissingKey | | useTranslation(ns?) | Returns { t, formatDate, formatNumber, locale, ready, i18n } | | <Trans> | Component translation with string or rich JSX children |

Props

<I18nProvider
  locale="en"               // required — BCP 47 locale tag
  translations={{}}         // optional — starts ready:false when omitted or empty
  onMissingKey={callback}   // optional — called for every missing key
>
  <App />
</I18nProvider>

useTranslation

const { t, formatDate, formatNumber, locale, ready, i18n } = useTranslation('checkout');
// t            — translation function bound to the namespace
// formatDate   — Intl.DateTimeFormat for the active locale
// formatNumber — Intl.NumberFormat for the active locale
// locale       — current active locale
// ready        — false until translations have been loaded
// i18n         — stable I18n instance (same reference across re-renders)

<Trans> component

Shipped: string children and rich JSX children (links, emphasis, nested elements).

<Trans i18nKey="legal.terms_intro" values={{ company: 'Velox' }}>
  Welcome to Velox.
</Trans>

<Trans>
  Please read our <a href="/terms">terms of service</a>.
</Trans>

Rich children serialize to <0>…</0> templates (matching the static analyzer). Translators may reorder slot tags in translated strings; the original React elements are preserved by slot name.


Locale Switch & Hot-reload

Shipped. I18nProvider maintains a stable I18n instance. Changing the locale or translations prop updates the instance without remounting the tree. The ready flag transitions false → true when the first non-empty translations catalog is applied.

function App() {
  const [locale, setLocale] = useState('en');
  const translations = locale === 'es' ? esTranslations : enTranslations;

  return (
    <I18nProvider locale={locale} translations={translations}>
      <button onClick={() => setLocale('es')}>Switch to Spanish</button>
      <MyComponent />
    </I18nProvider>
  );
}

Async / external hot-reload

Code holding a reference to the i18n instance can call setLocale / setTranslations from outside React. All I18nProvider subtrees will re-render automatically. ready becomes true when the active locale catalog is first non-empty — including after imperative setTranslations / replaceTranslations (not only when the translations prop changes).

// Async translation loading — triggers re-render when done; ready flips true
const { i18n, ready } = useTranslation();

async function loadLocale(locale: string) {
  const data = await fetch(`/api/translations/${locale}`).then(r => r.json());
  i18n.setLocale(locale);
  i18n.setTranslations(locale, data);
}

requireExplicitKeys Mode

When requireExplicitKeys: true, every t() call must provide an explicit key. Calling t('Some string') without a dotted key or { key: '...' } option throws a MissingExplicitKeyError with an actionable message.

import { configure, t, MissingExplicitKeyError } from '@veloxdevworks/i18n';

// Call before using module t() or creating new I18n / I18nProvider instances
configure({ requireExplicitKeys: true });

t('my.key', 'Default value')   // ✓ explicit dotted key
t('Text', { key: 'my.key' })   // ✓ explicit key via option
t('Some text')                  // ✗ throws MissingExplicitKeyError

Per-instance (overrides or instead of module configure):

const i18n = new I18n('en', {}, undefined, { requireExplicitKeys: true });

Key config parity (analyzer ↔ runtime)

The analyzer reads velox.config.json; the runtime reads I18nConfig. These must match or derived keys diverge and lookups miss (app silently shows English).

| velox.config.json | Runtime configure() / constructor | |---------------------|--------------------------------------| | i18n.inferred_key_format | inferredKeyFormat (underscored_crc32 | underscored | literal | hash) | | i18n.key_max_length | keyLength (default 50; unused when format is hash) | | i18n.require_explicit_keys | requireExplicitKeys |

CRC digests (formats underscored_crc32 and hash) are full unpadded hex, matching i18nliner’s CRC algorithm. There is no separate hash-length knob (legacy key_hash_length is ignored).

hash format: key is CRC hex only ({"f3624ac4":"Hello, world!"}) — opt-in for smaller multi-MFE catalogs. Same content → same key; the analyzer errors if two different source strings collide on one derived key. Default remains underscored_crc32 for i18nliner drop-in. Keep analyzer and runtime format in sync.

Migrating key formats (seamless)

App source stays t('Hello, world!'). Build a reverse map from an analyzer catalog (default_value + dual derive), rewrite locale JSON keys only, and flip config:

# 1. Export catalog under the *current* format
npx velox-analyzer -o ./catalog.json -c velox.config.json app packages

# 2. Dry-run remap (no writes)
npx velox-i18n migrate-keys \
  --from underscored_crc32 \
  --to hash \
  --catalog ./catalog.json \
  --locales ./locales \
  --report ./key-migration-report.json

# 3. Apply: rewrites catalog + locale keys; optional config flip
npx velox-i18n migrate-keys \
  --from underscored_crc32 \
  --to hash \
  --catalog ./catalog.json \
  --locales ./locales \
  --write \
  --update-config

# 4. Runtime must match (same release as new locale bundles)
# configure({ inferredKeyFormat: 'hash' })

Fails closed on collisions and orphan locale keys (use --drop-orphans only if intentional). Deploy locale bundles and runtime inferredKeyFormat together.


Missing Key Behavior

When a key is not found in the loaded catalog:

  1. Auto-key mode — the original source string (the default English text) is returned as-is. The user always sees readable text.
  2. Explicit key mode — the key string itself is returned (e.g., "legal.terms_intro").
  3. An optional onMissingKey(key, locale, namespace) callback on <I18nProvider> or the I18n constructor is called so you can report missing keys to an error tracker.
const i18n = new I18n('en', {}, (key, locale, ns) => {
  myErrorTracker.capture({ key, locale, namespace: ns });
});

Runtime — Planned / Not Yet Shipped

The following features appear in marketing material or the ROADMAP but are not yet implemented in the current runtime:

| Feature | Status | Notes | |---|---|---| | JSX component interpolation in t() | Planned | t('Click <link>here</link>') with render-prop values — use <Trans> for JSX children today | | Rich <Trans> children | Shipped | JSX → <0>…</0> templates; runtime reconstructs slot elements | | ICU plural subset | Shipped | {count, plural, …} with =N, offset:N, CLDR categories | | ICU select / gender | Shipped | {gender, select, …} | | ICU selectordinal | Shipped | {place, selectordinal, …} via ordinal PluralRules | | Date / number formatting | Shipped | i18n.formatDate / formatNumber and useTranslation() helpers | | Relative-time formatting | Shipped | i18n.formatRelativeTime + selectRelativeTimeUnit helper | | Locale-bundle splitting / lazy load | Planned | Load namespace bundles on demand — see ROADMAP | | velox.config.json runtime loading | Node-only stretch | Config from disk is not loaded automatically in browser |

See ROADMAP.md for details on planned work.


Static Analyzer

Scope: analyzer vs locale validation

Many i18nliner adopters run a two-layer pipeline. Velox is aimed at replacing only the first layer (what i18nliner does today). Keep your existing locale scripts unless you explicitly migrate them.

| Layer | Typical tools | Velox responsibility | |-------|---------------|----------------------| | Source → English catalog | i18nliner export, i18nliner check | Yesvelox-analyzer scan, key derivation, default.json export, call-site validation | | Catalog ↔ locale files | Custom validateTranslations, needsTranslation.json, Google/CSV helpers | No (for now) — missing translations per locale, pruning translations/*.json, import merge | | Runtime | i18n-js + i18nliner extension, react-native-i18n, app loaders | Runtime package — unrelated to the Rust CLI |

Example monorepo flow (i18nliner-style):

I18n.t('Hello') in app/packages
        │
        ▼
i18nliner export  →  default.json (en keys + English defaults)
        │
        ▼
validateTranslations.js  →  sync/prune translations/{locale}.json
        │                    missing? → needsTranslation.json + exit 1
        ▼
[translators / import-translations / CSV]
        │
        ▼
Runtime loads translations/{locale}.json

Drop-in trial: swap only the export/check step, e.g. velox-analyzer export instead of i18nliner export, then leave node scripts/validateTranslations/... unchanged on the path to default.json.

Planned CLI (not all shipped yet): check (read-only, source + optional catalog diff), export (write default.json, optional --prune for stale keys in the English catalog). Locale completeness checks may become a separate validate-locales command or Velox API feature later; they are not part of i18nliner's published CLI.

Goals

The Rust-based static analyzer (packages/i18n-js/src/) serves as the bridge between application source code and translation catalogs (and eventually the Velox API). Its primary responsibilities are:

  1. Extract — scan TypeScript/JavaScript source files and find every resolved t(...) / I18n.t(...) call-site (import-aware).
  2. Derive keys — compute the canonical translation key from the source string (auto-key mode) or validate explicit keys.
  3. Detect drift — compare extracted keys against default.json (unused/changed keys in the English catalog).
  4. Sync — export a catalog, then use velox-i18n sync to push keys to Velox Platform (see Integration with Velox API).
  5. Lint — emit actionable errors/warnings when call sites are invalid (dynamic keys, plural issues, explicit-key collisions).

Architecture Overview

src/analyzer/
├── main.rs              # CLI entry point (clap)
├── config.rs            # Reads velox.config.json / CLI flags
├── scanner.rs           # File-system walk, file filtering
├── parser/
│   ├── mod.rs           # Parser trait + dispatch
│   ├── swc_parser.rs    # Uses SWC (via swc_ecma_parser crate) to parse JS/TS → AST
│   └── visitor.rs       # AST visitor that identifies i18n call-sites
├── extractor.rs         # Call-site extraction (CallSite + namespace from hooks/Trans)
├── keygen.rs            # Auto-key generation from source strings
├── catalog.rs           # Loads/writes -o catalog JSON (KeyEntry + optional namespace)
├── api_client.rs        # HTTP client for Velox REST/GraphQL API
├── differ.rs            # Compares extracted keys vs. known catalog keys
├── reporter.rs          # Formats output (human, JSON, GitHub annotations)
└── lib.rs               # Public library surface for WASM / Node.js FFI future use

Module Breakdown (src/analyzer)

scanner.rs

Recursively walks the project directory, respecting .gitignore and a configurable include/exclude list. Emits a stream of (path, source_text) pairs to the parser stage.

Supported file extensions: .ts, .tsx, .js, .jsx, .mts, .cts. Declaration files (*.d.ts, *.d.mts, *.d.cts) are always skipped.

scan.exclude uses gitignore-style globs (via globset), not Jest regexes. Matching directories are pruned during the walk. If exclude is omitted from config, defaults apply (**/__tests__/**, **/__generated__/**, **/*.test.*, **/*.spec.*). Set "exclude": [] to scan those paths.

| Jest testPathIgnorePatterns (regex) | Velox scan.exclude (glob) | |---------------------------------------|-------------------------------| | /__tests__/ | **/__tests__/** | | /__generated__/ | **/__generated__/** | | \.test\.tsx?$ | **/*.test.ts, **/*.test.tsx |

Use --profile on large monorepos to print phase timings and pass statistics (parsed vs prefilter_skip vs pass-2 reparsed) to stderr before tuning scan.include / exclude.

parser/swc_parser.rs + parser/visitor.rs

Uses the swc_ecma_parser crate to produce a full AST without spawning a Node.js process. The visitor walks the AST looking for:

| Pattern | Example | |---|---| | t("string") call expression | Import-bound t('Hello, world!') | | t("string", opts) with options | t('{{count}} item', { count }) | | t({ one: "...", other: "..." }, opts) | Plural shorthand | | t(['a', 'b']) key-array fallback | One catalog entry per static string key | | t(..., { context: 'male' }) | Static context → -o emits base key and key_context | | const { t } = useTranslation() | Hook-derived bare t(...) (also { t: translate }) | | const hook = useTranslation(); hook.t(...) | Hook object member form | | useTranslation('ns') namespace | Applied to hook/import t() and <Trans> in the same function scope | | <Trans i18nKey="..."> JSX element | <Trans i18nKey="legal.intro"> | | <Trans> with JSX children (rich) | Serialized to <0>…</0> templates; component_slots populated | | <Trans>default string</Trans> | Inline default without explicit key |

Each match produces a CallSite record:

pub struct CallSite {
    pub file: PathBuf,
    pub line: u32,
    pub col: u32,
    pub namespace: Option<String>,
    pub raw_string: Option<String>,       // source string / plural map
    pub explicit_key: Option<String>,     // from key: "..." option
    pub interpolations: Vec<String>,      // %{var} / {{var}} / {var} names (deduped)
    pub component_slots: Vec<String>,     // <Trans> slots, or t() wrappers indices/keys
    pub count_param: bool,                // whether `count` is passed
    pub context: Option<String>,          // static context: "..." option
}

extractor.rs

Collects CallSite records from the AST. Namespace is taken from useTranslation('…') in the enclosing function (including when t is bound from the hook return value) and from <Trans namespace="…"> when set. When writing -o catalogs, each entry’s namespace is CallSite.namespace, or the analyzer --namespace default if the call site has none. A static context option also emits the suffixed key {base}_{context} (e.g. friend + friend_male) so translators see the variant the runtime looks up first.

keygen.rs

Derives a stable, human-readable key from a source string:

  1. Lowercase and trim.
  2. Replace non-alphanumeric runs with _ (ASCII slugify, same as velox-analyzer; non-ASCII is not transliterated).
  3. Truncate to keyLength characters (default: 50).
  4. Append unpadded CRC32 hex suffix (in underscored_crc32 mode, the default). In hash mode, the key is only that CRC (no slug).
"Welcome to Velox!" → "welcome_to_velox_<crc32>"

This exactly matches i18nliner's keyifyUnderscoredCrc32 algorithm (including the charCodeAt(0) & 0xFF byte conversion for CRC32 compatibility).

catalog.rs

-o catalog JSON shape:

{
  "keys": {
    "nav.home": {
      "default_value": "Home",
      "plural_forms": null,
      "interpolations": [],
      "component_slots": [],
      "namespace": "labels"
    }
  }
}

namespace is optional for backward compatibility. When present, velox-i18n sync uses it per key; otherwise sync falls back to --namespace / config / common.

api_client.rs

Authenticates against the Velox API using a project API token (read from VELOX_API_TOKEN env var or velox.config.json). Provides:

  • GET /api/v1/projects/:id/keys — fetch registered keys
  • POST /api/v1/projects/:id/keys/batch — push new keys
  • DELETE /api/v1/projects/:id/keys/:key — remove unused keys (with --prune flag)

differ.rs

Compares extracted keys against the keys known to the catalog/API:

| Status | Description | |---|---| | New | Found in source, not in catalog → push to API | | Unchanged | Matches catalog exactly | | Changed | Key exists but default string differs → flag for re-translation | | Unused | In catalog but not found in source → warn / prune | | Duplicate | Same key derived from two different source strings → error |

reporter.rs

Emits results in one of three formats controlled by --format:

  • human (default) — colored terminal output with file:line references
  • json — machine-readable JSON array of findings
  • github — GitHub Actions annotation format (::error file=...::)

Key Extraction Rules

| Rule | Behavior | |---|---| | Dynamic key/string | t(variable) — emits a warning, cannot be statically analyzed | | Concatenated string | t("Hello" + name) — emits an error; interpolation must use {{}} | | Template literal | t(`Hello ${name}`) — emits an error; use {{name}} interpolation instead | | Nested t() call | t(t('inner')) — emits an error | | count without plural forms | t('item', { count }) without plural map — emits a warning | | Explicit key collision | Two call-sites with the same explicit key but different defaults — emits an error |


CLI Interface

velox-analyzer [OPTIONS] [PATH]

Arguments:
  [PATH]  Root directory to scan (default: current directory)

Options:
  -c, --config <FILE>       Path to velox.config.json [default: ./velox.config.json]
      --locale <LOCALE>     Source locale to operate on [default: en]
      --namespace <NS>      Default namespace for -o catalog entries
                            (overridden by useTranslation / Trans namespace)
      --format <FORMAT>     Output format: human | json | github [default: human]
  -o, --output <FILE>       Write translation catalog JSON
  -v, --verbose             Print scan roots and file count
      --profile             Phase timings and pass stats (stderr)
      --check               Exit nonzero on validation errors
      --strict              Treat warnings as errors with --check
  -h, --help                Print help

Platform key sync is a separate command: `velox-i18n sync` (see Integration with Velox API).

Example — extract one MFE with a namespace default:

velox-analyzer packages/labels -o labels.json --namespace labels
velox-i18n sync --catalog labels.json --project "$PROJECT_ID" --org "$ORG_ID"

(Sync --namespace is optional when every catalog entry already carries namespace.)


Output Formats

human

✔  312 keys extracted from 47 files

  NEW (3)
  ├─ checkout.place_order          src/pages/CheckoutPage.tsx:42
  ├─ checkout.order_summary        src/pages/CheckoutPage.tsx:67
  └─ settings.account.change_pw   src/pages/AccountSettings.tsx:18

  CHANGED (1)
  └─ welcome_to_velox              src/pages/HomePage.tsx:12
       was: "Welcome to Velox"
       now: "Welcome to Velox!"

  UNUSED (2)
  ├─ old_hero_headline
  └─ beta_banner_text

  ERRORS (0)
  WARNINGS (0)

json

{
  "summary": { "extracted": 312, "new": 3, "changed": 1, "unused": 2, "errors": 0, "warnings": 0 },
  "findings": [
    {
      "status": "new",
      "key": "checkout.place_order",
      "namespace": "checkout",
      "defaultValue": "Place your order",
      "locations": [{ "file": "src/pages/CheckoutPage.tsx", "line": 42, "col": 10 }]
    }
  ]
}

Integration with Velox API

Key sync to Velox Platform is a two-step CI flow (analyzer extract, then Node sync CLI):

# 1. Extract keys from source (assignment default for entries without call-site ns)
velox-analyzer packages/labels -o ./catalog.json --namespace labels -c velox.config.json

# 2a. Drift check (exit 2 when the preview change list is non-empty)
VELOX_I18N_API_KEY=$API_KEY \
  velox-i18n sync --catalog ./catalog.json --project "$PROJECT_ID" --org "$ORG_ID" --dry-run

# 2b. Apply
VELOX_I18N_API_KEY=$API_KEY \
  velox-i18n sync --catalog ./catalog.json --project "$PROJECT_ID" --org "$ORG_ID"

The sync CLI maps each analyzer catalog key’s default_value to platform sourceText. Namespace is taken from the catalog entry’s namespace when set; otherwise from sync --namespace, else velox.config.json namespaces[0], else common.

Optional platform defaults in velox.config.json:

{
  "source_locale": "en",
  "namespaces": ["common"],
  "platform": {
    "apiUrl": "https://i18n.velox.test",
    "orgId": "<org-uuid>",
    "projectId": "<project-uuid>",
    "namespace": "common"
  }
}

Auth is VELOX_I18N_API_KEY (Better Auth API key or OAuth bearer with i18n:write). Never commit the key. Flag → config → env precedence applies for --api-url, --org, --project, and --namespace.

velox-analyzer --sync in older docs is not shipped; use velox-i18n sync instead.


Package Structure

packages/i18n-js/
├── src/
│   ├── runtime/
│   │   ├── index.ts          # I18n class, configure(), t(), MissingExplicitKeyError
│   │   ├── react.tsx         # I18nProvider, useTranslation, Trans
│   │   ├── keygen.ts         # deriveKey(), EXPLICIT_KEY_PATTERN
│   │   └── pluralize.ts      # English auto-plural helper (single-word shorthand)
│   └── (Rust source)         # Rust static analyzer (see Static Analyzer section)
├── Cargo.toml                # Rust analyzer binary
├── package.json              # JS runtime package
├── tsup.config.ts            # Dual CJS/ESM build
├── ROADMAP.md                # Planned future features
└── tsconfig.json