SDKs
featurely-i18n
Translations client with ETag caching, pluralization, and namespace lazy-loading.
featurely-i18n
Installation
npm install featurely-i18nRequires a public:read API key.
Features
- ETag-based caching — zero bytes transferred when nothing changed (304 Not Modified)
- Instant rendering from
localStoragecache on repeat visits - ICU pluralization format with CLDR plural rules
- Runtime locale switching with
onChange()emitter - Namespace lazy-loading via
loadNamespace() preload()to warm cache before switching locale- SSR / React Server Component support via
createServerI18n() - Missing key telemetry — auto-reports missing keys to the dashboard for your team to catch
Configuration options
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | API key with public:read permission |
projectId | string | Yes | Your project ID |
locale | string | Yes | BCP 47 locale code (e.g. en, no, fr) |
apiUrl | string | No | Custom API endpoint |
fallbackLocale | string | No | Locale to use when a key is missing in the active locale |
debug | boolean | No | Log debug info to the console. Default: false |
initialData | Record<string, string> | No | Pre-loaded translations for SSR — skips localStorage and network |
namespace | string | No | Load only this namespace on init, reducing payload size |
reportMissingKeys | boolean | No | Send telemetry to Featurely when a key is missing. Default: true |
Basic usage
import { FeaturelyI18n } from 'featurely-i18n';
const i18n = new FeaturelyI18n({
apiKey: process.env.NEXT_PUBLIC_FEATURELY_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FEATURELY_PROJECT_ID!,
locale: 'en',
fallbackLocale: 'en',
});
await i18n.init();
// Simple key lookup
console.log(i18n.t('nav.home')); // "Home"
// Fallback when key is missing (second argument)
console.log(i18n.t('missing.key', 'Default text')); // "Default text"
// Interpolation (third argument)
console.log(i18n.t('greeting', undefined, { name: 'World' })); // "Hello, World!"
// Pluralization via ICU format (store in dashboard: "You have {count, plural, one {# item} other {# items}}")
console.log(i18n.t('cart.items', undefined, { count: 3 })); // "You have 3 items"The t() signature is:
t(key: string, fallbackText?: string, vars?: Record<string, string | number>): stringkey— translation keyfallbackText— text to show when key is not foundvars— interpolation variables; includecount(a number) to activate ICU pluralization
Locale switching
// Preload translations before switching to avoid any flash
await i18n.preload('no');
await i18n.setLocale('no');
// Listen for locale/translation changes (callback receives no arguments)
const unsubscribe = i18n.onChange(() => {
console.log('Locale is now:', i18n.getLocale());
});
// Stop listening
unsubscribe();Namespace lazy-loading
const i18n = new FeaturelyI18n({
apiKey: 'ft_live_your_api_key_here',
projectId: 'your-project-id',
locale: 'en',
namespace: 'common', // load only "common" namespace on init
});
await i18n.init();
// Load another namespace on demand (e.g. when user navigates to admin area)
await i18n.loadNamespace('admin');
console.log(i18n.t('admin.title'));React integration
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { FeaturelyI18n } from 'featurely-i18n';
const I18nContext = createContext<FeaturelyI18n | null>(null);
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [, forceUpdate] = useState(0);
const [i18n] = useState(() => new FeaturelyI18n({
apiKey: process.env.NEXT_PUBLIC_FEATURELY_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FEATURELY_PROJECT_ID!,
locale: 'en',
}));
useEffect(() => {
i18n.init();
// Re-render when translations change (e.g. after locale switch)
return i18n.onChange(() => forceUpdate((n) => n + 1));
}, [i18n]);
return <I18nContext.Provider value={i18n}>{children}</I18nContext.Provider>;
}
export function useTranslation() {
const i18n = useContext(I18nContext);
if (!i18n) throw new Error('useTranslation must be used inside I18nProvider');
return {
t: i18n.t.bind(i18n),
setLocale: i18n.setLocale.bind(i18n),
locale: i18n.getLocale(),
};
}Next.js SSR
Use createServerI18n() to fetch translations server-side and pass them as initialData to the client, eliminating a second network request:
// app/page.tsx — Server Component
import { createServerI18n } from 'featurely-i18n';
export default async function Page() {
const i18n = await createServerI18n({
apiKey: process.env.FEATURELY_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FEATURELY_PROJECT_ID!,
locale: 'en',
namespace: 'common',
});
return (
<>
{/* Render directly in the Server Component */}
<h1>{i18n.t('hero.title')}</h1>
{/* Pass translations to a Client Component — no second network call */}
<ClientNav initialData={i18n.translations} />
</>
);
}// components/ClientNav.tsx — Client Component
'use client';
import { FeaturelyI18n } from 'featurely-i18n';
export function ClientNav({ initialData }: { initialData: Record<string, string> }) {
// initialData pre-populates the SDK — init() is instant, no network call
const i18n = new FeaturelyI18n({
apiKey: process.env.NEXT_PUBLIC_FEATURELY_API_KEY!,
projectId: process.env.NEXT_PUBLIC_FEATURELY_PROJECT_ID!,
locale: 'en',
initialData,
});
await i18n.init(); // no-op when initialData is provided
return <nav>{i18n.t('nav.home')}</nav>;
}ICU pluralization
Store translations in the dashboard using ICU syntax, then pass count in vars:
| Dashboard value | Call | Result |
|---|---|---|
You have {count, plural, one {# item} other {# items}} | i18n.t('cart.items', undefined, { count: 1 }) | You have 1 item |
You have {count, plural, one {# item} other {# items}} | i18n.t('cart.items', undefined, { count: 5 }) | You have 5 items |
Supported CLDR plural categories: zero, one, two, few, many, other. Covers English, Norwegian, German, French, Arabic, Russian, Polish, and more.
All instance methods
| Method | Description |
|---|---|
init() | Load translations (from cache, then validate against server) |
t(key, fallbackText?, vars?) | Translate a key with optional interpolation and pluralization |
setLocale(locale) | Switch to a new locale; notifies onChange listeners |
preload(locale) | Pre-fetch a locale into cache without switching |
loadNamespace(ns) | Load an additional namespace at runtime |
onChange(listener) | Register a change listener; returns an unsubscribe function |
getLocale() | Returns the currently active locale |
isReady() | Returns true when the initial fetch has completed |
getKeys() | Returns all loaded translation keys for the current locale |
TypeScript exports
FeaturelyI18n, I18nConfig, TranslationResponse, ServerI18n, createServerI18n