diff --git a/src/components/article/block.tsx b/src/components/article/block.tsx index 2480826..92bf825 100644 --- a/src/components/article/block.tsx +++ b/src/components/article/block.tsx @@ -2,11 +2,12 @@ import React from "react"; import clsx from "clsx"; import {Divider, Popconfirm} from "antd"; -import {IconAdd, IconAddCircle, IconDelete, IconWarningCircle} from "@/components/icons"; +import {IconAdd, IconDelete, IconWarningCircle} from "@/components/icons"; import ImageList from "@/components/article/list.tsx"; import { BlockText} from "./item.tsx"; import styles from './article.module.scss' +import {useTranslation} from "react-i18next"; type Props = { children?: React.ReactNode; @@ -49,6 +50,7 @@ export default function ArticleBlock( onChange, index, }: Props) { + const {t} = useTranslation() const blocks = rebuildBlockArray(defaultBlocks) const handleBlockChange = (index: number, block: BlockContent) => { @@ -59,7 +61,7 @@ export default function ArticleBlock( return
{editable && index == 1 &&
- onAdd?.(1)} className="article-action-add" title="新增分组"> + onAdd?.(1)} className="article-action-add" title={t('news.edit_add_group')}>
}
@@ -83,12 +85,12 @@ export default function ArticleBlock( placement={'left'} arrow={false} icon={} - title={
请确认删除此分组?
} + title={
{t('news.edit_delete_group_confirm')}
} onConfirm={onRemove} - okText="删除" - cancelText="取消" + okText={t('delete')} + cancelText={t('cancel')} > - + : diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx index b0f4c05..167646f 100644 --- a/src/components/article/edit-modal.tsx +++ b/src/components/article/edit-modal.tsx @@ -6,6 +6,7 @@ import * as article from "@/service/api/article.ts"; import {regenerate} from "@/service/api/video.ts"; import {push2video} from "@/service/api/article.ts"; import {showErrorToast, showToast} from "@/components/message.ts"; +import {useTranslation} from "react-i18next"; type Props = { id?: number; @@ -64,7 +65,7 @@ function rebuildGroups(groups: BlockContent[][]) { } export default function ArticleEditModal(props: Props) { - + const {t} = useTranslation() const [groups, setGroups] = useState([]); const [title, setTitle] = useState('') @@ -88,7 +89,7 @@ export default function ArticleEditModal(props: Props) { save(title, groups[0][0].content, groups.slice(1), props.id && props.id > 0 ? props.id : undefined).then(() => { props.onClose?.(true) }).catch(e => { - setState({error: e.data || '保存失败,请重试!'}) + setState({error: e.data || t('news.edit_save_failed')}) }).finally(() => { setState({loading: false}) }); @@ -106,7 +107,7 @@ export default function ArticleEditModal(props: Props) { setState({generating:true}) await article.save(title, groups[0][0].content, groups.slice(1), props.id) push2video([props.id]).then(() => { - showToast('推流成功', 'success') + showToast(t('news.push_stream_success'), 'success') // navigate('/create?state=push-success',{ // state: 'push-success' // }) @@ -144,13 +145,13 @@ export default function ArticleEditModal(props: Props) { onCancel={() => props.onClose?.()} okButtonProps={{loading: state.loading}} onOk={handleSave} - okText={props.type == 'news' ? '确定' : '重新生成'} + okText={props.type == 'news' ? t('confirm') : t('news.edit_generate_video_again')} >
{ setTitle(e.target.value) - setState({msgTitle: e.target.value ? '' : '请输入标题内容'}) - }} placeholder={'请输入文章标题'}/> + setState({msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1')}) + }} placeholder={t('news.edit_notice_enter_article_title')}/>
{state.msgTitle}
@@ -159,7 +160,7 @@ export default function ArticleEditModal(props: Props) { errorMessage={state.msgGroup} editable groups={groups} onChange={list => { setGroups(() => list) - setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''}); + setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_notice_enter_article_content') : ''}); }} />
@@ -167,9 +168,9 @@ export default function ArticleEditModal(props: Props) {
- {props.type == 'news' && props.id ? : null} - - + {props.type == 'news' && props.id ? : null} + +
); diff --git a/src/components/article/group.tsx b/src/components/article/group.tsx index 1f75d22..ff8bd0f 100644 --- a/src/components/article/group.tsx +++ b/src/components/article/group.tsx @@ -4,6 +4,7 @@ import ArticleBlock from "@/components/article/block.tsx"; import styles from './article.module.scss' import {showToast} from "@/components/message.ts"; import React from "react"; +import {useTranslation} from "react-i18next"; type Props = { groups: BlockContent[][]; @@ -14,6 +15,7 @@ type Props = { export default function ArticleGroup({groups, editable, onChange, errorMessage}: Props) { + const {t,i18n} = useTranslation() // const groups = rebuildGroups(_groups) /** * 添加一个组 @@ -24,7 +26,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}: const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1]; // 判断 if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) { - showToast('请先添加内容') + showToast(t('news.edit_notice_enter_text')) return; } } @@ -47,15 +49,15 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}: return
- 数字人主播台编辑区 - (出现数字人形象) + {t('news.edit_digital_text')} + {i18n.language == 'zh-CN' && (出现数字人形象)}
{/* value={groups || groups[0][0].content}*/}
{editable ?
0 ? groups[0][0].content : ''} autoSize={{minRows: 20, maxRows: 21}} variant={"borderless"} @@ -63,14 +65,14 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}: handleDigitalPersonContentChange(e.target.value) }} /> -
:

12123

} +
:

{groups && groups.length > 0 ? groups[0][0].content : ''}

}
- 素材融合呈现编辑区 - (文、图、视频,不出现数字人形象) + {t('news.edit_other_text')} + {i18n.language == 'zh-CN' && (文、图、视频,不出现数字人形象)}
@@ -91,7 +93,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}: }} onRemove={async () => { if (groups.length == 1) { - message.warning('至少保留一个内容块') + message.warning(t('news.edit_notice_keep_1')) return; } onChange?.(groups.filter((_, idx) => index !== idx)) diff --git a/src/components/article/item.tsx b/src/components/article/item.tsx index 6c0eec9..9c3cc31 100644 --- a/src/components/article/item.tsx +++ b/src/components/article/item.tsx @@ -7,6 +7,7 @@ import styles from './article.module.scss' import {getOssPolicy} from "@/service/api/common.ts"; import {showToast} from "@/components/message.ts"; import {IconAddImage, IconWarningCircle} from "@/components/icons"; +import {useTranslation} from "react-i18next"; type Props = { children?: React.ReactNode; @@ -25,7 +26,7 @@ const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg'] const Data: { uploadConfig?: TOSSPolicy } = {} export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) { - + const {t} = useTranslation() const [loading, setLoading] = useState(-1) // oss上传文件所需的数据 const getUploadData: UploadProps['data'] = (file) => ({ @@ -57,7 +58,7 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url}) } else if (file.status == 'error') { setLoading(-1) - showToast('上传图片失败,请重试', 'warning') + showToast(t('upload.upload_failed'), 'warning') } else if (file.status == 'uploading') { setLoading(file.percent) } @@ -79,10 +80,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima {!onlyUpload && 请确认删除此删除此图片?
} + title={
{t('upload.delete_confirm')}
} onConfirm={onRemove} - okText="删除" - cancelText="取消" + okText={t('delete')} + cancelText={t('cancel')} > } @@ -90,7 +91,7 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima :
-
上传图片
+
{t('upload.upload_image')}
}
@@ -104,10 +105,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima placement={'right'} arrow={false} icon={} - title={
请确认删除此图片?
} + title={
{t('upload.delete_confirm')}
} onConfirm={onRemove} - okText="删除" - cancelText="取消" + okText={t('delete')} + cancelText={t('cancel')} > } @@ -117,15 +118,17 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima } export function BlockText({data, editable, onChange, isFirstBlock}: Props) { + const {t} = useTranslation() return
{editable ?
+ {/*请输入文本内容*/} { onChange?.({type: 'text', content: e.target.value}) }} - placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 4, maxRows: 5}} + placeholder={t('news.edit_notice_enter_article_content')} value={data.content} autoSize={{minRows: 4, maxRows: 5}} variant={"borderless"}/>
:

{data.content}

}
diff --git a/src/components/button-batch.tsx b/src/components/button-batch.tsx index 3abce15..434a05b 100644 --- a/src/components/button-batch.tsx +++ b/src/components/button-batch.tsx @@ -56,7 +56,7 @@ export default function ButtonBatch( if(confirmMessage){ modal.confirm({ wrapClassName: 'root-modal-confirm', - title: title || t('notice.title'), + title: title || t('confirm.title'), centered: true, icon: , content: confirmMessage, diff --git a/src/components/form/tag-select.tsx b/src/components/form/tag-select.tsx index cc21e49..3b0fa1e 100644 --- a/src/components/form/tag-select.tsx +++ b/src/components/form/tag-select.tsx @@ -2,6 +2,7 @@ import React, {useEffect, useMemo, useRef} from "react"; import {Checkbox, Popover} from "antd"; import {useBoolean, useClickAway} from "ahooks"; import {CaretUpOutlined} from "@ant-design/icons"; +import {useTranslation} from "react-i18next"; type ValueType = Id[][]; type ValueFunc = (prev:ValueType)=>ValueType; @@ -101,6 +102,7 @@ const TagSelect = (props: { return parentList.findIndex(s => s[1] == item.value) != -1; } + const {t} = useTranslation() const ref = useRef(null) useClickAway(()=>{ set(false) @@ -120,7 +122,7 @@ const TagSelect = (props: { set(!visible) }} > - {checkedAll || selectValues.length == 0 ? '全部来源' : '来源'} + {checkedAll || selectValues.length == 0 ? t('news.news_all_source') : t('news.source')}
@@ -129,7 +131,7 @@ const TagSelect = (props: {
  • handleAllChanged(!checkedAll)}>全部来源 + onClick={() => handleAllChanged(!checkedAll)}>t('news.news_all_source') handleAllChanged(e.target.checked)}/>
    diff --git a/src/components/form/time-select.tsx b/src/components/form/time-select.tsx index 1874ebf..5f1d07d 100644 --- a/src/components/form/time-select.tsx +++ b/src/components/form/time-select.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from "react"; import {CaretUpOutlined} from "@ant-design/icons"; +import {useTranslation} from "react-i18next"; export type TimeSelectProps = { value: number; @@ -10,33 +11,35 @@ type OptionItem = { label: string; value: number; } -const AllTimeOption: OptionItem[] = [ - { - label: '半小时内', - value: 1 - }, - { - label: '一小时内', - value: 2 - }, - { - label: '四小时内', - value: 3 - }, - { - label: '一天内', - value: 4 - }, - { - label: '近一周', - value: 5 - }, - { - label: '所有时间', - value: 0 - } -] + const TimeSelect = (props: TimeSelectProps) => { + const {t,i18n} = useTranslation(); + const AllTimeOption: OptionItem[] = useMemo(()=>([ + { + label: t('time_filter.past_30_min'), + value: 1 + }, + { + label: t('time_filter.past_hour'), + value: 2 + }, + { + label: t('time_filter.past_4_hour'), + value: 3 + }, + { + label: t('time_filter.past_24_hour'), + value: 4 + }, + { + label: t('time_filter.last_week'), + value: 5 + }, + { + label: t('time_filter.all'), + value: 0 + } + ]),[i18n.language]) const selectLabel = useMemo(() => { return AllTimeOption.find(item => item.value == props.value)?.label || '' }, [props.value]) diff --git a/src/components/icons/logo.tsx b/src/components/icons/logo.tsx index fe00238..4016b5c 100644 --- a/src/components/icons/logo.tsx +++ b/src/components/icons/logo.tsx @@ -1,5 +1,6 @@ import React from "react"; import useConfig from "@/hooks/useConfig.ts"; +import {useTranslation} from "react-i18next"; const AppLogo = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'color' }) => { return ( @@ -21,11 +22,11 @@ const AppLogo = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'col ) } export const LogoText = ({style, className}: { style?: React.CSSProperties, className?: string }) => { - const {appName} = useConfig() + const {t} = useTranslation() return (
    - {appName} + {t('AppTitle')}
    ) } diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index f56875d..894c788 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -7,6 +7,7 @@ import ImageCover from '@/assets/images/cover.png' import {IconDelete, IconEdit, IconPlaying, IconWarningCircle} from "@/components/icons"; import {VideoStatus} from "@/service/api/video.ts"; import {formatTime} from "@/util/strings.ts"; +import {useTranslation} from "react-i18next"; type Props = { video: VideoInfo | LiveVideoInfo, @@ -37,7 +38,7 @@ export const VideoListItem = ( setNodeRef, transform } = useSortable({resizeObserverConfig: {}, id}) - + const {t} = useTranslation() const [state, setState] = useSetState<{ checked?: boolean }>({}) useEffect(() => { setState({checked}) @@ -60,14 +61,14 @@ export const VideoListItem = ( {generating &&
    - 视频生成中 + {t('video.generating')}
    } {/* && active*/} {!generating && playing &&
    -
    播放中
    +
    {t('video.playing')}
    }
  • @@ -107,7 +108,7 @@ export const VideoListItem = ( placement={'left'} arrow={false} icon={} - title={'你确定要删除此视频吗?'} + title={t('video.delete_confirm_title')} // description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`} onConfirm={onRemove} >} diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 0a0e201..33bac84 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -6,6 +6,7 @@ import LangCN from './translations/zh-CN.json'; console.log('AppConfig',AppMode) i18next.use(initReactI18next).init({ debug: true, + lng:'en-US', fallbackLng: 'en-US', resources: { 'en-US': {translation:LangEN}, diff --git a/src/i18n/translations/en-US.json b/src/i18n/translations/en-US.json index 1c463d9..1a20b84 100644 --- a/src/i18n/translations/en-US.json +++ b/src/i18n/translations/en-US.json @@ -1,10 +1,152 @@ { - "AppTitle": "Digital Human Live", + "AppTitle": "Metahuman Streaming platform", "Hello": "Hello", - "login": { - "text": "Login" + "close": "Close", + "confirm": { + "push_title": "Push Notice", + "push_video": "Are you sure editing selected news?", + "title": "Notice" }, - "notice": { - "title": "操作提示" + "delete_batch": "Delete Select", + "delete_failed": "Delete failed", + "delete_success": "Delete success", + "download": "Download", + "generating": { + "title": "Preview - Click video item to play" + }, + "history": { + "delete_confirm": "Are you sure you want to delete this video?", + "push_success": "Streaming success", + "search_key": "Please enter keywords for video title", + "text": "History video" + }, + "live": { + "duration": "Duration", + "edit_locked": "Disable to sort", + "edit_unlock": "Unlock", + "play_first": "Play first video", + "playlist_count": "Current playlist total has {{count}} items", + "title": "Live" + }, + "login": { + "code_sending": "Sending...", + "invalid_username_or_pwd": "Invalid phone number or code", + "loading": "Login...", + "password": "Enter the verification code", + "send_sms_code": "Send code", + "text": "Sign in", + "title": "Sign in", + "username": "Please enter your phone number", + "welcome": "Welcome" + }, + "nav": { + "editing": "Editing", + "generating": "Generating", + "live": "Streaming", + "materials": "News Materials" + }, + "news": { + "delete_confirm": "Are you sure you want to delete this item?", + "delete_confirm_count": "Are you sure you want to delete these {{count}} items?", + "delete_description": "This item will be deleted.
    It can be recovered from the “news” page. ", + "delete_empty": "Please select the items to delete", + "download_empty": "Please select the news to download", + "download_failed": "Download failed!", + "edit_form_search": "Please enter keywords for news title", + "editing": "Editing", + "filter_all": "All", + "filter_source": "News source", + "generate_video": "Generating", + "get_detail": "Get news details", + "get_detail_error": "Get new details failed", + "image_count": "image", + "materials": { + "title": "News Materials" + }, + "news_all_source": "source", + "push_empty": "please select the news to edit", + "push_failed": "Failed to editing", + "push_stream_empty": "please select the news to streaming", + "push_stream_success": "Success", + "push_streaming": "Pushing...", + "push_success": "Push success", + "push_to_edit": "Editing", + "pushed": "Pushed", + "search_key_title": "Please enter keywords", + "source": "source", + "title": "News content", + "title_image_count": "Number of pictures", + "title_operate": "Operation", + "title_time": "time", + "title_word_count": "Wordcount", + "word_count": "word", + + "edit_digital_text": "MetaHuman materiel", + "edit_other_text": "Other media materiel", + "edit_generate_video_again": "Regenerate", + "edit_generate_again": "Regenerate", + "edit_generate_video": "Video generation", + "edit_save_failed": "Save failed!", + + "edit_notice_keep_1": "Keep at least one content block", + "edit_notice_enter_text": "Please enter content", + "edit_notice_enter_article_title": "Please enter title", + "edit_notice_enter_article_title1": "Please enter news title", + "edit_notice_enter_article_content": "Please enter content", + + "edit_add_group": "Add Group", + "edit_delete_group": "Delete Group", + "edit_delete_group_confirm": "Are you sure delete the group?", + "delete_the_picture": "Are you sure delete the picture?" + }, + "upload": { + "upload_failed": "Upload failed", + "delete_confirm": "Are you sure delete the picture?", + "upload_image": "Upload Picture" + }, + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm", + "select": { + "pushed": "Pushed: {{count}}", + "select_all": "Select all", + "selected": "Selected", + "selected_some": "Selected: {{count}}", + "text": "Select", + "total": "Total: {{count}}" + }, + "time_filter": { + "all": "All time", + "last_week": "Last week", + "past_24_hour": "Past 24 hour", + "past_30_min": "Past 30 min", + "past_4_hour": "Past 4 hour", + "past_hour": "Past 1 hour" + }, + "user": { + "logout": "Logout" + }, + "video": { + "delete_confirm_title": "Are you sure you want to delete this video?", + "delete_confirm": "These videos will be deleted.They can be recovered from the “news” page. ", + "delete_description": "Are you sure you want to delete these {{count}} videos?", + "delete_empty": "Select the video you want to delete", + "download": "Download", + "push_confirm": "Are you sure you want to streaming these video?", + "push_empty": "Select the video you want to streaming", + "push_failed": "some video streaming failed!", + "push_success": "Streaming success,please goto Streaming!", + "push_to_live": "Streaming", + "sort_modify_confirm": "Are you change video sequence?", + "sort_modify_failed": "Video sequence change failed", + "sort_modify_live_success": "Video sequence changed", + "sort_modify_rollback": "Exit and video sequence restored!", + "sort_modify_success": "Video sequence changed", + "title": "Title", + "title_generated_time": "Time stamp", + "title_operation": "Operation", + "title_thumb": "Cover", + "playing": "Playing", + "generating": "Generating" } } \ No newline at end of file diff --git a/src/i18n/translations/zh-CN.json b/src/i18n/translations/zh-CN.json index adc9c1c..89af5d8 100644 --- a/src/i18n/translations/zh-CN.json +++ b/src/i18n/translations/zh-CN.json @@ -1,10 +1,151 @@ { "AppTitle": "数字人直播", "Hello": "你好", - "login": { - "text": "登录" + "close": "关闭", + "confirm": { + "push_title": "推流提示", + "push_video": "是否确定一键推流选中新闻视频?", + "title": "提示" }, - "notice": { - "title": "Notice" + "delete_batch": "批量删除", + "delete_failed": "删除失败", + "delete_success": "删除成功", + "download": "下载", + "generating": { + "title": "预览视频 - 点击视频列表播放" + }, + "history": { + "delete_confirm": "是否要删除该视频", + "push_success": "一键推流成功,已推流至数字人直播间,请查看!", + "search_key": "请输入视频标题关键字进行信息", + "text": "历史视频" + }, + "live": { + "duration": "时长", + "edit_locked": "锁定状态不可排序", + "edit_unlock": "已解锁", + "play_first": "即将播放第一条视频", + "playlist_count": "当前播放列表共 {{count}} 条", + "title": "直播界面" + }, + "login": { + "code_sending": "发送中", + "invalid_username_or_pwd": "账号或密码错误", + "loading": "登录中...", + "password": "请输入验证码", + "send_sms_code": "获取验证码", + "text": "立即登录", + "title": "登录", + "username": "请输入账号", + "welcome": "欢迎登录" + }, + "nav": { + "editing": "新闻编辑", + "generating": "视频生成", + "live": "数字人直播间", + "materials": "新闻素材" + }, + "news": { + "delete_confirm": "你确定要删除吗?", + "delete_confirm_count": "你确定要删除选择的 {{count}} 条新闻吗?", + "delete_description": "删除后需从新闻素材中重新选择", + "delete_empty": "请选择要删除的新闻", + "download_empty": "请选择要下载的新闻", + "download_failed": "下载新闻失败,请重试!", + "edit_form_search": "请输入新闻标题关键词进行搜索", + "editing": "新闻编辑", + "filter_all": "全部", + "filter_source": "新闻来源", + "generate_video": "生成视频", + "get_detail": "获取新闻详情", + "get_detail_error": "获取新闻详情失败", + "image_count": "图片数", + "materials": { + "title": "新闻素材" + }, + "news_all_source": "全部来源", + "push_empty": "请选择要推入编辑的新闻", + "push_failed": "推送失败", + "push_stream_empty": "请选择要开播的新闻", + "push_stream_success": "推流成功", + "push_streaming": "推流中...", + "push_success": "推送成功", + "push_to_edit": "推入编辑", + "pushed": "已推送", + "search_key_title": "请输入新闻标题关键词进行搜索", + "source": "来源", + "title": "标题", + "title_image_count": "图片数", + "title_operate": "操作", + "title_time": "时间", + "title_word_count": "字数", + "word_count": "字数", + + "edit_digital_text": "数字人主播台编辑区", + "edit_other_text": "素材融合呈现编辑区", + "edit_generate_video_again": "重新生成", + "edit_generate_again": "重新生成", + "edit_generate_video": "生成视频", + "edit_save_failed": "保存失败,请重试!", + + "edit_notice_keep_1": "至少保留一个内容块", + "edit_notice_enter_text": "请先添加内容", + "edit_notice_enter_article_title": "请输入文章标题", + "edit_notice_enter_article_title1": "请输入标题内容", + "edit_notice_enter_article_content": "请输入正文文本内容", + + "edit_add_group": "新增分组", + "edit_delete_group": "删除此分组", + "edit_delete_group_confirm": "请确认删除此分组?", + "delete_the_picture": "请确认删除此图片" + }, + "upload": { + "upload_failed": "上传图片失败,请重试", + "delete_confirm": "请确认删除此图片?", + "upload_image": "上传图片" + }, + "delete": "删除", + "cancel": "取消", + "confirm": "确定", + "select": { + "pushed": "已推送: {{count}} 条", + "select_all": "全选", + "selected": "已选", + "selected_some": "已选 {{count}} 条", + "text": "选择", + "total": "总共 {{count}} 条" + }, + "time_filter": { + "all": "所有时间", + "last_week": "近一周", + "past_24_hour": "一天内", + "past_30_min": "半小时内", + "past_4_hour": "四小时内", + "past_hour": "一小时内" + }, + "user": { + "logout": "退出登录" + }, + "video": { + "delete_confirm_title": "你确定要删除此视频吗 ", + "delete_confirm": "删除后需重新生成视频", + "delete_description": "已选择{{count}}条,确定要全部删除吗?", + "delete_empty": "请选择要删除的视频", + "download": "下载视频", + "push_confirm": "是否确定一键推流选中新闻视频?", + "push_empty": "请选择要推流的新闻视频", + "push_failed": "选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!", + "push_success": "一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!", + "push_to_live": "一键推流", + "sort_modify_confirm": "是否采纳移动视频位置操作?", + "sort_modify_failed": "调整视频顺序失败,请重试!", + "sort_modify_live_success": "已完成直播队列的修改", + "sort_modify_rollback": "退出并恢复之前的直播队列!", + "sort_modify_success": "调整视频顺序成功", + "title": "标题", + "title_generated_time": "生成时间", + "title_operation": "操作", + "title_thumb": "缩略图", + "playing": "播放中" } } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 354eeba..53a7d7a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom/client' - +import '@/i18n/config.ts' import App from './App.tsx' import '@/assets/index.scss' diff --git a/src/pages/library/components/search-form.tsx b/src/pages/library/components/search-form.tsx index 1cde588..4e7da49 100644 --- a/src/pages/library/components/search-form.tsx +++ b/src/pages/library/components/search-form.tsx @@ -3,6 +3,7 @@ import {useSetState} from "ahooks"; import {SearchOutlined} from "@ant-design/icons"; import React from "react"; import TimeSelect from "@/components/form/time-select.tsx"; +import {useTranslation} from "react-i18next"; type Props = { onSearch?: (params: VideoSearchParams) => void; @@ -11,6 +12,7 @@ type Props = { } export default function SearchForm({onSearch}: Props) { + const {t} = useTranslation() const [state, setState] = useSetState<{ pushing?: boolean; time_flag: number; @@ -44,7 +46,7 @@ export default function SearchForm({onSearch}: Props) { onPressEnter={() => onFinish(state)} onBlur={() => onFinish(state)} allowClear - placeholder={'请输入视频标题关键字进行信息'} + placeholder={t("history.search_key")} /> void } export default function VideoDetail({video, onClose,autoPlay}: Props) { + const {t} = useTranslation() const [state, setState] = useSetState({ exporting: false, pushing: false, @@ -22,7 +24,7 @@ export default function VideoDetail({video, onClose,autoPlay}: Props) { if (state.pushing) return setState({pushing: true}) push2room([video.id]).then(() => { - showToast('一键推流成功,已推流至数字人直播间,请查看!', 'success') + showToast(t('history.push_success'), 'success') }).catch(showErrorToast).finally(() => { setState({pushing: false}) }) @@ -51,11 +53,11 @@ export default function VideoDetail({video, onClose,autoPlay}: Props) {
    - + - +
    diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx index 2c7730c..0a0779f 100644 --- a/src/pages/library/index.tsx +++ b/src/pages/library/index.tsx @@ -11,12 +11,14 @@ import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infini import ButtonBatch from "@/components/button-batch.tsx"; import ButtonToTop from "@/components/scoller/button-to-top.tsx"; import {IconArrowRight, IconDelete} from "@/components/icons"; +import {useTranslation} from "react-i18next"; const DEFAULT_PAGE_LIMIT = { page: 1, limit: 12 } export default function LibraryIndex() { + const {t} = useTranslation() const [modal, contextHolder] = Modal.useModal(); const [checkedIdArray, setCheckedIdArray] = useState([]) const [params, setParams] = useState({ @@ -48,8 +50,8 @@ export default function LibraryIndex() { const handleRemove = (video: VideoInfo) => { modal.confirm({ - title: '删除提示', - content: '是否要删除该视频', + title: t('confirm.title'), + content: t('history.delete_confirm'), onOk: () => { console.log('OK', video); } @@ -58,8 +60,8 @@ export default function LibraryIndex() { const handleLive = async () => { if (checkedIdArray.length == 0) return; modal.confirm({ - title: '推流提示', - content: '是否确定一键推流选中新闻视频?', + title: t('confirm.push_title'), + content: t('confirm.push_video'), onOk: () => { console.log('OK'); } @@ -105,13 +107,13 @@ export default function LibraryIndex() {
    - 总共 {data?.list.length || 0} 条 - 已推送: {state.pushedCount} 条 - 已选: {checkedIdArray.length} 条 + {t('select.total',{count:data?.list.length || 0})} + {t('select.pushed',{count:state.pushedCount})} + {t('select.selected_some',{count:checkedIdArray.length})} } - title={`你确定要删除选择的 ${checkedIdArray.length} 条视频吗?`} - emptyMessage={'请选择要删除的视频'} - confirmMessage={'删除后需重新生成视频'} + title={t('video.delete_description',{count:checkedIdArray.length})} + emptyMessage={t('video.delete_empty')} + confirmMessage={t('video.delete_confirm')} onProcess={deleteHistories} - >批量删除} + >{t('delete_batch')}} {checkedIdArray?.length > 0 && } onProcess={push2room} - emptyMessage={'请选择要推流的视频'} - >一键推流} + emptyMessage={t('video.push_empty')} + >{t('video.push_to_live')}}
    ) } \ No newline at end of file diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index 6eb385b..b74104d 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -15,10 +15,11 @@ import {Player, PlayerInstance} from "@/components/video/player.tsx"; import {IconDelete, IconLocked, IconUnlock} from "@/components/icons"; import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx"; import ButtonToTop from "@/components/scoller/button-to-top.tsx"; +import {useTranslation} from "react-i18next"; const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {} export default function LiveIndex() { - + const {t} = useTranslation() const player = useRef(null) const [videoData, setVideoData] = useState([]) @@ -65,7 +66,7 @@ export default function LiveIndex() { const _activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : activeIndex.current + 1) setState({activeIndex: _activeIndex}) if (endToFirst) { - showToast('即将播放第一条视频'); + showToast(t("live.play_first")); } // 找到对应video item 并显示在视图可见区域 showVideoItem(_activeIndex) @@ -143,7 +144,7 @@ export default function LiveIndex() { const processDeleteVideo = async (ids: Id[]) => { deleteByIds(ids).then(() => { - showToast('删除成功!', 'success') + showToast(t('delete_success'), 'success') loadList() }).catch(showErrorToast) } @@ -158,20 +159,20 @@ export default function LiveIndex() { return; } modal.confirm({ - title: '提示', - content: '是否采纳移动视频位置操作?', + title: t('confirm.title'), + content: t('video.sort_modify_confirm'), centered: true, onOk: () => { //showToast('编辑成功!!!', 'info'); modifyOrder(videoData.map(s => s.id)).then(() => { - showToast('已完成直播队列的修改!', 'success') + showToast(t('video.sort_modify_live_success'), 'success') setEditable(false) }).catch(() => { - showToast('调整视频顺序失败,请重试!', 'warning') + showToast(t('video.sort_modify_failed'), 'warning') }) }, onCancel: () => { - showToast('退出并恢复之前的直播队列!', 'info'); + showToast(t('video.sort_modify_rollback'), 'info'); loadList() setEditable(false) } @@ -210,7 +211,7 @@ export default function LiveIndex() {
    -
    直播界面
    +
    {t('live.title')}
    @@ -224,7 +225,7 @@ export default function LiveIndex() {
    - 视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)} + {t('live.duration')}: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}
    @@ -233,14 +234,14 @@ export default function LiveIndex() {
    {/*视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}*/} - 当前播放列表共 {videoData.length} 条 + {t('live.playlist_count',{count:videoData.length})}
    - {editable ? '已解锁' : '锁定状态不可排序'} + {editable ? t('live.edit_unlock') : t('live.edit_locked')} {editable ? : } @@ -248,7 +249,7 @@ export default function LiveIndex() {
    handleAllCheckedChange()}/> @@ -258,10 +259,10 @@ export default function LiveIndex() {
    No.
    -
    缩略图
    -
    标题
    -
    生成时间
    -
    操作
    +
    {t('video.title_thumb')}
    +
    {t('video.title')}
    +
    {t('video.title_generated_time')}
    +
    {t('video.title_operation')}
    @@ -322,12 +323,12 @@ export default function LiveIndex() { {checkedIdArray.length > 0 && - 批量删除 + {t('delete_batch')} }
    diff --git a/src/pages/news/components/button-delete-batch.tsx b/src/pages/news/components/button-delete-batch.tsx index a78bc07..cb2aa14 100644 --- a/src/pages/news/components/button-delete-batch.tsx +++ b/src/pages/news/components/button-delete-batch.tsx @@ -3,17 +3,20 @@ import {showToast} from "@/components/message.ts"; import React, {useState} from "react"; import {IconDelete, IconWarningCircle} from "@/components/icons"; import {deleteByIds} from "@/service/api/article.ts"; +import {useTranslation} from "react-i18next"; +import {divide} from "lodash"; export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => void; }) { + const {t} = useTranslation() const {modal} = App.useApp(); const [loading, setLoading] = useState(false) const handlePush = () => { setLoading(true) deleteByIds(props.ids).then(() => { props.onSuccess?.(); - showToast('删除成功', 'success') + showToast(t('delete_success'), 'success') }).catch(() => { - showToast('删除失败', 'error') + showToast(t('delete_failed'), 'error') }).finally(() => { setLoading(false) }) @@ -21,14 +24,14 @@ export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => v const onPushClick = () => { if(loading) return; if (props.ids.length === 0) { - showToast('请选择要删除的新闻', 'warning') + showToast(t('news.delete_empty'), 'warning') return } modal.confirm({ wrapClassName:'root-modal-confirm', icon: , - title: `你确定要删除选择的 ${props.ids.length} 条新闻吗?`, - content: '删除后需从新闻素材中重新选择', + title: t('news.delete_confirm_count',{count:props.ids.length}), + content: , onOk: handlePush, centered: true }) @@ -40,7 +43,7 @@ export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => v onClick={onPushClick} className='bg-gray-300 hover:bg-gray-400 text-white' > - 批量删除 + {t('delete_batch')}
    diff --git a/src/pages/news/components/button-news-download.tsx b/src/pages/news/components/button-news-download.tsx index 5fa50c5..3635dcc 100644 --- a/src/pages/news/components/button-news-download.tsx +++ b/src/pages/news/components/button-news-download.tsx @@ -7,6 +7,7 @@ import {getById} from "@/service/api/news.ts"; import {showToast} from "@/components/message.ts"; import {IconDownload} from "@/components/icons"; +import {useTranslation} from "react-i18next"; /** @@ -63,11 +64,12 @@ async function downloadAsZip(list: NewsInfo[]) { } export default function ButtonNewsDownload(props: { ids: Id[] }) { + const {t} = useTranslation() const [loading, setLoading] = useState(false) const onDownloadClick = async (ids: Id[]) => { if(loading) return; if (props.ids.length === 0) { - showToast('请选择要下载的新闻', 'warning') + showToast(t('news.download_empty'), 'warning') return } setLoading(true) @@ -75,7 +77,7 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) { const list = await getAllNewsContent(ids) await downloadAsZip(list) } catch (e) { - showToast('下载新闻失败,请重试!', 'error') + showToast(t('news.download_failed'), 'error') } finally { setLoading(false) } @@ -86,7 +88,7 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) { className={'btn-action bg-[#eef5ff] text-gray-800 hover:bg-[#d2e3ff]'} onClick={() => onDownloadClick(props.ids)} > - 下载 + {t('download')} ) diff --git a/src/pages/news/components/button-push-news2article.tsx b/src/pages/news/components/button-push-news2article.tsx index 64721e7..4b22b57 100644 --- a/src/pages/news/components/button-push-news2article.tsx +++ b/src/pages/news/components/button-push-news2article.tsx @@ -4,20 +4,22 @@ import {push2article} from "@/service/api/news.ts"; import {IconArrowRight} from "@/components/icons"; import {useNavigate} from "react-router-dom"; import {useIndexArrayCache} from "@/hooks/useCache.ts"; +import {useTranslation} from "react-i18next"; export default function ButtonPushNews2Article(props: { ids: Id[]; }) { // const {modal} = App.useApp(); + const {t}= useTranslation() const [loading,setLoading] = useState(false) const navigate = useNavigate(); const {set} = useIndexArrayCache(); const handlePush = () => { setLoading(true) push2article(props.ids).then(() => { - showToast('推送成功', 'success') + showToast(t('news.push_success'), 'success') set([]) navigate('/edit') }).catch(() => { - showToast('推送失败', 'error') + showToast(t('news.push_failed'), 'error') }).finally(() => { setLoading(false) }) @@ -25,7 +27,7 @@ export default function ButtonPushNews2Article(props: { ids: Id[]; }) { const onPushClick = () => { if(loading) return; if (props.ids.length === 0) { - showToast('请选择要推入编辑的新闻', 'warning') + showToast(t('news.push_empty'), 'warning') return } handlePush(); @@ -42,7 +44,7 @@ export default function ButtonPushNews2Article(props: { ids: Id[]; }) { onClick={onPushClick} className='bg-[#4096ff] hover:bg-blue-600 text-white' > - 推入编辑 + {t('news.push_to_edit')} ) diff --git a/src/pages/news/components/button-push2video.tsx b/src/pages/news/components/button-push2video.tsx index d840959..1ab32df 100644 --- a/src/pages/news/components/button-push2video.tsx +++ b/src/pages/news/components/button-push2video.tsx @@ -3,16 +3,18 @@ import {showErrorToast, showToast} from "@/components/message.ts"; import {push2video} from "@/service/api/article.ts"; import {IconArrowRight} from "@/components/icons"; import {useNavigate} from "react-router-dom"; +import {useTranslation} from "react-i18next"; export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => void; }) { const [loading, setLoading] = useState(false) + const {t} = useTranslation() const navigate = useNavigate() const handlePush = () => { setLoading(true) push2video(props.ids).then(() => { - showToast('推流成功', 'success') - navigate('/create?state=push-success',{ + showToast(t('news.push_stream_success'), 'success') + navigate('/create?state=push-success', { state: 'push-success' }) // props.onSuccess?.() @@ -23,7 +25,7 @@ export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => v const onPushClick = () => { if (loading) return; if (props.ids.length === 0) { - showToast('请选择要开播的新闻', 'warning') + showToast(t('news.push_stream_empty'), 'warning') return } // Modal.confirm({ @@ -40,7 +42,7 @@ export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => v className='bg-[#4096ff] hover:bg-blue-600 text-white' onClick={onPushClick} > - {loading?'推送中...':'生成视频'} + {loading ? t('news.push_streaming') : t('news.generate_video')}
    diff --git a/src/pages/news/components/edit-search-form.tsx b/src/pages/news/components/edit-search-form.tsx index 1ae249b..ad8058b 100644 --- a/src/pages/news/components/edit-search-form.tsx +++ b/src/pages/news/components/edit-search-form.tsx @@ -4,20 +4,22 @@ import React, {useEffect, useState} from "react"; import {useSetState} from "ahooks"; import useArticleTags from "@/hooks/useArticleTags.ts"; import TagSelect from "@/components/form/tag-select.tsx"; +import {useTranslation} from "react-i18next"; export default function EditSearchForm(props: { onSubmit: (values: ApiArticleSearchParams) => void; defaultParams?: Partial; }) { + const {t} = useTranslation() const articleTags = useArticleTags() const [tags, _setTags] = useState([]); - const [prevSearchName, setPrevSearchName] = useState(props.defaultParams?.title||'') + const [prevSearchName, setPrevSearchName] = useState(props.defaultParams?.title || '') const [params, setParams] = useSetState({ pagination: {limit: 10, page: 1}, - title:props.defaultParams?.title||'' + title: props.defaultParams?.title || '' }); - const handleSubmit = (_tags?:Id[][],from?:'input') => { + const handleSubmit = (_tags?: Id[][], from?: 'input') => { if (from == 'input' && (params.title == prevSearchName || (!params.title && !prevSearchName))) return params.title = prevSearchName; setParams({title: prevSearchName}) @@ -42,21 +44,21 @@ export default function EditSearchForm(props: { } }) } - useEffect(()=>{ + useEffect(() => { const {defaultParams} = props; - if(!defaultParams){ + if (!defaultParams) { return; } - const tags:Id[][] = [] + const tags: Id[][] = [] - if(defaultParams.tags){ - defaultParams.tags.forEach(it=>{ + if (defaultParams.tags) { + defaultParams.tags.forEach(it => { tags.push([it.level1, it.level2]) }) _setTags(tags) } - },[articleTags]) - const setTags = (_tags: Id[][])=>{ + }, [articleTags]) + const setTags = (_tags: Id[][]) => { console.log(_tags) _setTags(_tags) @@ -70,9 +72,9 @@ export default function EditSearchForm(props: { onChange={e => setPrevSearchName(e.target.value)} type="text" className="rounded-3xl px-3 w-[270px]" prefix={} - placeholder="请输入新闻标题关键词进行搜索" - onPressEnter={()=>handleSubmit(undefined,'input')} - onBlur={()=>handleSubmit(undefined,'input')} + placeholder={t('news.edit_form_search')} + onPressEnter={() => handleSubmit(undefined, 'input')} + onBlur={() => handleSubmit(undefined, 'input')} /> {/*来源*/} {/* void; @@ -24,6 +25,7 @@ const DEFAULT_STATE = { } export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) { const tags = useArticleTags(); + const {t} = useTranslation() const [params, setParams] = useSetState({ pagination, time_flag:1, @@ -146,13 +148,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) value={prevSearchName} onChange={e => setPrevSearchName(e.target.value)} className="w-[270px] rounded-3xl" - placeholder={'请输入新闻标题关键词进行搜索'} + placeholder={t('news.search_key_title')} onPressEnter={onFinish} onBlur={onFinish} prefix={} /> @@ -167,7 +169,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) onClick={() => { handleFilter({tag_level_1_id: -1, tag_level_2_id: -1}) setSubOptions([]) - }}>全部 + }}>{t('news.filter_all')}
    {pinnedList.filter(s => (Number(s.value) !== 999999)).map(it => (
    -
    新闻来源
    +
    {t('news.filter_source')}
    diff --git a/src/pages/news/components/style.module.scss b/src/pages/news/components/style.module.scss index 7efcc42..14f97c6 100644 --- a/src/pages/news/components/style.module.scss +++ b/src/pages/news/components/style.module.scss @@ -64,7 +64,11 @@ .source{ width: 180px; } - .count-picture,.count-words{ + .count-picture{ + width: 160px; + text-align: center; + } + .count-words{ width: 120px; text-align: center; } diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx index d6daea4..086b42a 100644 --- a/src/pages/news/edit.tsx +++ b/src/pages/news/edit.tsx @@ -15,11 +15,13 @@ import {clsx} from "clsx"; import ButtonToTop from "@/components/scoller/button-to-top.tsx"; import ButtonDeleteBatch from "@/pages/news/components/button-delete-batch.tsx"; import {showErrorToast, showToast} from "@/components/message.ts"; +import {useTranslation} from "react-i18next"; const FilterCache: Partial = { tags: [], } export default function NewEdit() { + const {t} = useTranslation() const [state, setState] = useState<{ checkAll?: boolean; showToTop?: boolean; @@ -68,7 +70,7 @@ export default function NewEdit() { const handleDelete = (id) => { deleteByIds([id]).then(() => { refresh() - showToast('删除成功', 'success') + showToast(t('delete_success'), 'success') }).catch(showErrorToast) } @@ -80,15 +82,15 @@ export default function NewEdit() {
    -
    - - 总共 {data?.list?.length || 0} 条 - 已选 {selectedRowKeys.length} 条 +
    + + {t('select.total',{count:data?.list?.length || 0})} + {t('select.selected_some',{count:selectedRowKeys.length})}
    { handleCheckAll(!state.checkAll) - }}>全选 + }}>{t('select.select_all')} { handleCheckAll(e.target.checked) @@ -97,12 +99,12 @@ export default function NewEdit() {
    -
    标题
    -
    来源
    -
    图片数
    -
    字数
    -
    时间
    -
    操作
    +
    {t('news.title')}
    +
    {t('news.source')}
    +
    {t('news.title_image_count')}
    +
    {t('news.title_word_count')}
    +
    {t('news.title_time')}
    +
    {t('news.title_operate')}
    { setParams(prev => ({ @@ -142,8 +144,8 @@ export default function NewEdit() { placement={'left'} arrow={false} icon={} - title={'你确定要删除吗?'} - description={'删除后需从新闻素材中重新选择'} + title={t('news.delete_confirm')} + description={} onConfirm={() => { handleDelete(item.id) }} diff --git a/src/pages/news/index.tsx b/src/pages/news/index.tsx index b947528..59a4117 100644 --- a/src/pages/news/index.tsx +++ b/src/pages/news/index.tsx @@ -13,11 +13,13 @@ import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx"; import ButtonToTop from "@/components/scoller/button-to-top.tsx"; import {useIndexArrayCache} from "@/hooks/useCache.ts"; +import {useTranslation} from "react-i18next"; const FilterCache: Partial = { time_flag: 1, } export default function NewsIndex() { + const {t} = useTranslation() const [params, setParams] = useState({ pagination: {page: 1, limit: 12}, ...FilterCache @@ -55,12 +57,12 @@ export default function NewsIndex() { }) const handleViewNewsDetail = (id: number) => { - const {update, close} = showLoading('获取新闻详情...') + const {update, close} = showLoading(`${t('news.get_detail')}...`) getById(id).then(res => { close() setActiveNews({...res, id}) }).catch(() => { - update('获取新闻详情失败', 'info') + update(t('news.get_detail_error'), 'info') }) } @@ -113,21 +115,21 @@ export default function NewsIndex() { handleCheckChange(activeNews!.id)} - >选择 + >{t('select.text')}
    }
    -
    - - 总共 {data?.list?.length || 0} 条 - 已选 {checkedId.length} 条 +
    + + {t('select.total',{count:data?.list?.length || 0})} + {t('select.selected_some',{count:checkedId.length})}
    { handleCheckAll(!state.checkAll) - }}>全选 + }}>{t('select.select_all')} { handleCheckAll(e.target.checked) }}> @@ -169,15 +171,15 @@ export default function NewsIndex() {
    }
    -
    来源: {item.data_source_name}
    +
    {t('news.source')}: {item.data_source_name}
    {formatTime(item.publish_time, 'min')}
    -
    图片数: {item.img_num}
    -
    字数: {item.content_word_count}
    +
    {t('news.image_count')}: {item.img_num}
    +
    {t('news.word_count')}: {item.content_word_count}
    {item.internal_article_id > 0 ? - 已推送 : + {t('news.pushed')} : { handleCheckChange(item.id) }}/>} diff --git a/src/pages/user/components/form-login.tsx b/src/pages/user/components/form-login.tsx index 10a3b9c..90838cf 100644 --- a/src/pages/user/components/form-login.tsx +++ b/src/pages/user/components/form-login.tsx @@ -7,6 +7,7 @@ import styles from './../style.module.scss' import useAuth from "@/hooks/useAuth.ts"; import {useSmsCode} from "@/components/form/sms-code.tsx"; +import {useTranslation} from "react-i18next"; type FieldType = { username?: string; @@ -15,6 +16,7 @@ type FieldType = { export default function FormLogin() { + const {t} = useTranslation() const [disabled, setDisabled] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState() @@ -26,7 +28,7 @@ export default function FormLogin() { const onFinish: FormProps['onFinish'] = (values) => { if(disabled || loading) return if (!values.username || !/^1\d{10}$/.test(values.username)) { - setError('账号或密码错误') + setError(t("login.invalid_username_or_pwd")) return } setLoading(true) @@ -38,7 +40,7 @@ export default function FormLogin() { }; return (
    -
    欢迎登录
    +
    {t("login.welcome")}
    name="basic" style={{maxWidth: 600}} @@ -53,18 +55,18 @@ export default function FormLogin() { > name="username">
    - +
    + placeholder={t("login.password")}/> 0 || sending || !phone ? 'text-gray-400 cursor-not-allowed' : 'text-blue-500 cursor-pointer'}`)} onClick={() => sendCode(phone)}> - {sending ? '发送中...' : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : '获取验证码')} + {sending ? `${t('login.code_sending')}...` : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : t('login.send_sms_code'))}
    @@ -75,7 +77,7 @@ export default function FormLogin() {
    {error}
    diff --git a/src/pages/video/components/button-push2room.tsx b/src/pages/video/components/button-push2room.tsx index f9b5499..4e44cdc 100644 --- a/src/pages/video/components/button-push2room.tsx +++ b/src/pages/video/components/button-push2room.tsx @@ -3,10 +3,12 @@ import React, {useState} from "react"; import {showErrorToast, showToast} from "@/components/message.ts"; import {push2room, VideoStatus} from "@/service/api/video.ts"; import {IconArrowRight, IconWarningCircle} from "@/components/icons"; +import {useTranslation} from "react-i18next"; export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];onSuccess?:()=>void; }) { const [loading, setLoading] = useState(false) + const {t} = useTranslation() const handlePush = () => { setLoading(true) // 只需要已经生成视频的数据id @@ -14,9 +16,9 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on push2room(vids).then(() => { props.onSuccess?.() if(props.ids.length == vids.length){ - showToast('一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!', 'success') + showToast(t("video.push_success"), 'success') }else{ - showToast('选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!', 'success') + showToast(t("video.push_failed"), 'success') } }).catch(showErrorToast).finally(() => { setLoading(false) @@ -25,14 +27,14 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on const onPushClick = () => { if(loading) return; if (props.ids.length === 0) { - showToast('请选择要推流的新闻', 'warning') + showToast(t("video.push_empty"), 'warning') return } Modal.confirm({ wrapClassName:'root-modal-confirm', title: '操作提示', icon: , - content: '是否确定一键推流选中新闻视频??', + content: t("video.push_confirm"), onOk: handlePush }) } @@ -44,7 +46,7 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on className='bg-[#4096ff] hover:bg-blue-600 text-white' onClick={onPushClick} > - 一键推流 + {t("video.push_to_live")}
    diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index c323ed6..b603b10 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -16,8 +16,10 @@ import ButtonToTop from "@/components/scoller/button-to-top.tsx"; import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx"; import {IconDelete} from "@/components/icons"; import {useLocation} from "react-router-dom"; +import {useTranslation} from "react-i18next"; export default function VideoIndex() { + const {t} = useTranslation() const [editId, setEditId] = useState(-1) const loc = useLocation() const [videoData, setVideoData] = useState([]) @@ -96,10 +98,10 @@ export default function VideoIndex() { const handleModifySort = (items: VideoInfo[]) => { modifyOrder(items.map(s => s.id)).then(() => { - showToast('调整视频顺序成功!', 'success') + showToast(t('video.sort_modify_success'), 'success') }).catch(() => { loadList(); - showToast('调整视频顺序失败,请重试!', 'warning') + showToast(t('video.sort_modify_failed'), 'warning') }) return ()=>{ @@ -132,7 +134,7 @@ export default function VideoIndex() { }, [videoData, scrollerRef]) const processDeleteVideo = async (ids: Id[]) => { deleteFromList(ids).then(() => { - showToast('删除成功!', 'success') + showToast(t('delete_success'), 'success') loadList() }).catch(showErrorToast) } @@ -142,7 +144,7 @@ export default function VideoIndex() {
    -
    预览视频 - 点击视频列表播放
    +
    {t("generating.title")}
    - 总共 {videoData.length || 0} 条 - 已选 {checkedIdArray.length} 条 + {t('select.selected_some',{count:videoData.length || 0})} + {t('select.selected_some',{count:checkedIdArray.length})} handleAllCheckedChange()}/> @@ -185,10 +187,10 @@ export default function VideoIndex() {
    No.
    -
    缩略图
    -
    标题
    -
    生成时间
    -
    操作
    +
    {t('video.title_thumb')}
    +
    {t('video.title')}
    +
    {t('video.title_generated_time')}
    +
    {t('video.title_operation')}
    setState({showToTop: top > 30})}> @@ -258,16 +260,16 @@ export default function VideoIndex() { {checkedIdArray.length > 0 && { - showToast('删除成功!', 'success') + showToast(t('delete_success'), 'success') loadList() }} > - 批量删除 + {t('delete_batch')} } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index bc5be41..8d92709 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -29,12 +29,15 @@ const router = createBrowserRouter([ // future={{v7_startTransition: true,v7_relativeSplatPath: true}} const AppRouter = () => { - const {t} = useTranslation(); + const {t,i18n:langConfig} = useTranslation(); const {i18n} = useConfig(); useEffect(() => { if (i18n && i18n == 'zh-CN') { dayjs.locale('zh-cn'); + }else{ + dayjs.locale('en') } + langConfig.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n)) }, [i18n]) return ( { key: 'profile', label:
    navigate('/history')}> - 视频库 + {t('history.text')}
    , }, // { @@ -43,7 +44,7 @@ const NavigationUserContainer = () => { className={`flex items-center rounded-3xl ${user ? 'bg-[#e3eeff]' : 'bg-primary-blue'} p-1 pr-2 cursor-pointer rounded`}> {user ? {hidePhone(user.nickname)} : ( - {t('login.text')} + {t('login.title')} )}
    ) return (
    @@ -65,7 +66,7 @@ const NavigationUserContainer = () => {
    -
    退出登录
    +
    {t('user.logout')}
    @@ -76,6 +77,7 @@ const NavigationUserContainer = () => {
    ) } export const BaseLayout: React.FC = ({children}) => { + const {i18n,onChangeLocalization} = useConfig(); return (
    @@ -84,6 +86,13 @@ export const BaseLayout: React.FC = ({children}) => {
    + { + i18n == 'zh-CN'?( + + ):( + + ) + }
    diff --git a/src/routes/layout/dashboard-navigation.tsx b/src/routes/layout/dashboard-navigation.tsx index e15686a..15a5918 100644 --- a/src/routes/layout/dashboard-navigation.tsx +++ b/src/routes/layout/dashboard-navigation.tsx @@ -2,42 +2,46 @@ import {clsx} from "clsx"; import {NavLink} from "react-router-dom"; import {IconNavigationArrow} from "@/components/icons"; import useAuth from "@/hooks/useAuth.ts"; +import {useMemo} from "react"; +import {useTranslation} from "react-i18next"; + -const NavItems = [ - { - key: 'news', - name: '新闻素材', - icon: 'news', - path: '/' - }, - { - key: 'video', - name: '新闻编辑', - icon: 'e', - path: '/edit' - }, - { - key: 'create', - name: '视频生成', - icon: 'ai', - path: '/create' - }, - // { - // key: 'library', - // name: '视频库', - // icon: '+', - // path:'/library' - // }, - { - key: 'live', - name: '数字人直播间', - icon: 'v', - path: '/live' - } -] export function DashboardNavigation() { + const {t,i18n} = useTranslation() const {user} = useAuth() + const NavItems = useMemo(()=>([ + { + key: 'news', + name: t('nav.materials'), + icon: 'news', + path: '/' + }, + { + key: 'video', + name: t('nav.editing'), + icon: 'e', + path: '/edit' + }, + { + key: 'create', + name: t('nav.generating'), + icon: 'ai', + path: '/create' + }, + // { + // key: 'library', + // name: '视频库', + // icon: '+', + // path:'/library' + // }, + { + key: 'live', + name: t('nav.live'), + icon: 'v', + path: '/live' + } + ]),[i18n.language]) return (
    {NavItems.map((it, idx) => (
    {user ?