diff --git a/apps/accounts/src/pages/_app.tsx b/apps/accounts/src/pages/_app.tsx index 9e359537f..5131bed29 100644 --- a/apps/accounts/src/pages/_app.tsx +++ b/apps/accounts/src/pages/_app.tsx @@ -10,7 +10,7 @@ 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'; diff --git a/apps/auth/src/pages/_app.tsx b/apps/auth/src/pages/_app.tsx index 210c12328..e7f611a16 100644 --- a/apps/auth/src/pages/_app.tsx +++ b/apps/auth/src/pages/_app.tsx @@ -32,7 +32,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'; diff --git a/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx b/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx index 4e656a833..d2436839a 100644 --- a/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx +++ b/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx @@ -2,7 +2,7 @@ import DropdownInput, { DropdownOption } from 'components/DropdownInput'; import { useLocalState } from '@ente/shared/hooks/useLocalState'; import { t } from 'i18next'; import { useRouter } from 'next/router'; -import { Language, getBestPossibleUserLocale } from '@ente/shared/i18n'; +import { Language, getBestPossibleUserLocale } from '@/ui/i18n'; import { LS_KEYS } from '@ente/shared/storage/localStorage'; import { getUserLocaleString } from '@ente/shared/storage/localStorage/helpers'; diff --git a/apps/photos/src/pages/_app.tsx b/apps/photos/src/pages/_app.tsx index 2e31bc0cc..6341e1017 100644 --- a/apps/photos/src/pages/_app.tsx +++ b/apps/photos/src/pages/_app.tsx @@ -74,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'; diff --git a/packages/shared/i18n/index.ts b/packages/ui/i18n.ts similarity index 63% rename from packages/shared/i18n/index.ts rename to packages/ui/i18n.ts index c59c1bca6..0138b308f 100644 --- a/packages/shared/i18n/index.ts +++ b/packages/ui/i18n.ts @@ -1,8 +1,9 @@ -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 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"; /** * Load translations. @@ -32,7 +33,7 @@ export const setupI18n = async (savedLocaleString?: string) => { .init({ debug: isDevBuild, returnEmptyString: false, - fallbackLng: 'en', + fallbackLng: "en", lng: lng, interpolation: { escapeValue: false, // not needed for react as it escapes by default @@ -40,23 +41,23 @@ export const setupI18n = async (savedLocaleString?: string) => { react: { useSuspense: false, transKeepBasicHtmlNodesFor: [ - 'div', - 'strong', - 'h2', - 'span', - 'code', - 'p', - 'br', + "div", + "strong", + "h2", + "span", + "code", + "p", + "br", ], }, - load: 'languageOnly', + load: "languageOnly", }); - i18n.services.formatter.add('dateTime', (value, lng) => { + i18n.services.formatter?.add("dateTime", (value, lng) => { return new Date(value / 1000).toLocaleDateString(lng, { - year: 'numeric', - month: 'long', - day: 'numeric', + year: "numeric", + month: "long", + day: "numeric", }); }); }; @@ -71,29 +72,19 @@ export const setupI18n = async (savedLocaleString?: string) => { * 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'; +export type SupportedLocale = "en" | "fr" | "zh" | "nl" | "es"; /** * List of all {@link SupportedLocale}s. */ export const supportedLocales: SupportedLocale[] = [ - 'en', - 'fr', - 'zh', - 'nl', - 'es', + "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. * @@ -103,44 +94,45 @@ export const isSupportedLocale = (s: string): s is SupportedLocale => { */ export const currentLocale = () => { const locale = i18n.resolvedLanguage; - return isSupportedLocale(locale) ? locale : 'en'; + return locale && includes(supportedLocales, locale) ? locale : "en"; }; /** Enums of supported locale */ export enum Language { - en = 'en', - fr = 'fr', - zh = 'zh', - nl = 'nl', - es = 'es', + en = "en", + fr = "fr", + zh = "zh", + nl = "nl", + es = "es", } -export function getBestPossibleUserLocale(savedLocaleString: string): Language { - const locale = savedLocaleString; - switch (locale) { - case 'en': +export function getBestPossibleUserLocale( + savedLocaleString?: string, +): Language { + switch (savedLocaleString) { + case "en": return Language.en; - case 'fr': + case "fr": return Language.fr; - case 'zh': + case "zh": return Language.zh; - case 'nl': + case "nl": return Language.nl; - case 'es': + case "es": return Language.es; } const userLocales = getUserLocales(); for (const lc of userLocales) { - if (lc.startsWith('en')) { + if (lc.startsWith("en")) { return Language.en; - } else if (lc.startsWith('fr')) { + } else if (lc.startsWith("fr")) { return Language.fr; - } else if (lc.startsWith('zh')) { + } else if (lc.startsWith("zh")) { return Language.zh; - } else if (lc.startsWith('nl')) { + } else if (lc.startsWith("nl")) { return Language.nl; - } else if (lc.startsWith('es')) { + } else if (lc.startsWith("es")) { return Language.es; } } diff --git a/packages/utils/type-guards.ts b/packages/utils/type-guards.ts new file mode 100644 index 000000000..1309a5bea --- /dev/null +++ b/packages/utils/type-guards.ts @@ -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(us: readonly U[], t: T): t is U { + // @ts-expect-error @typescript-eslint/no-unsafe-argument + return us.includes(t); +}