Files
waseel/lib/i18n.tsx

130 lines
3.3 KiB
TypeScript

import {
createContext,
useContext,
useEffect,
useMemo,
type ReactNode,
} from "react";
import { useSettingsStore, type Lang } from "@/lib/settings";
import { ar } from "@/lib/translations/ar";
import { en } from "@/lib/translations/en";
import { fr } from "@/lib/translations/fr";
export type Vars = Record<string, string | number>;
/**
* Lightweight i18n. No external dependency: three languages, dot-path keys,
* `{var}` interpolation, and a `one`/`other`/`zero` plural convention that
* covers the few counts this app shows (minutes, seats).
*/
const DICTS: Record<Lang, Record<string, unknown>> = { en, ar, fr };
const interpolate = (text: string, vars?: Vars): string =>
vars
? text.replace(/\{(\w+)\}/g, (_, key: string) => String(vars[key] ?? ""))
: text;
const readPath = (dict: Record<string, unknown>, path: string): unknown =>
path.split(".").reduce<unknown>(
(acc, segment) =>
acc && typeof acc === "object"
? (acc as Record<string, unknown>)[segment]
: undefined,
dict,
);
const translate = (
lang: Lang,
key: string,
vars?: Vars,
count?: number,
): string => {
const dict = DICTS[lang] ?? en;
if (count !== undefined) {
const pluralSuffix =
count === 0 ? "zero" : count === 1 ? "one" : "other";
const pluralValue = readPath(dict, `${key}.${pluralSuffix}`);
if (typeof pluralValue === "string") {
return interpolate(pluralValue, { n: count, ...vars });
}
}
const value = readPath(dict, key);
if (typeof value === "string") return interpolate(value, vars);
// Fall back to English, then to the key itself, so a missing translation
// never renders an empty string.
if (lang !== "en") {
const fallback = readPath(en, key);
if (typeof fallback === "string") return interpolate(fallback, vars);
}
return key;
};
/**
* Module-level translator for non-React helpers (`lib/utils.ts`,
* `lib/pricing.ts`) that can't call `useT`. The I18nProvider sets this on
* mount and whenever the language changes; until then it defaults to English.
*/
let activeLang: Lang = "en";
export const setTranslator = (lang: Lang) => {
activeLang = lang;
};
export const tr = (key: string, vars?: Vars, count?: number): string =>
translate(activeLang, key, vars, count);
type I18nContextValue = {
lang: Lang;
isRTL: boolean;
t: (key: string, vars?: Vars, count?: number) => string;
};
const I18nContext = createContext<I18nContextValue | null>(null);
export const I18nProvider = ({ children }: { children: ReactNode }) => {
const lang = useSettingsStore((state) => state.lang);
useEffect(() => {
setTranslator(lang);
}, [lang]);
const value = useMemo<I18nContextValue>(
() => ({
lang,
isRTL: lang === "ar",
t: (key, vars, count) => translate(lang, key, vars, count),
}),
[lang],
);
return (
<I18nContext.Provider value={value}>{children}</I18nContext.Provider>
);
};
export const useT = (): I18nContextValue["t"] => {
const context = useContext(I18nContext);
if (!context) {
throw new Error("useT must be used within I18nProvider.");
}
return context.t;
};
export const useI18n = (): I18nContextValue => {
const context = useContext(I18nContext);
if (!context) {
throw new Error("useI18n must be used within I18nProvider.");
}
return context;
};