Bridge Training
i18n

Usage

How to integrate and use @workspace/i18n in your application.

Installation

Add the dependency to your app's package.json:

{
  "dependencies": {
    "@workspace/i18n": "workspace:*"
  }
}

Import Styles

The package ships a Tailwind CSS @source directive so Tailwind scans the component files and generates their classes. You must import it in your app's root stylesheet:

globals.css
@import "@workspace/i18n/styles.css";

This is required for the LanguageSelect, LanguageDrawer and LanguageBanner components to render correctly.

Constants

Language Codes

Use ALL_LANGUAGE_CODES for the full list, or define an app-specific subset:

import { ALL_LANGUAGE_CODES, Lang, type LanguageCode } from "@workspace/i18n";

// Full list (11 languages)
const allLanguages = ALL_LANGUAGE_CODES;

// App-specific subset
const SUPPORTED_LANGUAGES = ["fr", "en"] as const;
type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];

Type-Safe References

Use the Lang constant to avoid string typos:

import { Lang } from "@workspace/i18n";

const defaultLocale = Lang.FR; // "fr"
const fallback = Lang.EN;      // "en"

Language Names

Native display names for each language, useful for UI labels:

import { LANGUAGE_NAMES } from "@workspace/i18n";

LANGUAGE_NAMES.fr; // "Français"
LANGUAGE_NAMES.en; // "English"
LANGUAGE_NAMES.de; // "Deutsch"

OpenGraph Locales

Standard language_TERRITORY format for meta tags and social sharing:

import { OG_LOCALES } from "@workspace/i18n";

OG_LOCALES.fr; // "fr_FR"
OG_LOCALES.en; // "en_US"

Language Detection

Detect the user's preferred language using browser signals:

import { detectPreferredLanguage, saveLanguagePreference } from "@workspace/i18n";

// Priority: localStorage → navigator.language → fallback
const preferred = detectPreferredLanguage(ALL_LANGUAGE_CODES, Lang.FR);

// Save user's explicit choice
saveLanguagePreference(Lang.EN);

The detection works in both SSR and browser contexts (returns the fallback on the server).

Validation

Parse and Validate

import { parseLanguage, isValidLanguage } from "@workspace/i18n";

// Validate and return a supported language (or fallback)
const lang = parseLanguage(userInput, Lang.FR, ["fr", "en"] as const);

// Type guard
if (isValidLanguage(someString)) {
  // someString is LanguageCode
}

Selector Options

Generate { value, label } options for dropdown components:

import {
  getLanguageSelectorOptions,
  ALL_LANGUAGE_SELECTOR_OPTIONS,
} from "@workspace/i18n";

// Pre-computed options for all languages
ALL_LANGUAGE_SELECTOR_OPTIONS;
// [{ value: "ca", label: "Català" }, { value: "de", label: "Deutsch" }, ...]

// Options for an app-specific subset
const options = getLanguageSelectorOptions(["fr", "en"] as const);
// [{ value: "fr", label: "Français" }, { value: "en", label: "English" }]

Content Languages

UI languages are limited to ALL_LANGUAGE_CODES, but content (a deal, a label) can be written in any language. For those pickers, use the world-language helpers:

import {
  getLanguageDisplayName,
  getWorldLanguageSelectorOptions,
  WORLD_LANGUAGE_CODES,
} from "@workspace/i18n";

const options = getWorldLanguageSelectorOptions(i18n.language); // every ISO 639-1 code, localized + sorted

getLanguageDisplayName("de", i18n.language); // "allemand" when i18n.language is "fr"

URL Locale Replacement

For apps with locale-prefixed URLs (e.g. /fr/about → /en/about):

import { replaceLocaleInPathname } from "@workspace/i18n";

replaceLocaleInPathname("/fr/about", "en"); // "/en/about"

Components

LanguageSelect

A language dropdown built on the shared Radix UI Select component.

Components are imported from their own sub-path, never from the root entry.

import { ALL_LANGUAGE_SELECTOR_OPTIONS } from "@workspace/i18n";
import { LanguageSelect } from "@workspace/i18n/components/language-select";

<LanguageSelect
  languages={ALL_LANGUAGE_SELECTOR_OPTIONS}
  value={currentLang}
  onValueChange={setLanguage}
/>

Display modes:

ModeDescription
"label"Language name only (default)
"icon"Globe icon only (name is sr-only)
"icon-and-label"Globe icon + language name
// Icon-only for compact UI
<LanguageSelect
  display="icon"
  languages={options}
  value={lang}
  onValueChange={setLang}
/>

LanguageDrawer

A mobile alternative to LanguageSelect: a globe icon button opening a bottom drawer.

import { LanguageDrawer } from "@workspace/i18n/components/language-drawer";

<LanguageDrawer
  label={label}
  languages={ALL_LANGUAGE_SELECTOR_OPTIONS}
  onValueChange={setLanguage}
  value={currentLang}
/>

LanguageBanner

A banner that suggests switching to the user's detected preferred language. Shows only when the detected language differs from the current one.

import { ALL_LANGUAGE_CODES } from "@workspace/i18n";
import { LanguageBanner } from "@workspace/i18n/components/language-banner";

<LanguageBanner
  currentLanguage={locale}
  supportedLanguages={ALL_LANGUAGE_CODES}
  onSwitchLanguage={(lang) => switchTo(lang)}
/>

Behavior:

  1. Detects preferred language via detectPreferredLanguage()
  2. Displays banner if detected language differs from current
  3. "Switch" button — calls onSwitchLanguage (label shown in the suggested language)
  4. "Stay" button — saves preference to localStorage, calls onStayInCurrentLanguage, closes banner
  5. "X" button — dismisses without saving (banner reappears on next visit)

See Components for the full props and the Localized compound component.

Custom labels:

<LanguageBanner
  currentLanguage={locale}
  supportedLanguages={ALL_LANGUAGE_CODES}
  labels={{
    message: (langName) => `Il semble que vous préfériez ${langName}`,
    stayIn: (langName) => `Rester en ${langName}`,
  }}
  onSwitchLanguage={handleSwitch}
/>

Integration Patterns

URL-Based Routing (Next.js)

Used in apps/web — language is part of the URL (/fr/about, /en/about).

Configuration:

lib/i18n-config.ts
import {
  ALL_LANGUAGE_CODES,
  LANGUAGE_NAMES,
  type LanguageCode,
  OG_LOCALES,
  parseLanguage,
} from "@workspace/i18n";

export const i18n = {
  defaultLocale: "fr" as const satisfies LanguageCode,
  locales: ALL_LANGUAGE_CODES,
} as const;

export const localeNames = LANGUAGE_NAMES;
export const ogLocales = OG_LOCALES;

export function parseLocale(lang: string): LanguageCode {
  return parseLanguage(lang, i18n.defaultLocale, i18n.locales);
}

Language banner with URL switching:

getLocalizedPath is an apps/web helper that translates localized slugs (themes) and otherwise swaps the locale prefix; for plain /{locale}/… routes, replaceLocaleInPathname does the same job.

components/language-banner-wrapper.tsx
"use client";

import { ALL_LANGUAGE_CODES, type LanguageCode } from "@workspace/i18n";
import { LanguageBanner } from "@workspace/i18n/components/language-banner";
import { usePathname, useRouter } from "next/navigation";
import { getLocalizedPath } from "@/lib/localized-path";

export function LanguageBannerWrapper({ locale, dictionary }: LanguageBannerWrapperProps) {
  const pathname = usePathname();
  const router = useRouter();

  const handleSwitchLanguage = (targetLocale: LanguageCode) => {
    router.push(getLocalizedPath(pathname, locale, targetLocale));
  };

  return (
    <LanguageBanner
      currentLanguage={locale}
      labels={{
        message: (langName) => `${dictionary.message} ${langName}`,
        stayIn: (langName) => `${dictionary.stayIn} ${langName}`,
      }}
      onSwitchLanguage={handleSwitchLanguage}
      supportedLanguages={ALL_LANGUAGE_CODES}
    />
  );
}

Language selector: apps/web renders LanguageSelect from lg up and LanguageDrawer below, both fed with ALL_LANGUAGE_SELECTOR_OPTIONS and the same router.push(getLocalizedPath(...)) handler.

i18next-Based (Vite + React)

Used in apps/app and apps/player — language is managed by i18next state, translations are fetched over HTTP.

Configuration with app-specific subset:

lib/i18n/constants.ts
import { getLanguageSelectorOptions, parseLanguage } from "@workspace/i18n";

export const SUPPORTED_LANGUAGES = ["fr", "en"] as const;
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
export const DEFAULT_LANGUAGE: SupportedLanguage = "fr";

// Generate selector options with native language labels
export const LANGUAGES = getLanguageSelectorOptions(SUPPORTED_LANGUAGES);

// Parse and validate language strings
export const getSupportedLanguage = (language: string | undefined | null) =>
  parseLanguage(language, DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES);

Language selector with i18next:

components/fields/language-field.tsx
import { LanguageSelect } from "@workspace/i18n/components/language-select";
import { LANGUAGES } from "@/lib/i18n/constants";

<LanguageSelect
  className="w-full"
  id={id}
  languages={LANGUAGES}
  onValueChange={(value) => i18n.changeLanguage(value)}
  placeholder={placeholder}
  value={currentLang}
/>

Language banner with i18next:

components/unauthenticated/public-language-banner.tsx
import { LanguageBanner } from "@workspace/i18n/components/language-banner";
import {
  getSupportedLanguage,
  SUPPORTED_LANGUAGES,
  type SupportedLanguage,
} from "@/lib/i18n/constants";

export function PublicLanguageBanner() {
  const { i18n } = useCommonTranslation();
  const currentLanguage = getSupportedLanguage(i18n.language);

  return (
    <LanguageBanner
      currentLanguage={currentLanguage}
      onSwitchLanguage={(next: SupportedLanguage) => i18n.changeLanguage(next)}
      supportedLanguages={SUPPORTED_LANGUAGES}
    />
  );
}

Loading translations: the JSON files come from @workspace/i18n-messages; the @workspace/i18n/vite and @workspace/i18n/load-path helpers copy them into the build and version their URLs. See Serving Translations.

Translation Messages

@workspace/i18n holds no translation strings. All messages, namespaces and the generation pipeline live in @workspace/i18n-messages. It also exports primitivesResources, a static i18next resources object bundling the primitives and rotation namespaces for every language, for setups that initialize i18next without the HTTP backend:

import { primitivesResources } from "@workspace/i18n-messages";

primitivesResources.fr.primitives;
primitivesResources.fr.rotation;

Summary

AspectURL-Based (web)i18next (app, player)
LanguagesAll 11app: fr, en · player: all 11
Switchingrouter.push()i18n.changeLanguage()
DetectiondetectPreferredLanguage() (banner)i18next-browser-languagedetector
ComponentsLanguageBanner + LanguageSelect + LanguageDrawerLanguageSelect + LanguageBanner (app only)
Styles@import "@workspace/i18n/styles.css"@import "@workspace/i18n/styles.css" (app)
Translations@workspace/i18n-messages/web dictionariesHTTP backend + @workspace/i18n/vite / load-path