Chore: Migrate i18n to Typescript (#3988)

* Chore: Migrate i18n to Typescript and fix the Left and Right actions in RoomItem

* remove fix to roomItem

* update storyshot

* Chore: Migrate i18n to Typescript and fix the Left and Right actions in RoomItem

* remove fix to roomItem

* update storyshot

* fix removed itens

* fix changes requested

* interface for i18n, added resolveJsonModule to tsconfig.json

* tweak at error alert

* fix storyshot

* refactor comments

* create function isTranslated

* fix accessibilityLabel

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Reinaldo Neto 2022-04-14 17:30:41 -03:00 committed by GitHub
parent 397cd3d9b8
commit 25c37c1a60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 59 additions and 19 deletions

View File

@ -91,7 +91,7 @@ const Content = React.memo(
<View style={styles.textContainer}> <View style={styles.textContainer}>
<View style={styles.textAlertContainer}> <View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title} {translateTitle && title ? I18n.t(title) : title}
</Text> </Text>
{alert ? ( {alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' /> <CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />

View File

@ -20,7 +20,10 @@ const BaseButton = ({ accessibilityLabel, icon, color, ...props }: Partial<IBase
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<BorderlessButton {...props} style={styles.actionButton}> <BorderlessButton {...props} style={styles.actionButton}>
<View accessible accessibilityLabel={i18n.t(accessibilityLabel)} accessibilityRole='button'> <View
accessible
accessibilityLabel={accessibilityLabel ? i18n.t(accessibilityLabel) : accessibilityLabel}
accessibilityRole='button'>
<CustomIcon name={icon} size={24} color={color || themes[theme].auxiliaryTintColor} /> <CustomIcon name={icon} size={24} color={color || themes[theme].auxiliaryTintColor} />
</View> </View>
</BorderlessButton> </BorderlessButton>

View File

@ -680,7 +680,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (result.success) { if (result.success) {
return true; 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; return false;
}; };

View File

@ -19,7 +19,7 @@ interface IPasscodeBase {
type: string; type: string;
previousPasscode?: string; previousPasscode?: string;
title: string; title: string;
subtitle?: string; subtitle?: string | null;
showBiometry?: boolean; showBiometry?: boolean;
onEndProcess: Function; onEndProcess: Function;
onError?: Function; onError?: Function;

View File

@ -14,7 +14,7 @@ interface IPasscodeChoose {
const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => { const PasscodeChoose = ({ finishProcess, force = false }: IPasscodeChoose) => {
const chooseRef = useRef<IBase>(null); const chooseRef = useRef<IBase>(null);
const confirmRef = useRef<IBase>(null); const confirmRef = useRef<IBase>(null);
const [subtitle, setSubtitle] = useState(null); const [subtitle, setSubtitle] = useState<string | null>(null);
const [status, setStatus] = useState(TYPE.CHOOSE); const [status, setStatus] = useState(TYPE.CHOOSE);
const [previousPasscode, setPreviouPasscode] = useState(''); const [previousPasscode, setPreviouPasscode] = useState('');

View File

@ -29,7 +29,7 @@ import { themes } from '../../lib/constants';
export { default as MarkdownPreview } from './Preview'; export { default as MarkdownPreview } from './Preview';
interface IMarkdownProps { interface IMarkdownProps {
msg?: string; msg?: string | null;
theme: TSupportedThemes; theme: TSupportedThemes;
md?: MarkdownAST; md?: MarkdownAST;
mentions?: IUserMention[]; mentions?: IUserMention[];

View File

@ -2,7 +2,7 @@ import { TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISCUSSION } from './constants'; 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; const discussion = type === DISCUSSION;
let text = discussion ? I18n.t('No_messages_yet') : null; let text = discussion ? I18n.t('No_messages_yet') : null;
if (!count) { if (!count) {

View File

@ -6,10 +6,19 @@ import 'moment/min/locales';
import { toMomentLocale } from '../utils/moment'; import { toMomentLocale } from '../utils/moment';
import { isRTL } from './isRTL'; import { isRTL } from './isRTL';
import englishJson from './locales/en.json';
type TTranslatedKeys = keyof typeof englishJson;
export { isRTL }; export { isRTL };
export const LANGUAGES = [ interface ILanguage {
label: string;
value: string;
file: () => any;
}
export const LANGUAGES: ILanguage[] = [
{ {
label: 'English', label: 'English',
value: 'en', value: 'en',
@ -82,12 +91,16 @@ export const LANGUAGES = [
} }
]; ];
interface ITranslations {
[language: string]: () => typeof englishJson;
}
const translations = LANGUAGES.reduce((ret, item) => { const translations = LANGUAGES.reduce((ret, item) => {
ret[item.value] = item.file; ret[item.value] = item.file;
return ret; return ret;
}, {}); }, {} as ITranslations);
export const setLanguage = l => { export const setLanguage = (l: string) => {
if (!l) { if (!l) {
return; return;
} }
@ -104,6 +117,8 @@ export const setLanguage = l => {
i18n.translations = { ...i18n.translations, [locale]: translations[locale]?.() }; i18n.translations = { ...i18n.translations, [locale]: translations[locale]?.() };
I18nManager.forceRTL(isRTL(locale)); I18nManager.forceRTL(isRTL(locale));
I18nManager.swapLeftAndRightInRTL(isRTL(locale)); I18nManager.swapLeftAndRightInRTL(isRTL(locale));
// TODO: Review this logic
// @ts-ignore
i18n.isRTL = I18nManager.isRTL; i18n.isRTL = I18nManager.isRTL;
moment.locale(toMomentLocale(locale)); moment.locale(toMomentLocale(locale));
}; };
@ -113,7 +128,16 @@ const defaultLanguage = { languageTag: 'en', isRTL: false };
const availableLanguages = Object.keys(translations); const availableLanguages = Object.keys(translations);
const { languageTag } = RNLocalize.findBestAvailableLanguage(availableLanguages) || defaultLanguage; const { languageTag } = RNLocalize.findBestAvailableLanguage(availableLanguages) || defaultLanguage;
// @ts-ignore
i18n.isTranslated = (text?: string) => text in englishJson;
setLanguage(languageTag); setLanguage(languageTag);
i18n.fallbacks = true; 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;

View File

@ -1,4 +1,4 @@
// https://github.com/zoontek/react-native-localize/blob/master/src/constants.ts#L5 // 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']; 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);

View File

@ -16,7 +16,7 @@ import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import log, { events, logEvent } from '../../utils/log'; import log, { events, logEvent } from '../../utils/log';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { OPTIONS } from './options'; import { IOptionsField, OPTIONS } from './options';
import { ChatsStackParamList } from '../../stacks/types'; import { ChatsStackParamList } from '../../stacks/types';
import { IRoomNotifications } from '../../definitions'; import { IRoomNotifications } from '../../definitions';
@ -131,10 +131,10 @@ class NotificationPreferencesView extends React.Component<INotificationPreferenc
renderPickerOption = (key: string) => { renderPickerOption = (key: string) => {
const { room } = this.state; const { room } = this.state;
const { theme } = this.props; 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 ( return (
<Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}> <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>
{I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })} {text?.label ? I18n.t(text?.label, { defaultValue: text?.label, second: text?.second }) : text?.label}
</Text> </Text>
); );
}; };

View File

@ -25,7 +25,7 @@ const Channel = ({ room }: { room: ISubscription }) => {
/> />
<Item <Item
label={I18n.t('Broadcast_Channel')} label={I18n.t('Broadcast_Channel')}
content={room.broadcast && I18n.t('Broadcast_channel_Description')} content={room.broadcast ? I18n.t('Broadcast_channel_Description') : ''}
testID='room-info-view-broadcast' testID='room-info-view-broadcast'
/> />
</> </>

View File

@ -72,7 +72,11 @@ class UserNotificationPreferencesView extends React.Component<
renderPickerOption = (key: TKey) => { renderPickerOption = (key: TKey) => {
const { theme } = this.props; const { theme } = this.props;
const text = this.findDefaultOption(key); const text = this.findDefaultOption(key);
return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{I18n.t(text?.label)}</Text>; return (
<Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>
{text?.label ? I18n.t(text?.label) : text?.label}
</Text>
);
}; };
pickerSelection = (title: string, key: TKey) => { pickerSelection = (title: string, key: TKey) => {
@ -82,7 +86,9 @@ class UserNotificationPreferencesView extends React.Component<
const defaultOption = this.findDefaultOption(key); const defaultOption = this.findDefaultOption(key);
if (OPTIONS[key][0]?.value !== 'default') { 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; label: string;
value: string; value: string;
}; };

View File

@ -148,6 +148,7 @@
"@testing-library/react-native": "^9.0.0", "@testing-library/react-native": "^9.0.0",
"@types/bytebuffer": "^5.0.43", "@types/bytebuffer": "^5.0.43",
"@types/ejson": "^2.1.3", "@types/ejson": "^2.1.3",
"@types/i18n-js": "^3.8.2",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/lodash": "^4.14.171", "@types/lodash": "^4.14.171",
"@types/react": "^17.0.14", "@types/react": "^17.0.14",

View File

@ -67,7 +67,8 @@
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */, "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__"] "exclude": ["node_modules", "e2e/docker", "__mocks__"]
} }

View File

@ -4514,6 +4514,11 @@
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"
integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== 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": "@types/is-function@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83"