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:
@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:
| Mode | Description |
|---|---|
"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:
- Detects preferred language via
detectPreferredLanguage() - Displays banner if detected language differs from current
- "Switch" button — calls
onSwitchLanguage(label shown in the suggested language) - "Stay" button — saves preference to localStorage, calls
onStayInCurrentLanguage, closes banner - "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:
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.
"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:
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:
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:
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
| Aspect | URL-Based (web) | i18next (app, player) |
|---|---|---|
| Languages | All 11 | app: fr, en · player: all 11 |
| Switching | router.push() | i18n.changeLanguage() |
| Detection | detectPreferredLanguage() (banner) | i18next-browser-languagedetector |
| Components | LanguageBanner + LanguageSelect + LanguageDrawer | LanguageSelect + LanguageBanner (app only) |
| Styles | @import "@workspace/i18n/styles.css" | @import "@workspace/i18n/styles.css" (app) |
| Translations | @workspace/i18n-messages/web dictionaries | HTTP backend + @workspace/i18n/vite / load-path |