mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 04:40:59 +00:00
feat(localization): added zh_TW (#2913)
* feat(localization): added zh_TW * fixed based on @coderabbitai * updated false translation for zh_TW * new workflow * revert * fixed a lot of translations * turned most zh to zh-CN * fallbacklang * bruh * eliminate ALL _ * fix: paths and other miscs thanks @Calcium-Ion * fixed translation and temp fix for preferencessettings.js * fixed translation error * fixed issue about legacy support * reverted stupid coderabbit's suggestion
This commit is contained in:
@@ -29,12 +29,17 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
|
||||
{/* Language sorting: Order by English name (Chinese, English, French, Japanese, Russian) */}
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('zh')}
|
||||
className={`!px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
onClick={() => onLanguageChange('zh-CN')}
|
||||
className={`!px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh-CN' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
>
|
||||
中文
|
||||
简体中文
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('zh-TW')}
|
||||
className={`!px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh-TW' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
>
|
||||
繁體中文
|
||||
</Dropdown.Item> <Dropdown.Item
|
||||
onClick={() => onLanguageChange('en')}
|
||||
className={`!px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'en' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
>
|
||||
|
||||
@@ -17,154 +17,160 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Card, Select, Typography, Avatar } from '@douyinfe/semi-ui';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showSuccess, showError } from '../../../../helpers';
|
||||
import { UserContext } from '../../../../context/User';
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { Card, Select, Typography, Avatar } from "@douyinfe/semi-ui";
|
||||
import { Languages } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { API, showSuccess, showError } from "../../../../helpers";
|
||||
import { UserContext } from "../../../../context/User";
|
||||
|
||||
// Language options with native names
|
||||
const languageOptions = [
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'vi', label: 'Tiếng Việt' },
|
||||
{ value: "zh-CN", label: "简体中文" },
|
||||
{ value: "zh-TW", label: "繁體中文" },
|
||||
{ value: "en", label: "English" },
|
||||
{ value: 'fr', label: 'Français'},
|
||||
{ value: 'ru', label: 'Русский'},
|
||||
{ value: 'ja', label: '日本語'},
|
||||
{ value: "vi", label: "Tiếng Việt" },
|
||||
];
|
||||
|
||||
const PreferencesSettings = ({ t }) => {
|
||||
const { i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'zh');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [currentLanguage, setCurrentLanguage] = useState(
|
||||
i18n.language || "zh-CN",
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load saved language preference from user settings
|
||||
useEffect(() => {
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
if (settings.language) {
|
||||
setCurrentLanguage(settings.language);
|
||||
// Sync i18n with saved preference
|
||||
if (i18n.language !== settings.language) {
|
||||
i18n.changeLanguage(settings.language);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}, [userState?.user?.setting, i18n]);
|
||||
// Load saved language preference from user settings
|
||||
useEffect(() => {
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
if (settings.language) {
|
||||
// Normalize legacy "zh" to "zh-CN" for backward compatibility
|
||||
const lang = settings.language === "zh" ? "zh-CN" : settings.language;
|
||||
setCurrentLanguage(lang);
|
||||
// Sync i18n with saved preference
|
||||
if (i18n.language !== lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}, [userState?.user?.setting, i18n]);
|
||||
|
||||
const handleLanguagePreferenceChange = async (lang) => {
|
||||
if (lang === currentLanguage) return;
|
||||
const handleLanguagePreferenceChange = async (lang) => {
|
||||
if (lang === currentLanguage) return;
|
||||
|
||||
setLoading(true);
|
||||
const previousLang = currentLanguage;
|
||||
setLoading(true);
|
||||
const previousLang = currentLanguage;
|
||||
|
||||
try {
|
||||
// Update language immediately for responsive UX
|
||||
setCurrentLanguage(lang);
|
||||
i18n.changeLanguage(lang);
|
||||
try {
|
||||
// Update language immediately for responsive UX
|
||||
setCurrentLanguage(lang);
|
||||
i18n.changeLanguage(lang);
|
||||
|
||||
// Save to backend
|
||||
const res = await API.put('/api/user/self', {
|
||||
language: lang,
|
||||
});
|
||||
// Save to backend
|
||||
const res = await API.put("/api/user/self", {
|
||||
language: lang,
|
||||
});
|
||||
|
||||
if (res.data.success) {
|
||||
showSuccess(t('语言偏好已保存'));
|
||||
// Update user context with new setting
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
settings.language = lang;
|
||||
userDispatch({
|
||||
type: 'login',
|
||||
payload: {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showError(res.data.message || t('保存失败'));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('保存失败,请重试'));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (res.data.success) {
|
||||
showSuccess(t("语言偏好已保存"));
|
||||
// Update user context with new setting
|
||||
if (userState?.user?.setting) {
|
||||
try {
|
||||
const settings = JSON.parse(userState.user.setting);
|
||||
settings.language = lang;
|
||||
userDispatch({
|
||||
type: "login",
|
||||
payload: {
|
||||
...userState.user,
|
||||
setting: JSON.stringify(settings),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showError(res.data.message || t("保存失败"));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t("保存失败,请重试"));
|
||||
// Revert on error
|
||||
setCurrentLanguage(previousLang);
|
||||
i18n.changeLanguage(previousLang);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* Card Header */}
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='violet' className='mr-3 shadow-md'>
|
||||
<Languages size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('偏好设置')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600 dark:text-gray-400'>
|
||||
{t('界面语言和其他个人偏好')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Card className="!rounded-2xl shadow-sm border-0">
|
||||
{/* Card Header */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="violet" className="mr-3 shadow-md">
|
||||
<Languages size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">
|
||||
{t("偏好设置")}
|
||||
</Typography.Text>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{t("界面语言和其他个人偏好")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Language Setting Card */}
|
||||
<Card className="!rounded-xl border dark:border-gray-700">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<Languages
|
||||
size={20}
|
||||
className="text-violet-600 dark:text-violet-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
{t("语言偏好")}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t("选择您的首选界面语言,设置将自动保存并同步到所有设备")}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onChange={handleLanguagePreferenceChange}
|
||||
style={{ width: 180 }}
|
||||
loading={loading}
|
||||
optionList={languageOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Language Setting Card */}
|
||||
<Card className='!rounded-xl border dark:border-gray-700'>
|
||||
<div className='flex flex-col sm:flex-row items-start sm:items-center sm:justify-between gap-4'>
|
||||
<div className='flex items-start w-full sm:w-auto'>
|
||||
<div className='w-12 h-12 rounded-full bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center mr-4 flex-shrink-0'>
|
||||
<Languages
|
||||
size={20}
|
||||
className='text-violet-600 dark:text-violet-400'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className='mb-1'>
|
||||
{t('语言偏好')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type='tertiary' className='text-sm'>
|
||||
{t('选择您的首选界面语言,设置将自动保存并同步到所有设备')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onChange={handleLanguagePreferenceChange}
|
||||
style={{ width: 180 }}
|
||||
loading={loading}
|
||||
optionList={languageOptions.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Additional info */}
|
||||
<div className='mt-4 text-xs text-gray-500 dark:text-gray-400'>
|
||||
<Typography.Text type='tertiary'>
|
||||
{t('提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
{/* Additional info */}
|
||||
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<Typography.Text type="tertiary">
|
||||
{t(
|
||||
"提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。",
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesSettings;
|
||||
|
||||
@@ -24,14 +24,14 @@ import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
const SyncWizardModal = ({ visible, onClose, onConfirm, loading, t }) => {
|
||||
const [step, setStep] = useState(0);
|
||||
const [option, setOption] = useState('official');
|
||||
const [locale, setLocale] = useState('zh');
|
||||
const [locale, setLocale] = useState('zh-CN');
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setStep(0);
|
||||
setOption('official');
|
||||
setLocale('zh');
|
||||
setLocale('zh-CN');
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
@@ -113,13 +113,16 @@ const SyncWizardModal = ({ visible, onClose, onConfirm, loading, t }) => {
|
||||
name='sync-locale-selection'
|
||||
>
|
||||
<Radio value='en' extra='English'>
|
||||
EN
|
||||
en
|
||||
</Radio>
|
||||
<Radio value='zh' extra='中文'>
|
||||
ZH
|
||||
<Radio value='zh-CN' extra='简体中文'>
|
||||
zh-CN
|
||||
</Radio>
|
||||
<Radio value='zh-TW' extra='繁體中文'>
|
||||
zh-TW
|
||||
</Radio>
|
||||
<Radio value='ja' extra='日本語'>
|
||||
JA
|
||||
ja
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import frTranslation from './locales/fr.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
import zhCNTranslation from './locales/zh-CN.json';
|
||||
import zhTWTranslation from './locales/zh-TW.json';
|
||||
import ruTranslation from './locales/ru.json';
|
||||
import jaTranslation from './locales/ja.json';
|
||||
import viTranslation from './locales/vi.json';
|
||||
@@ -32,16 +33,17 @@ i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
load: 'languageOnly',
|
||||
load: 'currentOnly',
|
||||
resources: {
|
||||
en: enTranslation,
|
||||
zh: zhTranslation,
|
||||
'zh-CN': zhCNTranslation,
|
||||
'zh-TW': zhTWTranslation,
|
||||
fr: frTranslation,
|
||||
ru: ruTranslation,
|
||||
ja: jaTranslation,
|
||||
vi: viTranslation,
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
fallbackLng: 'zh-CN',
|
||||
nsSeparator: false,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
2791
web/src/i18n/locales/zh-TW.json
Normal file
2791
web/src/i18n/locales/zh-TW.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user