add i18n for comment

This commit is contained in:
hjlarry 2025-09-23 16:19:04 +08:00
parent 21f7a49b4e
commit d5dd73cacf
15 changed files with 315 additions and 20 deletions

View File

@ -1,5 +1,6 @@
import type { FC } from 'react'
import { memo, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
import { MentionInput } from './mention-input'
@ -13,6 +14,7 @@ type CommentInputProps = {
export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, onCancel }) => {
const [content, setContent] = useState('')
const { t } = useTranslation()
const { userProfile } = useAppContext()
useEffect(() => {
@ -71,7 +73,7 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
value={content}
onChange={setContent}
onSubmit={handleMentionSubmit}
placeholder="Add a comment"
placeholder={t('workflow.comments.placeholder.add')}
autoFocus
className="relative"
/>

View File

@ -4,6 +4,7 @@ import type { FC, ReactNode } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useParams } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { RiArrowUpLine, RiAtLine } from '@remixicon/react'
import Textarea from 'react-textarea-autosize'
import Button from '@/app/components/base/button'
@ -29,7 +30,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
onChange,
onSubmit,
onCancel,
placeholder = 'Add a comment',
placeholder,
disabled = false,
loading = false,
className,
@ -37,6 +38,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
autoFocus = false,
}) => {
const params = useParams()
const { t } = useTranslation()
const appId = params.appId as string
const textareaRef = useRef<HTMLTextAreaElement>(null)
@ -46,6 +48,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
const [mentionPosition, setMentionPosition] = useState(0)
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([])
const resolvedPlaceholder = placeholder ?? t('workflow.comments.placeholder.add')
const mentionNameList = useMemo(() => {
const names = mentionUsers
@ -306,7 +309,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
'body-lg-regular relative z-10 w-full resize-none bg-transparent p-1 leading-6 text-transparent caret-primary-500 outline-none',
'placeholder:text-text-tertiary',
)}
placeholder={placeholder}
placeholder={resolvedPlaceholder}
autoFocus={autoFocus}
minRows={isEditing ? 4 : 1}
maxRows={4}
@ -345,7 +348,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
</div>
<div className='flex items-center gap-2'>
<Button variant='secondary' size='small' onClick={onCancel} disabled={loading}>
Cancel
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
@ -353,7 +356,7 @@ export const MentionInput: FC<MentionInputProps> = memo(({
disabled={loading || !value.trim()}
onClick={() => handleSubmit()}
>
Save
{t('common.operation.save')}
</Button>
</div>
</div>

View File

@ -3,6 +3,7 @@
import type { FC, ReactNode } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useReactFlow, useViewport } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
import Avatar from '@/app/components/base/avatar'
import Divider from '@/app/components/base/divider'
@ -146,6 +147,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
const { flowToScreenPosition } = useReactFlow()
const viewport = useViewport()
const { userProfile } = useAppContext()
const { t } = useTranslation()
const [replyContent, setReplyContent] = useState('')
const [activeReplyMenuId, setActiveReplyMenuId] = useState<string | null>(null)
const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' })
@ -221,14 +223,14 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
>
<div className='relative flex h-[360px] flex-col overflow-hidden rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'>
<div className='flex items-center justify-between rounded-t-2xl border-b border-components-panel-border bg-components-panel-bg-blur px-4 py-3'>
<div className=' font-semibold uppercase text-text-primary'>Comment</div>
<div className='font-semibold uppercase text-text-primary'>{t('workflow.comments.panelTitle')}</div>
<div className='flex items-center gap-1'>
<button
type='button'
disabled={loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onDelete}
aria-label='Delete comment'
aria-label={t('workflow.comments.aria.deleteComment')}
>
<RiDeleteBinLine className='h-4 w-4' />
</button>
@ -237,7 +239,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
disabled={comment.resolved || loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onResolve}
aria-label='Resolve comment'
aria-label={t('workflow.comments.aria.resolveComment')}
>
{comment.resolved ? <RiCheckboxCircleFill className='h-4 w-4' /> : <RiCheckboxCircleLine className='h-4 w-4' />}
</button>
@ -247,7 +249,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
disabled={!canGoPrev || loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onPrev}
aria-label='Previous comment'
aria-label={t('workflow.comments.aria.previousComment')}
>
<RiArrowUpSLine className='h-4 w-4' />
</button>
@ -256,7 +258,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
disabled={!canGoNext || loading}
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
onClick={onNext}
aria-label='Next comment'
aria-label={t('workflow.comments.aria.nextComment')}
>
<RiArrowDownSLine className='h-4 w-4' />
</button>
@ -264,7 +266,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
type='button'
className='flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
onClick={onClose}
aria-label='Close comment'
aria-label={t('workflow.comments.aria.closeComment')}
>
<RiCloseLine className='h-4 w-4' />
</button>
@ -273,7 +275,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
<div className='relative mt-2 flex-1 overflow-y-auto px-4'>
<ThreadMessage
authorId={comment.created_by_account?.id || ''}
authorName={comment.created_by_account?.name || 'User'}
authorName={comment.created_by_account?.name || t('workflow.comments.fallback.user')}
avatarUrl={comment.created_by_account?.avatar_url || null}
createdAt={comment.created_at}
content={comment.content}
@ -297,7 +299,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
e.stopPropagation()
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
}}
aria-label='Reply actions'
aria-label={t('workflow.comments.aria.replyActions')}
>
<RiMoreFill className='h-4 w-4' />
</button>
@ -307,7 +309,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
className='flex w-full items-center justify-start px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => handleStartEdit(reply)}
>
Edit reply
{t('workflow.comments.actions.editReply')}
</button>
<button
className='text-negative flex w-full items-center justify-start px-3 py-2 text-left text-sm hover:bg-state-base-hover'
@ -316,7 +318,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
onReplyDelete?.(reply.id)
}}
>
Delete reply
{t('workflow.comments.actions.deleteReply')}
</button>
</div>
)}
@ -329,7 +331,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
onSubmit={handleEditSubmit}
onCancel={handleCancelEdit}
placeholder="Edit reply"
placeholder={t('workflow.comments.placeholder.editReply')}
disabled={loading}
loading={loading}
isEditing={true}
@ -340,7 +342,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
) : (
<ThreadMessage
authorId={reply.created_by_account?.id || ''}
authorName={reply.created_by_account?.name || 'User'}
authorName={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
avatarUrl={reply.created_by_account?.avatar_url || null}
createdAt={reply.created_at}
content={reply.content}
@ -355,7 +357,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
</div>
{loading && (
<div className='bg-components-panel-bg/70 absolute inset-0 z-30 flex items-center justify-center text-sm text-text-tertiary'>
Loading
{t('workflow.comments.loading')}
</div>
)}
{onReply && (
@ -363,7 +365,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
<div className='flex items-center gap-3'>
<Avatar
avatar={userProfile?.avatar_url || null}
name={userProfile?.name || 'You'}
name={userProfile?.name || t('common.you')}
size={24}
className='h-8 w-8'
/>
@ -372,7 +374,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
value={replyContent}
onChange={setReplyContent}
onSubmit={handleReplySubmit}
placeholder='Reply'
placeholder={t('workflow.comments.placeholder.reply')}
disabled={loading}
loading={loading}
className='px-2'

View File

@ -192,6 +192,30 @@ const translation = {
nodeDragStop: 'Knoten verschoben',
nodeDelete: 'Knoten gelöscht',
},
comments: {
panelTitle: 'Kommentar',
loading: 'Laden…',
placeholder: {
add: 'Kommentar hinzufügen',
reply: 'Antworten',
editReply: 'Antwort bearbeiten',
},
aria: {
deleteComment: 'Kommentar löschen',
resolveComment: 'Kommentar abschließen',
previousComment: 'Vorheriger Kommentar',
nextComment: 'Nächster Kommentar',
closeComment: 'Kommentar schließen',
replyActions: 'Antwortaktionen',
},
actions: {
editReply: 'Antwort bearbeiten',
deleteReply: 'Antwort löschen',
},
fallback: {
user: 'Benutzer',
},
},
errorMsg: {
fieldRequired: '{{field}} ist erforderlich',
authRequired: 'Autorisierung ist erforderlich',

View File

@ -198,6 +198,30 @@ const translation = {
noteDelete: 'Note deleted',
edgeDelete: 'Node disconnected',
},
comments: {
panelTitle: 'Comment',
loading: 'Loading…',
placeholder: {
add: 'Add a comment',
reply: 'Reply',
editReply: 'Edit reply',
},
aria: {
deleteComment: 'Delete comment',
resolveComment: 'Resolve comment',
previousComment: 'Previous comment',
nextComment: 'Next comment',
closeComment: 'Close comment',
replyActions: 'Reply actions',
},
actions: {
editReply: 'Edit reply',
deleteReply: 'Delete reply',
},
fallback: {
user: 'User',
},
},
errorMsg: {
fieldRequired: '{{field}} is required',
rerankModelRequired: 'A configured Rerank Model is required',

View File

@ -192,6 +192,30 @@ const translation = {
nodeDescriptionChange: 'Descripción del nodo cambiada',
nodeResize: 'Nodo redimensionado',
},
comments: {
panelTitle: 'Comentario',
loading: 'Cargando…',
placeholder: {
add: 'Añadir un comentario',
reply: 'Responder',
editReply: 'Editar respuesta',
},
aria: {
deleteComment: 'Eliminar comentario',
resolveComment: 'Resolver comentario',
previousComment: 'Comentario anterior',
nextComment: 'Comentario siguiente',
closeComment: 'Cerrar comentario',
replyActions: 'Acciones de respuesta',
},
actions: {
editReply: 'Editar respuesta',
deleteReply: 'Eliminar respuesta',
},
fallback: {
user: 'Usuario',
},
},
errorMsg: {
fieldRequired: 'Se requiere {{field}}',
authRequired: 'Se requiere autorización',

View File

@ -192,6 +192,30 @@ const translation = {
nodeDescriptionChange: 'شرح نود تغییر کرد',
nodeChange: 'نود تغییر کرد',
},
comments: {
panelTitle: 'دیدگاه',
loading: 'در حال بارگذاری…',
placeholder: {
add: 'افزودن دیدگاه',
reply: 'پاسخ',
editReply: 'ویرایش پاسخ',
},
aria: {
deleteComment: 'حذف دیدگاه',
resolveComment: 'حل‌کردن دیدگاه',
previousComment: 'دیدگاه قبلی',
nextComment: 'دیدگاه بعدی',
closeComment: 'بستن دیدگاه',
replyActions: 'عملیات پاسخ',
},
actions: {
editReply: 'ویرایش پاسخ',
deleteReply: 'حذف پاسخ',
},
fallback: {
user: 'کاربر',
},
},
errorMsg: {
fieldRequired: '{{field}} الزامی است',
authRequired: 'احراز هویت ضروری است',

View File

@ -192,6 +192,30 @@ const translation = {
nodeAdd: 'Nœud ajouté',
nodeDescriptionChange: 'La description du nœud a changé',
},
comments: {
panelTitle: 'Commentaire',
loading: 'Chargement…',
placeholder: {
add: 'Ajouter un commentaire',
reply: 'Répondre',
editReply: 'Modifier la réponse',
},
aria: {
deleteComment: 'Supprimer le commentaire',
resolveComment: 'Résoudre le commentaire',
previousComment: 'Commentaire précédent',
nextComment: 'Commentaire suivant',
closeComment: 'Fermer le commentaire',
replyActions: 'Actions de réponse',
},
actions: {
editReply: 'Modifier la réponse',
deleteReply: 'Supprimer la réponse',
},
fallback: {
user: 'Utilisateur',
},
},
errorMsg: {
fieldRequired: '{{field}} est requis',
authRequired: 'Autorisation requise',

View File

@ -195,6 +195,30 @@ const translation = {
nodePaste: 'नोड चिपका हुआ',
nodeDescriptionChange: 'नोड का वर्णन बदल गया',
},
comments: {
panelTitle: 'टिप्पणी',
loading: 'लोड हो रहा है…',
placeholder: {
add: 'टिप्पणी जोड़ें',
reply: 'जवाब दें',
editReply: 'जवाब संपादित करें',
},
aria: {
deleteComment: 'टिप्पणी हटाएं',
resolveComment: 'टिप्पणी समाधान करें',
previousComment: 'पिछली टिप्पणी',
nextComment: 'अगली टिप्पणी',
closeComment: 'टिप्पणी बंद करें',
replyActions: 'जवाब क्रियाएं',
},
actions: {
editReply: 'जवाब संपादित करें',
deleteReply: 'जवाब हटाएं',
},
fallback: {
user: 'उपयोगकर्ता',
},
},
errorMsg: {
fieldRequired: '{{field}} आवश्यक है',
authRequired: 'प्राधिकरण आवश्यक है',

View File

@ -186,6 +186,30 @@ const translation = {
edgeDelete: 'Node terputus',
nodeChange: 'Node diubah',
},
comments: {
panelTitle: 'Komentar',
loading: 'Memuat…',
placeholder: {
add: 'Tambahkan komentar',
reply: 'Balas',
editReply: 'Edit balasan',
},
aria: {
deleteComment: 'Hapus komentar',
resolveComment: 'Selesaikan komentar',
previousComment: 'Komentar sebelumnya',
nextComment: 'Komentar berikutnya',
closeComment: 'Tutup komentar',
replyActions: 'Aksi balasan',
},
actions: {
editReply: 'Edit balasan',
deleteReply: 'Hapus balasan',
},
fallback: {
user: 'Pengguna',
},
},
errorMsg: {
fields: {
variable: 'Nama Variabel',

View File

@ -197,6 +197,30 @@ const translation = {
nodeDragStop: 'Nodo spostato',
nodeConnect: 'Nodo connesso',
},
comments: {
panelTitle: 'Commento',
loading: 'Caricamento…',
placeholder: {
add: 'Aggiungi un commento',
reply: 'Rispondi',
editReply: 'Modifica risposta',
},
aria: {
deleteComment: 'Elimina commento',
resolveComment: 'Risolvi commento',
previousComment: 'Commento precedente',
nextComment: 'Commento successivo',
closeComment: 'Chiudi commento',
replyActions: 'Azioni di risposta',
},
actions: {
editReply: 'Modifica risposta',
deleteReply: 'Elimina risposta',
},
fallback: {
user: 'Utente',
},
},
errorMsg: {
fieldRequired: '{{field}} è richiesto',
authRequired: 'È richiesta l\'autorizzazione',

View File

@ -196,6 +196,30 @@ const translation = {
noteDelete: '注釈が削除されました',
edgeDelete: 'ブロックの接続が解除されました',
},
comments: {
panelTitle: 'コメント',
loading: '読み込み中…',
placeholder: {
add: 'コメントを追加',
reply: '返信',
editReply: '返信を編集',
},
aria: {
deleteComment: 'コメントを削除',
resolveComment: 'コメントを解決',
previousComment: '前のコメント',
nextComment: '次のコメント',
closeComment: 'コメントを閉じる',
replyActions: '返信アクション',
},
actions: {
editReply: '返信を編集',
deleteReply: '返信を削除',
},
fallback: {
user: 'ユーザー',
},
},
errorMsg: {
fieldRequired: '{{field}} は必須です',
rerankModelRequired: 'Rerank モデルが設定されていません',

View File

@ -200,6 +200,30 @@ const translation = {
edgeDelete: '노드가 연결이 끊어졌습니다.',
nodeTitleChange: '노드 제목이 변경됨',
},
comments: {
panelTitle: '댓글',
loading: '불러오는 중…',
placeholder: {
add: '댓글 추가',
reply: '답글',
editReply: '답글 편집',
},
aria: {
deleteComment: '댓글 삭제',
resolveComment: '댓글 해결',
previousComment: '이전 댓글',
nextComment: '다음 댓글',
closeComment: '댓글 닫기',
replyActions: '답글 작업',
},
actions: {
editReply: '답글 편집',
deleteReply: '답글 삭제',
},
fallback: {
user: '사용자',
},
},
errorMsg: {
fieldRequired: '{{field}}가 필요합니다',
authRequired: '인증이 필요합니다',

View File

@ -198,6 +198,30 @@ const translation = {
noteDelete: '注释已删除',
edgeDelete: '块已断开连接',
},
comments: {
panelTitle: '评论',
loading: '加载中…',
placeholder: {
add: '添加评论',
reply: '回复',
editReply: '编辑回复',
},
aria: {
deleteComment: '删除评论',
resolveComment: '解决评论',
previousComment: '上一条评论',
nextComment: '下一条评论',
closeComment: '关闭评论',
replyActions: '回复操作',
},
actions: {
editReply: '编辑回复',
deleteReply: '删除回复',
},
fallback: {
user: '用户',
},
},
errorMsg: {
fieldRequired: '{{field}} 不能为空',
rerankModelRequired: '未配置 Rerank 模型',

View File

@ -192,6 +192,30 @@ const translation = {
edgeDelete: '區塊已斷開連接',
noteDelete: '註釋已刪除',
},
comments: {
panelTitle: '評論',
loading: '載入中…',
placeholder: {
add: '新增評論',
reply: '回覆',
editReply: '編輯回覆',
},
aria: {
deleteComment: '刪除評論',
resolveComment: '解決評論',
previousComment: '上一則評論',
nextComment: '下一則評論',
closeComment: '關閉評論',
replyActions: '回覆操作',
},
actions: {
editReply: '編輯回覆',
deleteReply: '刪除回覆',
},
fallback: {
user: '使用者',
},
},
errorMsg: {
fieldRequired: '{{field}} 不能為空',
authRequired: '請先授權',