From 083cb7c1fafd48d57f03aa1393faf80036caab33 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:39:08 -0700 Subject: [PATCH 01/25] Add ListUtils.sortAlphabetically util --- src/libs/ListUtils.ts | 11 +++++++++++ tests/unit/ListUtilsTest.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/libs/ListUtils.ts create mode 100644 tests/unit/ListUtilsTest.ts diff --git a/src/libs/ListUtils.ts b/src/libs/ListUtils.ts new file mode 100644 index 0000000000000..4a449f496b34b --- /dev/null +++ b/src/libs/ListUtils.ts @@ -0,0 +1,11 @@ +import deburr from 'lodash/deburr'; + +/** + * Sort a list of string alphabetically, ignoring case and normalizing unicode. + */ +function sortAlphabetically(list: string[]): string[] { + return list.sort((a, b) => deburr(a.toLowerCase()).localeCompare(deburr(b.toLowerCase()))); +} + +// eslint-disable-next-line import/prefer-default-export +export {sortAlphabetically}; diff --git a/tests/unit/ListUtilsTest.ts b/tests/unit/ListUtilsTest.ts new file mode 100644 index 0000000000000..14df5497fa128 --- /dev/null +++ b/tests/unit/ListUtilsTest.ts @@ -0,0 +1,31 @@ +import {sortAlphabetically} from '@libs/ListUtils'; + +describe('ListUtils', () => { + describe('sortAlphabetically', () => { + test('sorts strings alphabetically ignoring case', () => { + const input = ['banana', 'Apple', 'cherry', 'apple']; + const expected = ['Apple', 'apple', 'banana', 'cherry']; + expect(sortAlphabetically(input)).toEqual(expected); + }); + + test('returns empty array when given empty array', () => { + expect(sortAlphabetically([])).toEqual([]); + }); + + test('handles single element array', () => { + expect(sortAlphabetically(['z'])).toEqual(['z']); + }); + + test('sorts strings with mixed uppercase and lowercase correctly', () => { + const input = ['b', 'A', 'c', 'a']; + const expected = ['A', 'a', 'b', 'c']; + expect(sortAlphabetically(input)).toEqual(expected); + }); + + test('sorts strings with unicode characters correctly', () => { + const input = ['Éclair', 'eclair', 'Banana', 'banana']; + const expected = ['Banana', 'banana', 'Éclair', 'eclair']; + expect(sortAlphabetically(input)).toEqual(expected); + }); + }); +}); From 706b7e818b04daa20b492be328b5518bc035bb67 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:40:54 -0700 Subject: [PATCH 02/25] Update Locale types --- src/CONST/LOCALES.ts | 93 +++++++++++++++++++++++++++----------------- src/CONST/index.ts | 3 +- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index 49fa44697ee2f..c388fcfd416d2 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -1,15 +1,21 @@ -import type {Spread, TupleToUnion} from 'type-fest'; +import type {ValueOf} from 'type-fest'; +import {sortAlphabetically} from '@libs/ListUtils'; -const LOCALES = { +/** + * These locales are fully supported. + */ +const FULLY_SUPPORTED_LOCALES = { EN: 'en', ES: 'es', - ES_ES: 'es-ES', - ES_ES_ONFIDO: 'es_ES', - - DEFAULT: 'en', } as const; -const UPCOMING_LOCALES = { +/** + * These are newly-added locales that aren't yet fully supported. i.e: + * + * - No emoji keyword support + * - Unaudited translations + */ +const BETA_LOCALES = { DE: 'de', FR: 'fr', IT: 'it', @@ -18,40 +24,55 @@ const UPCOMING_LOCALES = { PL: 'pl', PT_BR: 'pt-BR', ZH_HANS: 'zh-hans', -}; +} as const; + +/** + * These are additional locales that are not valid values of the preferredLocale NVP. + */ +const EXTENDED_LOCALES = { + ES_ES: 'es-ES', + ES_ES_ONFIDO: 'es_ES', +} as const; -const LANGUAGES = [LOCALES.EN, LOCALES.ES] as const; +/** + * Locales that are valid values of the preferredLocale NVP. + */ +const LOCALES = { + DEFAULT: FULLY_SUPPORTED_LOCALES.EN, + ...FULLY_SUPPORTED_LOCALES, + ...BETA_LOCALES, +} as const; -const UPCOMING_LANGUAGES = [ - UPCOMING_LOCALES.DE, - UPCOMING_LOCALES.FR, - UPCOMING_LOCALES.IT, - UPCOMING_LOCALES.JA, - UPCOMING_LOCALES.NL, - UPCOMING_LOCALES.PL, - UPCOMING_LOCALES.PT_BR, - UPCOMING_LOCALES.ZH_HANS, -] as const; +/** + * Locales that are valid translation targets. This does not include English, because it's used as the source of truth. + */ +const {EN, ...TRANSLATION_TARGET_LOCALES} = {...LOCALES} as const; const LOCALE_TO_LANGUAGE_STRING = { - [LOCALES.EN]: 'English', - [LOCALES.ES]: 'Español', - [UPCOMING_LOCALES.DE]: 'Deutsch', - [UPCOMING_LOCALES.FR]: 'Français', - [UPCOMING_LOCALES.IT]: 'Italiano', - [UPCOMING_LOCALES.JA]: '日本語', - [UPCOMING_LOCALES.NL]: 'Nederlands', - [UPCOMING_LOCALES.PL]: 'Polski', - [UPCOMING_LOCALES.PT_BR]: 'Português (BR)', - [UPCOMING_LOCALES.ZH_HANS]: '中文 (简体)', + [FULLY_SUPPORTED_LOCALES.EN]: 'English', + [FULLY_SUPPORTED_LOCALES.ES]: 'Español', + [BETA_LOCALES.DE]: 'Deutsch', + [BETA_LOCALES.FR]: 'Français', + [BETA_LOCALES.IT]: 'Italiano', + [BETA_LOCALES.JA]: '日本語', + [BETA_LOCALES.NL]: 'Nederlands', + [BETA_LOCALES.PL]: 'Polski', + [BETA_LOCALES.PT_BR]: 'Português (BR)', + [BETA_LOCALES.ZH_HANS]: '中文 (简体)', } as const; -type SupportedLanguage = TupleToUnion>; +type FullySupportedLocale = ValueOf; +type Locale = FullySupportedLocale | ValueOf; +type TranslationTargetLocale = ValueOf; -/** - * We can translate into any language but English (which is used as the source language). - */ -type TranslationTargetLanguage = Exclude; +const SORTED_LOCALES = [ + LOCALE_TO_LANGUAGE_STRING[FULLY_SUPPORTED_LOCALES.EN], + ...sortAlphabetically(Object.values(TRANSLATION_TARGET_LOCALES).map((localeCode) => LOCALE_TO_LANGUAGE_STRING[localeCode])), +] as Locale[]; + +function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale { + return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale); +} -export {LOCALES, LANGUAGES, UPCOMING_LANGUAGES, LOCALE_TO_LANGUAGE_STRING}; -export type {SupportedLanguage, TranslationTargetLanguage}; +export {BETA_LOCALES, EXTENDED_LOCALES, FULLY_SUPPORTED_LOCALES, LOCALES, LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES, TRANSLATION_TARGET_LOCALES, isFullySupportedLocale}; +export type {FullySupportedLocale, Locale, TranslationTargetLocale}; diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 021f9ad531b5f..ab131da27f784 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -13,7 +13,7 @@ import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; -import {LANGUAGES, LOCALES} from './LOCALES'; +import {LOCALES} from './LOCALES'; // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. @@ -2883,7 +2883,6 @@ const CONST = { }, LOCALES, - LANGUAGES, PRONOUNS_LIST: [ 'coCos', From 83620c49a558961dd6278899373375b35b4bb46f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:41:54 -0700 Subject: [PATCH 03/25] TranslationTargetLanguage -> TranslationTargetLocale --- prompts/translation/base.ts | 4 ++-- scripts/generateTranslations.ts | 16 ++++++++-------- scripts/utils/Translator/ChatGPTTranslator.ts | 4 ++-- scripts/utils/Translator/DummyTranslator.ts | 4 ++-- scripts/utils/Translator/Translator.ts | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/prompts/translation/base.ts b/prompts/translation/base.ts index 17bdfd1bbf24b..0ef9852f27747 100644 --- a/prompts/translation/base.ts +++ b/prompts/translation/base.ts @@ -1,11 +1,11 @@ import dedent from '@libs/StringUtils/dedent'; import {LOCALE_TO_LANGUAGE_STRING} from '@src/CONST/LOCALES'; -import type {TranslationTargetLanguage} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; /** * This file contains the base translation prompt used to translate static strings in en.ts to other languages. */ -export default function (targetLang: TranslationTargetLanguage): string { +export default function (targetLang: TranslationTargetLocale): string { return dedent(` You are a professional translator, translating strings for the Expensify app. Translate the following text to ${LOCALE_TO_LANGUAGE_STRING[targetLang]}. Adhere to the following rules while performing translations: diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index a02480e4e5dff..3c4370440e992 100755 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -11,7 +11,7 @@ import ts from 'typescript'; import dedent from '@libs/StringUtils/dedent'; import hashStr from '@libs/StringUtils/hash'; import {LANGUAGES, UPCOMING_LANGUAGES} from '@src/CONST/LOCALES'; -import type {TranslationTargetLanguage} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; import CLI from './utils/CLI'; import Prettier from './utils/Prettier'; import PromisePool from './utils/PromisePool'; @@ -59,7 +59,7 @@ class TranslationGenerator { /** * The languages to generate translations for. */ - private readonly targetLanguages: TranslationTargetLanguage[]; + private readonly targetLanguages: TranslationTargetLocale[]; /** * The directory where translations are stored. @@ -76,7 +76,7 @@ class TranslationGenerator { */ private readonly translator: Translator; - constructor(config: {targetLanguages: TranslationTargetLanguage[]; languagesDir: string; sourceFile: string; translator: Translator}) { + constructor(config: {targetLanguages: TranslationTargetLocale[]; languagesDir: string; sourceFile: string; translator: Translator}) { this.targetLanguages = config.targetLanguages; this.languagesDir = config.languagesDir; const sourceCode = fs.readFileSync(config.sourceFile, 'utf8'); @@ -426,7 +426,7 @@ class TranslationGenerator { } } -function isTranslationTargetLanguage(str: string): str is TranslationTargetLanguage { +function isTranslationTargetLocale(str: string): str is TranslationTargetLocale { return (LANGUAGES as readonly string[]).includes(str) || (UPCOMING_LANGUAGES as readonly string[]).includes(str); } @@ -445,12 +445,12 @@ async function main(): Promise { // By default, generate translations for all supported languages. Can be overridden with the --locales flag locales: { description: 'Locales to generate translations for.', - default: UPCOMING_LANGUAGES as unknown as TranslationTargetLanguage[], - parse: (val: string): TranslationTargetLanguage[] => { + default: UPCOMING_LANGUAGES as unknown as TranslationTargetLocale[], + parse: (val: string): TranslationTargetLocale[] => { const rawLocales = val.split(','); - const validatedLocales: TranslationTargetLanguage[] = []; + const validatedLocales: TranslationTargetLocale[] = []; for (const locale of rawLocales) { - if (!isTranslationTargetLanguage(locale)) { + if (!isTranslationTargetLocale(locale)) { throw new Error(`Invalid locale ${String(locale)}`); } validatedLocales.push(locale); diff --git a/scripts/utils/Translator/ChatGPTTranslator.ts b/scripts/utils/Translator/ChatGPTTranslator.ts index 00488e03cfff4..2fbae3008dd94 100644 --- a/scripts/utils/Translator/ChatGPTTranslator.ts +++ b/scripts/utils/Translator/ChatGPTTranslator.ts @@ -1,7 +1,7 @@ import dedent from '@libs/StringUtils/dedent'; import getBasePrompt from '@prompts/translation/base'; import getContextPrompt from '@prompts/translation/context'; -import type {TranslationTargetLanguage} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; import OpenAIUtils from '../OpenAIUtils'; import Translator from './Translator'; @@ -21,7 +21,7 @@ class ChatGPTTranslator extends Translator { this.openai = new OpenAIUtils(apiKey); } - protected async performTranslation(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise { + protected async performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise { const systemPrompt = dedent(` ${getBasePrompt(targetLang)} ${getContextPrompt(context)} diff --git a/scripts/utils/Translator/DummyTranslator.ts b/scripts/utils/Translator/DummyTranslator.ts index 874e653ee21c5..79a547df97866 100644 --- a/scripts/utils/Translator/DummyTranslator.ts +++ b/scripts/utils/Translator/DummyTranslator.ts @@ -1,8 +1,8 @@ -import type {TranslationTargetLanguage} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; import Translator from './Translator'; class DummyTranslator extends Translator { - protected performTranslation(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise { + protected performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise { return Promise.resolve(`[${targetLang}]${context ? `[ctx: ${context}]` : ''} ${text}`); } } diff --git a/scripts/utils/Translator/Translator.ts b/scripts/utils/Translator/Translator.ts index 8d5927e2f80fb..ace23f8a6c5ea 100644 --- a/scripts/utils/Translator/Translator.ts +++ b/scripts/utils/Translator/Translator.ts @@ -1,4 +1,4 @@ -import type {TranslationTargetLanguage} from '@src/CONST/LOCALES'; +import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; /** * Base Translator class standardizes interface for translators and implements common logging. @@ -8,7 +8,7 @@ abstract class Translator { * Translate a string to the given locale. * Implements common logging logic, while concrete subclasses handle actual translations. */ - public async translate(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise { + public async translate(targetLang: TranslationTargetLocale, text: string, context?: string): Promise { const isEmpty = !text || text.trim().length === 0; if (isEmpty) { return ''; @@ -25,7 +25,7 @@ abstract class Translator { /** * Translate a string to the given locale. */ - protected abstract performTranslation(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise; + protected abstract performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise; /** * Trim a string to keep logs readable. From 857abff81c5d2c7fb7f36f46085b15a914198272 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:44:04 -0700 Subject: [PATCH 04/25] Update TranslationStore --- src/languages/TranslationStore.ts | 120 ++++++++++++++------ src/languages/__mocks__/TranslationStore.ts | 20 ++-- 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/languages/TranslationStore.ts b/src/languages/TranslationStore.ts index a90f05c7590fe..8cfda5d9b3cc2 100644 --- a/src/languages/TranslationStore.ts +++ b/src/languages/TranslationStore.ts @@ -1,9 +1,19 @@ import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; +import {LOCALES} from '@src/CONST/LOCALES'; +import type {Locale} from '@src/CONST/LOCALES'; import ONYXKEYS from '@src/ONYXKEYS'; +import type de from './de'; +import type en from './en'; +import type es from './es'; import flattenObject from './flattenObject'; +import type fr from './fr'; +import type it from './it'; +import type ja from './ja'; +import type nl from './nl'; +import type pl from './pl'; +import type ptBR from './pt-BR'; import type {FlatTranslationsObject, TranslationPaths} from './types'; +import type zhHans from './zh-hans'; // This function was added here to avoid circular dependencies function setAreTranslationsLoading(areTranslationsLoading: boolean) { @@ -12,46 +22,86 @@ function setAreTranslationsLoading(areTranslationsLoading: boolean) { } class TranslationStore { - private static currentLocale: ValueOf | undefined = undefined; + private static currentLocale: Locale | undefined = undefined; - private static localeCache = new Map, FlatTranslationsObject>(); + private static cache = new Map(); - private static loaders: Partial, () => Promise>> = { - [CONST.LOCALES.EN]: () => { - if (this.localeCache.has(CONST.LOCALES.EN)) { - return Promise.resolve(); - } - return import('./en').then((module) => { - // it is needed because in jest test the modules are imported in double nested default object - const flattened = flattenObject( - (module.default as Record).default ? ((module.default as Record).default as typeof module.default) : module.default, - ); - this.localeCache.set(CONST.LOCALES.EN, flattened); - }); - }, - [CONST.LOCALES.ES]: () => { - if (this.localeCache.has(CONST.LOCALES.ES)) { - return Promise.resolve(); - } - return import('./es').then((module) => { - // it is needed because in jest test the modules are imported in double nested default object - const flattened = flattenObject( - (module.default as Record).default ? ((module.default as Record).default as typeof module.default) : module.default, - ); - this.localeCache.set(CONST.LOCALES.ES, flattened); - }); - }, + /** + * Set of loaders for each locale. + * Note that this can't be trivially DRYed up because dynamic imports must use string literals in metro: https://github.com/facebook/metro/issues/52 + */ + private static loaders: Record Promise> = { + [LOCALES.DE]: () => + this.cache.has(LOCALES.DE) + ? Promise.resolve() + : import('./de').then((module: {default: typeof de}) => { + this.cache.set(LOCALES.DE, flattenObject(module.default)); + }), + [LOCALES.EN]: () => + this.cache.has(LOCALES.EN) + ? Promise.resolve() + : import('./en').then((module: {default: typeof en}) => { + this.cache.set(LOCALES.EN, flattenObject(module.default)); + }), + [LOCALES.ES]: () => + this.cache.has(LOCALES.ES) + ? Promise.resolve() + : import('./es').then((module: {default: typeof es}) => { + this.cache.set(LOCALES.ES, flattenObject(module.default)); + }), + [LOCALES.FR]: () => + this.cache.has(LOCALES.FR) + ? Promise.resolve() + : import('./fr').then((module: {default: typeof fr}) => { + this.cache.set(LOCALES.FR, flattenObject(module.default)); + }), + [LOCALES.IT]: () => + this.cache.has(LOCALES.IT) + ? Promise.resolve() + : import('./it').then((module: {default: typeof it}) => { + this.cache.set(LOCALES.IT, flattenObject(module.default)); + }), + [LOCALES.JA]: () => + this.cache.has(LOCALES.JA) + ? Promise.resolve() + : import('./ja').then((module: {default: typeof ja}) => { + this.cache.set(LOCALES.JA, flattenObject(module.default)); + }), + [LOCALES.NL]: () => + this.cache.has(LOCALES.NL) + ? Promise.resolve() + : import('./nl').then((module: {default: typeof nl}) => { + this.cache.set(LOCALES.NL, flattenObject(module.default)); + }), + [LOCALES.PL]: () => + this.cache.has(LOCALES.PL) + ? Promise.resolve() + : import('./pl').then((module: {default: typeof pl}) => { + this.cache.set(LOCALES.PL, flattenObject(module.default)); + }), + [LOCALES.PT_BR]: () => + this.cache.has(LOCALES.PT_BR) + ? Promise.resolve() + : import('./pt-BR').then((module: {default: typeof ptBR}) => { + this.cache.set(LOCALES.PT_BR, flattenObject(module.default)); + }), + [LOCALES.ZH_HANS]: () => + this.cache.has(LOCALES.ZH_HANS) + ? Promise.resolve() + : import('./zh-hans').then((module: {default: typeof zhHans}) => { + this.cache.set(LOCALES.ZH_HANS, flattenObject(module.default)); + }), }; - static getCurrentLocale() { + public static getCurrentLocale() { return this.currentLocale; } - static load(locale: ValueOf) { + public static load(locale: Locale) { if (this.currentLocale === locale) { return Promise.resolve(); } - const loaderPromise = this.loaders[locale] ?? (() => Promise.resolve()); + const loaderPromise = this.loaders[locale]; setAreTranslationsLoading(true); return loaderPromise() .then(() => { @@ -62,12 +112,12 @@ class TranslationStore { }); } - static get(key: TPath, locale?: ValueOf) { - const localeToUse = locale && this.localeCache.has(locale) ? locale : this.currentLocale; + public static get(key: TPath, locale?: Locale) { + const localeToUse = locale && this.cache.has(locale) ? locale : this.currentLocale; if (!localeToUse) { return null; } - const translations = this.localeCache.get(localeToUse); + const translations = this.cache.get(localeToUse); return translations?.[key] ?? null; } } diff --git a/src/languages/__mocks__/TranslationStore.ts b/src/languages/__mocks__/TranslationStore.ts index 12c0dad98c37f..438adf39cc402 100644 --- a/src/languages/__mocks__/TranslationStore.ts +++ b/src/languages/__mocks__/TranslationStore.ts @@ -1,14 +1,14 @@ -import type {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; +import {LOCALES} from '@src/CONST/LOCALES'; +import type {Locale} from '@src/CONST/LOCALES'; import flattenObject from '@src/languages/flattenObject'; import type {FlatTranslationsObject, TranslationPaths} from '@src/languages/types'; class TranslationStore { - private static currentLocale: ValueOf | undefined = 'en'; + private static currentLocale: Locale | undefined = 'en'; - private static localeCache = new Map, FlatTranslationsObject>([ + private static localeCache = new Map([ [ - CONST.LOCALES.EN, + LOCALES.EN, flattenObject({ testKey1: 'English', testKey2: 'Test Word 2', @@ -25,7 +25,7 @@ class TranslationStore { }), ], [ - CONST.LOCALES.ES, + LOCALES.ES, flattenObject({ testKey1: 'Spanish', testKey2: 'Spanish Word 2', @@ -39,11 +39,11 @@ class TranslationStore { ], ]); - private static loaders: Partial, () => Promise>> = { - [CONST.LOCALES.EN]: () => { + private static loaders: Partial Promise>> = { + [LOCALES.EN]: () => { return Promise.resolve(); }, - [CONST.LOCALES.ES]: () => { + [LOCALES.ES]: () => { return Promise.resolve(); }, }; @@ -56,7 +56,7 @@ class TranslationStore { return Promise.resolve(); } - static get(key: TPath, locale?: ValueOf) { + static get(key: TPath, locale?: Locale) { const localeToUse = locale && this.localeCache.has(locale) ? locale : this.currentLocale; if (!localeToUse) { return null; From 04fd4da3af56ee390ceb6c52b8802c0ef168803c Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:45:31 -0700 Subject: [PATCH 05/25] Delete Language type --- src/types/onyx/Language.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/types/onyx/Language.ts diff --git a/src/types/onyx/Language.ts b/src/types/onyx/Language.ts deleted file mode 100644 index e7eb8af19c13f..0000000000000 --- a/src/types/onyx/Language.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {Spread, TupleToUnion} from 'type-fest'; -import type {LANGUAGES, UPCOMING_LANGUAGES} from '@src/CONST/LOCALES'; - -/** - * Supported (or soon-to-be supported) languages in the app. - */ -type Language = TupleToUnion>; - -export default Language; From 311cc70888076fc7153159d0d9f73e208cb2bfa7 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:48:15 -0700 Subject: [PATCH 06/25] Fix generateTranslations types --- scripts/generateTranslations.ts | 8 ++------ src/CONST/LOCALES.ts | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index 3c4370440e992..ec4258af812ec 100755 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -10,7 +10,7 @@ import type {TemplateExpression} from 'typescript'; import ts from 'typescript'; import dedent from '@libs/StringUtils/dedent'; import hashStr from '@libs/StringUtils/hash'; -import {LANGUAGES, UPCOMING_LANGUAGES} from '@src/CONST/LOCALES'; +import {isTranslationTargetLocale, TRANSLATION_TARGET_LOCALES} from '@src/CONST/LOCALES'; import type {TranslationTargetLocale} from '@src/CONST/LOCALES'; import CLI from './utils/CLI'; import Prettier from './utils/Prettier'; @@ -426,10 +426,6 @@ class TranslationGenerator { } } -function isTranslationTargetLocale(str: string): str is TranslationTargetLocale { - return (LANGUAGES as readonly string[]).includes(str) || (UPCOMING_LANGUAGES as readonly string[]).includes(str); -} - /** * The main function mostly contains CLI and file I/O logic, while TS parsing and translation logic is encapsulated in TranslationGenerator. */ @@ -445,7 +441,7 @@ async function main(): Promise { // By default, generate translations for all supported languages. Can be overridden with the --locales flag locales: { description: 'Locales to generate translations for.', - default: UPCOMING_LANGUAGES as unknown as TranslationTargetLocale[], + default: TRANSLATION_TARGET_LOCALES, parse: (val: string): TranslationTargetLocale[] => { const rawLocales = val.split(','); const validatedLocales: TranslationTargetLocale[] = []; diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index c388fcfd416d2..4910a10934e69 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -74,5 +74,19 @@ function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale); } -export {BETA_LOCALES, EXTENDED_LOCALES, FULLY_SUPPORTED_LOCALES, LOCALES, LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES, TRANSLATION_TARGET_LOCALES, isFullySupportedLocale}; +function isTranslationTargetLocale(locale: string): locale is TranslationTargetLocale { + return (Object.values(TRANSLATION_TARGET_LOCALES) as readonly string[]).includes(locale); +} + +export { + BETA_LOCALES, + EXTENDED_LOCALES, + FULLY_SUPPORTED_LOCALES, + LOCALES, + LOCALE_TO_LANGUAGE_STRING, + SORTED_LOCALES, + TRANSLATION_TARGET_LOCALES, + isFullySupportedLocale, + isTranslationTargetLocale, +}; export type {FullySupportedLocale, Locale, TranslationTargetLocale}; From f69f0adc6608aa28004449997ad358e09a9f577f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:49:41 -0700 Subject: [PATCH 07/25] Use SORTED_LOCALES in LocalePicker --- src/components/LocalePicker.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index d25cf7f3b3933..a40b6e9898a68 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -5,7 +5,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import AccountUtils from '@libs/AccountUtils'; import {setLocale} from '@userActions/App'; -import CONST from '@src/CONST'; +import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; import ONYXKEYS from '@src/ONYXKEYS'; import Picker from './Picker'; import type {PickerSize} from './Picker/types'; @@ -21,11 +21,11 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { const {translate} = useLocalize(); const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); - const localesToLanguages = CONST.LANGUAGES.map((language) => ({ - value: language, - label: translate(`languagePage.languages.${language}.label`), - keyForList: language, - isSelected: preferredLocale === language, + const localesToLanguages = SORTED_LOCALES.map((locale) => ({ + value: locale, + label: LOCALE_TO_LANGUAGE_STRING[locale], + keyForList: locale, + isSelected: preferredLocale === locale, })); const shouldDisablePicker = AccountUtils.isValidateCodeFormSubmitting(account); From cb97036e78da5a73fc1fcfa5f9f8333065e000c2 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:51:45 -0700 Subject: [PATCH 08/25] Use SORTED_LOCALES in LanguagePage --- src/pages/settings/Preferences/LanguagePage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index 0b23210b8ff14..284ac533ec2e8 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -1,5 +1,4 @@ import React, {useRef} from 'react'; -import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -8,21 +7,22 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import {setLocaleAndNavigate} from '@userActions/App'; import type {ListItem} from '@src/components/SelectionList/types'; -import CONST from '@src/CONST'; +import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; +import type {Locale} from '@src/types/onyx/Locale'; type LanguageEntry = ListItem & { - value: ValueOf; + value: Locale; }; function LanguagePage() { const {translate, preferredLocale} = useLocalize(); const isOptionSelected = useRef(false); - const localesToLanguages = CONST.LANGUAGES.map((language) => ({ - value: language, - text: translate(`languagePage.languages.${language}.label`), - keyForList: language, - isSelected: preferredLocale === language, + const localesToLanguages = SORTED_LOCALES.map((locale) => ({ + value: locale, + text: LOCALE_TO_LANGUAGE_STRING[locale], + keyForList: locale, + isSelected: preferredLocale === locale, })); const updateLanguage = (selectedLanguage: LanguageEntry) => { From 88baa5afe2dc11bc6aa2846c81f58e2bf82a0ed7 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:58:21 -0700 Subject: [PATCH 09/25] Remove full language names from translation files --- src/CONST/LOCALES.ts | 3 +++ src/components/LocalePicker.tsx | 2 +- src/languages/de.ts | 12 +----------- src/languages/en.ts | 12 +----------- src/languages/es.ts | 12 +----------- src/languages/fr.ts | 12 +----------- src/languages/it.ts | 12 +----------- src/languages/ja.ts | 12 +----------- src/languages/nl.ts | 12 +----------- src/languages/pl.ts | 12 +----------- src/languages/pt-BR.ts | 12 +----------- src/languages/zh-hans.ts | 12 +----------- src/pages/settings/Preferences/LanguagePage.tsx | 2 +- src/pages/settings/Preferences/PreferencesPage.tsx | 5 +++-- 14 files changed, 18 insertions(+), 114 deletions(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index 4910a10934e69..ec68b8afe9b97 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -48,6 +48,9 @@ const LOCALES = { */ const {EN, ...TRANSLATION_TARGET_LOCALES} = {...LOCALES} as const; +/** + * These strings are never translated. + */ const LOCALE_TO_LANGUAGE_STRING = { [FULLY_SUPPORTED_LOCALES.EN]: 'English', [FULLY_SUPPORTED_LOCALES.ES]: 'Español', diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index a40b6e9898a68..473e27726b413 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -31,7 +31,7 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { return ( { if (locale === preferredLocale) { return; diff --git a/src/languages/de.ts b/src/languages/de.ts index 16c97f5606334..552b5ba336ff5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2069,17 +2069,7 @@ const translations = { 'Da Sie die letzte Person hier sind, wird das Verlassen diesen Chat f\u00FCr alle Mitglieder unzug\u00E4nglich machen. Sind Sie sicher, dass Sie verlassen m\u00F6chten?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Gruppenchat von ${displayName}`, }, - languagePage: { - language: 'Sprache', - languages: { - en: { - label: 'Englisch', - }, - es: { - label: 'Spanisch', - }, - }, - }, + language: 'Sprache', themePage: { theme: 'Thema', themes: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 8f9b0d2567b3d..3fea0310ac162 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2028,17 +2028,7 @@ const translations = { lastMemberWarning: "Since you're the last person here, leaving will make this chat inaccessible to all members. Are you sure you want to leave?", defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}'s group chat`, }, - languagePage: { - language: 'Language', - languages: { - en: { - label: 'English', - }, - es: { - label: 'Español', - }, - }, - }, + language: 'Language', themePage: { theme: 'Theme', themes: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 20602ee5597b2..1440ab18a7481 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2030,17 +2030,7 @@ const translations = { lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat de grupo de ${displayName}`, }, - languagePage: { - language: 'Idioma', - languages: { - en: { - label: 'English', - }, - es: { - label: 'Español', - }, - }, - }, + language: 'Idioma', themePage: { theme: 'Tema', themes: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d59978faeedf4..3d45a61ee8f89 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2075,17 +2075,7 @@ const translations = { lastMemberWarning: 'Puisque vous \u00EAtes la derni\u00E8re personne ici, partir rendra ce chat inaccessible \u00E0 tous les membres. \u00CAtes-vous s\u00FBr de vouloir partir ?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Discussion de groupe de ${displayName}`, }, - languagePage: { - language: 'Langue', - languages: { - en: { - label: 'Anglais', - }, - es: { - label: 'Espagnol', - }, - }, - }, + language: 'Langue', themePage: { theme: 'Th\u00E8me', themes: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 2d730f2384a1b..9a29f83c1500d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2053,17 +2053,7 @@ const translations = { lastMemberWarning: "Poich\u00E9 sei l'ultima persona qui, uscire render\u00E0 questa chat inaccessibile a tutti i membri. Sei sicuro di voler uscire?", defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat di gruppo di ${displayName}`, }, - languagePage: { - language: 'Lingua', - languages: { - en: { - label: 'Inglese', - }, - es: { - label: 'Spagnolo', - }, - }, - }, + language: 'Lingua', themePage: { theme: 'Tema', themes: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 977749de540b0..93982775807b9 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2276,17 +2276,7 @@ const translations = { '\u3042\u306A\u305F\u304C\u6700\u5F8C\u306E\u4EBA\u306A\u306E\u3067\u3001\u9000\u51FA\u3059\u308B\u3068\u3053\u306E\u30C1\u30E3\u30C3\u30C8\u306F\u3059\u3079\u3066\u306E\u30E1\u30F3\u30D0\u30FC\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u306A\u304F\u306A\u308A\u307E\u3059\u3002\u672C\u5F53\u306B\u9000\u51FA\u3057\u307E\u3059\u304B\uFF1F', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}\u306E\u30B0\u30EB\u30FC\u30D7\u30C1\u30E3\u30C3\u30C8`, }, - languagePage: { - language: '\u8A00\u8A9E', - languages: { - en: { - label: '\u82F1\u8A9E', - }, - es: { - label: 'Espa\u00F1ol', - }, - }, - }, + language: '\u8A00\u8A9E', themePage: { theme: '\u30C6\u30FC\u30DE', themes: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 1fadda63c78ad..a42f6e87736b4 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2049,17 +2049,7 @@ const translations = { lastMemberWarning: 'Aangezien je de laatste persoon hier bent, zal het verlaten van deze chat deze ontoegankelijk maken voor alle leden. Weet je zeker dat je wilt vertrekken?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Groepschat van ${displayName}`, }, - languagePage: { - language: 'Taal', - languages: { - en: { - label: 'Engels', - }, - es: { - label: 'Spaans', - }, - }, - }, + language: 'Taal', themePage: { theme: 'Thema', themes: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 87fc0c62749ef..c040f719913d6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2086,17 +2086,7 @@ const translations = { 'Poniewa\u017C jeste\u015B ostatni\u0105 osob\u0105 tutaj, opuszczenie spowoduje, \u017Ce ten czat stanie si\u0119 niedost\u0119pny dla wszystkich cz\u0142onk\u00F3w. Czy na pewno chcesz opu\u015Bci\u0107?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Czat grupowy ${displayName}`, }, - languagePage: { - language: 'J\u0119zyk', - languages: { - en: { - label: 'Angielski', - }, - es: { - label: 'Spanish', - }, - }, - }, + language: 'J\u0119zyk', themePage: { theme: 'Motyw', themes: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index e5d7af7a534f0..e40045da82dc5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2059,17 +2059,7 @@ const translations = { lastMemberWarning: 'Como voc\u00EA \u00E9 a \u00FAltima pessoa aqui, sair tornar\u00E1 este chat inacess\u00EDvel para todos os membros. Tem certeza de que deseja sair?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat em grupo de ${displayName}`, }, - languagePage: { - language: 'Idioma', - languages: { - en: { - label: 'Ingl\u00EAs', - }, - es: { - label: 'Portugu\u00EAs (BR)', - }, - }, - }, + language: 'Idioma', themePage: { theme: 'Tema', themes: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5b55754665727..ab45922a78ffd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2119,17 +2119,7 @@ const translations = { '\u7531\u4E8E\u60A8\u662F\u8FD9\u91CC\u7684\u6700\u540E\u4E00\u4E2A\u4EBA\uFF0C\u79BB\u5F00\u5C06\u4F7F\u6240\u6709\u6210\u5458\u65E0\u6CD5\u8BBF\u95EE\u6B64\u804A\u5929\u3002\u60A8\u786E\u5B9A\u8981\u79BB\u5F00\u5417\uFF1F', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}\u7684\u7FA4\u804A`, }, - languagePage: { - language: '\u8BED\u8A00', - languages: { - en: { - label: '\u82F1\u8BED', - }, - es: { - label: 'Espa\u00F1ol', - }, - }, - }, + language: '\u8BED\u8A00', themePage: { theme: '\u4E3B\u9898', themes: { diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index 284ac533ec2e8..3f8a0c5d75ceb 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -39,7 +39,7 @@ function LanguagePage() { testID={LanguagePage.displayName} > Navigation.goBack()} /> Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)} wrapperStyle={styles.sectionMenuItemTopDescription} /> From d47dc70e35386e7fd7b39b3fe5944794b22b695e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 15:59:48 -0700 Subject: [PATCH 10/25] Remove unused src/libs/LocaleUtils --- src/libs/LocaleUtils.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/libs/LocaleUtils.ts diff --git a/src/libs/LocaleUtils.ts b/src/libs/LocaleUtils.ts deleted file mode 100644 index 1802a26d6ffe0..0000000000000 --- a/src/libs/LocaleUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type {TupleToUnion, ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; - -function getLanguageFromLocale(locale: ValueOf): TupleToUnion { - switch (locale) { - case CONST.LOCALES.ES_ES: - case CONST.LOCALES.ES_ES_ONFIDO: - case CONST.LOCALES.ES: - return CONST.LOCALES.ES; - case CONST.LOCALES.EN: - return CONST.LOCALES.EN; - default: - return CONST.LOCALES.DEFAULT; - } -} - -export default {getLanguageFromLocale}; From e73411e9abc234c9b03a9551fa3db8742e5a99ea Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 16:04:16 -0700 Subject: [PATCH 11/25] Fix src/libs/Localize types --- src/libs/Localize/index.ts | 23 ++++++------------- .../localeEventCallback/index.desktop.ts | 2 +- .../Localize/localeEventCallback/index.ts | 2 +- .../Localize/localeEventCallback/types.ts | 10 +++----- .../settings/Preferences/LanguagePage.tsx | 2 +- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index c7e2deada8e9a..d854678070aaa 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -28,13 +28,6 @@ Onyx.connect({ let CONJUNCTION_LIST_FORMATS_FOR_LOCALES: Record; function init() { CONJUNCTION_LIST_FORMATS_FOR_LOCALES = Object.values(CONST.LOCALES).reduce((memo: Record, locale) => { - // This is not a supported locale, so we'll use ES instead - if (locale === CONST.LOCALES.ES_ES_ONFIDO) { - // eslint-disable-next-line no-param-reassign - memo[locale] = new Intl.ListFormat(CONST.LOCALES.ES, {style: 'long', type: 'conjunction'}); - return memo; - } - // eslint-disable-next-line no-param-reassign memo[locale] = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); return memo; @@ -54,7 +47,7 @@ function init() { * phrase and stores the translated value in the cache and returns * the translated value. */ -function getTranslatedPhrase(language: 'en' | 'es', phraseKey: TKey, ...parameters: TranslationParameters): string | null { +function getTranslatedPhrase(language: Locale, phraseKey: TKey, ...parameters: TranslationParameters): string | null { const translatedPhrase = TranslationStore.get(phraseKey, language); if (translatedPhrase) { @@ -112,18 +105,16 @@ const memoizedGetTranslatedPhrase = memoize(getTranslatedPhrase, { /** * Return translated string for given locale and phrase * - * @param [desiredLanguage] eg 'en', 'es' + * @param [locale] eg 'en', 'es' * @param [parameters] Parameters to supply if the phrase is a template literal. */ -function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES' | undefined, path: TPath, ...parameters: TranslationParameters): string { - if (!desiredLanguage) { +function translate(locale: Locale | undefined, path: TPath, ...parameters: TranslationParameters): string { + if (!locale) { // If no language is provided, return the path as is return Array.isArray(path) ? path.join('.') : path; } - // Search phrase in full locale e.g. es-ES - const language = ([CONST.LOCALES.ES_ES_ONFIDO, CONST.LOCALES.ES_ES] as string[]).includes(desiredLanguage) ? CONST.LOCALES.ES : (desiredLanguage as 'en' | 'es'); - const translatedPhrase = memoizedGetTranslatedPhrase(language, path, ...parameters); + const translatedPhrase = memoizedGetTranslatedPhrase(locale, path, ...parameters); if (translatedPhrase !== null && translatedPhrase !== undefined) { return translatedPhrase; } @@ -132,13 +123,13 @@ function translate(desiredLanguage: 'en' | 'es' // on development throw an error if (Config.IS_IN_PRODUCTION || Config.IS_IN_STAGING) { const phraseString = Array.isArray(path) ? path.join('.') : path; - Log.alert(`${phraseString} was not found in the ${language} locale`); + Log.alert(`${phraseString} was not found in the ${locale} locale`); if (userEmail.includes(CONST.EMAIL.EXPENSIFY_EMAIL_DOMAIN)) { return CONST.MISSING_TRANSLATION; } return phraseString; } - throw new Error(`${path} was not found in the ${language} locale`); + throw new Error(`${path} was not found in the ${locale} locale`); } /** diff --git a/src/libs/Localize/localeEventCallback/index.desktop.ts b/src/libs/Localize/localeEventCallback/index.desktop.ts index 4d257b14dbd2a..ba786126e87e1 100644 --- a/src/libs/Localize/localeEventCallback/index.desktop.ts +++ b/src/libs/Localize/localeEventCallback/index.desktop.ts @@ -1,5 +1,5 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -import type {LocaleEventCallback} from './types'; +import type LocaleEventCallback from './types'; const localeEventCallback: LocaleEventCallback = (value) => { // Send the updated locale to the Electron main process diff --git a/src/libs/Localize/localeEventCallback/index.ts b/src/libs/Localize/localeEventCallback/index.ts index 805cefdbc8788..590ee366afd50 100644 --- a/src/libs/Localize/localeEventCallback/index.ts +++ b/src/libs/Localize/localeEventCallback/index.ts @@ -1,4 +1,4 @@ -import type {LocaleEventCallback} from './types'; +import type LocaleEventCallback from './types'; const localeEventCallback: LocaleEventCallback = () => {}; diff --git a/src/libs/Localize/localeEventCallback/types.ts b/src/libs/Localize/localeEventCallback/types.ts index 2ebd4c9f22609..7b3e40b2b66fe 100644 --- a/src/libs/Localize/localeEventCallback/types.ts +++ b/src/libs/Localize/localeEventCallback/types.ts @@ -1,9 +1,5 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; -type BaseLocale = ValueOf; +type LocaleEventCallback = (locale?: Locale) => void; -type LocaleEventCallback = (locale?: BaseLocale) => void; - -export type {LocaleEventCallback}; -export default BaseLocale; +export default LocaleEventCallback; diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index 3f8a0c5d75ceb..f8b196b97f6a8 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -8,7 +8,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {setLocaleAndNavigate} from '@userActions/App'; import type {ListItem} from '@src/components/SelectionList/types'; import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; -import type {Locale} from '@src/types/onyx/Locale'; +import type Locale from '@src/types/onyx/Locale'; type LanguageEntry = ListItem & { value: Locale; From 17a05fecd0a528086523d9837ffe125b3c75eb99 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 16:09:08 -0700 Subject: [PATCH 12/25] DRY locale type --- src/components/LocaleContextProvider.tsx | 5 +---- src/libs/API/parameters/BeginAppleSignInParams.ts | 5 ++--- src/libs/API/parameters/BeginGoogleSignInParams.ts | 5 ++--- src/libs/API/parameters/SignInUserParams.ts | 5 ++--- src/libs/API/parameters/SignInUserWithLinkParams.ts | 5 ++--- src/libs/API/parameters/SignUpUserParams.ts | 5 ++--- .../API/parameters/UpdatePreferredLocaleParams.ts | 5 +---- src/libs/DateUtils.ts | 2 +- src/libs/LocaleDigitUtils.ts | 5 +---- src/libs/NumberFormatUtils/index.ts | 6 +++--- src/libs/actions/App.ts | 12 +++++------- src/libs/actions/Session/index.ts | 4 ++-- 12 files changed, 24 insertions(+), 40 deletions(-) diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 181aa9acd1582..f92df850af9c7 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -1,20 +1,17 @@ import React, {createContext, useEffect, useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import DateUtils from '@libs/DateUtils'; import {fromLocaleDigit as fromLocaleDigitLocaleDigitUtils, toLocaleDigit as toLocaleDigitLocaleDigitUtils, toLocaleOrdinal as toLocaleOrdinalLocaleDigitUtils} from '@libs/LocaleDigitUtils'; import {formatPhoneNumber as formatPhoneNumberLocalePhoneNumber} from '@libs/LocalePhoneNumber'; import {translate as translateLocalize} from '@libs/Localize'; import {format} from '@libs/NumberFormatUtils'; -import type CONST from '@src/CONST'; import TranslationStore from '@src/languages/TranslationStore'; import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type Locale from '@src/types/onyx/Locale'; import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; -type Locale = ValueOf; - type LocaleContextProviderProps = { /** Actual content wrapped by this component */ children: React.ReactNode; diff --git a/src/libs/API/parameters/BeginAppleSignInParams.ts b/src/libs/API/parameters/BeginAppleSignInParams.ts index c427d99fcef93..cec2bd8a235c3 100644 --- a/src/libs/API/parameters/BeginAppleSignInParams.ts +++ b/src/libs/API/parameters/BeginAppleSignInParams.ts @@ -1,9 +1,8 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; type BeginAppleSignInParams = { idToken: string | undefined | null; - preferredLocale: ValueOf | null; + preferredLocale: Locale | null; }; export default BeginAppleSignInParams; diff --git a/src/libs/API/parameters/BeginGoogleSignInParams.ts b/src/libs/API/parameters/BeginGoogleSignInParams.ts index fae84d76b0d93..3466299350e38 100644 --- a/src/libs/API/parameters/BeginGoogleSignInParams.ts +++ b/src/libs/API/parameters/BeginGoogleSignInParams.ts @@ -1,9 +1,8 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; type BeginGoogleSignInParams = { token: string | null; - preferredLocale: ValueOf | null; + preferredLocale: Locale | null; }; export default BeginGoogleSignInParams; diff --git a/src/libs/API/parameters/SignInUserParams.ts b/src/libs/API/parameters/SignInUserParams.ts index 9fe973c428629..ddb148fd338a7 100644 --- a/src/libs/API/parameters/SignInUserParams.ts +++ b/src/libs/API/parameters/SignInUserParams.ts @@ -1,10 +1,9 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; type SignInUserParams = { twoFactorAuthCode?: string; email?: string; - preferredLocale: ValueOf | null; + preferredLocale: Locale | null; validateCode?: string; deviceInfo: string; }; diff --git a/src/libs/API/parameters/SignInUserWithLinkParams.ts b/src/libs/API/parameters/SignInUserWithLinkParams.ts index ae3589d4e305e..bc479d9f5b6d3 100644 --- a/src/libs/API/parameters/SignInUserWithLinkParams.ts +++ b/src/libs/API/parameters/SignInUserWithLinkParams.ts @@ -1,11 +1,10 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; type SignInUserWithLinkParams = { accountID: number; validateCode?: string; twoFactorAuthCode?: string; - preferredLocale: ValueOf | null; + preferredLocale: Locale | null; deviceInfo: string; }; diff --git a/src/libs/API/parameters/SignUpUserParams.ts b/src/libs/API/parameters/SignUpUserParams.ts index 00080017d30f3..cb11a7229e90d 100644 --- a/src/libs/API/parameters/SignUpUserParams.ts +++ b/src/libs/API/parameters/SignUpUserParams.ts @@ -1,9 +1,8 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; type SignUpUserParams = { email?: string; - preferredLocale: ValueOf | null; + preferredLocale: Locale | null; }; export default SignUpUserParams; diff --git a/src/libs/API/parameters/UpdatePreferredLocaleParams.ts b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts index 5dd991dea3b58..96dad4c6b8381 100644 --- a/src/libs/API/parameters/UpdatePreferredLocaleParams.ts +++ b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts @@ -1,7 +1,4 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; - -type Locale = ValueOf; +import type Locale from '@src/types/onyx/Locale'; type UpdatePreferredLocaleParams = { value: Locale; diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index c7ae919d5a38d..2495cec366800 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -42,6 +42,7 @@ import CONST from '@src/CONST'; import TranslationStore from '@src/languages/TranslationStore'; import ONYXKEYS from '@src/ONYXKEYS'; import {timezoneBackwardToNewMap, timezoneNewToBackwardMap} from '@src/TIMEZONES'; +import type Locale from '@src/types/onyx/Locale'; import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import {setCurrentDate} from './actions/CurrentDate'; import {setNetworkLastOffline} from './actions/Network'; @@ -50,7 +51,6 @@ import Log from './Log'; import memoize from './memoize'; type CustomStatusTypes = ValueOf; -type Locale = ValueOf; type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6; const TIMEZONE_UPDATE_THROTTLE_MINUTES = 5; diff --git a/src/libs/LocaleDigitUtils.ts b/src/libs/LocaleDigitUtils.ts index 8ba628ede7983..c922e8e16585b 100644 --- a/src/libs/LocaleDigitUtils.ts +++ b/src/libs/LocaleDigitUtils.ts @@ -1,12 +1,9 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import type Locale from '@src/types/onyx/Locale'; import {translate} from './Localize'; import memoize from './memoize'; import {format, formatToParts} from './NumberFormatUtils'; -type Locale = ValueOf; - const STANDARD_DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', ',']; const INDEX_DECIMAL = 10; diff --git a/src/libs/NumberFormatUtils/index.ts b/src/libs/NumberFormatUtils/index.ts index b9aba78898a7a..02bb14b29eed7 100644 --- a/src/libs/NumberFormatUtils/index.ts +++ b/src/libs/NumberFormatUtils/index.ts @@ -1,17 +1,17 @@ -import type {ValueOf} from 'type-fest'; import memoize from '@libs/memoize'; import CONST from '@src/CONST'; +import type Locale from '@src/types/onyx/Locale'; import initPolyfill from './intlPolyfill'; initPolyfill(); const MemoizedNumberFormat = memoize(Intl.NumberFormat, {maxSize: 10, monitoringName: 'NumberFormatUtils'}); -function format(locale: ValueOf | undefined, number: number, options?: Intl.NumberFormatOptions): string { +function format(locale: Locale | undefined, number: number, options?: Intl.NumberFormatOptions): string { return new MemoizedNumberFormat(locale ?? CONST.LOCALES.DEFAULT, options).format(number); } -function formatToParts(locale: ValueOf | undefined, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { +function formatToParts(locale: Locale | undefined, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { return new MemoizedNumberFormat(locale ?? CONST.LOCALES.DEFAULT, options).formatToParts(number); } diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 6c759155d3302..45cbc84c1028e 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -39,8 +39,6 @@ type PolicyParamsForOpenOrReconnect = { policyIDList: string[]; }; -type Locale = ValueOf; - let currentUserAccountID: number | undefined; let currentUserEmail: string; Onyx.connect({ @@ -64,9 +62,9 @@ Onyx.connect({ callback: (val) => { preferredLocale = val; if (preferredLocale) { - TranslationStore.load(preferredLocale as Locale); - importEmojiLocale(preferredLocale as Locale).then(() => { - buildEmojisTrie(preferredLocale as Locale); + TranslationStore.load(preferredLocale as OnyxTypes.Locale); + importEmojiLocale(preferredLocale as OnyxTypes.Locale).then(() => { + buildEmojisTrie(preferredLocale as OnyxTypes.Locale); }); localeEventCallback(val); } @@ -183,7 +181,7 @@ function getNonOptimisticPolicyIDs(policies: OnyxCollection): .filter((id): id is string => !!id); } -function setLocale(locale: Locale) { +function setLocale(locale: OnyxTypes.Locale) { if (locale === preferredLocale) { return; } @@ -210,7 +208,7 @@ function setLocale(locale: Locale) { API.write(WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE, parameters, {optimisticData}); } -function setLocaleAndNavigate(locale: Locale) { +function setLocaleAndNavigate(locale: OnyxTypes.Locale) { setLocale(locale); Navigation.goBack(); } diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index eccda3b31ebf4..93d85c0bc8b33 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -5,7 +5,6 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import {InteractionManager, Linking} from 'react-native'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; import type { @@ -58,6 +57,7 @@ import type {HybridAppRoute, Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type Credentials from '@src/types/onyx/Credentials'; +import type Locale from '@src/types/onyx/Locale'; import type Response from '@src/types/onyx/Response'; import type Session from '@src/types/onyx/Session'; import type {AutoAuthState} from '@src/types/onyx/Session'; @@ -111,7 +111,7 @@ Onyx.connect({ callback: (value) => (stashedCredentials = value ?? {}), }); -let preferredLocale: ValueOf | null = null; +let preferredLocale: Locale | null = null; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, callback: (val) => (preferredLocale = val ?? null), From d2adc1565830b7f927ee67849a4df8056d66f558 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 16:15:20 -0700 Subject: [PATCH 13/25] Fix miscelaneous types --- src/components/Onfido/BaseOnfidoWeb.tsx | 3 ++- src/libs/actions/Policy/Policy.ts | 4 +++- src/pages/settings/Preferences/PreferencesPage.tsx | 1 - tests/unit/TranslateTest.ts | 9 ++------- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/Onfido/BaseOnfidoWeb.tsx b/src/components/Onfido/BaseOnfidoWeb.tsx index 3c2e58d40d2a0..47723c02526dc 100644 --- a/src/components/Onfido/BaseOnfidoWeb.tsx +++ b/src/components/Onfido/BaseOnfidoWeb.tsx @@ -9,6 +9,7 @@ import type {ThemeColors} from '@styles/theme/types'; import FontUtils from '@styles/utils/FontUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import {EXTENDED_LOCALES} from '@src/CONST/LOCALES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import './index.css'; import type {OnfidoElement, OnfidoProps} from './types'; @@ -106,7 +107,7 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, language: { // We need to use ES_ES as locale key because the key `ES` is not a valid config key for Onfido - locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : (preferredLocale ?? CONST.LOCALES.DEFAULT), + locale: preferredLocale === CONST.LOCALES.ES ? EXTENDED_LOCALES.ES_ES_ONFIDO : (preferredLocale ?? CONST.LOCALES.DEFAULT), // Provide a custom phrase for the back button so that the first letter is capitalized, // and translate the phrase while we're at it. See the issue and documentation for more context. diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 0a79a8b84e24e..e8ec0b13ad4be 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1670,7 +1670,9 @@ function generateDefaultWorkspaceName(email = ''): string { // find default named workspaces and increment the last number const escapedName = escapeRegExp(displayNameForWorkspace); - const workspaceTranslations = CONST.LANGUAGES.map((lang) => translate(lang, 'workspace.common.workspace')).join('|'); + const workspaceTranslations = Object.values(CONST.LOCALES) + .map((lang) => translate(lang, 'workspace.common.workspace')) + .join('|'); const workspaceRegex = new RegExp(`^(?=.*${escapedName})(?:.*(?:${workspaceTranslations})\\s*(\\d+)?)`, 'i'); diff --git a/src/pages/settings/Preferences/PreferencesPage.tsx b/src/pages/settings/Preferences/PreferencesPage.tsx index d3c2349790428..5d0165a24be39 100755 --- a/src/pages/settings/Preferences/PreferencesPage.tsx +++ b/src/pages/settings/Preferences/PreferencesPage.tsx @@ -17,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {togglePlatformMute, updateNewsletterSubscription} from '@libs/actions/User'; import {getCurrencySymbol} from '@libs/CurrencyUtils'; import getPlatform from '@libs/getPlatform'; -import LocaleUtils from '@libs/LocaleUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index 23ceefeed7ffb..e0ae6b31ea7ee 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -17,19 +17,14 @@ const originalTranslations = { }; describe('translate', () => { - it('Test when key is not found in full locale, but present in language', () => { - expect(translate(CONST.LOCALES.ES_ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2'); - expect(translate(CONST.LOCALES.ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2'); - }); - test('Test when key is not found in default', () => { - expect(() => translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toThrow(Error); + expect(() => translate(CONST.LOCALES.EN, 'testKey4' as TranslationPaths)).toThrow(Error); }); test('Test when key is not found in default (Production Mode)', () => { const ORIGINAL_IS_IN_PRODUCTION = CONFIG.IS_IN_PRODUCTION; asMutable(CONFIG).IS_IN_PRODUCTION = true; - expect(translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4'); + expect(translate(CONST.LOCALES.EN, 'testKey4' as TranslationPaths)).toBe('testKey4'); asMutable(CONFIG).IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION; }); From 9d3228b034bfc3b8d6a121c7d1fb0f31584905fe Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 16:36:00 -0700 Subject: [PATCH 14/25] Filter LocalePicker and LanguagePage by beta --- src/CONST/index.ts | 1 + src/components/LocalePicker.tsx | 26 ++++++++++++------- .../settings/Preferences/LanguagePage.tsx | 26 ++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ab131da27f784..abad345a981dc 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -832,6 +832,7 @@ const CONST = { PLAID_COMPANY_CARDS: 'plaidCompanyCards', NATIVE_CONTACT_IMPORT: 'nativeContactImport', TRACK_FLOWS: 'trackFlows', + STATIC_AI_TRANSLATIONS: 'staticAITranslations', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 473e27726b413..db4b8b6e20136 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import AccountUtils from '@libs/AccountUtils'; import {setLocale} from '@userActions/App'; -import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; +import CONST from '@src/CONST'; +import {isFullySupportedLocale, LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; import ONYXKEYS from '@src/ONYXKEYS'; import Picker from './Picker'; import type {PickerSize} from './Picker/types'; @@ -21,12 +23,18 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { const {translate} = useLocalize(); const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: false}); - const localesToLanguages = SORTED_LOCALES.map((locale) => ({ - value: locale, - label: LOCALE_TO_LANGUAGE_STRING[locale], - keyForList: locale, - isSelected: preferredLocale === locale, - })); + const {isBetaEnabled} = usePermissions(); + + const locales = useMemo(() => { + const sortedLocales = isBetaEnabled(CONST.BETAS.STATIC_AI_TRANSLATIONS) ? SORTED_LOCALES : SORTED_LOCALES.filter((locale) => isFullySupportedLocale(locale)); + return sortedLocales.map((locale) => ({ + value: locale, + label: LOCALE_TO_LANGUAGE_STRING[locale], + keyForList: locale, + isSelected: preferredLocale === locale, + })); + }, [isBetaEnabled, preferredLocale]); + const shouldDisablePicker = AccountUtils.isValidateCodeFormSubmitting(account); return ( @@ -40,7 +48,7 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { setLocale(locale); }} isDisabled={shouldDisablePicker} - items={localesToLanguages} + items={locales} shouldAllowDisabledStyle={false} shouldShowOnlyTextWhenDisabled={false} size={size} diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index f8b196b97f6a8..52d649cf67476 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -1,13 +1,15 @@ -import React, {useRef} from 'react'; +import React, {useMemo, useRef} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import Navigation from '@libs/Navigation/Navigation'; import {setLocaleAndNavigate} from '@userActions/App'; import type {ListItem} from '@src/components/SelectionList/types'; -import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; +import CONST from '@src/CONST'; +import {isFullySupportedLocale, LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES'; import type Locale from '@src/types/onyx/Locale'; type LanguageEntry = ListItem & { @@ -17,13 +19,17 @@ type LanguageEntry = ListItem & { function LanguagePage() { const {translate, preferredLocale} = useLocalize(); const isOptionSelected = useRef(false); + const {isBetaEnabled} = usePermissions(); - const localesToLanguages = SORTED_LOCALES.map((locale) => ({ - value: locale, - text: LOCALE_TO_LANGUAGE_STRING[locale], - keyForList: locale, - isSelected: preferredLocale === locale, - })); + const locales = useMemo(() => { + const sortedLocales = isBetaEnabled(CONST.BETAS.STATIC_AI_TRANSLATIONS) ? SORTED_LOCALES : SORTED_LOCALES.filter((locale) => isFullySupportedLocale(locale)); + return sortedLocales.map((locale) => ({ + value: locale, + label: LOCALE_TO_LANGUAGE_STRING[locale], + keyForList: locale, + isSelected: preferredLocale === locale, + })); + }, [isBetaEnabled, preferredLocale]); const updateLanguage = (selectedLanguage: LanguageEntry) => { if (isOptionSelected.current) { @@ -43,11 +49,11 @@ function LanguagePage() { onBackButtonPress={() => Navigation.goBack()} /> locale.isSelected)?.keyForList} + initiallyFocusedOptionKey={locales.find((locale) => locale.isSelected)?.keyForList} /> ); From b9bf799bab78434c3ef4617121eb72e3f450a84e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 16:45:48 -0700 Subject: [PATCH 15/25] Add hintText for ai-generated locales --- src/components/LocalePicker.tsx | 2 +- src/languages/de.ts | 5 ++++- src/languages/en.ts | 5 ++++- src/languages/es.ts | 5 ++++- src/languages/fr.ts | 5 ++++- src/languages/it.ts | 5 ++++- src/languages/ja.ts | 5 ++++- src/languages/nl.ts | 5 ++++- src/languages/pl.ts | 5 ++++- src/languages/pt-BR.ts | 5 ++++- src/languages/zh-hans.ts | 5 ++++- src/pages/settings/Preferences/LanguagePage.tsx | 2 +- src/pages/settings/Preferences/PreferencesPage.tsx | 5 +++-- 13 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index db4b8b6e20136..c489684d6b2e7 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -39,7 +39,7 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { return ( { if (locale === preferredLocale) { return; diff --git a/src/languages/de.ts b/src/languages/de.ts index 552b5ba336ff5..ee60545d2a8f0 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2069,7 +2069,10 @@ const translations = { 'Da Sie die letzte Person hier sind, wird das Verlassen diesen Chat f\u00FCr alle Mitglieder unzug\u00E4nglich machen. Sind Sie sicher, dass Sie verlassen m\u00F6chten?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Gruppenchat von ${displayName}`, }, - language: 'Sprache', + languagePage: { + language: 'Sprache', + aiGenerated: 'Die Übersetzungen für diese Sprache werden automatisch erstellt und können Fehler enthalten.', + }, themePage: { theme: 'Thema', themes: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 3fea0310ac162..e25cd4c676e57 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2028,7 +2028,10 @@ const translations = { lastMemberWarning: "Since you're the last person here, leaving will make this chat inaccessible to all members. Are you sure you want to leave?", defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}'s group chat`, }, - language: 'Language', + languagePage: { + language: 'Language', + aiGenerated: 'The translations for this language are generated automatically and may contain errors.', + }, themePage: { theme: 'Theme', themes: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1440ab18a7481..3bf1fbb298565 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2030,7 +2030,10 @@ const translations = { lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat de grupo de ${displayName}`, }, - language: 'Idioma', + languagePage: { + language: 'Idioma', + aiGenerated: 'Las traducciones para este idioma se generan automáticamente y pueden contener errores.', + }, themePage: { theme: 'Tema', themes: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3d45a61ee8f89..62f2e72829a6c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2075,7 +2075,10 @@ const translations = { lastMemberWarning: 'Puisque vous \u00EAtes la derni\u00E8re personne ici, partir rendra ce chat inaccessible \u00E0 tous les membres. \u00CAtes-vous s\u00FBr de vouloir partir ?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Discussion de groupe de ${displayName}`, }, - language: 'Langue', + languagePage: { + language: 'Langue', + aiGenerated: 'Les traductions pour cette langue sont générées automatiquement et peuvent contenir des erreurs.', + }, themePage: { theme: 'Th\u00E8me', themes: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 9a29f83c1500d..f4273d0765c81 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2053,7 +2053,10 @@ const translations = { lastMemberWarning: "Poich\u00E9 sei l'ultima persona qui, uscire render\u00E0 questa chat inaccessibile a tutti i membri. Sei sicuro di voler uscire?", defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat di gruppo di ${displayName}`, }, - language: 'Lingua', + languagePage: { + language: 'Lingua', + aiGenerated: 'Le traduzioni per questa lingua sono generate automaticamente e possono contenere errori.', + }, themePage: { theme: 'Tema', themes: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 93982775807b9..d64cd6454afcd 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2276,7 +2276,10 @@ const translations = { '\u3042\u306A\u305F\u304C\u6700\u5F8C\u306E\u4EBA\u306A\u306E\u3067\u3001\u9000\u51FA\u3059\u308B\u3068\u3053\u306E\u30C1\u30E3\u30C3\u30C8\u306F\u3059\u3079\u3066\u306E\u30E1\u30F3\u30D0\u30FC\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u306A\u304F\u306A\u308A\u307E\u3059\u3002\u672C\u5F53\u306B\u9000\u51FA\u3057\u307E\u3059\u304B\uFF1F', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}\u306E\u30B0\u30EB\u30FC\u30D7\u30C1\u30E3\u30C3\u30C8`, }, - language: '\u8A00\u8A9E', + languagePage: { + language: '\u8A00\u8A9E', + aiGenerated: 'この言語の翻訳は自動生成されており、誤りが含まれている可能性があります。', + }, themePage: { theme: '\u30C6\u30FC\u30DE', themes: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a42f6e87736b4..f99d63ea8bd6c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2049,7 +2049,10 @@ const translations = { lastMemberWarning: 'Aangezien je de laatste persoon hier bent, zal het verlaten van deze chat deze ontoegankelijk maken voor alle leden. Weet je zeker dat je wilt vertrekken?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Groepschat van ${displayName}`, }, - language: 'Taal', + languagePage: { + language: 'Taal', + aiGenerated: 'De vertalingen voor deze taal worden automatisch gegenereerd en kunnen fouten bevatten.', + }, themePage: { theme: 'Thema', themes: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c040f719913d6..c9e37490a74ad 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2086,7 +2086,10 @@ const translations = { 'Poniewa\u017C jeste\u015B ostatni\u0105 osob\u0105 tutaj, opuszczenie spowoduje, \u017Ce ten czat stanie si\u0119 niedost\u0119pny dla wszystkich cz\u0142onk\u00F3w. Czy na pewno chcesz opu\u015Bci\u0107?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Czat grupowy ${displayName}`, }, - language: 'J\u0119zyk', + languagePage: { + language: 'J\u0119zyk', + aiGenerated: 'Tłumaczenia dla tego języka są generowane automatycznie i mogą zawierać błędy.', + }, themePage: { theme: 'Motyw', themes: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index e40045da82dc5..ca9698d99d492 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2059,7 +2059,10 @@ const translations = { lastMemberWarning: 'Como voc\u00EA \u00E9 a \u00FAltima pessoa aqui, sair tornar\u00E1 este chat inacess\u00EDvel para todos os membros. Tem certeza de que deseja sair?', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `Chat em grupo de ${displayName}`, }, - language: 'Idioma', + languagePage: { + language: 'Idioma', + aiGenerated: 'As traduções para este idioma são geradas automaticamente e podem conter erros.', + }, themePage: { theme: 'Tema', themes: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ab45922a78ffd..532679d2c36c4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2119,7 +2119,10 @@ const translations = { '\u7531\u4E8E\u60A8\u662F\u8FD9\u91CC\u7684\u6700\u540E\u4E00\u4E2A\u4EBA\uFF0C\u79BB\u5F00\u5C06\u4F7F\u6240\u6709\u6210\u5458\u65E0\u6CD5\u8BBF\u95EE\u6B64\u804A\u5929\u3002\u60A8\u786E\u5B9A\u8981\u79BB\u5F00\u5417\uFF1F', defaultReportName: ({displayName}: ReportArchiveReasonsClosedParams) => `${displayName}\u7684\u7FA4\u804A`, }, - language: '\u8BED\u8A00', + languagePage: { + language: '\u8BED\u8A00', + aiGenerated: '该语言的翻译是自动生成的,可能包含错误。', + }, themePage: { theme: '\u4E3B\u9898', themes: { diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index 52d649cf67476..fa1510641cb62 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -45,7 +45,7 @@ function LanguagePage() { testID={LanguagePage.displayName} > Navigation.goBack()} /> Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)} wrapperStyle={styles.sectionMenuItemTopDescription} + hintText={!preferredLocale || !isFullySupportedLocale(preferredLocale) ? translate('languagePage.aiGenerated') : ''} /> Date: Mon, 16 Jun 2025 16:56:01 -0700 Subject: [PATCH 16/25] Fix emoji types --- assets/emojis/index.ts | 6 ++++-- src/components/EmojiWithTooltip/index.tsx | 2 +- src/libs/EmojiUtils.tsx | 21 ++++++--------------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 4b05c8caddfa3..2e0824e0067ae 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,10 +1,12 @@ +import type {FullySupportedLocale} from '@src/CONST/LOCALES'; +import {isFullySupportedLocale, LOCALES} from '@src/CONST/LOCALES'; import type {Locale} from '@src/types/onyx'; import emojis from './common'; import type {Emoji, EmojisList} from './types'; type EmojiTable = Record; -type LocaleEmojis = Partial>; +type LocaleEmojis = Partial>; const emojiNameTable = emojis.reduce((prev, cur) => { const newValue = prev; @@ -33,7 +35,7 @@ const localeEmojis: LocaleEmojis = { }; const importEmojiLocale = (locale: Locale) => { - const normalizedLocale = locale.toLowerCase().split('-').at(0) as Locale; + const normalizedLocale = isFullySupportedLocale(locale) ? locale : LOCALES.EN; if (!localeEmojis[normalizedLocale]) { const emojiImportPromise = normalizedLocale === 'en' ? import('./en') : import('./es'); return emojiImportPromise.then((esEmojiModule) => { diff --git a/src/components/EmojiWithTooltip/index.tsx b/src/components/EmojiWithTooltip/index.tsx index 32103544b3aa0..a2a9209c19976 100644 --- a/src/components/EmojiWithTooltip/index.tsx +++ b/src/components/EmojiWithTooltip/index.tsx @@ -11,7 +11,7 @@ function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) { const {preferredLocale} = useLocalize(); const styles = useThemeStyles(); const emoji = EmojiUtils.findEmojiByCode(emojiCode); - const emojiName = EmojiUtils.getEmojiName(emoji, preferredLocale); + const emojiName = EmojiUtils.getLocalizedEmojiName(emoji.name, preferredLocale); const emojiTooltipContent = useCallback( () => ( diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 5c7eecfaa533f..0b35f7eec62b7 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -8,6 +8,7 @@ import * as Emojis from '@assets/emojis'; import type {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types'; import Text from '@components/Text'; import CONST from '@src/CONST'; +import {isFullySupportedLocale} from '@src/CONST/LOCALES'; import ONYXKEYS from '@src/ONYXKEYS'; import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; @@ -80,27 +81,18 @@ Onyx.connect({ }, }); -const getEmojiName = (emoji: Emoji, lang: Locale = CONST.LOCALES.DEFAULT): string => { - if (!emoji) { - return ''; - } - if (lang === CONST.LOCALES.DEFAULT) { - return emoji.name; - } - - return Emojis.localeEmojis?.[lang]?.[emoji.code]?.name ?? ''; -}; - /** * Given an English emoji name, get its localized version */ -const getLocalizedEmojiName = (name: string, lang: OnyxEntry): string => { - if (lang === CONST.LOCALES.DEFAULT) { +const getLocalizedEmojiName = (name: string, locale: OnyxEntry): string => { + const normalizedLocale = locale && isFullySupportedLocale(locale) ? locale : CONST.LOCALES.EN; + + if (normalizedLocale === CONST.LOCALES.DEFAULT) { return name; } const emojiCode = Emojis.emojiNameTable[name]?.code ?? ''; - return (lang && Emojis.localeEmojis[lang]?.[emojiCode]?.name) ?? ''; + return Emojis.localeEmojis[normalizedLocale]?.[emojiCode]?.name ?? ''; }; /** @@ -676,7 +668,6 @@ export type {HeaderIndices, EmojiPickerList, EmojiPickerListItem}; export { findEmojiByName, findEmojiByCode, - getEmojiName, getLocalizedEmojiName, getProcessedText, getHeaderEmojis, From 7ac1224fd1970697861c3dd8a00627284f401b39 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 17:04:10 -0700 Subject: [PATCH 17/25] Fix lint-changed --- src/components/EmojiWithTooltip/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/EmojiWithTooltip/index.tsx b/src/components/EmojiWithTooltip/index.tsx index a2a9209c19976..60f22dd5b2798 100644 --- a/src/components/EmojiWithTooltip/index.tsx +++ b/src/components/EmojiWithTooltip/index.tsx @@ -4,14 +4,14 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as EmojiUtils from '@libs/EmojiUtils'; +import {findEmojiByCode, getLocalizedEmojiName} from '@libs/EmojiUtils'; import type EmojiWithTooltipProps from './types'; function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) { const {preferredLocale} = useLocalize(); const styles = useThemeStyles(); - const emoji = EmojiUtils.findEmojiByCode(emojiCode); - const emojiName = EmojiUtils.getLocalizedEmojiName(emoji.name, preferredLocale); + const emoji = findEmojiByCode(emojiCode); + const emojiName = getLocalizedEmojiName(emoji.name, preferredLocale); const emojiTooltipContent = useCallback( () => ( From 76115877874f058fbdcbdf70292a99428abc59cd Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 17:08:00 -0700 Subject: [PATCH 18/25] Fix TRANSLATION_TARGET_LOCALES --- src/CONST/LOCALES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index ec68b8afe9b97..b2741a310794e 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -46,7 +46,7 @@ const LOCALES = { /** * Locales that are valid translation targets. This does not include English, because it's used as the source of truth. */ -const {EN, ...TRANSLATION_TARGET_LOCALES} = {...LOCALES} as const; +const {DEFAULT, EN, ...TRANSLATION_TARGET_LOCALES} = {...LOCALES} as const; /** * These strings are never translated. From 851349dc1f91e3a40d9f2ed553441a65730e71e2 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 17:15:39 -0700 Subject: [PATCH 19/25] Fix LanguagePage --- src/CONST/LOCALES.ts | 5 +---- src/pages/settings/Preferences/LanguagePage.tsx | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index b2741a310794e..b03f6253a024e 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -68,10 +68,7 @@ type FullySupportedLocale = ValueOf; type Locale = FullySupportedLocale | ValueOf; type TranslationTargetLocale = ValueOf; -const SORTED_LOCALES = [ - LOCALE_TO_LANGUAGE_STRING[FULLY_SUPPORTED_LOCALES.EN], - ...sortAlphabetically(Object.values(TRANSLATION_TARGET_LOCALES).map((localeCode) => LOCALE_TO_LANGUAGE_STRING[localeCode])), -] as Locale[]; +const SORTED_LOCALES = [FULLY_SUPPORTED_LOCALES.EN, ...sortAlphabetically(Object.values(TRANSLATION_TARGET_LOCALES))] as Locale[]; function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale { return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale); diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index fa1510641cb62..59b650efb72b7 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -25,7 +25,7 @@ function LanguagePage() { const sortedLocales = isBetaEnabled(CONST.BETAS.STATIC_AI_TRANSLATIONS) ? SORTED_LOCALES : SORTED_LOCALES.filter((locale) => isFullySupportedLocale(locale)); return sortedLocales.map((locale) => ({ value: locale, - label: LOCALE_TO_LANGUAGE_STRING[locale], + text: LOCALE_TO_LANGUAGE_STRING[locale], keyForList: locale, isSelected: preferredLocale === locale, })); From aa4263b8155e90cb5b22a089d5b5480c45312cae Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 17:18:44 -0700 Subject: [PATCH 20/25] Sort Spanish right after English --- src/CONST/LOCALES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index b03f6253a024e..452aafebd8f41 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -68,7 +68,7 @@ type FullySupportedLocale = ValueOf; type Locale = FullySupportedLocale | ValueOf; type TranslationTargetLocale = ValueOf; -const SORTED_LOCALES = [FULLY_SUPPORTED_LOCALES.EN, ...sortAlphabetically(Object.values(TRANSLATION_TARGET_LOCALES))] as Locale[]; +const SORTED_LOCALES = [FULLY_SUPPORTED_LOCALES.EN, FULLY_SUPPORTED_LOCALES.ES, ...sortAlphabetically(Object.values(BETA_LOCALES))] as Locale[]; function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale { return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale); From 1cebeda06ae3a68d46f31091a7a3c4a43b8992ac Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 17:24:28 -0700 Subject: [PATCH 21/25] Wrap LanguagePage in FullPageOfflineBlockingView --- src/pages/settings/Preferences/LanguagePage.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Preferences/LanguagePage.tsx b/src/pages/settings/Preferences/LanguagePage.tsx index 59b650efb72b7..0105ae6903788 100644 --- a/src/pages/settings/Preferences/LanguagePage.tsx +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -1,4 +1,5 @@ import React, {useMemo, useRef} from 'react'; +import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -48,13 +49,15 @@ function LanguagePage() { title={translate('languagePage.language')} onBackButtonPress={() => Navigation.goBack()} /> - locale.isSelected)?.keyForList} - /> + + locale.isSelected)?.keyForList} + /> + ); } From 9e94a1233544a3a91ef213ad7b09a2727ef304a4 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 18:16:57 -0700 Subject: [PATCH 22/25] Fix tests --- src/languages/TranslationStore.ts | 42 ++++++++++++++------------ src/libs/extractModuleDefaultExport.ts | 13 ++++++++ src/types/utils/DynamicModule.ts | 9 ++++++ 3 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 src/libs/extractModuleDefaultExport.ts create mode 100644 src/types/utils/DynamicModule.ts diff --git a/src/languages/TranslationStore.ts b/src/languages/TranslationStore.ts index 8cfda5d9b3cc2..4ad25d3d2e963 100644 --- a/src/languages/TranslationStore.ts +++ b/src/languages/TranslationStore.ts @@ -1,7 +1,9 @@ import Onyx from 'react-native-onyx'; +import extractModuleDefaultExport from '@libs/extractModuleDefaultExport'; import {LOCALES} from '@src/CONST/LOCALES'; import type {Locale} from '@src/CONST/LOCALES'; import ONYXKEYS from '@src/ONYXKEYS'; +import type DynamicModule from '@src/types/utils/DynamicModule'; import type de from './de'; import type en from './en'; import type es from './es'; @@ -34,62 +36,62 @@ class TranslationStore { [LOCALES.DE]: () => this.cache.has(LOCALES.DE) ? Promise.resolve() - : import('./de').then((module: {default: typeof de}) => { - this.cache.set(LOCALES.DE, flattenObject(module.default)); + : import('./de').then((module: DynamicModule) => { + this.cache.set(LOCALES.DE, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.EN]: () => this.cache.has(LOCALES.EN) ? Promise.resolve() - : import('./en').then((module: {default: typeof en}) => { - this.cache.set(LOCALES.EN, flattenObject(module.default)); + : import('./en').then((module: DynamicModule) => { + this.cache.set(LOCALES.EN, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.ES]: () => this.cache.has(LOCALES.ES) ? Promise.resolve() - : import('./es').then((module: {default: typeof es}) => { - this.cache.set(LOCALES.ES, flattenObject(module.default)); + : import('./es').then((module: DynamicModule) => { + this.cache.set(LOCALES.ES, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.FR]: () => this.cache.has(LOCALES.FR) ? Promise.resolve() - : import('./fr').then((module: {default: typeof fr}) => { - this.cache.set(LOCALES.FR, flattenObject(module.default)); + : import('./fr').then((module: DynamicModule) => { + this.cache.set(LOCALES.FR, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.IT]: () => this.cache.has(LOCALES.IT) ? Promise.resolve() - : import('./it').then((module: {default: typeof it}) => { - this.cache.set(LOCALES.IT, flattenObject(module.default)); + : import('./it').then((module: DynamicModule) => { + this.cache.set(LOCALES.IT, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.JA]: () => this.cache.has(LOCALES.JA) ? Promise.resolve() - : import('./ja').then((module: {default: typeof ja}) => { - this.cache.set(LOCALES.JA, flattenObject(module.default)); + : import('./ja').then((module: DynamicModule) => { + this.cache.set(LOCALES.JA, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.NL]: () => this.cache.has(LOCALES.NL) ? Promise.resolve() - : import('./nl').then((module: {default: typeof nl}) => { - this.cache.set(LOCALES.NL, flattenObject(module.default)); + : import('./nl').then((module: DynamicModule) => { + this.cache.set(LOCALES.NL, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.PL]: () => this.cache.has(LOCALES.PL) ? Promise.resolve() - : import('./pl').then((module: {default: typeof pl}) => { - this.cache.set(LOCALES.PL, flattenObject(module.default)); + : import('./pl').then((module: DynamicModule) => { + this.cache.set(LOCALES.PL, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.PT_BR]: () => this.cache.has(LOCALES.PT_BR) ? Promise.resolve() - : import('./pt-BR').then((module: {default: typeof ptBR}) => { - this.cache.set(LOCALES.PT_BR, flattenObject(module.default)); + : import('./pt-BR').then((module: DynamicModule) => { + this.cache.set(LOCALES.PT_BR, flattenObject(extractModuleDefaultExport(module))); }), [LOCALES.ZH_HANS]: () => this.cache.has(LOCALES.ZH_HANS) ? Promise.resolve() - : import('./zh-hans').then((module: {default: typeof zhHans}) => { - this.cache.set(LOCALES.ZH_HANS, flattenObject(module.default)); + : import('./zh-hans').then((module: DynamicModule) => { + this.cache.set(LOCALES.ZH_HANS, flattenObject(extractModuleDefaultExport(module))); }), }; diff --git a/src/libs/extractModuleDefaultExport.ts b/src/libs/extractModuleDefaultExport.ts new file mode 100644 index 0000000000000..ebf97b0032158 --- /dev/null +++ b/src/libs/extractModuleDefaultExport.ts @@ -0,0 +1,13 @@ +import type DynamicModule from '@src/types/utils/DynamicModule'; + +/** + * Extract the default export from something that's been dynamically imported with ESM import(). + * It should not be necessary, except that our Jest config mangles imports. + */ +export default function (module: DynamicModule): T { + const topLevelDefault = module.default; + if (topLevelDefault && typeof topLevelDefault === 'object' && 'default' in topLevelDefault) { + return topLevelDefault.default; + } + return topLevelDefault; +} diff --git a/src/types/utils/DynamicModule.ts b/src/types/utils/DynamicModule.ts new file mode 100644 index 0000000000000..fabf48291ca45 --- /dev/null +++ b/src/types/utils/DynamicModule.ts @@ -0,0 +1,9 @@ +/** + * This type is what results when you use ESM dynamic import of a module with a default export. + * The first one ({default: T}) is what you'd see in the actual app. + * HOWEVER, our Jest setup seems to mangle these imports so we have "double nested" default exports. + * I wish we didn't need this type. + */ +type DynamicModule = {default: T} | {default: {default: T}}; + +export default DynamicModule; From 55f2ad4ee37bda0cc707512942e9d9cf0dec9446 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 16 Jun 2025 18:23:59 -0700 Subject: [PATCH 23/25] Remove unused es-ES locale --- src/CONST/LOCALES.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index 452aafebd8f41..5838aa78cde0f 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -30,7 +30,6 @@ const BETA_LOCALES = { * These are additional locales that are not valid values of the preferredLocale NVP. */ const EXTENDED_LOCALES = { - ES_ES: 'es-ES', ES_ES_ONFIDO: 'es_ES', } as const; From ba19c9914b1ae3731f1b475fab44be371f50a6b4 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 17 Jun 2025 09:24:49 -0700 Subject: [PATCH 24/25] Fix English emoji fallback --- assets/emojis/index.ts | 11 ++++------- src/CONST/LOCALES.ts | 5 +++++ src/libs/EmojiTrie.ts | 34 ++++++++++++---------------------- src/libs/EmojiUtils.tsx | 26 ++++++++++++-------------- src/libs/actions/App.ts | 23 +++++++++++++++-------- tests/unit/CIGitLogicTest.ts | 2 -- 6 files changed, 48 insertions(+), 53 deletions(-) diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts index 2e0824e0067ae..a5d64aba2bb7b 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,6 +1,4 @@ import type {FullySupportedLocale} from '@src/CONST/LOCALES'; -import {isFullySupportedLocale, LOCALES} from '@src/CONST/LOCALES'; -import type {Locale} from '@src/types/onyx'; import emojis from './common'; import type {Emoji, EmojisList} from './types'; @@ -34,13 +32,12 @@ const localeEmojis: LocaleEmojis = { es: undefined, }; -const importEmojiLocale = (locale: Locale) => { - const normalizedLocale = isFullySupportedLocale(locale) ? locale : LOCALES.EN; - if (!localeEmojis[normalizedLocale]) { - const emojiImportPromise = normalizedLocale === 'en' ? import('./en') : import('./es'); +const importEmojiLocale = (locale: FullySupportedLocale) => { + if (!localeEmojis[locale]) { + const emojiImportPromise = locale === 'en' ? import('./en') : import('./es'); return emojiImportPromise.then((esEmojiModule) => { // it is needed because in jest test the modules are imported in double nested default object - localeEmojis[normalizedLocale] = esEmojiModule.default.default ? (esEmojiModule.default.default as unknown as EmojisList) : esEmojiModule.default; + localeEmojis[locale] = esEmojiModule.default.default ? (esEmojiModule.default.default as unknown as EmojisList) : esEmojiModule.default; }); } return Promise.resolve(); diff --git a/src/CONST/LOCALES.ts b/src/CONST/LOCALES.ts index 5838aa78cde0f..e2fc79ca906d6 100644 --- a/src/CONST/LOCALES.ts +++ b/src/CONST/LOCALES.ts @@ -69,6 +69,10 @@ type TranslationTargetLocale = ValueOf; const SORTED_LOCALES = [FULLY_SUPPORTED_LOCALES.EN, FULLY_SUPPORTED_LOCALES.ES, ...sortAlphabetically(Object.values(BETA_LOCALES))] as Locale[]; +function isSupportedLocale(locale: string): locale is Locale { + return (Object.values(LOCALES) as readonly string[]).includes(locale); +} + function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale { return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale); } @@ -85,6 +89,7 @@ export { LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES, TRANSLATION_TARGET_LOCALES, + isSupportedLocale, isFullySupportedLocale, isTranslationTargetLocale, }; diff --git a/src/libs/EmojiTrie.ts b/src/libs/EmojiTrie.ts index 5ae601844537a..f115e74806394 100644 --- a/src/libs/EmojiTrie.ts +++ b/src/libs/EmojiTrie.ts @@ -1,8 +1,8 @@ -import type {TupleToUnion} from 'type-fest'; import emojis, {localeEmojis} from '@assets/emojis'; import type {Emoji, HeaderEmoji, PickerEmoji} from '@assets/emojis/types'; import CONST from '@src/CONST'; -import type {Locale} from '@src/types/onyx'; +import {FULLY_SUPPORTED_LOCALES} from '@src/CONST/LOCALES'; +import type {FullySupportedLocale} from '@src/CONST/LOCALES'; import Timing from './actions/Timing'; import StringUtils from './StringUtils'; import Trie from './Trie'; @@ -14,12 +14,8 @@ type EmojiMetaData = { name?: string; }; -const supportedLanguages = [CONST.LOCALES.DEFAULT, CONST.LOCALES.ES] as const; - -type SupportedLanguage = TupleToUnion; - -type EmojiTrie = { - [key in SupportedLanguage]?: Trie; +type EmojiTrieForLocale = { + [key in FullySupportedLocale]?: Trie; }; /** @@ -67,7 +63,7 @@ function getNameParts(name: string): string[] { return nameSplit.map((namePart, index) => nameSplit.slice(index).join('_')); } -function createTrie(lang: SupportedLanguage = CONST.LOCALES.DEFAULT): Trie { +function createTrie(lang: FullySupportedLocale = CONST.LOCALES.DEFAULT): Trie { const trie = new Trie(); const langEmojis = localeEmojis[lang]; const defaultLangEmojis = localeEmojis[CONST.LOCALES.DEFAULT]; @@ -115,25 +111,19 @@ function createTrie(lang: SupportedLanguage = CONST.LOCALES.DEFAULT): Trie { +const emojiTrieForLocale: EmojiTrieForLocale = Object.values(FULLY_SUPPORTED_LOCALES).reduce((acc, lang) => { acc[lang] = undefined; return acc; -}, {} as EmojiTrie); - -const buildEmojisTrie = (locale: Locale) => { - Timing.start(CONST.TIMING.TRIE_INITIALIZATION); - // Normalize the locale to lowercase and take the first part before any dash - const normalizedLocale = locale.toLowerCase().split('-').at(0); - const localeToUse = supportedLanguages.includes(normalizedLocale as SupportedLanguage) ? (normalizedLocale as SupportedLanguage) : undefined; +}, {} as EmojiTrieForLocale); - if (!localeToUse || emojiTrie[localeToUse]) { +const buildEmojisTrie = (locale: FullySupportedLocale) => { + if (emojiTrieForLocale[locale]) { return; // Return early if the locale is not supported or the trie is already built } - emojiTrie[localeToUse] = createTrie(localeToUse); + Timing.start(CONST.TIMING.TRIE_INITIALIZATION); + emojiTrieForLocale[locale] = createTrie(locale); Timing.end(CONST.TIMING.TRIE_INITIALIZATION); }; -export default emojiTrie; +export default emojiTrieForLocale; export {buildEmojisTrie}; - -export type {SupportedLanguage}; diff --git a/src/libs/EmojiUtils.tsx b/src/libs/EmojiUtils.tsx index 0b35f7eec62b7..e71e06df8bce8 100644 --- a/src/libs/EmojiUtils.tsx +++ b/src/libs/EmojiUtils.tsx @@ -14,7 +14,6 @@ import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import type IconAsset from '@src/types/utils/IconAsset'; import type EmojiTrie from './EmojiTrie'; -import type {SupportedLanguage} from './EmojiTrie'; import memoize from './memoize'; type HeaderIndices = {code: string; index: number; icon: IconAsset}; @@ -322,11 +321,12 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * Replace any emoji name in a text with the emoji icon. * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ -function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, locale: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; - const trie = emojisTrie[lang as SupportedLanguage]; + const normalizedLocale = locale && isFullySupportedLocale(locale) ? locale : CONST.LOCALES.EN; + const trie = emojisTrie[normalizedLocale]; if (!trie) { return {text, emojis: []}; } @@ -345,11 +345,10 @@ function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { - const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); +function replaceAndExtractEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, locale: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { + const normalizedLocale = locale && isFullySupportedLocale(locale) ? locale : CONST.LOCALES.EN; + + const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, normalizedLocale); return { text: convertedText, @@ -400,15 +401,12 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone: OnyxEntry('./EmojiTrie').default; - if (!lang) { - return []; - } - - const trie = emojisTrie[lang as SupportedLanguage]; + const normalizedLocale = locale && isFullySupportedLocale(locale) ? locale : CONST.LOCALES.EN; + const trie = emojisTrie[normalizedLocale]; if (!trie) { return []; } diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 45cbc84c1028e..0d73106efc10f 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -21,12 +21,14 @@ import {isPublicRoom, isValidReport} from '@libs/ReportUtils'; import {isLoggingInAsNewUser as isLoggingInAsNewUserSessionUtils} from '@libs/SessionUtils'; import {clearSoundAssetsCache} from '@libs/Sound'; import CONST from '@src/CONST'; +import {isFullySupportedLocale, isSupportedLocale} from '@src/CONST/LOCALES'; import TranslationStore from '@src/languages/TranslationStore'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxKey} from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type Locale from '@src/types/onyx/Locale'; import type {OnyxData} from '@src/types/onyx/Request'; import {setShouldForceOffline} from './Network'; import {getAll, rollbackOngoingRequest, save} from './PersistedRequests'; @@ -56,18 +58,23 @@ Onyx.connect({ initWithStoredValues: false, }); -let preferredLocale: string | undefined; +let preferredLocale: Locale | undefined; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, callback: (val) => { - preferredLocale = val; - if (preferredLocale) { - TranslationStore.load(preferredLocale as OnyxTypes.Locale); - importEmojiLocale(preferredLocale as OnyxTypes.Locale).then(() => { - buildEmojisTrie(preferredLocale as OnyxTypes.Locale); - }); - localeEventCallback(val); + if (!val || !isSupportedLocale(val)) { + return; } + + preferredLocale = val; + TranslationStore.load(val); + localeEventCallback(val); + + // For locales without emoji support, fallback on English + const normalizedLocale = isFullySupportedLocale(val) ? val : CONST.LOCALES.DEFAULT; + importEmojiLocale(normalizedLocale).then(() => { + buildEmojisTrie(normalizedLocale); + }); }, }); diff --git a/tests/unit/CIGitLogicTest.ts b/tests/unit/CIGitLogicTest.ts index be6a10f2466c0..74c6bf926ff0c 100644 --- a/tests/unit/CIGitLogicTest.ts +++ b/tests/unit/CIGitLogicTest.ts @@ -481,9 +481,7 @@ Appended content setupGitAsHuman(); exec('git switch main'); exec('git switch -c pr-10'); - console.log('RORY_DEBUG BEFORE:', fs.readFileSync('myFile.txt', {encoding: 'utf8'})); fs.writeFileSync('myFile.txt', initialFileContent); - console.log('RORY_DEBUG AFTER:', fs.readFileSync('myFile.txt', {encoding: 'utf8'})); exec('git add myFile.txt'); exec('git commit -m "Revert append and prepend"'); mergePR(10); From ecae44e7fc27a82c52b33cf685a0ff5d3833e803 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 17 Jun 2025 10:16:21 -0700 Subject: [PATCH 25/25] Reimplement GetUserLanguage for Apple Sign In --- .../AppleSignIn/AppleSignInLocales.ts | 63 +++++++++++++++++++ .../SignInButtons/AppleSignIn/index.tsx | 5 +- .../SignInButtons/GetUserLanguage.ts | 19 ------ src/libs/Localize/index.ts | 2 +- 4 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 src/components/SignInButtons/AppleSignIn/AppleSignInLocales.ts delete mode 100644 src/components/SignInButtons/GetUserLanguage.ts diff --git a/src/components/SignInButtons/AppleSignIn/AppleSignInLocales.ts b/src/components/SignInButtons/AppleSignIn/AppleSignInLocales.ts new file mode 100644 index 0000000000000..b7ba84a4c9fc5 --- /dev/null +++ b/src/components/SignInButtons/AppleSignIn/AppleSignInLocales.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type {ValueOf} from 'type-fest'; +import {LOCALES} from '@src/CONST/LOCALES'; +import type Locale from '@src/types/onyx/Locale'; + +/** + * The set of locales supported by Apple Sign In. + * Reference: https://developer.apple.com/documentation/signinwithapple/incorporating-sign-in-with-apple-into-other-platforms + */ +const APPLE_SIGN_IN_LOCALES = { + ar_SA: 'ar_SA', + ca_ES: 'ca_ES', + cs_CZ: 'cs_CZ', + da_DK: 'da_DK', + de_DE: 'de_DE', + el_GR: 'el_GR', + en_GB: 'en_GB', + en_US: 'en_US', + es_ES: 'es_ES', + es_MX: 'es_MX', + fi_FI: 'fi_FI', + fr_CA: 'fr_CA', + fr_FR: 'fr_FR', + hr_HR: 'hr_HR', + hu_HU: 'hu_HU', + id_ID: 'id_ID', + it_IT: 'it_IT', + iw_IL: 'iw_IL', + ja_JP: 'ja_JP', + ko_KR: 'ko_KR', + ms_MY: 'ms_MY', + nl_NL: 'nl_NL', + no_NO: 'no_NO', + pl_PL: 'pl_PL', + pt_BR: 'pt_BR', + pt_PT: 'pt_PT', + ro_RO: 'ro_RO', + ru_RU: 'ru_RU', + sk_SK: 'sk_SK', + sv_SE: 'sv_SE', + th_TH: 'th_TH', + tr_TR: 'tr_TR', + uk_UA: 'uk_UA', + vi_VI: 'vi_VI', + zh_CN: 'zh_CN', + zh_HK: 'zh_HK', + zh_TW: 'zh_TW', +} as const; + +const MAP_EXFY_LOCALE_TO_APPLE_LOCALE: Record> = { + [LOCALES.DE]: APPLE_SIGN_IN_LOCALES.de_DE, + [LOCALES.EN]: APPLE_SIGN_IN_LOCALES.en_US, + [LOCALES.ES]: APPLE_SIGN_IN_LOCALES.es_ES, + [LOCALES.FR]: APPLE_SIGN_IN_LOCALES.fr_FR, + [LOCALES.IT]: APPLE_SIGN_IN_LOCALES.it_IT, + [LOCALES.JA]: APPLE_SIGN_IN_LOCALES.ja_JP, + [LOCALES.NL]: APPLE_SIGN_IN_LOCALES.nl_NL, + [LOCALES.PL]: APPLE_SIGN_IN_LOCALES.pl_PL, + [LOCALES.PT_BR]: APPLE_SIGN_IN_LOCALES.pt_BR, + [LOCALES.ZH_HANS]: APPLE_SIGN_IN_LOCALES.zh_CN, +}; + +export default MAP_EXFY_LOCALE_TO_APPLE_LOCALE; diff --git a/src/components/SignInButtons/AppleSignIn/index.tsx b/src/components/SignInButtons/AppleSignIn/index.tsx index c9c5107c97fc0..6b093ed48316b 100644 --- a/src/components/SignInButtons/AppleSignIn/index.tsx +++ b/src/components/SignInButtons/AppleSignIn/index.tsx @@ -2,12 +2,13 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; import type {NativeConfig} from 'react-native-config'; import Config from 'react-native-config'; -import getUserLanguage from '@components/SignInButtons/GetUserLanguage'; import {beginAppleSignIn} from '@libs/actions/Session'; +import {getDevicePreferredLocale} from '@libs/Localize'; import Log from '@libs/Log'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import type {AppleIDSignInOnFailureEvent, AppleIDSignInOnSuccessEvent} from '@src/types/modules/dom'; +import MAP_EXFY_LOCALE_TO_APPLE_LOCALE from './AppleSignInLocales'; // react-native-config doesn't trim whitespace on iOS for some reason so we // add a trim() call to lodashGet here to prevent headaches. @@ -127,7 +128,7 @@ function AppleSignIn({isDesktopFlow = false, onPointerDown}: AppleSignInProps) { return; } - const localeCode = getUserLanguage(); + const localeCode = MAP_EXFY_LOCALE_TO_APPLE_LOCALE[getDevicePreferredLocale()]; const script = document.createElement('script'); script.src = `https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1//${localeCode}/appleid.auth.js`; script.async = true; diff --git a/src/components/SignInButtons/GetUserLanguage.ts b/src/components/SignInButtons/GetUserLanguage.ts deleted file mode 100644 index 9fda67e1079b5..0000000000000 --- a/src/components/SignInButtons/GetUserLanguage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type {ValueOf} from 'type-fest'; - -const localeCodes = { - en: 'en_US', - es: 'es_ES', -} as const; - -type LanguageCode = keyof typeof localeCodes; -type LocaleCode = ValueOf; - -const GetUserLanguage = (): LocaleCode => { - const userLanguage = navigator.language || navigator.userLanguage; - const languageCode = userLanguage.split('-').at(0) as LanguageCode; - return localeCodes[languageCode] || 'en_US'; -}; - -GetUserLanguage.displayName = 'GetUserLanguage'; - -export default GetUserLanguage; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index d854678070aaa..c6f5c6a88744e 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -188,7 +188,7 @@ function formatMessageElementList(elements: readon * Returns the user device's preferred language. */ function getDevicePreferredLocale(): Locale { - return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; + return RNLocalize.findBestAvailableLanguage(Object.values(CONST.LOCALES))?.languageTag ?? CONST.LOCALES.DEFAULT; } export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale};