Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
083cb7c
Add ListUtils.sortAlphabetically util
roryabraham Jun 16, 2025
706b7e8
Update Locale types
roryabraham Jun 16, 2025
83620c4
TranslationTargetLanguage -> TranslationTargetLocale
roryabraham Jun 16, 2025
857abff
Update TranslationStore
roryabraham Jun 16, 2025
04fd4da
Delete Language type
roryabraham Jun 16, 2025
311cc70
Fix generateTranslations types
roryabraham Jun 16, 2025
f69f0ad
Use SORTED_LOCALES in LocalePicker
roryabraham Jun 16, 2025
cb97036
Use SORTED_LOCALES in LanguagePage
roryabraham Jun 16, 2025
88baa5a
Remove full language names from translation files
roryabraham Jun 16, 2025
d47dc70
Remove unused src/libs/LocaleUtils
roryabraham Jun 16, 2025
e73411e
Fix src/libs/Localize types
roryabraham Jun 16, 2025
17a05fe
DRY locale type
roryabraham Jun 16, 2025
d2adc15
Fix miscelaneous types
roryabraham Jun 16, 2025
9d3228b
Filter LocalePicker and LanguagePage by beta
roryabraham Jun 16, 2025
b9bf799
Add hintText for ai-generated locales
roryabraham Jun 16, 2025
9559c74
Fix emoji types
roryabraham Jun 16, 2025
7ac1224
Fix lint-changed
roryabraham Jun 17, 2025
7611587
Fix TRANSLATION_TARGET_LOCALES
roryabraham Jun 17, 2025
851349d
Fix LanguagePage
roryabraham Jun 17, 2025
aa4263b
Sort Spanish right after English
roryabraham Jun 17, 2025
1cebeda
Wrap LanguagePage in FullPageOfflineBlockingView
roryabraham Jun 17, 2025
9e94a12
Fix tests
roryabraham Jun 17, 2025
55f2ad4
Remove unused es-ES locale
roryabraham Jun 17, 2025
ba19c99
Fix English emoji fallback
roryabraham Jun 17, 2025
ecae44e
Reimplement GetUserLanguage for Apple Sign In
roryabraham Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions assets/emojis/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {Locale} from '@src/types/onyx';
import type {FullySupportedLocale} from '@src/CONST/LOCALES';
import emojis from './common';
import type {Emoji, EmojisList} from './types';

type EmojiTable = Record<string, Emoji>;

type LocaleEmojis = Partial<Record<Locale, EmojisList>>;
type LocaleEmojis = Partial<Record<FullySupportedLocale, EmojisList>>;

const emojiNameTable = emojis.reduce<EmojiTable>((prev, cur) => {
const newValue = prev;
Expand Down Expand Up @@ -32,13 +32,12 @@ const localeEmojis: LocaleEmojis = {
es: undefined,
};

const importEmojiLocale = (locale: Locale) => {
const normalizedLocale = locale.toLowerCase().split('-').at(0) as Locale;
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();
Expand Down
4 changes: 2 additions & 2 deletions prompts/translation/base.ts
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
20 changes: 8 additions & 12 deletions scripts/generateTranslations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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 type {TranslationTargetLanguage} 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';
import PromisePool from './utils/PromisePool';
Expand Down Expand Up @@ -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.
Expand All @@ -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');
Expand Down Expand Up @@ -426,10 +426,6 @@ class TranslationGenerator {
}
}

function isTranslationTargetLanguage(str: string): str is TranslationTargetLanguage {
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.
*/
Expand All @@ -445,12 +441,12 @@ async function main(): Promise<void> {
// 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: TRANSLATION_TARGET_LOCALES,
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);
Expand Down
4 changes: 2 additions & 2 deletions scripts/utils/Translator/ChatGPTTranslator.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -21,7 +21,7 @@ class ChatGPTTranslator extends Translator {
this.openai = new OpenAIUtils(apiKey);
}

protected async performTranslation(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise<string> {
protected async performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise<string> {
const systemPrompt = dedent(`
${getBasePrompt(targetLang)}
${getContextPrompt(context)}
Expand Down
4 changes: 2 additions & 2 deletions scripts/utils/Translator/DummyTranslator.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
protected performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise<string> {
return Promise.resolve(`[${targetLang}]${context ? `[ctx: ${context}]` : ''} ${text}`);
}
}
Expand Down
6 changes: 3 additions & 3 deletions scripts/utils/Translator/Translator.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<string> {
public async translate(targetLang: TranslationTargetLocale, text: string, context?: string): Promise<string> {
const isEmpty = !text || text.trim().length === 0;
if (isEmpty) {
return '';
Expand All @@ -25,7 +25,7 @@ abstract class Translator {
/**
* Translate a string to the given locale.
*/
protected abstract performTranslation(targetLang: TranslationTargetLanguage, text: string, context?: string): Promise<string>;
protected abstract performTranslation(targetLang: TranslationTargetLocale, text: string, context?: string): Promise<string>;

/**
* Trim a string to keep logs readable.
Expand Down
111 changes: 75 additions & 36 deletions src/CONST/LOCALES.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -18,40 +24,73 @@ const UPCOMING_LOCALES = {
PL: 'pl',
PT_BR: 'pt-BR',
ZH_HANS: 'zh-hans',
};

const LANGUAGES = [LOCALES.EN, LOCALES.ES] as const;
} 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;
/**
* These are additional locales that are not valid values of the preferredLocale NVP.
*/
const EXTENDED_LOCALES = {
ES_ES_ONFIDO: 'es_ES',
} 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]: '中文 (简体)',
/**
* Locales that are valid values of the preferredLocale NVP.
*/
const LOCALES = {
DEFAULT: FULLY_SUPPORTED_LOCALES.EN,
...FULLY_SUPPORTED_LOCALES,
...BETA_LOCALES,
} as const;

type SupportedLanguage = TupleToUnion<Spread<typeof LANGUAGES, typeof UPCOMING_LANGUAGES>>;
/**
* Locales that are valid translation targets. This does not include English, because it's used as the source of truth.
*/
const {DEFAULT, EN, ...TRANSLATION_TARGET_LOCALES} = {...LOCALES} as const;

/**
* We can translate into any language but English (which is used as the source language).
* These strings are never translated.
*/
type TranslationTargetLanguage = Exclude<SupportedLanguage, typeof LOCALES.EN>;
const LOCALE_TO_LANGUAGE_STRING = {
[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 FullySupportedLocale = ValueOf<typeof FULLY_SUPPORTED_LOCALES>;
type Locale = FullySupportedLocale | ValueOf<typeof BETA_LOCALES>;
type TranslationTargetLocale = ValueOf<typeof TRANSLATION_TARGET_LOCALES>;

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);
}

export {LOCALES, LANGUAGES, UPCOMING_LANGUAGES, LOCALE_TO_LANGUAGE_STRING};
export type {SupportedLanguage, TranslationTargetLanguage};
function isFullySupportedLocale(locale: Locale): locale is FullySupportedLocale {
return (Object.values(FULLY_SUPPORTED_LOCALES) as Locale[]).includes(locale);
}

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,
isSupportedLocale,
isFullySupportedLocale,
isTranslationTargetLocale,
};
export type {FullySupportedLocale, Locale, TranslationTargetLocale};
4 changes: 2 additions & 2 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -2883,7 +2884,6 @@ const CONST = {
},

LOCALES,
LANGUAGES,

PRONOUNS_LIST: [
'coCos',
Expand Down
6 changes: 3 additions & 3 deletions src/components/EmojiWithTooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.getEmojiName(emoji, preferredLocale);
const emoji = findEmojiByCode(emojiCode);
const emojiName = getLocalizedEmojiName(emoji.name, preferredLocale);

const emojiTooltipContent = useCallback(
() => (
Expand Down
5 changes: 1 addition & 4 deletions src/components/LocaleContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof CONST.LOCALES>;

type LocaleContextProviderProps = {
/** Actual content wrapped by this component */
children: React.ReactNode;
Expand Down
Loading
Loading