mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-06 04:01:58 +02:00
feat: enhance i18n support by adding new translation keys and updating existing ones
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"save_changes": "Save Changes",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"creating": "Creating...",
|
||||
"success": "Success"
|
||||
},
|
||||
"settings": {
|
||||
@@ -50,7 +51,15 @@
|
||||
"title": "Authenticator App Enabled",
|
||||
"description": "Store the codes below somewhere safe. If you lose access to your authenticator app you can use these backup codes to sign in.",
|
||||
"alert": "These codes will not be shown again."
|
||||
},
|
||||
"disable": {
|
||||
"title": "Remove Authenticator App",
|
||||
"description": "Removing your authenticator app will make your account less secure."
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"update_email": "Update Email",
|
||||
"updated_successfully": "Your primary email has been updated."
|
||||
}
|
||||
},
|
||||
"api_key_modal": {
|
||||
@@ -72,7 +81,11 @@
|
||||
"delete_api_key_desc": "All requests using the {{key}} key will be invalidated.",
|
||||
"no_api_keys": "No API keys exist for this account.",
|
||||
"last_used": "Last used:",
|
||||
"never": "Never"
|
||||
"never": "Never",
|
||||
"key_description": "Description",
|
||||
"key_description_description": "A description of this API key.",
|
||||
"allowed_ips": "Allowed IPs",
|
||||
"allowed_ips_description": "Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line."
|
||||
},
|
||||
"activity_log": {
|
||||
"title": "Account Activity Log",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"language": "Ngôn ngữ",
|
||||
"save_changes": "Lưu thay đổi",
|
||||
"loading": "Đang tải...",
|
||||
"creating": "Đang tạo...",
|
||||
"error": "Lỗi",
|
||||
"success": "Thành công"
|
||||
},
|
||||
@@ -50,7 +51,15 @@
|
||||
"title": "Đã bật ứng dụng xác thực",
|
||||
"description": "Lưu trữ các mã dưới đây ở nơi an toàn. Nếu bạn mất quyền truy cập vào ứng dụng xác thực, bạn có thể sử dụng các mã dự phòng này để đăng nhập.",
|
||||
"alert": "Các mã này sẽ không được hiển thị lại."
|
||||
},
|
||||
"disable": {
|
||||
"title": "Xóa ứng dụng xác thực",
|
||||
"description": "Việc xóa ứng dụng xác thực sẽ làm cho tài khoản của bạn kém an toàn hơn."
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"update_email": "Cập nhật email",
|
||||
"updated_successfully": "Email chính của bạn đã được cập nhật."
|
||||
}
|
||||
},
|
||||
"api_key_modal": {
|
||||
@@ -80,6 +89,21 @@
|
||||
"delete_confirm": "Xóa Key",
|
||||
"delete_message": "Xóa SSH key {name} sẽ làm mất hiệu lực sử dụng của nó trên toàn bộ Panel."
|
||||
},
|
||||
"api": {
|
||||
"account_api": "API tài khoản",
|
||||
"create_api_key": "Tạo khóa API",
|
||||
"api_keys": "Các khóa API",
|
||||
"delete_api_key_title": "Xóa khóa API",
|
||||
"delete_key": "Xóa khóa",
|
||||
"delete_api_key_desc": "Tất cả các yêu cầu sử dụng khóa {{key}} sẽ bị vô hiệu hóa.",
|
||||
"no_api_keys": "Không có khóa API nào tồn tại cho tài khoản này.",
|
||||
"last_used": "Sử dụng lần cuối:",
|
||||
"never": "Chưa bao giờ",
|
||||
"key_description": "Mô tả",
|
||||
"key_description_description": "Mô tả về khóa API này.",
|
||||
"allowed_ips": "Các địa chỉ IP được phép",
|
||||
"allowed_ips_description": "Để trống để cho phép bất kỳ địa chỉ IP nào sử dụng khóa API này, nếu không, cung cấp từng địa chỉ IP trên một dòng mới."
|
||||
},
|
||||
"server_titles": {
|
||||
"schedules": "Lịch trình",
|
||||
"users": "Người dùng",
|
||||
|
||||
@@ -14,12 +14,6 @@ interface Props {
|
||||
const Container = styled.div<{ $type?: FlashMessageType }>``;
|
||||
Container.displayName = 'MessageBox.Container';
|
||||
|
||||
/**
|
||||
* Component hiển thị thông báo với tiêu đề và nội dung
|
||||
* @param title Tiêu đề thông báo (có thể là key i18n)
|
||||
* @param children Nội dung thông báo (có thể là key i18n)
|
||||
* @param type Loại thông báo: success, info, warning, error
|
||||
*/
|
||||
const MessageBox = ({ title, children, type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component cung cấp thông tin về phiên bản và môi trường Pyrodactyl
|
||||
* @param children Nội dung bên trong provider
|
||||
*/
|
||||
const PyrodactylProvider = ({ children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-pyro-pyrodactylprovider=''
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Code from '@/components/elements/Code';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { Field, Form, Formik, FormikHelpers } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { object, string } from 'yup';
|
||||
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
@@ -9,7 +9,6 @@ import ApiKeyModal from '@/components/dashboard/ApiKeyModal';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import Input from '@/components/elements/Input';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
||||
@@ -25,6 +24,7 @@ interface Values {
|
||||
}
|
||||
|
||||
export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
@@ -70,18 +70,18 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||
|
||||
{/* Description Field */}
|
||||
<FormikFieldWrapper
|
||||
label='Description'
|
||||
label={t('api.key_description')}
|
||||
name='description'
|
||||
description='A description of this API key.'
|
||||
description={t('api.key_description_description')}
|
||||
>
|
||||
<Field name='description' as={Input} className='w-full' />
|
||||
</FormikFieldWrapper>
|
||||
|
||||
{/* Allowed IPs Field */}
|
||||
<FormikFieldWrapper
|
||||
label='Allowed IPs'
|
||||
label={t('api.allowed_ips')}
|
||||
name='allowedIps'
|
||||
description='Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'
|
||||
description={t('api.allowed_ips_description')}
|
||||
>
|
||||
<Field name='allowedIps' as={Input} className='w-full' />
|
||||
</FormikFieldWrapper>
|
||||
@@ -89,7 +89,7 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||
{/* Submit Button below form fields */}
|
||||
<div className='flex justify-end mt-6'>
|
||||
<Button type='submit' disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Creating...' : 'Create API Key'}
|
||||
{isSubmitting ? t('common.creating') : t('api.create_api_key')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: replace with radix tooltip
|
||||
// import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
@@ -16,6 +17,7 @@ import { useStoreActions } from '@/state/hooks';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
|
||||
const DisableTOTPDialog = () => {
|
||||
const { t } = useTranslation();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||
@@ -47,7 +49,7 @@ const DisableTOTPDialog = () => {
|
||||
<form id={'disable-totp-form'} className={'mt-6'} onSubmit={submit}>
|
||||
<FlashMessageRender byKey={'account:two-step'} />
|
||||
<label className={'block pb-1'} htmlFor={'totp-password'}>
|
||||
Password
|
||||
{t('account_password')}
|
||||
</label>
|
||||
<Input.Text
|
||||
id={'totp-password'}
|
||||
@@ -57,14 +59,14 @@ const DisableTOTPDialog = () => {
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
<Button.Text onClick={close}>{t('cancel')}</Button.Text>
|
||||
{/* <Tooltip
|
||||
delay={100}
|
||||
disabled={password.length > 0}
|
||||
content={'You must enter your account password to continue.'}
|
||||
> */}
|
||||
<Button.Danger type={'submit'} form={'disable-totp-form'} disabled={submitting || !password.length}>
|
||||
Disable
|
||||
{t('settings.2fa.buttons.disable')}
|
||||
</Button.Danger>
|
||||
{/* </Tooltip> */}
|
||||
</Dialog.Footer>
|
||||
|
||||
@@ -109,7 +109,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
<Button.Text onClick={close}>{t('cancel')}</Button.Text>
|
||||
{/* <Tooltip
|
||||
disabled={password.length > 0 && value.length === 6}
|
||||
content={
|
||||
|
||||
@@ -17,11 +17,6 @@ interface Values {
|
||||
password: string;
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
email: Yup.string().email().required(),
|
||||
password: Yup.string().required('You must provide your current account password.'),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation();
|
||||
const user = useStoreState((state: State<ApplicationStore>) => state.user.data);
|
||||
@@ -29,6 +24,11 @@ export default () => {
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
email: Yup.string().email().required(t('auth.validation.email_required_reset')),
|
||||
password: Yup.string().required(t('auth.validation.password_required')),
|
||||
});
|
||||
|
||||
const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('account:email');
|
||||
|
||||
@@ -37,14 +37,14 @@ export default () => {
|
||||
addFlash({
|
||||
type: 'success',
|
||||
key: 'account:email',
|
||||
message: 'Your primary email has been updated.',
|
||||
message: t('settings.email.updated_successfully'),
|
||||
}),
|
||||
)
|
||||
.catch((error) =>
|
||||
addFlash({
|
||||
type: 'error',
|
||||
key: 'account:email',
|
||||
title: 'Error',
|
||||
title: t('error'),
|
||||
message: httpErrorToHuman(error),
|
||||
}),
|
||||
)
|
||||
@@ -60,7 +60,7 @@ export default () => {
|
||||
<Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||
<Form className={`m-0`}>
|
||||
<Field id={'current_email'} type={'email'} name={'email'} label={'Email'} />
|
||||
<Field id={'current_email'} type={'email'} name={'email'} label={t('auth.email')} />
|
||||
<div className={`mt-6`}>
|
||||
<Field
|
||||
id={'confirm_password'}
|
||||
@@ -70,7 +70,7 @@ export default () => {
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6`}>
|
||||
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
|
||||
<Button disabled={isSubmitting || !isValid}>{t('settings.email.update_email')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Fragment>
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
# Hướng dẫn di chuyển từ chuỗi tĩnh sang i18n
|
||||
|
||||
## Giới thiệu
|
||||
|
||||
Tài liệu này cung cấp hướng dẫn để di chuyển mã nguồn của bạn từ việc sử dụng chuỗi tĩnh (hardcoded string) sang sử dụng i18n để hỗ trợ đa ngôn ngữ.
|
||||
|
||||
## Cấu trúc thư mục
|
||||
|
||||
Dự án sử dụng thư viện i18next và react-i18next với cấu trúc thư mục như sau:
|
||||
|
||||
```
|
||||
/public/locales
|
||||
├── en/
|
||||
│ └── translation.json
|
||||
├── vi/
|
||||
│ └── translation.json
|
||||
```
|
||||
|
||||
## Hướng dẫn di chuyển
|
||||
|
||||
### Bước 1: Import hook useTranslation
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
```
|
||||
|
||||
### Bước 2: Sử dụng hook trong component
|
||||
|
||||
```tsx
|
||||
const { t } = useTranslation();
|
||||
```
|
||||
|
||||
### Bước 3: Thay thế chuỗi tĩnh bằng t function
|
||||
|
||||
Thay:
|
||||
|
||||
```tsx
|
||||
<h1>Tên máy chủ</h1>
|
||||
```
|
||||
|
||||
Bằng:
|
||||
|
||||
```tsx
|
||||
<h1>{t('server.settings.rename.server_name')}</h1>
|
||||
```
|
||||
|
||||
### Bước 4: Thêm khóa dịch vào file translation.json
|
||||
|
||||
Thêm vào `/public/locales/en/translation.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"settings": {
|
||||
"rename": {
|
||||
"server_name": "Server Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Thêm vào `/public/locales/vi/translation.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"settings": {
|
||||
"rename": {
|
||||
"server_name": "Tên máy chủ"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bước 5: Sử dụng variables trong chuỗi dịch
|
||||
|
||||
```tsx
|
||||
// Trong component
|
||||
<p>{t('welcome.message', { username: user.name })}</p>
|
||||
|
||||
// Trong file translation.json
|
||||
{
|
||||
"welcome": {
|
||||
"message": "Xin chào, {{username}}!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bước 6: Sử dụng Trans component cho HTML phức tạp
|
||||
|
||||
```tsx
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
<Trans i18nKey="description.part">
|
||||
Để biết thêm thông tin, <a href="https://example.com">nhấp vào đây</a>.
|
||||
</Trans>
|
||||
|
||||
// Trong file translation.json
|
||||
{
|
||||
"description": {
|
||||
"part": "Để biết thêm thông tin, <1>nhấp vào đây</1>."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Danh sách component đã di chuyển
|
||||
|
||||
Các component sau đã được di chuyển để sử dụng i18n:
|
||||
|
||||
1. `ShellContainer.tsx`
|
||||
2. `ReinstallServerBox.tsx`
|
||||
3. `SettingsContainer.tsx`
|
||||
4. `RenameServerBox.tsx`
|
||||
5. `ScheduleContainer.tsx`
|
||||
6. `AccountApiContainer.tsx`
|
||||
7. `UsersContainer.tsx`
|
||||
8. `ActivityLogContainer.tsx`
|
||||
9. `AccountOverviewContainer.tsx`
|
||||
10. `ConfigureTwoFactorForm.tsx`
|
||||
11. `ApiKeyModal.tsx`
|
||||
12. `RecoveryTokensDialog.tsx`
|
||||
13. `CreateSSHKeyForm.tsx`
|
||||
14. `ConfirmationModal.tsx`
|
||||
15. `SetupTOTPDialog.tsx`
|
||||
16. `UpdateEmailAddressForm.tsx`
|
||||
17. `UpdateLanguageForm.tsx`
|
||||
18. `ConfirmationDialog.tsx`
|
||||
19. `UpdatePasswordForm.tsx`
|
||||
20. `Modal.tsx`
|
||||
21. `PageContentBlock.tsx`
|
||||
22. `PermissionRoute.tsx`
|
||||
23. `ScreenBlock.tsx` (NotFound component)
|
||||
|
||||
## Danh sách component cần di chuyển
|
||||
|
||||
Các component sau cần được di chuyển để sử dụng i18n:
|
||||
|
||||
1. `ScreenBlock.tsx` (phần ScreenBlock & ServerError component)
|
||||
2. `MessageBox.tsx`
|
||||
3. `FlashMessageRender.tsx`
|
||||
4. `App.tsx` (phần Suspense "Loading...")
|
||||
|
||||
## Ví dụ di chuyển
|
||||
|
||||
### Ví dụ 1: Component MessageBox
|
||||
|
||||
**Trước khi di chuyển:**
|
||||
|
||||
```tsx
|
||||
const MessageBox = ({ title, children, type }: Props) => (
|
||||
<Container
|
||||
className='flex flex-col gap-2 bg-black border-[2px] border-brand/70 p-4 rounded-2xl mb-4'
|
||||
$type={type}
|
||||
role={'alert'}
|
||||
>
|
||||
{title && <h2 className='font-bold text-xl'>{title}</h2>}
|
||||
<Code>{children}</Code>
|
||||
</Container>
|
||||
);
|
||||
```
|
||||
|
||||
**Sau khi di chuyển:**
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MessageBox = ({ title, children, type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Container
|
||||
className='flex flex-col gap-2 bg-black border-[2px] border-brand/70 p-4 rounded-2xl mb-4'
|
||||
$type={type}
|
||||
role={'alert'}
|
||||
>
|
||||
{title && <h2 className='font-bold text-xl'>{t(title)}</h2>}
|
||||
<Code>{t(children)}</Code>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Ví dụ 2: Component ScreenBlock
|
||||
|
||||
**Trước khi di chuyển:**
|
||||
|
||||
```tsx
|
||||
const ScreenBlock = ({ title, message }) => {
|
||||
return (
|
||||
<>
|
||||
<div className='w-full h-full flex gap-12 items-center p-8 max-w-3xl mx-auto'>
|
||||
<div className='flex flex-col gap-8 max-w-sm text-left'>
|
||||
<h1 className='text-[32px] font-extrabold leading-[98%] tracking-[-0.11rem]'>{title}</h1>
|
||||
<p className=''>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Sau khi di chuyển:**
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ScreenBlock = ({ title, message }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full h-full flex gap-12 items-center p-8 max-w-3xl mx-auto'>
|
||||
<div className='flex flex-col gap-8 max-w-sm text-left'>
|
||||
<h1 className='text-[32px] font-extrabold leading-[98%] tracking-[-0.11rem]'>{t(title)}</h1>
|
||||
<p className=''>{t(message)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Lưu ý quan trọng
|
||||
|
||||
1. **Cấu trúc khóa**: Sử dụng cấu trúc phân cấp cho khóa dịch, ví dụ: `component.action.message`
|
||||
2. **Thống nhất cách sử dụng**: Đảm bảo sử dụng nhất quán `t` function trong toàn bộ ứng dụng
|
||||
3. **Kiểm tra hỗ trợ đa ngôn ngữ**: Sau khi di chuyển, kiểm tra ứng dụng hoạt động chính xác trên tất cả ngôn ngữ được hỗ trợ
|
||||
4. **Tham chiếu**: Xem code các component đã di chuyển để tham khảo cách thực hiện
|
||||
@@ -1,61 +0,0 @@
|
||||
# Trạng thái di chuyển i18n
|
||||
|
||||
Tài liệu này theo dõi trạng thái di chuyển các component từ chuỗi tĩnh sang sử dụng i18n.
|
||||
|
||||
## Components đã di chuyển
|
||||
|
||||
| Component | Trạng thái | Ghi chú |
|
||||
| ----------------------------- | ------------- | ------------- |
|
||||
| ShellContainer.tsx | ✅ Hoàn thành | |
|
||||
| ReinstallServerBox.tsx | ✅ Hoàn thành | |
|
||||
| SettingsContainer.tsx | ✅ Hoàn thành | |
|
||||
| RenameServerBox.tsx | ✅ Hoàn thành | |
|
||||
| ScheduleContainer.tsx | ✅ Hoàn thành | |
|
||||
| AccountApiContainer.tsx | ✅ Hoàn thành | |
|
||||
| UsersContainer.tsx | ✅ Hoàn thành | |
|
||||
| ActivityLogContainer.tsx | ✅ Hoàn thành | |
|
||||
| AccountOverviewContainer.tsx | ✅ Hoàn thành | |
|
||||
| ConfigureTwoFactorForm.tsx | ✅ Hoàn thành | |
|
||||
| ApiKeyModal.tsx | ✅ Hoàn thành | |
|
||||
| RecoveryTokensDialog.tsx | ✅ Hoàn thành | |
|
||||
| CreateSSHKeyForm.tsx | ✅ Hoàn thành | |
|
||||
| ConfirmationModal.tsx | ✅ Hoàn thành | |
|
||||
| SetupTOTPDialog.tsx | ✅ Hoàn thành | |
|
||||
| UpdateEmailAddressForm.tsx | ✅ Hoàn thành | |
|
||||
| UpdateLanguageForm.tsx | ✅ Hoàn thành | |
|
||||
| ConfirmationDialog.tsx | ✅ Hoàn thành | |
|
||||
| UpdatePasswordForm.tsx | ✅ Hoàn thành | |
|
||||
| Modal.tsx | ✅ Hoàn thành | |
|
||||
| PageContentBlock.tsx | ✅ Hoàn thành | |
|
||||
| PermissionRoute.tsx | ✅ Hoàn thành | |
|
||||
| ScreenBlock.tsx (NotFound) | ✅ Hoàn thành | |
|
||||
| ScreenBlock.tsx (ScreenBlock) | ✅ Hoàn thành | Mới di chuyển |
|
||||
| ScreenBlock.tsx (ServerError) | ✅ Hoàn thành | Mới di chuyển |
|
||||
| MessageBox.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| FlashMessageRender.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| App.tsx (Loading) | ✅ Hoàn thành | Mới di chuyển |
|
||||
| LoginContainer.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| ResetPasswordContainer.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| ForgotPasswordContainer.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| LoginCheckpointContainer.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| LoginFormContainer.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| AuthenticatedRoute.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| ErrorBoundary.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
| PyrodactylProvider.tsx | ✅ Hoàn thành | Mới di chuyển |
|
||||
|
||||
## Files JSON đa ngôn ngữ
|
||||
|
||||
Cập nhật các khóa dịch trong files:
|
||||
|
||||
- `/public/locales/en/translation.json`: Đã cập nhật ✅
|
||||
- `/public/locales/vi/translation.json`: Đã cập nhật ✅
|
||||
|
||||
## Tiến độ chung
|
||||
|
||||
- Tổng số components: 36
|
||||
- Số components đã di chuyển: 36 (100%)
|
||||
- Số components chờ di chuyển: 0 (0%)
|
||||
|
||||
## Hướng dẫn di chuyển
|
||||
|
||||
Xem chi tiết cách thực hiện di chuyển trong file [migration-guide.md](./migration-guide.md).
|
||||
Reference in New Issue
Block a user