diff --git a/app/containers/List/ListItem.tsx b/app/containers/List/ListItem.tsx index 2de344326..21f6a3d69 100644 --- a/app/containers/List/ListItem.tsx +++ b/app/containers/List/ListItem.tsx @@ -91,7 +91,7 @@ const Content = React.memo( - {translateTitle ? I18n.t(title) : title} + {translateTitle && title ? I18n.t(title) : title} {alert ? ( diff --git a/app/containers/MessageBox/buttons/BaseButton.tsx b/app/containers/MessageBox/buttons/BaseButton.tsx index 71bf8ec70..1bc739072 100644 --- a/app/containers/MessageBox/buttons/BaseButton.tsx +++ b/app/containers/MessageBox/buttons/BaseButton.tsx @@ -20,7 +20,10 @@ const BaseButton = ({ accessibilityLabel, icon, color, ...props }: Partial - + diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 812007340..fdadff83c 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -680,7 +680,7 @@ class MessageBox extends Component { if (result.success) { return true; } - Alert.alert(I18n.t('Error_uploading'), I18n.t(result.error)); + Alert.alert(I18n.t('Error_uploading'), result.error && I18n.isTranslated(result.error) ? I18n.t(result.error) : result.error); return false; }; diff --git a/app/containers/Passcode/Base/index.tsx b/app/containers/Passcode/Base/index.tsx index 940c5987f..4bc2b9dd0 100644 --- a/app/containers/Passcode/Base/index.tsx +++ b/app/containers/Passcode/Base/index.tsx @@ -19,7 +19,7 @@ interface IPasscodeBase { type: string; previousPasscode?: string; title: string; - subtitle?: string; + subtitle?: string | null; showBiometry?: boolean; onEndProcess: Function; onError?: Function; diff --git a/app/containers/Passcode/PasscodeChoose.tsx b/app/containers/Passcode/PasscodeChoose.tsx index cc21b8bfc..e7576168d 100644 --- a/app/containers/Passcode/PasscodeChoose.tsx +++ b/app/containers/Passcode/PasscodeChoose.tsx @@ -14,7 +14,7 @@ interface IPasscodeChoose { const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => { const chooseRef = useRef(null); const confirmRef = useRef(null); - const [subtitle, setSubtitle] = useState(null); + const [subtitle, setSubtitle] = useState(null); const [status, setStatus] = useState(TYPE.CHOOSE); const [previousPasscode, setPreviouPasscode] = useState(''); diff --git a/app/containers/markdown/index.tsx b/app/containers/markdown/index.tsx index 16bcaa27e..07dfaae40 100644 --- a/app/containers/markdown/index.tsx +++ b/app/containers/markdown/index.tsx @@ -29,7 +29,7 @@ import { themes } from '../../lib/constants'; export { default as MarkdownPreview } from './Preview'; interface IMarkdownProps { - msg?: string; + msg?: string | null; theme: TSupportedThemes; md?: MarkdownAST; mentions?: IUserMention[]; diff --git a/app/containers/message/utils.ts b/app/containers/message/utils.ts index 6d3149236..ff1dc7365 100644 --- a/app/containers/message/utils.ts +++ b/app/containers/message/utils.ts @@ -2,7 +2,7 @@ import { TMessageModel } from '../../definitions/IMessage'; import I18n from '../../i18n'; import { DISCUSSION } from './constants'; -export const formatMessageCount = (count?: number, type?: string): string => { +export const formatMessageCount = (count?: number, type?: string): string | null => { const discussion = type === DISCUSSION; let text = discussion ? I18n.t('No_messages_yet') : null; if (!count) { diff --git a/app/i18n/index.js b/app/i18n/index.ts similarity index 80% rename from app/i18n/index.js rename to app/i18n/index.ts index aa39cef7c..3b948c104 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.ts @@ -6,10 +6,19 @@ import 'moment/min/locales'; import { toMomentLocale } from '../utils/moment'; import { isRTL } from './isRTL'; +import englishJson from './locales/en.json'; + +type TTranslatedKeys = keyof typeof englishJson; export { isRTL }; -export const LANGUAGES = [ +interface ILanguage { + label: string; + value: string; + file: () => any; +} + +export const LANGUAGES: ILanguage[] = [ { label: 'English', value: 'en', @@ -82,12 +91,16 @@ export const LANGUAGES = [ } ]; +interface ITranslations { + [language: string]: () => typeof englishJson; +} + const translations = LANGUAGES.reduce((ret, item) => { ret[item.value] = item.file; return ret; -}, {}); +}, {} as ITranslations); -export const setLanguage = l => { +export const setLanguage = (l: string) => { if (!l) { return; } @@ -104,6 +117,8 @@ export const setLanguage = l => { 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)); }; @@ -113,7 +128,16 @@ 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; -export default i18n; +type Ti18n = { + isRTL: boolean; + t(scope: TTranslatedKeys, options?: any): string; + isTranslated: (text?: string) => boolean; +} & typeof i18n; + +export default i18n as Ti18n; diff --git a/app/i18n/isRTL.js b/app/i18n/isRTL.ts similarity index 65% rename from app/i18n/isRTL.js rename to app/i18n/isRTL.ts index 0f145ca14..11e6878a8 100644 --- a/app/i18n/isRTL.js +++ b/app/i18n/isRTL.ts @@ -1,4 +1,4 @@ // https://github.com/zoontek/react-native-localize/blob/master/src/constants.ts#L5 const USES_RTL_LAYOUT = ['ar', 'ckb', 'fa', 'he', 'ks', 'lrc', 'mzn', 'ps', 'ug', 'ur', 'yi']; -export const isRTL = locale => USES_RTL_LAYOUT.includes(locale); +export const isRTL = (locale?: string) => (locale ? USES_RTL_LAYOUT.includes(locale) : false); diff --git a/app/views/NotificationPreferencesView/index.tsx b/app/views/NotificationPreferencesView/index.tsx index 1a2606b6a..b706babce 100644 --- a/app/views/NotificationPreferencesView/index.tsx +++ b/app/views/NotificationPreferencesView/index.tsx @@ -16,7 +16,7 @@ import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import SafeAreaView from '../../containers/SafeAreaView'; import log, { events, logEvent } from '../../utils/log'; import sharedStyles from '../Styles'; -import { OPTIONS } from './options'; +import { IOptionsField, OPTIONS } from './options'; import { ChatsStackParamList } from '../../stacks/types'; import { IRoomNotifications } from '../../definitions'; @@ -131,10 +131,10 @@ class NotificationPreferencesView extends React.Component { const { room } = this.state; const { theme } = this.props; - const text = room[key] ? OPTIONS[key].find((option: any) => option.value === room[key]) : OPTIONS[key][0]; + const text = room[key] ? OPTIONS[key].find(option => option.value === room[key]) : (OPTIONS[key][0] as IOptionsField); return ( - {I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })} + {text?.label ? I18n.t(text?.label, { defaultValue: text?.label, second: text?.second }) : text?.label} ); }; diff --git a/app/views/RoomInfoView/Channel.tsx b/app/views/RoomInfoView/Channel.tsx index 3a1a03442..c82b7f032 100644 --- a/app/views/RoomInfoView/Channel.tsx +++ b/app/views/RoomInfoView/Channel.tsx @@ -25,7 +25,7 @@ const Channel = ({ room }: { room: ISubscription }) => { /> diff --git a/app/views/UserNotificationPreferencesView/index.tsx b/app/views/UserNotificationPreferencesView/index.tsx index fbb80ee1a..f1d460113 100644 --- a/app/views/UserNotificationPreferencesView/index.tsx +++ b/app/views/UserNotificationPreferencesView/index.tsx @@ -72,7 +72,11 @@ class UserNotificationPreferencesView extends React.Component< renderPickerOption = (key: TKey) => { const { theme } = this.props; const text = this.findDefaultOption(key); - return {I18n.t(text?.label)}; + return ( + + {text?.label ? I18n.t(text?.label) : text?.label} + + ); }; pickerSelection = (title: string, key: TKey) => { @@ -82,7 +86,9 @@ class UserNotificationPreferencesView extends React.Component< const defaultOption = this.findDefaultOption(key); if (OPTIONS[key][0]?.value !== 'default') { - const defaultValue = { label: `${I18n.t('Default')} (${I18n.t(defaultOption?.label)})` } as { + const defaultValue = { + label: `${I18n.t('Default')} (${defaultOption?.label ? I18n.t(defaultOption?.label) : defaultOption?.label})` + } as { label: string; value: string; }; diff --git a/package.json b/package.json index ef18e865a..7cba44706 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "@testing-library/react-native": "^9.0.0", "@types/bytebuffer": "^5.0.43", "@types/ejson": "^2.1.3", + "@types/i18n-js": "^3.8.2", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.171", "@types/react": "^17.0.14", diff --git a/tsconfig.json b/tsconfig.json index db7d37b56..f8fed1973 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -67,7 +67,8 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "resolveJsonModule": true }, "exclude": ["node_modules", "e2e/docker", "__mocks__"] } diff --git a/yarn.lock b/yarn.lock index 3f462d8c7..fb4ea8e15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4514,6 +4514,11 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== +"@types/i18n-js@^3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@types/i18n-js/-/i18n-js-3.8.2.tgz#957a3fa268124d09e3b3b34695f0184118f4bc4f" + integrity sha512-F+AuFCjllE1A0W/YUxJB13q2t7cWITMqXOTXQ/InfXxxT8nXrrqL7s/8Pv6XThGjFPemukElwk6QlMOKCEg7eQ== + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83"