| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- /*
- Copyright (C) 2025 QuantumNous
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- 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';
- // Language options with native names and flags
- const languageOptions = [
- { value: 'zh', label: '中文', flag: '🇨🇳' },
- { value: 'en', label: 'English', flag: '🇺🇸' },
- { value: 'fr', label: 'Français', flag: '🇫🇷' },
- { value: 'ru', label: 'Русский', flag: '🇷🇺' },
- { value: 'ja', label: '日本語', flag: '🇯🇵' },
- { value: 'vi', label: 'Tiếng Việt', flag: '🇻🇳' },
- ];
- const PreferencesSettings = ({ t }) => {
- const { i18n } = useTranslation();
- const [userState, userDispatch] = useContext(UserContext);
- const [currentLanguage, setCurrentLanguage] = useState(i18n.language || 'zh');
- 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]);
- const handleLanguagePreferenceChange = async (lang) => {
- if (lang === currentLanguage) return;
- setLoading(true);
- const previousLang = currentLanguage;
- 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,
- });
- 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>
- {/* 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: (
- <div className='flex items-center gap-2'>
- <span>{opt.flag}</span>
- <span>{opt.label}</span>
- </div>
- ),
- }))}
- renderSelectedItem={(optionNode) => {
- const selected = languageOptions.find(
- (opt) => opt.value === optionNode.value,
- );
- return (
- <div className='flex items-center gap-2'>
- <span>{selected?.flag}</span>
- <span>{selected?.label}</span>
- </div>
- );
- }}
- />
- </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;
|