Skip to content

Commit

Permalink
feat: i18n was added
Browse files Browse the repository at this point in the history
  • Loading branch information
Metastasis committed Mar 3, 2024
1 parent fd56d0e commit c789574
Show file tree
Hide file tree
Showing 52 changed files with 2,452 additions and 806 deletions.
2,360 changes: 1,737 additions & 623 deletions frontend/package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@
"@tramvai/state": "2.160.1",
"@tramvai/tokens-render": "2.160.1",
"@tramvai/tokens-router": "2.160.1",
"i18next": "23.10.0",
"i18next-browser-languagedetector": "7.2.0",
"postcss-nested": "6.0.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-hook-form": "7.22.5",
"react-i18next": "14.0.5",
"typescript": "4.9.3",
"uuid": "9.0.0"
}
}
}
5 changes: 4 additions & 1 deletion frontend/src/components/Copy/Copy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tooltip } from '@mantine/core';
// @ts-ignore for tree shaking purposes
import IconCopy from '@tabler/icons-react/dist/esm/icons/IconCopy';
import copyToClipboard from 'src/infrastructure/helpers/copy-to-clipboard';
import i18n from 'src/mockingbird/i18n';
import styles from './Copy.css';

type State = {
Expand Down Expand Up @@ -42,7 +43,9 @@ export default class Copy extends PureComponent<Props, State> {
copyToClipboard(this.props.targetValue, (err) => {
this.setState({
tooltipVisible: !err,
message: err ? 'Не удалось скопировать' : 'Скопировано',
message: err
? i18n.t('components.copy.error')
: i18n.t('components.copy.success'),
});
if (last) last.focus();
this.timer = setTimeout(() => {
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/List/ListEmpty.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text } from '@mantine/core';

interface Props {
text?: string;
}

export default function ListEmpty(props: Props) {
const { text = 'Данных нет' } = props;
const { t } = useTranslation();
const { text = t('components.list.textDefault') } = props;
return <Text size="sm">{text}</Text>;
}
6 changes: 4 additions & 2 deletions frontend/src/components/List/ListError.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, Button } from '@mantine/core';

interface Props {
Expand All @@ -7,13 +8,14 @@ interface Props {
}

export default function ListError(props: Props) {
const { text = 'Ошибка при загрузке данных ', onRetry } = props;
const { t } = useTranslation();
const { text = t('components.list.loadError'), onRetry } = props;
return (
<Text size="sm" color="red">
{text}
{onRetry && (
<Button variant="subtle" compact onClick={onRetry}>
Попробовать снова
{t('components.list.tryAgain')}
</Button>
)}
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import i18n from 'src/mockingbird/i18n';

export default function copyToClipboard(
text: string,
callback: (error: Error) => void
) {
if (text == null) throw new Error('copyToClipboard: text can not be null');
if (text == null) throw new Error(i18n.t('components.copy.textEmptyError'));
const el = window.document.createElement('textarea');
el.readOnly = true; // подавляем экранную клавиатуру на touch-устройствах
Object.assign(
Expand Down Expand Up @@ -33,7 +35,7 @@ export default function copyToClipboard(
el.select(); // для большинства
el.setSelectionRange(0, text.length); // для iOS
global.document.execCommand('copy');
if (!eventFired) throw new Error('Copy to clipboard failed');
if (!eventFired) throw new Error(i18n.t('components.copy.copyError'));
if (callback) callback();
} catch (e) {
if (callback) callback(e);
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/infrastructure/notifications/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { extractError } from 'src/mockingbird/infrastructure/helpers/state';
import i18n from 'src/mockingbird/i18n';
import { addToast } from './store/store';

export function getSuccessToast(title: string) {
Expand All @@ -10,15 +11,15 @@ export function getSuccessToast(title: string) {
}

export function getCreateErrorToast(e: any) {
return getErrorToast('Произошла ошибка при создании', e);
return getErrorToast(i18n.t('notifications.createError'), e);
}

export function getUpdateErrorToast(e: any) {
return getErrorToast('Произошла ошибка при обновлении', e);
return getErrorToast(i18n.t('notifications.updateError'), e);
}

export function getRemoveErrorToast(e: any) {
return getErrorToast('Произошла ошибка при удалении', e);
return getErrorToast(i18n.t('notifications.removeError'), e);
}

function getErrorToast(title: string, e: any) {
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/mockingbird/components/Language/Language.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import IconLanguage from '@tabler/icons-react/dist/esm/icons/IconLanguage';
import { ActionIcon } from '@mantine/core';

type Props = {
className?: string;
};

export function LanguageSwitcher({ className }: Props) {
const { i18n } = useTranslation();
const handleChange = () => {
const nextLanguage = i18n.language === 'en' ? 'ru' : 'en';
if (nextLanguage === i18n.language) return;
i18n.changeLanguage(nextLanguage);
};
return (
<ActionIcon className={className} onClick={handleChange}>
<IconLanguage color="grey" size="1.2rem" />
</ActionIcon>
);
}
1 change: 1 addition & 0 deletions frontend/src/mockingbird/components/Language/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LanguageSwitcher } from './Language';
1 change: 1 addition & 0 deletions frontend/src/mockingbird/components/Page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import type { ReactNode } from 'react';
import styles from './Page.css';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useController } from 'react-hook-form';
import type { FileInputProps } from '@mantine/core';
import { FileInput } from '@mantine/core';
import { extractError } from 'src/mockingbird/infrastructure/helpers/forms';
import { useTranslation } from 'react-i18next';

type Props = FileInputProps & {
name: string;
Expand All @@ -20,6 +21,7 @@ export default function AttachFile(props: Props) {
single = false,
...restProps
} = props;
const { t } = useTranslation();
const { field, formState, fieldState } = useController({
name,
control,
Expand All @@ -38,7 +40,9 @@ export default function AttachFile(props: Props) {
},
[onChangeField]
);
const defaultPlaceholder = single ? 'Выберите файл' : 'Выберите файлы';
const defaultPlaceholder = single
? t('form.attachFile.defaultPlaceholderSingle')
: t('form.attachFile.defaultPlaceholderPlural');
const uiPlaceholder = restProps.placeholder || defaultPlaceholder;
const uiError =
extractError(name, formState.errors) || fieldState.error?.message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mapSelectValue,
extractError,
} from 'src/mockingbird/infrastructure/helpers/forms';
import { useTranslation } from 'react-i18next';

type Props = Omit<MultiSelectProps, 'data' | 'name'> & {
name: string;
Expand All @@ -25,6 +26,7 @@ export default function InputSearchTagged(props: Props) {
disabled = false,
...restProps
} = props;
const { t } = useTranslation();
const { field, formState, fieldState } = useController({
name,
control,
Expand Down Expand Up @@ -54,7 +56,9 @@ export default function InputSearchTagged(props: Props) {
error={uiError}
searchable
creatable
getCreateLabel={(query) => `+ Добавить ${query}`}
getCreateLabel={(query) =>
`${t('form.inputSearchTagged.label')} ${query}`
}
onCreate={(q) => {
const query = q.trim().toLowerCase();
const item = { value: query, label: q };
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/mockingbird/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import ru from './translations/ru.json';
import en from './translations/en.json';

i18n
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
resources: {
ru: {
translation: ru,
},
en: {
translation: en,
},
},
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react!
},
debug: false,
});

export default i18n;
1 change: 1 addition & 0 deletions frontend/src/mockingbird/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Layout } from './layers/layout/Layout';
import Header from './layers/layout/Header';
import paths from './paths';
import { configureSettings } from './settings';
import './i18n';
import './main.css';

createApp({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FieldErrors, FieldError } from 'react-hook-form';
import i18n from 'src/mockingbird/i18n';

export function extractError(name: string, errors: FieldErrors): string | null {
const error = errors[name];
Expand All @@ -10,14 +11,14 @@ function getErrorMessage(error: FieldError) {
const { type, message } = error;
switch (type) {
case 'required':
return 'Поле обязательное';
return i18n.t('validation.required');
case 'validate':
return message;
}
}

export function validateJSON(value: string) {
const message = 'Невалидный json-объект';
const message = i18n.t('validation.invalidJson');
if (!value) return;
try {
if (!isObject(JSON.parse(value))) return message;
Expand All @@ -27,7 +28,7 @@ export function validateJSON(value: string) {
}

export function validateJSONArray(value: string) {
const message = 'Невалидный json-массив';
const message = i18n.t('validation.invalidArray');
if (!value) return;
try {
if (!Array.isArray(JSON.parse(value))) return message;
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/mockingbird/layers/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { connect } from '@tramvai/state';
import { Container, Space, Title } from '@mantine/core';
import { Container, Title, Flex } from '@mantine/core';
import { getJson } from 'src/infrastructure/request';
import { LanguageSwitcher } from 'src/mockingbird/components/Language';
import { Shadow } from './Shadow';

type Props = {
Expand All @@ -21,9 +22,12 @@ function Header({ assetsPrefix }: Props) {
return (
<Shadow>
<Container>
<Space h="sm" />
<Title order={2}>{title}</Title>
<Space h="sm" />
<Flex py="sm" align="center">
<Title order={2} mr="auto">
{title}
</Title>
<LanguageSwitcher />
</Flex>
</Container>
</Shadow>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/mockingbird/layers/layout/Shadow.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.root {
box-shadow: 0 4px 56px rgba(0, 0, 0, 0.12);
box-shadow: 0 4px 56px rgb(0 0 0 / 12%);
}
Loading

0 comments on commit c789574

Please sign in to comment.