Featurely Docs
SDKs

featurely-i18n

Translations client with ETag caching, pluralization, and namespace lazy-loading.

featurely-i18n

Installation

npm install featurely-i18n

Requires a public:read API key.

Features

  • ETag-based caching — zero bytes transferred when nothing changed (304 Not Modified)
  • Instant rendering from localStorage cache 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

OptionTypeRequiredDescription
apiKeystringYesAPI key with public:read permission
projectIdstringYesYour project ID
localestringYesBCP 47 locale code (e.g. en, no, fr)
apiUrlstringNoCustom API endpoint
fallbackLocalestringNoLocale to use when a key is missing in the active locale
debugbooleanNoLog debug info to the console. Default: false
initialDataRecord<string, string>NoPre-loaded translations for SSR — skips localStorage and network
namespacestringNoLoad only this namespace on init, reducing payload size
reportMissingKeysbooleanNoSend 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>): string
  • key — translation key
  • fallbackText — text to show when key is not found
  • vars — interpolation variables; include count (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 valueCallResult
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

MethodDescription
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