UNPKG

@astrojs/starlight

Version:

Build beautiful, high-performance documentation websites with Astro

135 lines (119 loc) 4.86 kB
import i18next, { type ExistsFunction, type TFunction } from 'i18next'; import type { i18nSchemaOutput } from '../schemas/i18n'; import builtinTranslations from '../translations/index'; import { BuiltInDefaultLocale } from './i18n'; import type { StarlightConfig } from './user-config'; import type { UserI18nKeys, UserI18nSchema } from './translations'; /** * The namespace for i18next resources used by Starlight. * All translations handled by Starlight are stored in the same namespace and Starlight always use * a new instance of i18next configured for this namespace. */ export const I18nextNamespace = 'starlight' as const; export async function createTranslationSystem<T extends i18nSchemaOutput>( config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>, userTranslations: Record<string, T>, pluginTranslations: Record<string, T> = {} ) { const defaultLocale = config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang; const translations = { [defaultLocale]: buildResources( builtinTranslations[defaultLocale], builtinTranslations[stripLangRegion(defaultLocale)], pluginTranslations[defaultLocale], userTranslations[defaultLocale] ), }; if (config.locales) { for (const locale in config.locales) { const lang = localeToLang(locale, config.locales, config.defaultLocale); translations[lang] = buildResources( builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)], pluginTranslations[lang], userTranslations[lang] ); } } const i18n = i18next.createInstance(); await i18n.init({ resources: translations, fallbackLng: config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang, }); /** * Generate a utility function that returns UI strings for the given language. * * Also includes a few utility methods: * - `all()` method for getting the entire dictionary. * - `exists()` method for checking if a key exists in the dictionary. * - `dir()` method for getting the text direction of the locale. * * @param {string | undefined} [lang] * @example * const t = useTranslations('en'); * const label = t('search.label'); * // => 'Search' * const dictionary = t.all(); * // => { 'skipLink.label': 'Skip to content', 'search.label': 'Search', ... } * const exists = t.exists('search.label'); * // => true * const dir = t.dir(); * // => 'ltr' */ return (lang: string | undefined) => { lang ??= config.defaultLocale?.lang || BuiltInDefaultLocale.lang; const t = i18n.getFixedT(lang, I18nextNamespace) as I18nT; t.all = () => i18n.getResourceBundle(lang, I18nextNamespace) as ReturnType<I18nT['all']>; t.exists = (key, options) => i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options }); t.dir = (dirLang = lang) => i18n.dir(dirLang); return t; }; } /** * Strips the region subtag from a BCP-47 lang string. * @param {string} [lang] * @example * const lang = stripLangRegion('en-GB'); // => 'en' */ function stripLangRegion(lang: string) { return lang.replace(/-[a-zA-Z]{2}/, ''); } /** * Get the BCP-47 language tag for the given locale. * @param locale Locale string or `undefined` for the root locale. */ function localeToLang( locale: string | undefined, locales: StarlightConfig['locales'], defaultLocale: StarlightConfig['defaultLocale'] ): string { const lang = locale ? locales?.[locale]?.lang : locales?.root?.lang; const defaultLang = defaultLocale?.lang || defaultLocale?.locale; return lang || defaultLang || BuiltInDefaultLocale.lang; } type BuiltInStrings = (typeof builtinTranslations)['en']; /** Build an i18next resources dictionary by layering preferred translation sources. */ function buildResources<T extends Record<string, string | undefined>>( ...dictionaries: (T | BuiltInStrings | undefined)[] ): { [I18nextNamespace]: BuiltInStrings & T } { const dictionary: Partial<BuiltInStrings> = {}; // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`. for (const dict of dictionaries) { for (const key in dict) { const value = dict[key as keyof typeof dict]; if (value) dictionary[key as keyof typeof dictionary] = value; } } return { [I18nextNamespace]: dictionary as BuiltInStrings & T }; } // `keyof BuiltInStrings` and `UserI18nKeys` may contain some identical keys, e.g. the built-in UI // strings. We let TypeScript merge them into a single union type so that plugins with a TypeScript // configuration preventing `UserI18nKeys` to be properly inferred can still get auto-completion // for built-in UI strings. export type I18nKeys = keyof BuiltInStrings | UserI18nKeys | keyof StarlightApp.I18n; export type I18nT = TFunction<'starlight', undefined> & { all: () => UserI18nSchema; exists: ExistsFunction; dir: (lang?: string) => 'ltr' | 'rtl'; };