"Aurebesh" — The ancient alphabet of the Star Wars galaxy, used by Jedi and Sith to preserve knowledge across worlds and generations.
| Domain | Capability |
|---|---|
| Translation | Namespace-scoped JSON files, dot-notation key lookup, SSR hydration, lazy HTTP loading |
| Pluralization | CLDR plural rules (cardinal + ordinal), exact count, numeric intervals (2-5, 6+), gender/case inflection |
| Inline shorthands | {{count : singular | plural}} for plurals, {{selector : key: text | fallback}} for selects |
| Interpolation | {{token}} with HTML escaping, nested dot-path tokens, configurable delimiters |
| Formatting | Locale-aware dates (4 presets + custom), times, relative time, numbers, percentages, lists |
| Currency | ISO 4217 money formatting, zero-decimal detection, compact notation |
| FX Engine | Live exchange rates (exchangerate.host), static fallback, failover chain, Next.js "use cache" integration, tag-based invalidation |
| Locale detection | Accept-Language header parsing with quality weights, cookie-first with RSC/client parity |
| Observability | Missing-key reporter (in-memory + pluggable), translation coverage reports for CI/CD |
| React | useTranslation, useMoney, FormatService, locale persistence (localStorage + cookie), SSR hydration provider |
@apollo-deploy/aurebesh
├── @apollo-deploy/aurebesh/config → Locale constants, I18nConfig, createI18nConfig()
├── @apollo-deploy/aurebesh/format → Pure Intl formatters (date, time, number, currency, relative, list)
├── @apollo-deploy/aurebesh/currency → Price types, resolution policies, FX snapshots, provider chain
├── @apollo-deploy/aurebesh/react → Client hooks & I18nProvider ← "use client"
├── @apollo-deploy/aurebesh/server → SSR loaders, locale detection, money(), fx-refresh handler ← server-only
└── @apollo-deploy/aurebesh/observability → Missing-key reporter interface + InMemoryMissingTranslationReporter
Server/client boundary is strictly enforced. @apollo-deploy/aurebesh/server imports server-only — any accidental client import fails at build time. @apollo-deploy/aurebesh/react carries "use client" directives.
bun install @apollo-deploy/aurebesh// lib/i18n.config.ts
import '@apollo-deploy/aurebesh/config';
export const i18nConfig = createI18nConfig(['settings', 'billing']);
// ^— additional namespaces beyond 'common' + 'auth'public/
locales/
en/
common.json
auth.json
settings.json
es/
common.json
auth.json
settings.json
fr/
common.json
auth.json
settings.json
// app/layout.tsx
import { join } from 'node:path';
import '@apollo-deploy/aurebesh/react';
import '@apollo-deploy/aurebesh/server';
import '@apollo-deploy/aurebesh/currency';
import '@apollo-deploy/aurebesh/currency'; // re-exported from server
import { i18nConfig } from '@/lib/i18n.config';
ensureDefaultFxProviders(); // idempotent — safe to call on every render
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const i18nState = await getI18nServerState('en', i18nConfig.namespaces, {
publicDir: join(process.cwd(), 'public'),
});
return (
<html lang="en">
<body>
<I18nProvider config={i18nConfig} {...i18nState}>
{children}
</I18nProvider>
</body>
</html>
);
}// components/Greeting.tsx
'use client';
import '@apollo-deploy/aurebesh/react';
export function Greeting({ name }: { name: string }) {
const { t, locale, changeLocale, format } = useTranslation('common');
return (
<div>
<h1>{t('greeting', { name })}</h1>
<p>{t('itemCount', { count: 5 })}</p>
<time>{format.date(new Date(), 'long')}</time>
<button onClick={() => changeLocale('es')}>Español</button>
</div>
);
}{
"greeting": "Hello, {{name}}!",
"navigation": {
"home": "Home",
"settings": "Settings"
}
}t('greeting', { name: 'Zara' }) // → "Hello, Zara!"
t('navigation.home') // → "Home"{
"itemCount": "{{count}} item",
"itemCount_other": "{{count}} items",
"itemCount_zero": "No items"
}t('itemCount', { count: 0 }) // → "No items"
t('itemCount', { count: 1 }) // → "1 item"
t('itemCount', { count: 5 }) // → "5 items"{
"messages": {
"one": "You have {{count}} message",
"other": "You have {{count}} messages",
"0": "No messages",
"2-5": "A few messages ({{count}})",
"6+": "Many messages"
}
}Resolution priority: exact count → interval → CLDR form → other
{
"rank": {
"one": "{{count}}st place",
"two": "{{count}}nd place",
"few": "{{count}}rd place",
"other": "{{count}}th place"
}
}t('rank', { count: 1, ordinal: true }) // → "1st place"
t('rank', { count: 3, ordinal: true }) // → "3rd place"{
"role": {
"male": { "nominative": "actor", "other": "actor" },
"female": { "nominative": "actress", "other": "actress" },
"other": { "nominative": "performer" }
}
}t('role', { gender: 'female', case: 'nominative' }) // → "actress"For plurals or gender variants embedded inside a longer sentence, use the
inline shorthand syntax. Everything stays inside {{}} — the same delimiters
used for plain variables.
{
"items": "You have {{count}} {{count : item | items}}.",
"results": "Found {{count : # result | # results}} matching your search."
}t('items', { count: 1 }) // → "You have 1 item."
t('items', { count: 5 }) // → "You have 5 items."
t('results', { count: 0 }) // → "Found 0 results matching your search."
t('results', { count: 1 }) // → "Found 1 result matching your search."When you need a distinct zero form, add a third branch:
{
"messages": "{{count : No unread messages | # unread message | # unread messages}}"
}t('messages', { count: 0 }) // → "No unread messages"
t('messages', { count: 1 }) // → "1 unread message"
t('messages', { count: 4 }) // → "4 unread messages"# inside a branch is replaced with the actual count value.
{
"confirmed": "{{gender : male: He | female: She | They}} confirmed the order.",
"status": "Status: {{state : active: Active | paused: Paused | Unknown}}"
}t('confirmed', { gender: 'male' }) // → "He confirmed the order."
t('confirmed', { gender: 'female' }) // → "She confirmed the order."
t('confirmed', { gender: 'other' }) // → "They confirmed the order."Rules:
- Branches with
key: textformat are named cases. - The last branch without
:is the fallback (other).
import '@apollo-deploy/aurebesh/react';
const { t, locale, changeLocale, isLoading, format } = useTranslation('settings');| Return | Type | Description |
|---|---|---|
t |
(key, opts?) => string |
Translate a key, falls back to key on miss |
locale |
SupportedLocale |
Current active locale code |
changeLocale |
(next: SupportedLocale) => Promise<void> |
Switch locale + persist to cookie/localStorage |
isLoading |
boolean |
true while namespaces are loading over HTTP |
format |
FormatService |
Locale-bound formatting utilities |
format.date(new Date(), 'long') // "January 15, 2024"
format.date(new Date(), 'short') // "1/15/24"
format.date(new Date(), { weekday: 'long', month: 'short' })
format.time(new Date()) // "2:30 PM"
format.dateTime(new Date()) // "Jan 15, 2024, 2:30 PM"
format.relative(new Date(Date.now() - 3600000)) // "1 hour ago"
format.number(1234567.89) // "1,234,567.89" (locale-aware)
format.currency(99.99, 'EUR') // "€99.99"
format.list(['apples', 'oranges', 'pears']) // "apples, oranges, and pears"
format.list(['cats', 'dogs'], 'disjunction') // "cats or dogs"'use client';
import '@apollo-deploy/aurebesh/react';
function PricingCard({ price }: { price: Price }) {
const money = useMoney();
return (
<div>
{/* AUTO: converts USD → EUR via FX snapshot */}
<span>{money(price, { target: 'EUR' })}</span>
{/* FIXED: no conversion, renders base currency as-is */}
<span>{money({ ...price, policy: 'FIXED' })}</span>
{/* Regional override: uses price.regional['EU'] if present */}
<span>{money(price, { region: 'EU' })}</span>
</div>
);
}Requires rates to be passed to <I18nProvider> from the server. Throws a dev-mode error when rates are missing for AUTO policy.
<I18nProvider
config={i18nConfig}
initialLocale="en"
initialMessages={{ common: { ... }, auth: { ... } }} // SSR hydration
rates={rateSnapshot} // FX snapshot for useMoney
missingKeyReporter={reporter} // optional observability
>
{children}
</I18nProvider>Load translation files from disk for SSR hydration. Skips missing files gracefully.
import '@apollo-deploy/aurebesh/server';
const state = await getI18nServerState('en', ['common', 'auth', 'settings'], {
publicDir: join(process.cwd(), 'public'),
pathTemplate: 'locales/{{locale}}/{{namespace}}.json', // default
});
// → { initialLocale: 'en', initialMessages: { common: {...}, auth: {...}, settings: {...} } }Detect the correct locale from an incoming request (proxy/middleware use case).
import '@apollo-deploy/aurebesh/server';
// In proxy.ts (Next.js 16)
const locale = resolveRequestLocale(
{
cookieLocale: request.cookies.get('apollo.locale')?.value,
acceptLanguage: request.headers.get('accept-language'),
},
i18nConfig, // { supportedLocales, defaultLocale }
);Follows cookie-first strategy — cookie wins over Accept-Language header. Falls back to defaultLocale when no supported match is found.
Parse an Accept-Language header with full quality-weight (q=) support.
detectLocaleFromAcceptLanguage('fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7', {
supportedLocales: ['en', 'es', 'fr'],
defaultLocale: 'en',
}); // → 'fr'Resolve and format a price against cached FX rates in a single call.
import '@apollo-deploy/aurebesh/server';
const price: Price = {
base: { amount: 99.99, currency: 'USD' },
regional: { EU: { amount: 84.99, currency: 'EUR' } },
};
await money(price, { target: 'EUR', locale: 'de-DE' }); // "84,99 €"
await money(price, { region: 'EU', locale: 'de-DE' }); // "84,99 €" (regional override)
await money(price, { locale: 'ja-JP', target: 'JPY' }); // "¥15,000"interface Price {
base: { amount: number; currency: string }; // ISO 4217, decimal units
regional?: Record<string, PriceAmount>; // keyed by region code, e.g. 'EU'
policy?: 'AUTO' | 'FIXED' | 'DISPLAY_ONLY';
}| Policy | Behaviour |
|---|---|
AUTO (default) |
Convert base amount to target currency via live FX rates |
FIXED |
Never convert — always render the base amount as-is (compliance use case) |
DISPLAY_ONLY |
Show base currency without conversion (informational display) |
import {
ExchangeRateHostProvider,
StaticFallbackProvider,
MultiProvider,
registerProvider,
ensureDefaultFxProviders,
} from '@apollo-deploy/@apollo-deploy/aurebesh/currency';
// Option A: use the built-in defaults (recommended)
ensureDefaultFxProviders();
// Registers: 'exchange-rate-host', 'static-fallback', 'multi:host+fallback'
// Option B: bring your own provider chain
const liveProvider = new ExchangeRateHostProvider({ apiKey: process.env.FX_KEY });
const fallback = new StaticFallbackProvider({
table: { USD: { USD: 1, EUR: 0.91, GBP: 0.78 } },
});
const chain = new MultiProvider([liveProvider, fallback], 'my-chain');
registerProvider(liveProvider);
registerProvider(fallback);
registerProvider(chain);Implement the CurrencyProvider interface to add any data source:
import '@apollo-deploy/aurebesh/currency';
class MyProvider implements CurrencyProvider {
readonly name = 'my-provider';
async fetchRates(base: string): Promise<RateSnapshot> {
const data = await myApi.getRates(base);
return {
base,
rates: data.rates,
fetchedAt: Date.now(),
source: this.name,
};
}
}getCachedRates uses the Next.js 16 Cache Components system:
import '@apollo-deploy/aurebesh/currency'; // server-only
// Read (cached 30 min, stale 15 min, expires 24h):
const snapshot = await getCachedRates('USD', 'multi:host+fallback');
// Invalidate when you know rates have changed:
invalidateFxRatesForBase('USD');
invalidateFxRates(); // invalidate ALL fx-rates:* entriesRequires cacheComponents: true in your app's next.config.ts.
// app/api/admin/fx-refresh/route.ts
import '@apollo-deploy/aurebesh/server';
import { verifyAdminToken } from '@/lib/auth';
export const POST = createFxRefreshHandler({
authorize: async (request) => {
const token = request.headers.get('x-admin-token');
return verifyAdminToken(token);
},
defaultBase: 'USD',
defaultWarm: true, // pre-warm cache after invalidation
});POST to /api/admin/fx-refresh with optional JSON body:
{ "scope": "base", "base": "USD", "warm": true }import '@apollo-deploy/aurebesh/observability';
const reporter = new InMemoryMissingTranslationReporter();
<I18nProvider config={i18nConfig} missingKeyReporter={reporter}>
{children}
</I18nProvider>
// Later — e.g. in an admin panel or CI check:
const metrics = reporter.snapshot(); // sorted by hit count descending
// [
// { key: 'billing.invoiceTitle', locale: 'es', namespace: 'billing', hits: 42, ... },
// ...
// ]
reporter.clear();import '@apollo-deploy/aurebesh/observability';
class DatadogMissingKeyReporter implements MissingTranslationReporter {
report(event: MissingTranslationEvent): void {
datadogRum.addError(new Error(`Missing i18n key: ${event.key}`), {
locale: event.locale,
namespace: event.namespace,
});
}
}Use getTranslationCoverageFromDisk in CI to gate deployments on translation completeness.
// scripts/check-translations.ts
import { join } from 'node:path';
import '@apollo-deploy/aurebesh/server';
const report = await getTranslationCoverageFromDisk({
publicDir: join(process.cwd(), 'public'),
locales: ['en', 'es', 'fr'],
namespaces: ['common', 'auth', 'settings'],
referenceLocale: 'en', // 'en' is the source of truth
});
console.log(`Overall coverage: ${(report.summary.coverage * 100).toFixed(1)}%`);
for (const locale of report.locales) {
if (locale.coverage < 0.9) {
console.error(`${locale.locale}: ${(locale.coverage * 100).toFixed(1)}% — below threshold`);
process.exit(1);
}
}Report shape:
{
generatedAt: 1713456789000,
referenceLocale: 'en',
namespaces: ['common', 'auth', 'settings'],
summary: {
localeCount: 3,
referenceKeyCount: 280,
translatedKeyCount: 252,
missingKeyCount: 28,
extraKeyCount: 4,
coverage: 0.9, // 0–1 ratio
},
locales: [
{
locale: 'es',
coverage: 0.94,
namespaces: [
{
locale: 'es',
namespace: 'settings',
coverage: 0.88,
missingKeys: ['billing.planName', 'billing.nextRenewal'],
extraKeys: ['legacy.oldKey'],
missingFile: false,
...
}
]
}
]
}// proxy.ts (Next.js 16 — replaces middleware.ts)
import { NextRequest, NextResponse } from 'next/server';
import '@apollo-deploy/aurebesh/server';
import { i18nConfig } from '@/lib/i18n.config';
export function proxy(request: NextRequest) {
const locale = resolveRequestLocale(
{
cookieLocale: request.cookies.get('apollo.locale')?.value ?? null,
acceptLanguage: request.headers.get('accept-language'),
},
i18nConfig,
);
const response = NextResponse.next();
response.headers.set('x-locale', locale);
return response;
}import '@apollo-deploy/aurebesh/config';
const config = createI18nConfig(['settings', 'billing', 'audit']);
// config.namespaces → ['common', 'auth', 'settings', 'billing', 'audit']
// config.defaultLocale → 'en'
// config.supportedLocales → ['en', 'es', 'fr']
// config.interpolation.prefix → '{{'
// config.interpolation.suffix → '}}'
// config.interpolation.escapeValue → true (HTML-escape by default)
// config.pluralization.simplifyPluralSuffix → true (_plural maps to 'other')
// config.loadPath → '/locales/{{locale}}/{{namespace}}.json'Edit src/config/locales.ts:
export const SUPPORTED_LOCALES = [
{ code: 'en', name: 'English', nativeName: 'English', direction: 'ltr' },
{ code: 'es', name: 'Spanish', nativeName: 'Español', direction: 'ltr' },
{ code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr' },
// add here:
{ code: 'de', name: 'German', nativeName: 'Deutsch', direction: 'ltr' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl' },
] as const;| Import path | Environment | Contents |
|---|---|---|
@apollo-deploy/aurebesh |
Both | Root types: TranslateFn, FormatService, UseTranslationResult + all config re-exports |
@apollo-deploy/aurebesh/config |
Both | I18nConfig, LocaleConfig, SupportedLocale, SUPPORTED_LOCALES, DEFAULT_LOCALE, BASE_NAMESPACES, baseI18nConfig, createI18nConfig |
@apollo-deploy/aurebesh/react |
Client only | I18nProvider, useTranslation, useMoney, buildFormatService, getPersistedLocale, persistLocale |
@apollo-deploy/aurebesh/server |
Server only | getI18nServerState, loadMessagesFromDisk, resolveRequestLocale, detectLocaleFromAcceptLanguage, money, resolveMoney, refreshFxRates, createFxRefreshHandler, getTranslationCoverageFromDisk |
@apollo-deploy/aurebesh/format |
Both | formatDate, formatTime, formatDateTime, formatRelative, formatNumber, formatPercent, formatList, formatMoney |
@apollo-deploy/aurebesh/currency |
Both (server for rates) | Price, PriceAmount, PricePolicy, RateSnapshot, RateInput, resolvePrice, pickRateSnapshot, isRateSnapshot, ExchangeRateHostProvider, StaticFallbackProvider, MultiProvider, registerProvider, ensureDefaultFxProviders, getCachedRates, invalidateFxRates |
@apollo-deploy/aurebesh/observability |
Both | MissingTranslationReporter, MissingTranslationEvent, MissingTranslationMetric, InMemoryMissingTranslationReporter |
| Dependency | Version |
|---|---|
| Node.js | 20+ |
| TypeScript | 5.x |
| Next.js | 16+ (peer dep) |
| React | 18+ or 19+ (peer dep) |
cacheComponents: true must be set in next.config.ts for FX rate caching to function.
# Install
pnpm install
# Typecheck
pnpm -F aurebesh build
# Tests
pnpm -F aurebesh test
# Watch mode
pnpm -F aurebesh test:watchTranslation JSON files live in each app's public/locales/ directory — the package itself ships no translations.
MIT