diff --git a/src/CONST.ts b/src/CONST.ts index fa44cda20720..f3ca1b8f9435 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -43,10 +43,21 @@ const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInpu // describes if a shortcut key can cause navigation const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; +const chatTypes = { + POLICY_ANNOUNCE: 'policyAnnounce', + POLICY_ADMINS: 'policyAdmins', + DOMAIN_ALL: 'domainAll', + POLICY_ROOM: 'policyRoom', + POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', +} as const; + // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { + MERGED_ACCOUNT_PREFIX: 'MERGED_', + DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, @@ -704,14 +715,7 @@ const CONST = { IOU: 'iou', TASK: 'task', }, - CHAT_TYPE: { - POLICY_ANNOUNCE: 'policyAnnounce', - POLICY_ADMINS: 'policyAdmins', - DOMAIN_ALL: 'domainAll', - POLICY_ROOM: 'policyRoom', - POLICY_EXPENSE_CHAT: 'policyExpenseChat', - SELF_DM: 'selfDM', - }, + CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', ADMINS: '#admins', diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 933aa7937560..460d5fc0fe9f 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {parsePhoneNumber} from './PhoneNumber'; @@ -18,6 +19,10 @@ function formatPhoneNumber(number: string): string { return ''; } + // do not parse the string, if it doesn't contain the SMS domain and it's not a phone number + if (number.indexOf(CONST.SMS.DOMAIN) === -1 && !CONST.REGEX.DIGITS_AND_PLUS.test(number)) { + return number; + } const numberWithoutSMSDomain = Str.removeSMSDomain(number); const parsedPhoneNumber = parsePhoneNumber(numberWithoutSMSDomain); diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 64d07897aa8a..0e65e5b8be87 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Log from '@libs/Log'; import type {MessageElementBase, MessageTextElement} from '@libs/MessageElement'; import Config from '@src/CONFIG'; @@ -50,34 +51,109 @@ type PhraseParameters = T extends (...args: infer A) => string ? A : never[]; type Phrase = TranslationFlatObject[TKey] extends (...args: infer A) => unknown ? (...args: A) => string : string; /** - * Return translated string for given locale and phrase + * Map to store translated values for each locale. + * This is used to avoid translating the same phrase multiple times. * - * @param [desiredLanguage] eg 'en', 'es-ES' - * @param [phraseParameters] Parameters to supply if the phrase is a template literal. + * The data is stored in the following format: + * + * { + * "en": { + * "name": "Name", + * } + * + * Note: We are not storing any translated values for phrases with variables, + * as they have higher chance of being unique, so we'll end up wasting space + * in our cache. */ -function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { - // Search phrase in full locale e.g. es-ES - const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; - let translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; +const translationCache = new Map, Map>( + Object.values(CONST.LOCALES).reduce((cache, locale) => { + cache.push([locale, new Map()]); + return cache; + }, [] as Array<[ValueOf, Map]>), +); + +/** + * Helper function to get the translated string for given + * locale and phrase. This function is used to avoid + * duplicate code in getTranslatedPhrase and translate functions. + * + * This function first checks if the phrase is already translated + * and in the cache, it returns the translated value from the cache. + * + * If the phrase is not translated, it checks if the phrase is + * available in the given locale. If it is, it translates the + * phrase and stores the translated value in the cache and returns + * the translated value. + * + * @param language + * @param phraseKey + * @param fallbackLanguage + * @param phraseParameters + */ +function getTranslatedPhrase( + language: 'en' | 'es' | 'es-ES', + phraseKey: TKey, + fallbackLanguage: 'en' | 'es' | null = null, + ...phraseParameters: PhraseParameters> +): string | null { + // Get the cache for the above locale + const cacheForLocale = translationCache.get(language); + + // Directly access and assign the translated value from the cache, instead of + // going through map.has() and map.get() to avoid multiple lookups. + const valueFromCache = cacheForLocale?.get(phraseKey); + + // If the phrase is already translated, return the translated value + if (valueFromCache) { + return valueFromCache; + } + + const translatedPhrase = translations?.[language]?.[phraseKey] as Phrase; + if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + if (typeof translatedPhrase === 'function') { + return translatedPhrase(...phraseParameters); + } + + // We set the translated value in the cache only for the phrases without parameters. + cacheForLocale?.set(phraseKey, translatedPhrase); + return translatedPhrase; + } + + if (!fallbackLanguage) { + return null; } // Phrase is not found in full locale, search it in fallback language e.g. es - const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; - translatedPhrase = translations?.[languageAbbreviation]?.[phraseKey] as Phrase; - if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + const fallbacktranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...phraseParameters); + + if (fallbacktranslatedPhrase) { + return fallbacktranslatedPhrase; } - if (languageAbbreviation !== CONST.LOCALES.DEFAULT) { - Log.alert(`${phraseKey} was not found in the ${languageAbbreviation} locale`); + if (fallbackLanguage !== CONST.LOCALES.DEFAULT) { + Log.alert(`${phraseKey} was not found in the ${fallbackLanguage} locale`); } // Phrase is not translated, search it in default language (en) - translatedPhrase = translations?.[CONST.LOCALES.DEFAULT]?.[phraseKey] as Phrase; + return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...phraseParameters); +} + +/** + * Return translated string for given locale and phrase + * + * @param [desiredLanguage] eg 'en', 'es-ES' + * @param [phraseParameters] Parameters to supply if the phrase is a template literal. + */ +function translate(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', phraseKey: TKey, ...phraseParameters: PhraseParameters>): string { + // Search phrase in full locale e.g. es-ES + const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage; + // Phrase is not found in full locale, search it in fallback language e.g. es + const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es'; + + const translatedPhrase = getTranslatedPhrase(language, phraseKey, languageAbbreviation, ...phraseParameters); if (translatedPhrase) { - return typeof translatedPhrase === 'function' ? translatedPhrase(...phraseParameters) : translatedPhrase; + return translatedPhrase; } // Phrase is not found in default language, on production and staging log an alert to server diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 65aadd440010..c8107c22bb1a 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -25,7 +25,13 @@ Onyx.connect({ }); function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string { - let displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : ''; + let displayName = passedPersonalDetails?.displayName ?? ''; + + // If the displayName starts with the merged account prefix, remove it. + if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { + // Remove the merged account prefix from the displayName. + displayName = displayName.substring(CONST.MERGED_ACCOUNT_PREFIX.length); + } // If the displayName is not set by the user, the backend sets the diplayName same as the login so // we need to remove the sms domain from the displayName if it is an sms login. @@ -37,9 +43,10 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial): boolean { * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].some((type) => type === getChatType(report)); + return CONST.DEFAULT_POLICY_ROOM_CHAT_TYPES.some((type) => type === getChatType(report)); } /**