diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index a2ff5f7..db11edf 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,6 +1,6 @@ "use strict"; require("electron"); -require("./main-Bm7LE7mJ.js"); +require("./main-EIYRjWjp.js"); require("electron-squirrel-startup"); require("electron-log"); require("bytenode"); diff --git a/src/components/channels/ChannelConfigFields.tsx b/src/components/channels/ChannelConfigFields.tsx index 79b9144..a734f98 100644 --- a/src/components/channels/ChannelConfigFields.tsx +++ b/src/components/channels/ChannelConfigFields.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import { useI18n } from '../../i18n'; import type { ChannelConfigFieldMeta, ChannelConfigFieldValueMap } from '../../lib/channel-types'; import ChannelTokenField from './ChannelTokenField'; @@ -145,10 +146,16 @@ export default function ChannelConfigFields({ showSecretLabel, hideSecretLabel, }: ChannelConfigFieldsProps) { + const { t } = useI18n(); + const resolvedEmptyStateText = emptyStateText ?? t('channels.modal.noAdditionalFields'); + const resolvedEnvVarLabel = envVarLabel ?? t('channels.modal.envVar'); + const resolvedShowSecretLabel = showSecretLabel ?? t('channels.modal.showSecret'); + const resolvedHideSecretLabel = hideSecretLabel ?? t('channels.modal.hideSecret'); + if (fields.length === 0) { return (
- {emptyStateText ?? 'No additional fields are required for this channel.'} + {resolvedEmptyStateText}
); } @@ -164,9 +171,9 @@ export default function ChannelConfigFields({ disabled, showSecretMap, onToggleSecret, - envVarLabel, - showSecretLabel, - hideSecretLabel, + envVarLabel: resolvedEnvVarLabel, + showSecretLabel: resolvedShowSecretLabel, + hideSecretLabel: resolvedHideSecretLabel, })} ))} diff --git a/src/components/channels/ChannelConfigModal.tsx b/src/components/channels/ChannelConfigModal.tsx index 7c5aa79..5daf431 100644 --- a/src/components/channels/ChannelConfigModal.tsx +++ b/src/components/channels/ChannelConfigModal.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AlertCircle, CheckCircle2, X } from 'lucide-react'; import { useI18n } from '../../i18n'; -import type { ChannelConfigFieldMeta, ChannelConfigFieldValueMap } from '../../lib/channel-types'; -import { getChannelMeta, type ChannelMeta } from '../../lib/channel-meta'; +import type { ChannelConfigFieldValueMap } from '../../lib/channel-types'; +import { getChannelMeta, localizeChannelMeta } from '../../lib/channel-meta'; import ChannelConfigActions from './ChannelConfigActions'; import ChannelConfigFields from './ChannelConfigFields'; import ChannelInstructionsPanel from './ChannelInstructionsPanel'; @@ -51,44 +51,6 @@ export type ChannelConfigModalProps = { validatingLabel?: string; }; -function withTranslatedMeta( - meta: ChannelMeta, - translate: (path: string, fallback: string) => string, -): ChannelMeta { - return { - ...meta, - name: translate(`channels.meta.${meta.type}.name`, meta.name), - description: translate(`channels.meta.${meta.type}.description`, meta.description), - docsUrl: meta.docsUrl - ? translate(`channels.meta.${meta.type}.docsUrl`, meta.docsUrl) - : meta.docsUrl, - instructions: meta.instructions.map((instruction, index) => ( - translate(`channels.meta.${meta.type}.instructions.${index}`, instruction) - )), - configFields: meta.configFields.map((field) => withTranslatedField(meta.type, field, translate)), - }; -} - -function withTranslatedField( - channelType: string, - field: ChannelConfigFieldMeta, - translate: (path: string, fallback: string) => string, -): ChannelConfigFieldMeta { - return { - ...field, - label: translate(`channels.meta.${channelType}.fields.${field.key}.label`, field.label), - placeholder: field.placeholder - ? translate(`channels.meta.${channelType}.fields.${field.key}.placeholder`, field.placeholder) - : field.placeholder, - description: field.description - ? translate(`channels.meta.${channelType}.fields.${field.key}.description`, field.description) - : field.description, - docsUrl: field.docsUrl - ? translate(`channels.meta.${channelType}.fields.${field.key}.docsUrl`, field.docsUrl) - : field.docsUrl, - }; -} - export default function ChannelConfigModal({ open, selectedChannelType, @@ -107,16 +69,16 @@ export default function ChannelConfigModal({ validateLabel, validatingLabel, }: ChannelConfigModalProps) { - const { t, hasMessage } = useI18n(); + const { t } = useI18n(); const [showSecrets, setShowSecrets] = useState>({}); const firstFieldRef = useRef(null); const translate = useCallback((path: string, fallback: string) => ( - hasMessage(path) ? t(path) : fallback - ), [hasMessage, t]); + t(path, undefined, fallback) + ), [t]); const activeMeta = useMemo( - () => withTranslatedMeta(getChannelMeta(selectedChannelType), translate), + () => localizeChannelMeta(getChannelMeta(selectedChannelType), translate), [selectedChannelType, translate], ); const supportedFields = activeMeta.configFields.filter((field) => field.key !== 'accountId'); diff --git a/src/components/channels/ChannelTokenField.tsx b/src/components/channels/ChannelTokenField.tsx index c81b783..c5a1a51 100644 --- a/src/components/channels/ChannelTokenField.tsx +++ b/src/components/channels/ChannelTokenField.tsx @@ -1,4 +1,5 @@ import { Eye, EyeOff } from 'lucide-react'; +import { useI18n } from '../../i18n'; type ChannelTokenFieldProps = { label: string; @@ -27,7 +28,10 @@ export default function ChannelTokenField({ hideSecretLabel, required, }: ChannelTokenFieldProps) { + const { t } = useI18n(); const isSecretField = typeof showSecret === 'boolean' && typeof onToggleSecret === 'function'; + const resolvedShowSecretLabel = showSecretLabel ?? t('channels.modal.showSecret'); + const resolvedHideSecretLabel = hideSecretLabel ?? t('channels.modal.hideSecret'); return (
@@ -55,7 +59,7 @@ export default function ChannelTokenField({ onClick={onToggleSecret} disabled={disabled} className="inline-flex h-[56px] w-[56px] shrink-0 items-center justify-center rounded-[18px] border border-black/10 bg-[#fbf8f1] text-[#667085] transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-[#1a1a1e] dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-100" - aria-label={showSecret ? (hideSecretLabel ?? 'Hide secret') : (showSecretLabel ?? 'Show secret')} + aria-label={showSecret ? resolvedHideSecretLabel : resolvedShowSecretLabel} > {showSecret ? : } diff --git a/src/components/channels/ChannelTypeSelector.tsx b/src/components/channels/ChannelTypeSelector.tsx index 6c06f00..aa9cd75 100644 --- a/src/components/channels/ChannelTypeSelector.tsx +++ b/src/components/channels/ChannelTypeSelector.tsx @@ -1,5 +1,7 @@ +import { useCallback, useMemo } from 'react'; import { BadgeCheck, ChevronDown, LayoutGrid, Plug, QrCode, Webhook } from 'lucide-react'; -import type { ChannelMeta } from '../../lib/channel-meta'; +import { useI18n } from '../../i18n'; +import { localizeChannelMeta, type ChannelMeta } from '../../lib/channel-meta'; type ChannelTypeSelectorProps = { label: string; @@ -31,9 +33,29 @@ export default function ChannelTypeSelector({ disabled, helpText, }: ChannelTypeSelectorProps) { - const selected = options.find((item) => item.type === value); + const { t } = useI18n(); + const translate = useCallback((path: string, fallback: string) => ( + t(path, undefined, fallback) + ), [t]); + + const localizedOptions = useMemo( + () => options.map((option) => localizeChannelMeta(option, translate)), + [options, translate], + ); + + const selected = localizedOptions.find((item) => item.type === value); const ConnectionIcon = getConnectionIcon(selected?.connectionType ?? helpText); + const connectionLabel = selected?.connectionType === 'qr' + ? t('channels.connectionType.qr') + : selected?.connectionType === 'webhook' + ? t('channels.connectionType.webhook') + : selected?.connectionType === 'plugin' + ? t('channels.page.pluginBadge') + : selected?.connectionType + ? t('channels.connectionType.token') + : helpText; + return (
@@ -44,7 +66,7 @@ export default function ChannelTypeSelector({
- {selected?.connectionType ?? helpText} + {connectionLabel}
@@ -56,7 +78,7 @@ export default function ChannelTypeSelector({ onChange={(event) => onChange(event.target.value)} className="h-[44px] w-full appearance-none rounded-[14px] border border-black/10 bg-white px-3 pr-10 text-[13px] text-foreground outline-none transition-colors focus:border-blue-500 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-[#101013] dark:text-gray-100" > - {options.map((option) => ( + {localizedOptions.map((option) => ( diff --git a/src/components/chat/ChatComposer.tsx b/src/components/chat/ChatComposer.tsx index 5b19570..726a7b1 100644 --- a/src/components/chat/ChatComposer.tsx +++ b/src/components/chat/ChatComposer.tsx @@ -1,5 +1,5 @@ import { useRef } from 'react'; -import { SendHorizontal, Square, X, Paperclip, FileText, Film, Music, FileArchive, File, Loader2, AtSign } from 'lucide-react'; +import { Paperclip, SendHorizontal, Square } from 'lucide-react'; import type { AttachedFileMeta } from '../../shared/chat-model'; import { useI18n } from '../../i18n'; @@ -63,7 +63,7 @@ export default function ChatComposer({ }} /> {attachments.length > 0 ? ( -
+
{attachments.map((attachment, index) => (
onRemoveAttachment(index)} + aria-label={t('conversation.composer.removeAttachment', { name: attachment.fileName })} > x @@ -81,7 +82,7 @@ export default function ChatComposer({ ))}
) : null} -
+
fileInputRef.current?.click()} + aria-label={t('conversation.composer.attachAria')} + title={t('conversation.composer.attachAria')} > @@ -106,6 +109,8 @@ export default function ChatComposer({ type="button" className="rounded-lg border border-[#E5E8EE] p-3 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300" onClick={isSending ? onStop : onSend} + aria-label={isSending ? t('conversation.composer.stopAria') : t('conversation.composer.sendAria')} + title={isSending ? t('conversation.composer.stopAria') : t('conversation.composer.sendAria')} > {isSending ? : } diff --git a/src/components/chat/ChatHistoryPanel.tsx b/src/components/chat/ChatHistoryPanel.tsx index c767521..e56710c 100644 --- a/src/components/chat/ChatHistoryPanel.tsx +++ b/src/components/chat/ChatHistoryPanel.tsx @@ -1,17 +1,17 @@ import { memo, useEffect, useRef, useState } from 'react'; -import type { ChatHistoryBucket } from './types'; import { ChevronDown, LoaderCircle, - Plus, MoreHorizontal, PanelLeftClose, PanelLeftOpen, PencilLine, + Plus, Trash2, } from 'lucide-react'; - +import type { ChatHistoryBucket } from './types'; import blueLogo from '../../assets/images/login/blue_logo.png'; +import { useI18n } from '../../i18n'; type ChatHistoryPanelProps = { buckets: ChatHistoryBucket[]; @@ -40,6 +40,7 @@ function ChatHistoryPanel({ onRenameConversation, onDeleteConversation, }: ChatHistoryPanelProps) { + const { t } = useI18n(); const panelRef = useRef(null); const [collapsedBuckets, setCollapsedBuckets] = useState>({}); const [menuState, setMenuState] = useState(null); @@ -104,6 +105,9 @@ function ChatHistoryPanel({ }, [menuState]); const panelWidthClass = isCompact ? 'md:w-[70px] lg:w-[70px]' : 'md:w-[240px] lg:w-[252px]'; + const toggleSidebarLabel = isCompact + ? t('conversation.historyPanel.expandSidebar') + : t('conversation.historyPanel.collapseSidebar'); return (