mirror of
https://github.com/ente-io/ente.git
synced 2025-05-24 03:59:22 +00:00
246 lines
8.8 KiB
TypeScript
246 lines
8.8 KiB
TypeScript
import { isDevBuild } from "@/base/env";
|
|
import log from "@/base/log";
|
|
import { includes } from "@/utils/type-guards";
|
|
import { getUserLocales } from "get-user-locale";
|
|
import i18n from "i18next";
|
|
import resourcesToBackend from "i18next-resources-to-backend";
|
|
import { initReactI18next } from "react-i18next";
|
|
|
|
/**
|
|
* List of all {@link SupportedLocale}s.
|
|
*
|
|
* Locales are combinations of a language code, and an optional region code.
|
|
*
|
|
* For example, "en", "en-US", "en-IN" (Indian English), "pt" (Portuguese),
|
|
* "pt-BR" (Brazilian Portuguese).
|
|
*
|
|
* In our Crowdin Project, we have work-in-progress translations into more
|
|
* languages than this. When a translation reaches a high enough coverage, say
|
|
* 90%, then we manually add it to this list of supported languages.
|
|
*/
|
|
export const supportedLocales = [
|
|
"en-US" /* English */,
|
|
"fr-FR" /* French */,
|
|
"de-DE" /* German */,
|
|
"zh-CN" /* Simplified Chinese */,
|
|
"nl-NL" /* Dutch */,
|
|
"es-ES" /* Spanish */,
|
|
"pt-BR" /* Portuguese, Brazilian */,
|
|
"ru-RU" /* Russian */,
|
|
"pl-PL" /* Polish */,
|
|
] as const;
|
|
|
|
/** The type of {@link supportedLocales}. */
|
|
export type SupportedLocale = (typeof supportedLocales)[number];
|
|
|
|
const defaultLocale: SupportedLocale = "en-US";
|
|
|
|
/**
|
|
* Load translations and add custom formatters.
|
|
*
|
|
* Localization and related concerns (aka "internationalization", or "i18n") for
|
|
* our apps is handled by i18n framework.
|
|
*
|
|
* In addition to the base i18next package, we use two of its plugins:
|
|
*
|
|
* - i18next-http-backend, for loading the JSON files containin the translations
|
|
* at runtime, and
|
|
*
|
|
* - react-i18next, which adds React specific APIs
|
|
*
|
|
* This function also adds our custom formatters. They can be used within the
|
|
* translated strings by using `{{val, formatterName}}`. For more details, see
|
|
* https://www.i18next.com/translation-function/formatting.
|
|
*
|
|
* Our custom formatters:
|
|
*
|
|
* - "date": Formats an epoch microsecond value into a string containing the
|
|
* year, month and day of the the date. For example, under "en-US" it'll
|
|
* produce a string like "July 19, 2024".
|
|
*/
|
|
export const setupI18n = async () => {
|
|
const localeString = localStorage.getItem("locale") ?? undefined;
|
|
const locale = closestSupportedLocale(localeString);
|
|
|
|
// https://www.i18next.com/overview/api
|
|
await i18n
|
|
// i18next-resources-to-backend: Use webpack to bundle translation, but
|
|
// still fetch them lazily using a dynamic import.
|
|
//
|
|
// The benefit of this is that, unlike the http backend that uses files
|
|
// from the public folder, these JSON files are content hash named and
|
|
// eminently cacheable.
|
|
//
|
|
// https://github.com/i18next/i18next-resources-to-backend
|
|
.use(
|
|
resourcesToBackend(
|
|
(language: string, namespace: string) =>
|
|
import(`./locales/${language}/${namespace}.json`),
|
|
),
|
|
)
|
|
// react-i18next: React support
|
|
// Pass the i18n instance to react-i18next.
|
|
.use(initReactI18next)
|
|
// Initialize i18next
|
|
// Option docs: https://www.i18next.com/overview/configuration-options
|
|
.init({
|
|
debug: isDevBuild,
|
|
// i18next calls it language, but it really is the locale
|
|
lng: locale,
|
|
// Tell i18next about the locales we support
|
|
supportedLngs: supportedLocales,
|
|
// Ask it to fetch only exact matches
|
|
//
|
|
// By default, if the lng was set to, say, en-GB, i18n would make
|
|
// network requests for ["en-GB", "en", "dev"] (where dev is the
|
|
// default fallback). By setting `load` to "currentOnly", we ask
|
|
// i18next to only try and fetch "en-GB" (i.e. the exact match).
|
|
load: "currentOnly",
|
|
// Disallow empty strings as valid translations.
|
|
//
|
|
// This way, empty strings will fallback to `fallbackLng`
|
|
returnEmptyString: false,
|
|
// The language to use if translation for a particular key in the
|
|
// current `lng` is not available.
|
|
fallbackLng: defaultLocale,
|
|
interpolation: {
|
|
escapeValue: false, // not needed for react as it escapes by default
|
|
},
|
|
react: {
|
|
useSuspense: false,
|
|
transKeepBasicHtmlNodesFor: [
|
|
"div",
|
|
"strong",
|
|
"h2",
|
|
"span",
|
|
"code",
|
|
"p",
|
|
"br",
|
|
],
|
|
},
|
|
});
|
|
|
|
// To use this in a translation, interpolate as `{{val, date}}`.
|
|
i18n.services.formatter?.addCached("date", (locale) => {
|
|
// The "long" dateStyle:
|
|
//
|
|
// - Includes: year (y), long-month (MMMM), day (d)
|
|
// - English pattern examples: MMMM d, y ("September 14, 1999")
|
|
//
|
|
const formatter = Intl.DateTimeFormat(locale, { dateStyle: "long" });
|
|
// Value is an epoch microsecond so that we can directly pass the
|
|
// timestamps we get from our API responses. The formatter expects
|
|
// milliseconds, so divide by 1000.
|
|
return (val) => formatter.format(val / 1000);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Return the closest / best matching {@link SupportedLocale}.
|
|
*
|
|
* It takes as input a {@link savedLocaleString}, which denotes the user's
|
|
* explicitly chosen preference (which we then persist in local storage).
|
|
* Subsequently, we use this to (usually literally) return the supported locale
|
|
* that it represents.
|
|
*
|
|
* If {@link savedLocaleString} is `undefined`, it tries to deduce the closest
|
|
* {@link SupportedLocale} that matches the browser's locale.
|
|
*/
|
|
const closestSupportedLocale = (
|
|
savedLocaleString?: string,
|
|
): SupportedLocale => {
|
|
const ss = savedLocaleString;
|
|
if (ss && includes(supportedLocales, ss)) return ss;
|
|
|
|
for (const ls of getUserLocales()) {
|
|
// Exact match
|
|
if (ls && includes(supportedLocales, ls)) return ls;
|
|
|
|
// Language match
|
|
if (ls.startsWith("en")) {
|
|
return "en-US";
|
|
} else if (ls.startsWith("fr")) {
|
|
return "fr-FR";
|
|
} else if (ls.startsWith("de")) {
|
|
return "de-DE";
|
|
} else if (ls.startsWith("zh")) {
|
|
return "zh-CN";
|
|
} else if (ls.startsWith("nl")) {
|
|
return "nl-NL";
|
|
} else if (ls.startsWith("es")) {
|
|
return "es-ES";
|
|
} else if (ls.startsWith("pt-BR")) {
|
|
// We'll never get here (it'd already be an exact match), just kept
|
|
// to keep this list consistent.
|
|
return "pt-BR";
|
|
} else if (ls.startsWith("ru")) {
|
|
return "ru-RU";
|
|
} else if (ls.startsWith("pl")) {
|
|
return "pl-PL";
|
|
}
|
|
}
|
|
|
|
// Fallback
|
|
return defaultLocale;
|
|
};
|
|
|
|
/**
|
|
* Return the locale that is currently being used to show the app's UI.
|
|
*
|
|
* Note that this may be different from the user's locale. For example, the
|
|
* browser might be set to en-GB, but since we don't support that specific
|
|
* variant of English, this value will be (say) en-US.
|
|
*/
|
|
export const getLocaleInUse = (): SupportedLocale => {
|
|
const locale = i18n.resolvedLanguage;
|
|
if (locale && includes(supportedLocales, locale)) {
|
|
return locale;
|
|
} else {
|
|
// This shouldn't have happened. Log an error to attract attention.
|
|
log.error(
|
|
`Expected the i18next locale to be one of the supported values, but instead found ${locale}`,
|
|
);
|
|
return defaultLocale;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the locale that should be used to show the app's UI.
|
|
*
|
|
* This updates both the i18next state, and also the corresponding user
|
|
* preference that is stored in local storage.
|
|
*/
|
|
export const setLocaleInUse = async (locale: SupportedLocale) => {
|
|
localStorage.setItem("locale", locale);
|
|
return i18n.changeLanguage(locale);
|
|
};
|
|
|
|
/**
|
|
* A no-op marker for strings that, for various reasons, pending addition to the
|
|
* translation dataset.
|
|
*
|
|
* This function does nothing, it just returns back the passed it string
|
|
* verbatim. It is only kept as a way for us to keep track of strings which
|
|
* we've not yet added to the list of strings that should be translated (e.g.
|
|
* perhaps we're awaiting feedback on the copy).
|
|
*
|
|
* It is the sibling of the {@link t} function provided by i18next.
|
|
*
|
|
* See also: {@link ut}.
|
|
*/
|
|
export const pt = (s: string) => s;
|
|
|
|
/**
|
|
* A no-op marker for strings that, for various reasons, are not translated.
|
|
*
|
|
* This function does nothing, it just returns back the passed it string
|
|
* verbatim. It is only kept as a way for us to keep track of strings that are
|
|
* not translated (and for some reason, are currently not meant to be), but
|
|
* still are user visible.
|
|
*
|
|
* It is the sibling of the {@link t} function provided by i18next.
|
|
*
|
|
* See also: {@link pt}.
|
|
*/
|
|
export const ut = (s: string) => s;
|