UNPKG

remix-i18next

Version:

The easiest way to translate your Full Stack React Router apps

206 lines (179 loc) 6.03 kB
import type { Cookie, SessionStorage } from "react-router"; import { getClientLocales } from "./get-client-locales.js"; import { pick } from "./parser.js"; export interface LanguageDetectorOption { /** * Define the list of supported languages, this is used to determine if one of * the languages requested by the user is supported by the application. * This should be be same as the supportedLngs in the i18next options. */ supportedLanguages: string[]; /** * Define the fallback language that it's going to be used in the case user * expected language is not supported. * This should be be same as the fallbackLng in the i18next options. */ fallbackLanguage: string; /** * If you want to use a cookie to store the user preferred language, you can * pass the Cookie object here. */ cookie?: Cookie; /** * If you want to use a session to store the user preferred language, you can * pass the SessionStorage object here. * When this is not defined, getting the locale will ignore the session. */ sessionStorage?: SessionStorage; /** * If defined a sessionStorage and want to change the default key used to * store the user preferred language, you can pass the key here. * @default "lng" */ sessionKey?: string; /** * If you want to use search parameters for language detection and want to * change the default key used to for the parameter name, * you can pass the key here. * @default "lng" */ searchParamKey?: string; /** * The order the library will use to detect the user preferred language. * By default the order is * - searchParams * - cookie * - session * - header * If customized, a an extra `custom` option can be added to the order. * And finally the fallback language. */ order?: Array<"searchParams" | "cookie" | "session" | "header" | "custom">; /** * A function that can be used to find the locale based on the request object * using any custom logic you want. * This can be useful to get the locale from the URL pathname, or to query it * from the database or fetch it from an API. * @param request The request object received by the server. */ findLocale?(request: Request): Promise<string | Array<string> | null>; } /** * The LanguageDetector contains the logic to detect the user preferred language * fully server-side by using a SessionStorage, Cookie, URLSearchParams, or * Headers. */ export class LanguageDetector { constructor(private options: LanguageDetectorOption) { this.isSessionOnly(options); this.isCookieOnly(options); } private isSessionOnly(options: LanguageDetectorOption) { if ( options.order?.length === 1 && options.order[0] === "session" && !options.sessionStorage ) { throw new Error( "You need a sessionStorage if you want to only get the locale from the session", ); } } private isCookieOnly(options: LanguageDetectorOption) { if ( options.order?.length === 1 && options.order[0] === "cookie" && !options.cookie ) { throw new Error( "You need a cookie if you want to only get the locale from the cookie", ); } } public async detect(request: Request): Promise<string> { let order = this.options.order ?? this.defaultOrder; for (let method of order) { let locale: string | null = null; if (method === "searchParams") { locale = this.fromSearchParams(request); } if (method === "cookie") { locale = await this.fromCookie(request); } if (method === "session") { locale = await this.fromSessionStorage(request); } if (method === "header") { locale = this.fromHeader(request); } if (method === "custom") { locale = await this.fromCustom(request); } if (locale) return locale; } return this.options.fallbackLanguage; } private get defaultOrder() { let order: Array< "searchParams" | "cookie" | "session" | "header" | "custom" > = ["searchParams", "cookie", "session", "header"]; if (this.options.findLocale) order.unshift("custom"); return order; } private fromSearchParams(request: Request): string | null { let url = new URL(request.url); if (!url.searchParams.has(this.options.searchParamKey ?? "lng")) { return null; } return this.fromSupported( url.searchParams.get(this.options.searchParamKey ?? "lng"), ); } private async fromCookie(request: Request): Promise<string | null> { if (!this.options.cookie) return null; let cookie = this.options.cookie; let lng = await cookie.parse(request.headers.get("Cookie")); if (typeof lng !== "string" || !lng) return null; return this.fromSupported(lng); } private async fromSessionStorage(request: Request): Promise<string | null> { if (!this.options.sessionStorage) return null; let session = await this.options.sessionStorage.getSession( request.headers.get("Cookie"), ); let lng = session.get(this.options.sessionKey ?? "lng"); if (!lng) return null; return this.fromSupported(lng); } private fromHeader(request: Request): string | null { let locales = getClientLocales(request); if (!locales) return null; if (Array.isArray(locales)) return this.fromSupported(locales.join(",")); return this.fromSupported(locales); } private async fromCustom(request: Request): Promise<string | null> { if (!this.options.findLocale) { throw new ReferenceError( "You tried to find a locale using `findLocale` but it iss not defined. Change your order to not include `custom` or provide a findLocale functions.", ); } let locales = await this.options.findLocale(request); if (!locales) return null; if (Array.isArray(locales)) return this.fromSupported(locales.join(",")); return this.fromSupported(locales); } private fromSupported(language: string | null) { return ( pick( this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: false }, ) || pick( this.options.supportedLanguages, language ?? this.options.fallbackLanguage, { loose: true }, ) ); } }