Skip to content
fluenti

Scaling & Enterprise

Fluenti’s compile-time architecture makes it uniquely suited for large codebases. Zero-runtime overhead at any scale, incremental caching for fast CI, and configuration inheritance for monorepo DRY.

  • Compile-time architecture — messages are compiled to plain strings and tiny functions at build time. Runtime cost is zero regardless of how many messages or locales you have.
  • Configuration-driven — non-invasive integration via fluenti.config.ts. No framework lock-in, no runtime dependencies in your components beyond a thin t() call.
  • Incremental caching — only changed files are re-extracted and only changed locales are re-compiled. Fewer file changes = fewer git conflicts.

Use the extends option to share a base configuration across packages in a monorepo. Child configs inherit all options from the parent and can override any field.

my-monorepo/
├── fluenti.config.ts # Root: shared locales, format, sourceLocale
├── packages/
│ ├── shared/
│ │ └── fluenti.config.ts # Shared lib: extends root
│ ├── app-web/
│ │ └── fluenti.config.ts # Web app: extends root, custom include/splitting
│ └── app-mobile/
│ └── fluenti.config.ts # Mobile app: extends root, custom catalogDir

Root config — shared defaults:

// fluenti.config.ts (monorepo root)
export default {
sourceLocale: 'en',
locales: ['en', 'ja', 'zh-CN', 'ko', 'de', 'fr'],
format: 'po',
catalogDir: './locales',
compileOutDir: './src/locales/compiled',
}

App config — extends root, overrides what’s different:

packages/app-web/fluenti.config.ts
export default {
extends: '../../fluenti.config.ts',
include: ['./src/**/*.tsx', './src/**/*.ts'],
splitting: 'dynamic',
catalogDir: './locales', // app-specific catalogs
compileOutDir: './src/i18n/compiled',
}
  1. Fluenti resolves the extends path relative to the current config file’s directory
  2. The parent config is loaded recursively (parents can also extend)
  3. Path fields are rebased from the parent’s directory to the child’s directory
  4. Child values override parent values — shallow merge, not deep merge
  5. Circular references and chains deeper than 10 levels are detected and rejected
PatternWhen to use
Shared catalogs — all apps use the same catalogDir at the monorepo rootSmall monorepo, shared translation team, consistent terminology
Independent catalogs — each app has its own catalogDirLarge monorepo, separate translation workflows, different release cycles
Hybrid — shared base + app-specific overrides via externalCatalogsShared UI library with app-specific strings

Fluenti caches extraction and compilation results to skip unchanged work.

The fluenti extract command tracks each source file by mtime + size. On subsequent runs, files that haven’t changed are skipped entirely:

Terminal window
$ fluenti extract
# First run: Found 847 messages in 312 files
# Second run: Found 847 messages in 312 files (310 cached)

The fluenti compile command hashes each locale’s catalog content. If the hash hasn’t changed since the last compilation, the locale is skipped:

Terminal window
$ fluenti compile
# Compiled en: 847 messages → src/locales/compiled/en.js
# Compiled ja: 847 messages → src/locales/compiled/ja.js
# 4 locale(s) unchanged — skipped

Cache files are stored in <catalogDir>/.cache/ alongside your catalogs. Add this to .gitignore:

locales/.cache/

Use --no-cache to bypass all caches:

Terminal window
fluenti extract --no-cache
fluenti compile --no-cache

For projects with many locales, compilation can be parallelized across worker threads.

Terminal window
# Auto-detect available parallelism
fluenti compile --parallel
# Limit to 4 workers
fluenti compile --parallel --concurrency 4
  • Single locale: compiled in-process (no worker overhead)
  • Multiple locales: spawns min(locales, availableParallelism()) worker threads
  • Each worker compiles one locale independently
  • Results are collected and written to disk by the main thread

You can enable parallel compilation by default in your config:

export default {
// ...
parallelCompile: true,
}

The --parallel CLI flag overrides this setting.

Use fluenti check to enforce minimum translation coverage in CI:

Terminal window
# Fail if any locale is below 90%
fluenti check --min-coverage 90
# GitHub Actions annotation format
fluenti check --min-coverage 90 --ci
# JSON output for custom dashboards
fluenti check --min-coverage 90 --format json
# Check a specific locale only
fluenti check --locale ja --min-coverage 95

The --ci flag is an alias for --format github, which outputs ::error and ::warning annotations that GitHub Actions renders inline on the PR.

name: i18n
on: [push, pull_request]
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Extract messages
run: pnpm fluenti extract
- name: Check translation coverage
run: pnpm fluenti check --min-coverage 90 --ci
- name: Compile catalogs
run: pnpm fluenti compile --parallel
- name: Build
run: pnpm build

The --format json output includes per-locale results:

{
"results": [
{ "locale": "ja", "total": 847, "translated": 820, "missing": 27, "fuzzy": 3, "coverage": 96.8 },
{ "locale": "zh-CN", "total": 847, "translated": 847, "missing": 0, "fuzzy": 0, "coverage": 100.0 }
],
"passed": true,
"minCoverage": 90,
"actualCoverage": 98.4
}

Fluenti’s t() function returns a branded LocalizedString type — not a plain string. This enables the compiler to catch untranslated strings at build time:

import type { LocalizedString } from '@fluenti/core'
function setPageTitle(title: LocalizedString) { /* ... */ }
setPageTitle(t`Welcome`) // ✅ Translated string
setPageTitle('raw string') // ❌ Compile error — not translated

LocalizedString is assignable to string (backward-compatible), but string cannot be assigned to LocalizedString without an explicit cast. This means existing code that accepts string continues to work, while new code can opt into strict type checking.

If the branded type is too strict for your codebase, you can disable it via module augmentation (see TypeScript Integration for full details):

src/fluenti.d.ts
declare module '@fluenti/core' {
interface FluentiTypeConfig {
localizedString: string // disables branded type
}
}

When you run fluenti compile, a messages.d.ts file is generated alongside your compiled catalogs. This file narrows FluentiTypeConfig['messageIds'] and FluentiTypeConfig['messageValues'] to the actual message IDs and value shapes in your project, enabling IDE autocompletion for message keys.

Integrate Fluenti compilation into custom build pipelines with lifecycle hooks:

export default {
sourceLocale: 'en',
locales: ['en', 'ja', 'zh-CN'],
// ...
onBeforeCompile: () => {
console.log('Starting i18n compilation...')
// Return false to skip compilation
},
onAfterCompile: async () => {
console.log('i18n compilation complete!')
// Post-compile tasks: notifications, uploads, etc.
},
}
HookSignatureDescription
onBeforeCompile() => boolean | void | Promise<boolean | void>Called before auto-compile. Return false to skip.
onAfterCompile() => void | Promise<void>Called after auto-compile completes successfully.

These hooks run during dev-mode auto-compile and build-time auto-compile. They do not run when you invoke fluenti compile from the CLI directly.

For complex routing, SEO, or RTL support, use LocaleObject instead of plain locale strings:

export default {
sourceLocale: 'en',
locales: [
{ code: 'en', name: 'English', iso: 'en-US', dir: 'ltr' },
{ code: 'ar', name: 'العربية', iso: 'ar-SA', dir: 'rtl' },
{ code: 'ja', name: '日本語', iso: 'ja-JP', dir: 'ltr' },
{ code: 'de', name: 'Deutsch', iso: 'de-DE', dir: 'ltr', domain: 'de.example.com' },
],
// ...
}

LocaleObject fields:

FieldTypeDescription
codestringLocale code (required) — e.g. 'en', 'ja', 'zh-CN'
namestring?Human-readable display name
isostring?BCP 47 language tag for SEO hreflang
dir'ltr' | 'rtl'Text direction
domainstring?Domain for domain-based routing

Plain locale strings and LocaleObject values can be mixed in the locales array. Fluenti resolves locale codes automatically via resolveLocaleCodes().