Locale changes (#1650)
This commit is contained in:
commit
372a9c979e
14 changed files with 208 additions and 166 deletions
|
@ -10,9 +10,10 @@ import {
|
|||
import EnteSpinner from '@ente/shared/components/EnteSpinner';
|
||||
import AppNavbar from '@ente/shared/components/Navbar/app';
|
||||
import { useLocalState } from '@ente/shared/hooks/useLocalState';
|
||||
import { setupI18n } from '@ente/shared/i18n';
|
||||
import { setupI18n } from '@/ui/i18n';
|
||||
import HTTPService from '@ente/shared/network/HTTPService';
|
||||
import { LS_KEYS, getData } from '@ente/shared/storage/localStorage';
|
||||
import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers';
|
||||
import { getTheme } from '@ente/shared/themes';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
import createEmotionCache from '@ente/shared/themes/createEmotionCache';
|
||||
|
@ -63,7 +64,7 @@ export default function App(props: EnteAppProps) {
|
|||
const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
|
||||
|
||||
useEffect(() => {
|
||||
setupI18n().finally(() => setIsI18nReady(true));
|
||||
setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true));
|
||||
}, []);
|
||||
|
||||
const setupPackageName = () => {
|
||||
|
|
|
@ -32,11 +32,12 @@ import { EnteAppProps } from '@ente/shared/apps/types';
|
|||
import createEmotionCache from '@ente/shared/themes/createEmotionCache';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
import { SetTheme } from '@ente/shared/themes/types';
|
||||
import { setupI18n } from '@ente/shared/i18n';
|
||||
import { setupI18n } from '@/ui/i18n';
|
||||
import { useLocalState } from '@ente/shared/hooks/useLocalState';
|
||||
import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages';
|
||||
import { getTheme } from '@ente/shared/themes';
|
||||
import '../../public/css/global.css';
|
||||
import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers';
|
||||
|
||||
type AppContextType = {
|
||||
showNavBar: (show: boolean) => void;
|
||||
|
@ -80,7 +81,7 @@ export default function App(props: EnteAppProps) {
|
|||
|
||||
useEffect(() => {
|
||||
//setup i18n
|
||||
setupI18n().finally(() => setIsI18nReady(true));
|
||||
setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true));
|
||||
// set client package name in headers
|
||||
HTTPService.setHeaders({
|
||||
'X-Client-Package': CLIENT_PACKAGE_NAMES.get(APPS.AUTH),
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
"ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm",
|
||||
"file-type": "^16.5.4",
|
||||
"formik": "^2.1.5",
|
||||
"get-user-locale": "^2.1.3",
|
||||
"hdbscan": "0.0.1-alpha.5",
|
||||
"heic-convert": "^2.0.0",
|
||||
"idb": "^7.1.1",
|
||||
|
|
|
@ -1,42 +1,51 @@
|
|||
import DropdownInput, { DropdownOption } from 'components/DropdownInput';
|
||||
import { Language } from '@ente/shared/i18n/locale';
|
||||
import { useLocalState } from '@ente/shared/hooks/useLocalState';
|
||||
import { t } from 'i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getBestPossibleUserLocale } from '@ente/shared/i18n/utils';
|
||||
import {
|
||||
type SupportedLocale,
|
||||
supportedLocales,
|
||||
closestSupportedLocale,
|
||||
} from '@/ui/i18n';
|
||||
import { LS_KEYS } from '@ente/shared/storage/localStorage';
|
||||
import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers';
|
||||
|
||||
const getLocaleDisplayName = (l: Language) => {
|
||||
switch (l) {
|
||||
case Language.en:
|
||||
/**
|
||||
* Human readable name for each supported locale
|
||||
*
|
||||
* TODO (MR): This names themselves should be localized.
|
||||
*/
|
||||
export const localeName = (locale: SupportedLocale) => {
|
||||
switch (locale) {
|
||||
case 'en':
|
||||
return 'English';
|
||||
case Language.fr:
|
||||
case 'fr':
|
||||
return 'Français';
|
||||
case Language.zh:
|
||||
case 'zh':
|
||||
return '中文';
|
||||
case Language.nl:
|
||||
case 'nl':
|
||||
return 'Nederlands';
|
||||
case Language.es:
|
||||
case 'es':
|
||||
return 'Español';
|
||||
}
|
||||
};
|
||||
|
||||
const getLanguageOptions = (): DropdownOption<Language>[] => {
|
||||
return Object.values(Language).map((lang) => ({
|
||||
label: getLocaleDisplayName(lang),
|
||||
value: lang,
|
||||
const getLanguageOptions = (): DropdownOption<SupportedLocale>[] => {
|
||||
return supportedLocales.map((locale) => ({
|
||||
label: localeName(locale),
|
||||
value: locale,
|
||||
}));
|
||||
};
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const [userLocale, setUserLocale] = useLocalState(
|
||||
LS_KEYS.LOCALE,
|
||||
getBestPossibleUserLocale()
|
||||
closestSupportedLocale(getUserLocaleString())
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const updateCurrentLocale = (newLocale: Language) => {
|
||||
const updateCurrentLocale = (newLocale: SupportedLocale) => {
|
||||
setUserLocale(newLocale);
|
||||
router.reload();
|
||||
};
|
||||
|
|
|
@ -66,6 +66,7 @@ import { REDIRECTS } from 'constants/redirects';
|
|||
import {
|
||||
getLocalMapEnabled,
|
||||
getToken,
|
||||
getUserLocaleString,
|
||||
setLocalMapEnabled,
|
||||
} from '@ente/shared/storage/localStorage/helpers';
|
||||
import { isExportInProgress } from 'utils/export';
|
||||
|
@ -73,7 +74,7 @@ import { EnteAppProps } from '@ente/shared/apps/types';
|
|||
import createEmotionCache from '@ente/shared/themes/createEmotionCache';
|
||||
import { THEME_COLOR } from '@ente/shared/themes/constants';
|
||||
import { SetTheme } from '@ente/shared/themes/types';
|
||||
import { setupI18n } from '@ente/shared/i18n';
|
||||
import { setupI18n } from '@/ui/i18n';
|
||||
import { useLocalState } from '@ente/shared/hooks/useLocalState';
|
||||
import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages';
|
||||
import { getTheme } from '@ente/shared/themes';
|
||||
|
@ -162,7 +163,7 @@ export default function App(props: EnteAppProps) {
|
|||
|
||||
useEffect(() => {
|
||||
//setup i18n
|
||||
setupI18n().finally(() => setIsI18nReady(true));
|
||||
setupI18n(getUserLocaleString()).finally(() => setIsI18nReady(true));
|
||||
// set client package name in headers
|
||||
HTTPService.setHeaders({
|
||||
'X-Client-Package': CLIENT_PACKAGE_NAMES.get(APPS.PHOTOS),
|
||||
|
|
|
@ -33,5 +33,14 @@ Emotion itself comes in many parts, of which we need the following three:
|
|||
* "@emotion/react" - React interface to Emotion. In particular, we set this as
|
||||
the package that handles the transformation of JSX into JS (via the
|
||||
`jsxImportSource` property in `tsconfig.json`).
|
||||
* "@emotion/styled"
|
||||
|
||||
* "@emotion/styled" - Provides the `styled` utility, a la styled-components. We
|
||||
don't use it directly, instead we import it from `@mui/material`. However, MUI
|
||||
docs
|
||||
[mention](https://mui.com/material-ui/integrations/interoperability/#styled-components)
|
||||
that
|
||||
|
||||
> Keep `@emotion/styled` as a dependency of your project. Even if you never
|
||||
> use it explicitly, it's a peer dependency of `@mui/material`.
|
||||
|
||||
* "@emotion/server"
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { getBestPossibleUserLocale } from './utils';
|
||||
import { isDevBuild } from '@/utils/env';
|
||||
|
||||
/**
|
||||
* Load translations.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
export const setupI18n = async () => {
|
||||
// https://www.i18next.com/overview/api
|
||||
await i18n
|
||||
// i18next-http-backend: Asynchronously loads translations over HTTP
|
||||
// https://github.com/i18next/i18next-http-backend
|
||||
.use(Backend)
|
||||
// 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,
|
||||
returnEmptyString: false,
|
||||
fallbackLng: 'en',
|
||||
lng: getBestPossibleUserLocale(),
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
transKeepBasicHtmlNodesFor: [
|
||||
'div',
|
||||
'strong',
|
||||
'h2',
|
||||
'span',
|
||||
'code',
|
||||
'p',
|
||||
'br',
|
||||
],
|
||||
},
|
||||
load: 'languageOnly',
|
||||
});
|
||||
|
||||
i18n.services.formatter.add('dateTime', (value, lng) => {
|
||||
return new Date(value / 1000).toLocaleDateString(lng, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 quite a
|
||||
* few languages. When a translation reaches a high enough coverage, say 90%,
|
||||
* then we manually add it to this list of supported languages.
|
||||
*/
|
||||
export type SupportedLocale = 'en' | 'fr' | 'zh' | 'nl' | 'es';
|
||||
|
||||
/**
|
||||
* List of all {@link SupportedLocale}s.
|
||||
*/
|
||||
export const supportedLocales: SupportedLocale[] = [
|
||||
'en',
|
||||
'fr',
|
||||
'zh',
|
||||
'nl',
|
||||
'es',
|
||||
];
|
||||
|
||||
/**
|
||||
* A type guard that returns true if the given string is a
|
||||
* {@link SupportedLocale}.
|
||||
*/
|
||||
export const isSupportedLocale = (s: string): s is SupportedLocale => {
|
||||
// The `as any` here is needed to work around current TS limitations
|
||||
// https://github.com/microsoft/TypeScript/issues/48247
|
||||
return supportedLocales.includes(s as any);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current locale in which our user interface is being shown.
|
||||
*
|
||||
* 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 currentLocale = () => {
|
||||
const locale = i18n.resolvedLanguage;
|
||||
return isSupportedLocale(locale) ? locale : 'en';
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
/** Enums of supported locale */
|
||||
export enum Language {
|
||||
en = 'en',
|
||||
fr = 'fr',
|
||||
zh = 'zh',
|
||||
nl = 'nl',
|
||||
es = 'es',
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { Language } from './locale';
|
||||
|
||||
import { getUserLocales } from 'get-user-locale';
|
||||
import { getUserLocale } from '@ente/shared/storage/localStorage/helpers';
|
||||
|
||||
export function getBestPossibleUserLocale(): Language {
|
||||
const locale = getUserLocale();
|
||||
if (locale) {
|
||||
return locale;
|
||||
}
|
||||
const userLocales = getUserLocales();
|
||||
for (const lc of userLocales) {
|
||||
if (lc.startsWith('en')) {
|
||||
return Language.en;
|
||||
} else if (lc.startsWith('fr')) {
|
||||
return Language.fr;
|
||||
} else if (lc.startsWith('zh')) {
|
||||
return Language.zh;
|
||||
} else if (lc.startsWith('nl')) {
|
||||
return Language.nl;
|
||||
} else if (lc.startsWith('es')) {
|
||||
return Language.es;
|
||||
}
|
||||
}
|
||||
return Language.en;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { LS_KEYS, getData, setData } from '.';
|
||||
import { Language } from '@ente/shared/i18n/locale';
|
||||
|
||||
export const getToken = (): string => {
|
||||
const token = getData(LS_KEYS.USER)?.token;
|
||||
|
@ -30,7 +29,7 @@ export function setLivePhotoInfoShownCount(count: boolean) {
|
|||
setData(LS_KEYS.LIVE_PHOTO_INFO_SHOWN_COUNT, { count });
|
||||
}
|
||||
|
||||
export function getUserLocale(): Language {
|
||||
export function getUserLocaleString(): string {
|
||||
return getData(LS_KEYS.LOCALE)?.value;
|
||||
}
|
||||
|
||||
|
|
147
packages/ui/i18n.ts
Normal file
147
packages/ui/i18n.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import { isDevBuild } from "@/utils/env";
|
||||
import { getUserLocales } from "get-user-locale";
|
||||
import { includes } from "@/utils/type-guards";
|
||||
|
||||
/**
|
||||
* 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", "fr", "zh", "nl", "es"] as const;
|
||||
/** The type of {@link supportedLocale}s. */
|
||||
export type SupportedLocale = (typeof supportedLocales)[number];
|
||||
|
||||
/**
|
||||
* Load translations.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
export const setupI18n = async (savedLocaleString?: string) => {
|
||||
const locale = closestSupportedLocale(savedLocaleString);
|
||||
|
||||
// https://www.i18next.com/overview/api
|
||||
await i18n
|
||||
// i18next-http-backend: Asynchronously loads translations over HTTP
|
||||
// https://github.com/i18next/i18next-http-backend
|
||||
.use(Backend)
|
||||
// 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,
|
||||
returnEmptyString: false,
|
||||
fallbackLng: "en",
|
||||
// i18next calls it language, but it really is the locale
|
||||
lng: locale,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
transKeepBasicHtmlNodesFor: [
|
||||
"div",
|
||||
"strong",
|
||||
"h2",
|
||||
"span",
|
||||
"code",
|
||||
"p",
|
||||
"br",
|
||||
],
|
||||
},
|
||||
load: "languageOnly",
|
||||
});
|
||||
|
||||
i18n.services.formatter?.add("dateTime", (value, lng) => {
|
||||
return new Date(value / 1000).toLocaleDateString(lng, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current locale in which our user interface is being shown.
|
||||
*
|
||||
* 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 currentLocale = () => {
|
||||
const locale = i18n.resolvedLanguage;
|
||||
return locale && includes(supportedLocales, locale) ? locale : "en";
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function closestSupportedLocale(
|
||||
savedLocaleString?: string,
|
||||
): SupportedLocale {
|
||||
const ss = savedLocaleString;
|
||||
if (ss && includes(supportedLocales, ss)) return ss;
|
||||
|
||||
/*
|
||||
switch (savedLocaleString) {
|
||||
case "en":
|
||||
return Language.en;
|
||||
case "fr":
|
||||
return Language.fr;
|
||||
case "zh":
|
||||
return Language.zh;
|
||||
case "nl":
|
||||
return Language.nl;
|
||||
case "es":
|
||||
return Language.es;
|
||||
}
|
||||
*/
|
||||
|
||||
for (const us of getUserLocales()) {
|
||||
// Exact match
|
||||
if (us && includes(supportedLocales, us)) return us;
|
||||
|
||||
// Language match
|
||||
if (us.startsWith("en")) {
|
||||
return "en";
|
||||
} else if (us.startsWith("fr")) {
|
||||
return "fr";
|
||||
} else if (us.startsWith("zh")) {
|
||||
return "zh";
|
||||
} else if (us.startsWith("nl")) {
|
||||
return "nl";
|
||||
} else if (us.startsWith("es")) {
|
||||
return "es";
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return "en";
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
"@emotion/styled": "^11.11",
|
||||
"@mui/icons-material": "^5.15",
|
||||
"@mui/material": "^5.15",
|
||||
"get-user-locale": "^2.3.1",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-http-backend": "^2.5.0",
|
||||
"react": "18.2.0",
|
||||
|
|
15
packages/utils/type-guards.ts
Normal file
15
packages/utils/type-guards.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* A variant of Array.includes that allows us to use it as a type guard.
|
||||
*
|
||||
* It allows us to narrow the type of an arbitrary T to U if it is one of U[].
|
||||
*
|
||||
* TypeScript currently doesn't allow us to use the standard Array.includes as a
|
||||
* type guard for checking if an arbitrary string is one of the known set of
|
||||
* values. This issue and this workaround is mentioned here:
|
||||
* - https://github.com/microsoft/TypeScript/issues/48247
|
||||
* - https://github.com/microsoft/TypeScript/issues/26255#issuecomment-502899689
|
||||
*/
|
||||
export function includes<T, U extends T>(us: readonly U[], t: T): t is U {
|
||||
// @ts-expect-error @typescript-eslint/no-unsafe-argument
|
||||
return us.includes(t);
|
||||
}
|
|
@ -2322,7 +2322,7 @@ get-tsconfig@^4.5.0:
|
|||
dependencies:
|
||||
resolve-pkg-maps "^1.0.0"
|
||||
|
||||
get-user-locale@^2.1.3:
|
||||
get-user-locale@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.1.tgz#fc7319429c8a70fac01b3b2a0b08b0c71c1d3fe2"
|
||||
integrity sha512-VEvcsqKYx7zhZYC1CjecrDC5ziPSpl1gSm0qFFJhHSGDrSC+x4+p1KojWC/83QX//j476gFhkVXP/kNUc9q+bQ==
|
||||
|
|
Loading…
Add table
Reference in a new issue