Skip to content

Commit 67f9f77

Browse files
authored
Merge pull request #25846 from BeeMargarida/feat/25466-flatten_translation_objects
Improve translations - flatten translation objects
2 parents 2bf7bf4 + 8f8420a commit 67f9f77

File tree

16 files changed

+268
-58
lines changed

16 files changed

+268
-58
lines changed

src/CONST.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,29 @@ const CONST = {
10901090
DEFAULT: 'en',
10911091
},
10921092

1093+
LANGUAGES: ['en', 'es'],
1094+
1095+
PRONOUNS_LIST: [
1096+
'coCos',
1097+
'eEyEmEir',
1098+
'heHimHis',
1099+
'heHimHisTheyThemTheirs',
1100+
'sheHerHers',
1101+
'sheHerHersTheyThemTheirs',
1102+
'merMers',
1103+
'neNirNirs',
1104+
'neeNerNers',
1105+
'perPers',
1106+
'theyThemTheirs',
1107+
'thonThons',
1108+
'veVerVis',
1109+
'viVir',
1110+
'xeXemXyr',
1111+
'zeZieZirHir',
1112+
'zeHirHirs',
1113+
'callMeByMyName',
1114+
],
1115+
10931116
POLICY: {
10941117
TYPE: {
10951118
FREE: 'free',
@@ -1661,6 +1684,61 @@ const CONST = {
16611684
ZW: 'Zimbabwe',
16621685
},
16631686

1687+
ALL_US_ISO_STATES: [
1688+
'AK',
1689+
'AL',
1690+
'AR',
1691+
'AZ',
1692+
'CA',
1693+
'CO',
1694+
'CT',
1695+
'DE',
1696+
'FL',
1697+
'GA',
1698+
'HI',
1699+
'IA',
1700+
'ID',
1701+
'IL',
1702+
'IN',
1703+
'KS',
1704+
'KY',
1705+
'LA',
1706+
'MA',
1707+
'MD',
1708+
'ME',
1709+
'MI',
1710+
'MN',
1711+
'MO',
1712+
'MS',
1713+
'MT',
1714+
'NC',
1715+
'ND',
1716+
'NE',
1717+
'NH',
1718+
'NJ',
1719+
'NM',
1720+
'NV',
1721+
'NY',
1722+
'OH',
1723+
'OK',
1724+
'OR',
1725+
'PA',
1726+
'PR',
1727+
'RI',
1728+
'SC',
1729+
'SD',
1730+
'TN',
1731+
'TX',
1732+
'UT',
1733+
'VA',
1734+
'VT',
1735+
'WA',
1736+
'WI',
1737+
'WV',
1738+
'WY',
1739+
'DC',
1740+
],
1741+
16641742
// Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427
16651743
// https://github.com/Expensify/App/issues/14958#issuecomment-1456026810
16661744
COUNTRY_ZIP_REGEX_DATA: {

src/components/CountryPicker/CountrySelectorModal.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,16 @@ function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySele
4949

5050
const countries = useMemo(
5151
() =>
52-
_.map(translate('allCountries'), (countryName, countryISO) => ({
53-
value: countryISO,
54-
keyForList: countryISO,
55-
text: countryName,
56-
isSelected: currentCountry === countryISO,
57-
searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
58-
})),
52+
_.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => {
53+
const countryName = translate(`allCountries.${countryISO}`);
54+
return {
55+
value: countryISO,
56+
keyForList: countryISO,
57+
text: countryName,
58+
isSelected: currentCountry === countryISO,
59+
searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
60+
};
61+
}),
5962
[translate, currentCountry],
6063
);
6164

src/components/CountryPicker/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ const defaultProps = {
3131

3232
function CountryPicker({value, errorText, onInputChange, forwardedRef}) {
3333
const {translate} = useLocalize();
34-
const allCountries = translate('allCountries');
3534
const [isPickerVisible, setIsPickerVisible] = useState(false);
3635
const [searchValue, setSearchValue] = useState('');
3736

@@ -48,7 +47,7 @@ function CountryPicker({value, errorText, onInputChange, forwardedRef}) {
4847
hidePickerModal();
4948
};
5049

51-
const title = allCountries[value] || '';
50+
const title = value ? translate(`allCountries.${value}`) : '';
5251
const descStyle = title.length === 0 ? styles.textNormal : null;
5352

5453
return (

src/components/LocalePicker.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ const defaultProps = {
2727
};
2828

2929
function LocalePicker(props) {
30-
const localesToLanguages = _.map(props.translate('languagePage.languages'), (language, key) => ({
31-
value: key,
32-
label: language.label,
30+
const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({
31+
value: language,
32+
label: props.translate(`languagePage.languages.${language}.label`),
33+
keyForList: language,
34+
isSelected: props.preferredLocale === language,
3335
}));
3436
return (
3537
<Picker

src/components/StatePicker/StateSelectorModal.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected,
5353

5454
const countryStates = useMemo(
5555
() =>
56-
_.map(translate('allStates'), (state) => ({
57-
value: state.stateISO,
58-
keyForList: state.stateISO,
59-
text: state.stateName,
60-
isSelected: currentState === state.stateISO,
61-
searchValue: StringUtils.sanitizeString(`${state.stateISO}${state.stateName}`),
62-
})),
56+
_.map(CONST.ALL_US_ISO_STATES, (state) => {
57+
const stateName = translate(`allStates.${state}.stateName`);
58+
const stateISO = translate(`allStates.${state}.stateISO`);
59+
return {
60+
value: stateISO,
61+
keyForList: stateISO,
62+
text: stateName,
63+
isSelected: currentState === stateISO,
64+
searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`),
65+
};
66+
}),
6367
[translate, currentState],
6468
);
6569

src/components/StatePicker/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const defaultProps = {
3535

3636
function StatePicker({value, errorText, onInputChange, forwardedRef, label}) {
3737
const {translate} = useLocalize();
38-
const allStates = translate('allStates');
3938
const [isPickerVisible, setIsPickerVisible] = useState(false);
4039
const [searchValue, setSearchValue] = useState('');
4140

@@ -52,7 +51,7 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label}) {
5251
hidePickerModal();
5352
};
5453

55-
const title = allStates[value] ? allStates[value].stateName : '';
54+
const title = value ? translate(`allStates.${value}.stateName`) : '';
5655
const descStyle = title.length === 0 ? styles.textNormal : null;
5756

5857
return (

src/languages/en.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
22
import CONST from '../CONST';
33
import type {
4+
Translation,
45
AddressLineParams,
56
CharacterLimitParams,
67
MaxParticipantsReachedParams,
@@ -1756,4 +1757,4 @@ export default {
17561757
selectSuggestedAddress: 'Please select a suggested address',
17571758
},
17581759
},
1759-
} as const;
1760+
} satisfies Translation;

src/languages/es.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import CONST from '../CONST';
22
import * as ReportActionsUtils from '../libs/ReportActionsUtils';
33
import type {
4+
Translation,
45
AddressLineParams,
56
CharacterLimitParams,
67
MaxParticipantsReachedParams,
@@ -2248,4 +2249,4 @@ export default {
22482249
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida',
22492250
},
22502251
},
2251-
};
2252+
} satisfies Translation;

src/languages/translations.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
import en from './en';
22
import es from './es';
33
import esES from './es-ES';
4+
import type {Translation, TranslationFlatObject} from './types';
5+
6+
/**
7+
* Converts an object to it's flattened version.
8+
*
9+
* Ex:
10+
* Input: { common: { yes: "Yes", no: "No" }}
11+
* Output: { "common.yes": "Yes", "common.no": "No" }
12+
*/
13+
// Necessary to export so that it is accessible to the unit tests
14+
// eslint-disable-next-line rulesdir/no-inline-named-export
15+
export function flattenObject(obj: Translation): TranslationFlatObject {
16+
const result: Record<string, unknown> = {};
17+
18+
const recursive = (data: Translation, key: string): void => {
19+
// If the data is a function or not a object (eg. a string or array),
20+
// it's the final value for the key being built and there is no need
21+
// for more recursion
22+
if (typeof data === 'function' || Array.isArray(data) || !(typeof data === 'object' && !!data)) {
23+
result[key] = data;
24+
} else {
25+
let isEmpty = true;
26+
27+
// Recursive call to the keys and connect to the respective data
28+
Object.keys(data).forEach((k) => {
29+
isEmpty = false;
30+
recursive(data[k] as Translation, key ? `${key}.${k}` : k);
31+
});
32+
33+
// Check for when the object is empty but a key exists, so that
34+
// it defaults to an empty object
35+
if (isEmpty && key) {
36+
result[key] = '';
37+
}
38+
}
39+
};
40+
41+
recursive(obj, '');
42+
return result as TranslationFlatObject;
43+
}
444

545
export default {
6-
en,
7-
es,
46+
en: flattenObject(en),
47+
es: flattenObject(es),
848
// eslint-disable-next-line @typescript-eslint/naming-convention
949
'es-ES': esES,
1050
};

src/languages/types.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import en from './en';
2+
13
type AddressLineParams = {
24
lineNumber: number;
35
};
@@ -190,7 +192,47 @@ type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string};
190192

191193
type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string};
192194

195+
/* Translation Object types */
196+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
197+
type TranslationBaseValue = string | string[] | ((...args: any[]) => string);
198+
199+
type Translation = {[key: string]: TranslationBaseValue | Translation};
200+
201+
/* Flat Translation Object types */
202+
// Flattens an object and returns concatenations of all the keys of nested objects
203+
type FlattenObject<TObject, TPrefix extends string = ''> = {
204+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
205+
[TKey in keyof TObject]: TObject[TKey] extends (...args: any[]) => any
206+
? `${TPrefix}${TKey & string}`
207+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
208+
TObject[TKey] extends any[]
209+
? `${TPrefix}${TKey & string}`
210+
: // eslint-disable-next-line @typescript-eslint/ban-types
211+
TObject[TKey] extends object
212+
? FlattenObject<TObject[TKey], `${TPrefix}${TKey & string}.`>
213+
: `${TPrefix}${TKey & string}`;
214+
}[keyof TObject];
215+
216+
// Retrieves a type for a given key path (calculated from the type above)
217+
type TranslateType<TObject, TPath extends string> = TPath extends keyof TObject
218+
? TObject[TPath]
219+
: TPath extends `${infer TKey}.${infer TRest}`
220+
? TKey extends keyof TObject
221+
? TranslateType<TObject[TKey], TRest>
222+
: never
223+
: never;
224+
225+
type TranslationsType = typeof en;
226+
227+
type TranslationPaths = FlattenObject<TranslationsType>;
228+
229+
type TranslationFlatObject = {
230+
[TKey in TranslationPaths]: TranslateType<TranslationsType, TKey>;
231+
};
232+
193233
export type {
234+
Translation,
235+
TranslationFlatObject,
194236
AddressLineParams,
195237
CharacterLimitParams,
196238
MaxParticipantsReachedParams,

src/libs/Localize/index.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function init() {
3838
* Return translated string for given locale and phrase
3939
*
4040
* @param {String} [desiredLanguage] eg 'en', 'es-ES'
41-
* @param {String|Array} phraseKey
41+
* @param {String} phraseKey
4242
* @param {Object} [phraseParameters] Parameters to supply if the phrase is a template literal.
4343
* @returns {String}
4444
*/
@@ -47,15 +47,15 @@ function translate(desiredLanguage = CONST.LOCALES.DEFAULT, phraseKey, phrasePar
4747
let translatedPhrase;
4848

4949
// Search phrase in full locale e.g. es-ES
50-
const desiredLanguageDictionary = lodashGet(translations, desiredLanguage);
51-
translatedPhrase = lodashGet(desiredLanguageDictionary, phraseKey);
50+
const desiredLanguageDictionary = translations[desiredLanguage] || {};
51+
translatedPhrase = desiredLanguageDictionary[phraseKey];
5252
if (translatedPhrase) {
5353
return Str.result(translatedPhrase, phraseParameters);
5454
}
5555

5656
// Phrase is not found in full locale, search it in fallback language e.g. es
57-
const fallbackLanguageDictionary = lodashGet(translations, languageAbbreviation);
58-
translatedPhrase = lodashGet(fallbackLanguageDictionary, phraseKey);
57+
const fallbackLanguageDictionary = translations[languageAbbreviation] || {};
58+
translatedPhrase = fallbackLanguageDictionary[phraseKey];
5959
if (translatedPhrase) {
6060
return Str.result(translatedPhrase, phraseParameters);
6161
}
@@ -64,8 +64,8 @@ function translate(desiredLanguage = CONST.LOCALES.DEFAULT, phraseKey, phrasePar
6464
}
6565

6666
// Phrase is not translated, search it in default language (en)
67-
const defaultLanguageDictionary = lodashGet(translations, CONST.LOCALES.DEFAULT, {});
68-
translatedPhrase = lodashGet(defaultLanguageDictionary, phraseKey);
67+
const defaultLanguageDictionary = translations[CONST.LOCALES.DEFAULT] || {};
68+
translatedPhrase = defaultLanguageDictionary[phraseKey];
6969
if (translatedPhrase) {
7070
return Str.result(translatedPhrase, phraseParameters);
7171
}

src/pages/settings/Preferences/LanguagePage.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal
77
import * as App from '../../../libs/actions/App';
88
import Navigation from '../../../libs/Navigation/Navigation';
99
import ROUTES from '../../../ROUTES';
10+
import CONST from '../../../CONST';
1011
import SelectionList from '../../../components/SelectionList';
1112

1213
const propTypes = {
@@ -17,11 +18,11 @@ const propTypes = {
1718
};
1819

1920
function LanguagePage(props) {
20-
const localesToLanguages = _.map(props.translate('languagePage.languages'), (language, key) => ({
21-
value: key,
22-
text: language.label,
23-
keyForList: key,
24-
isSelected: props.preferredLocale === key,
21+
const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({
22+
value: language,
23+
text: props.translate(`languagePage.languages.${language}.label`),
24+
keyForList: language,
25+
isSelected: props.preferredLocale === language,
2526
}));
2627

2728
return (

0 commit comments

Comments
 (0)