diff --git a/__mocks__/react-native-localize.js b/__mocks__/react-native-localize.js deleted file mode 100644 index 2800ee902..000000000 --- a/__mocks__/react-native-localize.js +++ /dev/null @@ -1,2 +0,0 @@ -export const initialConstants = null; -export const findBestAvailableLanguage = () => null; diff --git a/app/i18n/index.ts b/app/i18n/index.ts index 1c41842c6..9b9a1b491 100644 --- a/app/i18n/index.ts +++ b/app/i18n/index.ts @@ -1,11 +1,11 @@ -import i18n from 'i18n-js'; +import { createIntl, createIntlCache } from '@formatjs/intl'; import { I18nManager } from 'react-native'; import * as RNLocalize from 'react-native-localize'; import moment from 'moment'; import 'moment/min/locales'; import { toMomentLocale } from './moment'; -import { isRTL } from './isRTL'; +// import { isRTL } from './isRTL'; import englishJson from './locales/en.json'; type TTranslatedKeys = keyof typeof englishJson; @@ -115,44 +115,78 @@ const translations = LANGUAGES.reduce((ret, item) => { return ret; }, {} as ITranslations); -export const setLanguage = (l: string) => { - if (!l) { - return; - } - // server uses lowercase pattern (pt-br), but we're forced to use standard pattern (pt-BR) - let locale = LANGUAGES.find(ll => ll.value.toLowerCase() === l.toLowerCase())?.value; - if (!locale) { - locale = 'en'; - } - // don't go forward if it's the same language and default language (en) was setup already - if (i18n.locale === locale && i18n.translations?.en) { - return; - } - i18n.locale = locale; - i18n.translations = { ...i18n.translations, [locale]: translations[locale]?.() }; - I18nManager.forceRTL(isRTL(locale)); - I18nManager.swapLeftAndRightInRTL(isRTL(locale)); - // TODO: Review this logic - // @ts-ignore - i18n.isRTL = I18nManager.isRTL; - moment.locale(toMomentLocale(locale)); +// const translations = { +// ar: require("./translations/ar.json"), +// en: require("./translations/en.json"), +// fr: require("./translations/fr.json"), +// } as const; + +type Translation = keyof typeof translations; + +// fallback if no available language fits +const fallback = { languageTag: 'en', isRTL: false }; + +const { languageTag, isRTL } = RNLocalize.findBestAvailableLanguage(Object.keys(translations)) ?? fallback; + +// update layout direction +I18nManager.forceRTL(isRTL); + +const intl = createIntl( + { + defaultLocale: 'en', + locale: languageTag, + messages: translations[languageTag as Translation]?.() + }, + createIntlCache() +); + +type TranslationParams = Parameters[1]; + +const i18n = { + isRTL: false, + t: (key: string, params?: TranslationParams) => + intl.formatMessage({ id: key, defaultMessage: translations.en[key] }, params).toString(), + isTranslated: (text?: string) => (text ? text in englishJson : false) }; -i18n.translations = { en: translations.en?.() }; -const defaultLanguage = { languageTag: 'en', isRTL: false }; -const availableLanguages = Object.keys(translations); -const { languageTag } = RNLocalize.findBestAvailableLanguage(availableLanguages) || defaultLanguage; +export const setLanguage = (l: string) => { + if (!l) { + } -// @ts-ignore -i18n.isTranslated = (text?: string) => text in englishJson; + // // server uses lowercase pattern (pt-br), but we're forced to use standard pattern (pt-BR) + // let locale = LANGUAGES.find(ll => ll.value.toLowerCase() === l.toLowerCase())?.value; + // if (!locale) { + // locale = 'en'; + // } + // // don't go forward if it's the same language and default language (en) was setup already + // if (i18n.locale === locale && i18n.translations?.en) { + // return; + // } + // i18n.locale = locale; + // i18n.translations = { ...i18n.translations, [locale]: translations[locale]?.() }; + // I18nManager.forceRTL(isRTL(locale)); + // I18nManager.swapLeftAndRightInRTL(isRTL(locale)); + // // TODO: Review this logic + // // @ts-ignore + // i18n.isRTL = I18nManager.isRTL; + // moment.locale(toMomentLocale(locale)); +}; -setLanguage(languageTag); -i18n.fallbacks = true; +// i18n.translations = { en: translations.en?.() }; +// const defaultLanguage = { languageTag: 'en', isRTL: false }; +// const availableLanguages = Object.keys(translations); +// const { languageTag } = RNLocalize.findBestAvailableLanguage(availableLanguages) || defaultLanguage; + +// // @ts-ignore +// i18n.isTranslated = (text?: string) => text in englishJson; + +// setLanguage(languageTag); +// i18n.fallbacks = true; type Ti18n = { isRTL: boolean; t(scope: TTranslatedKeys, options?: any): string; isTranslated: (text?: string) => boolean; -} & typeof i18n; +}; export default i18n as Ti18n; diff --git a/jest.setup.js b/jest.setup.js index 0d59759b3..4feeb15fd 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,6 +1,8 @@ import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock.js'; import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'; +import mockRNLocalize from 'react-native-localize/mock'; + jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); global.__reanimatedWorkletInit = () => {}; @@ -67,3 +69,5 @@ jest.mock('react-native-math-view', () => { MathText: react.View // {...} Named export }; }); + +jest.mock('react-native-localize', () => mockRNLocalize); diff --git a/package.json b/package.json index 0c92d4e34..992d4d6f4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@bugsnag/react-native": "^7.19.0", "@codler/react-native-keyboard-aware-scroll-view": "^2.0.1", + "@formatjs/intl": "^2.6.9", "@gorhom/bottom-sheet": "^4.4.5", "@hookform/resolvers": "^2.9.10", "@nozbe/watermelondb": "^0.25.5", @@ -106,7 +107,7 @@ "react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker", "react-native-image-progress": "^1.1.1", "react-native-linear-gradient": "^2.6.2", - "react-native-localize": "2.1.1", + "react-native-localize": "^2.2.6", "react-native-math-view": "^3.9.5", "react-native-mime-types": "2.3.0", "react-native-mmkv-storage": "^0.9.1", diff --git a/yarn.lock b/yarn.lock index 3d24b7493..3d9b71db1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3884,6 +3884,76 @@ find-up "^5.0.0" js-yaml "^4.1.0" +"@formatjs/ecma402-abstract@1.14.3": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz#6428f243538a11126180d121ce8d4b2f17465738" + integrity sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg== + dependencies: + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" + +"@formatjs/fast-memoize@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b" + integrity sha512-M2GgV+qJn5WJQAYewz7q2Cdl6fobQa69S1AzSM2y0P68ZDbK5cWrJIcPCO395Of1ksftGZoOt4LYCO/j9BKBSA== + dependencies: + tslib "^2.4.0" + +"@formatjs/icu-messageformat-parser@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz#8e8fd577c3e39454ef14bba4963f2e1d5f2cc46c" + integrity sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/icu-skeleton-parser" "1.3.18" + tslib "^2.4.0" + +"@formatjs/icu-skeleton-parser@1.3.18": + version "1.3.18" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz#7aed3d60e718c8ad6b0e64820be44daa1e29eeeb" + integrity sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + tslib "^2.4.0" + +"@formatjs/intl-displaynames@6.2.6": + version "6.2.6" + resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.2.6.tgz#6bc02fe0bf6571391aac0e01e74ecbf38542ff32" + integrity sha512-scf5AQTk9EjpvPhboo5sizVOvidTdMOnajv9z+0cejvl7JNl9bl/aMrNBgC72UH+bP3l45usPUKAGskV6sNIrA== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" + +"@formatjs/intl-listformat@7.1.9": + version "7.1.9" + resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.1.9.tgz#0c2ce67b610054f215dd2635a6da7da308cfbe3d" + integrity sha512-5YikxwRqRXTVWVujhswDOTCq6gs+m9IcNbNZLa6FLtyBStAjEsuE2vAU+lPsbz9ZTST57D5fodjIh2JXT6sMWQ== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.2.32": + version "0.2.32" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" + integrity sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ== + dependencies: + tslib "^2.4.0" + +"@formatjs/intl@^2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.6.9.tgz#d4bdd8c21888f579a95341580141d0aafe761610" + integrity sha512-EtcMZ9O24YSASu/jGOaTQtArx7XROjlKiO4KmkxJ/3EyAQLCr5hrS+KKvNud0a7GIwBucOb3IFrZ7WiSm2A/Cw== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/fast-memoize" "2.0.1" + "@formatjs/icu-messageformat-parser" "2.3.0" + "@formatjs/intl-displaynames" "6.2.6" + "@formatjs/intl-listformat" "7.1.9" + intl-messageformat "10.3.3" + tslib "^2.4.0" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -12176,6 +12246,16 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +intl-messageformat@10.3.3: + version "10.3.3" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.3.tgz#576798d31c9f8d90f9beadaa5a3878b8d30177a2" + integrity sha512-un/f07/g2e/3Q8e1ghDKET+el22Bi49M7O/rHxd597R+oLpPOMykSv5s51cABVfu3FZW+fea4hrzf2MHu1W4hw== + dependencies: + "@formatjs/ecma402-abstract" "1.14.3" + "@formatjs/fast-memoize" "2.0.1" + "@formatjs/icu-messageformat-parser" "2.3.0" + tslib "^2.4.0" + invariant@*, invariant@2.2.4, invariant@^2.1.0, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -16952,10 +17032,10 @@ react-native-linear-gradient@^2.6.2: resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz#56598a76832724b2afa7889747635b5c80948f38" integrity sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ== -react-native-localize@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.1.1.tgz#da8f8776991d2b748708c408db05152602cefb38" - integrity sha512-+uyz2/b0vyLq19fcb7r1qU1gqmzbp3aC6EMTvOVXwfBuiN+aJXR/fDdFOJJ8D7+bLccKeuS2zBDrazh+ZayX/g== +react-native-localize@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.2.6.tgz#484f8c700bc629f230066e819265f80f6dd3ef58" + integrity sha512-EZETlC1ZlW/4g6xfsNCwAkAw5BDL2A6zk/08JjFR/GRGxYuKRD7iP1hHn1+h6DEu+xROjPpoNeXfMER2vkTVIQ== react-native-math-view@^3.9.5: version "3.9.5" @@ -19648,6 +19728,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"