From e1a044354e13916f6278ed04f42b18c22e0c725d Mon Sep 17 00:00:00 2001 From: device177 Date: Fri, 6 Nov 2026 19:30:26 +0300 Subject: [PATCH] feat(client): add Russian interface localization Add a small dependency-free i18n layer (src/client/i18n.ts) and a Russian dictionary (src/client/locales/ru.ts). UI strings are wrapped in t()/tp(), keyed by the English source text, so English remains the default and unchanged. The locale follows the browser language and can be switched in Settings > Appearance; the choice is stored in localStorage. Server-originated messages and agent-facing prompts stay in English. --- docs/SETUP.md | 4 + src/client/App.tsx | 210 +++++---- src/client/CallView.tsx | 45 +- src/client/Chat.tsx | 73 +-- src/client/ChatTranscript.tsx | 13 +- src/client/ComputerPanel.tsx | 182 ++++---- src/client/ComputerToolCard.tsx | 34 +- src/client/ConnectionActionCard.tsx | 64 +-- src/client/ConnectionsSection.tsx | 45 +- src/client/Mascot.tsx | 6 +- src/client/PageConversation.tsx | 36 +- src/client/PageDocument.tsx | 82 ++-- src/client/PageOutline.tsx | 9 +- src/client/PageReviewCard.tsx | 54 +-- src/client/ResultPane.tsx | 43 +- src/client/SpaceLibrary.tsx | 52 +-- src/client/SpaceNav.tsx | 9 +- src/client/SpaceWorkspace.tsx | 13 +- src/client/TaskActions.tsx | 17 +- src/client/TaskPresentation.tsx | 20 +- src/client/ThreadList.tsx | 13 +- src/client/WorkspaceDialog.tsx | 186 +++++--- src/client/api.ts | 10 +- src/client/editor/DocumentMenu.tsx | 5 +- src/client/editor/RichEditor.tsx | 56 +-- src/client/editor/autosave.ts | 19 +- src/client/editor/markdown.ts | 12 +- src/client/editor/slash-commands.ts | 47 +- src/client/i18n.ts | 72 +++ src/client/locales/ru.ts | 675 ++++++++++++++++++++++++++++ src/client/page-context.ts | 3 +- src/client/page-review-decision.ts | 5 +- src/client/poll-notice.ts | 5 +- src/client/useVoice.ts | 21 +- tests/i18n.test.ts | 57 +++ 35 files changed, 1626 insertions(+), 571 deletions(-) create mode 100644 src/client/i18n.ts create mode 100644 src/client/locales/ru.ts create mode 100644 tests/i18n.test.ts diff --git a/docs/SETUP.md b/docs/SETUP.md index 80b0c40d..ee513c45 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -85,6 +85,10 @@ A non-empty `INTELLIGENCE_GATEWAY_WS_URL` takes precedence over `INTELLIGENCE_WS The model environment variable names follow the configured provider adapter. Provider credentials belong in `.env`, not client-side variables or source code. Conversation history lives in the configured Intelligence project; copying the SQLite file alone does not back up that history. +## Interface language + +The interface is available in English and Russian. It follows the browser language and can be changed in Settings under Appearance; the choice is saved in this browser. Translations live in `src/client/locales/`. To add a language, add a dictionary keyed by the English source strings and register it in `src/client/i18n.ts`. Messages that come from the server (for example API errors) stay in English. + ## Pages and page conversations Select a Space to open its page library. Search for a document, switch between grid and list views, or create a new page. The visual editor supports formatting, headings, lists, checklists, tables, and slash commands. Use `/` to insert a block and Cmd/Ctrl+S to save immediately. Pages autosave after editing pauses; the save status tells you whether changes reached the server. diff --git a/src/client/App.tsx b/src/client/App.tsx index e5cc6466..33e2c710 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -48,6 +48,7 @@ import { TaskRow } from './TaskPresentation'; import { TaskActions } from './TaskActions'; import { WorkspaceDialog, type Dialog } from './WorkspaceDialog'; import { submitComposerOnEnter } from './chat-composer'; +import { t, tp } from './i18n'; function describeFailure(error: unknown, fallback: string) { return { @@ -72,7 +73,10 @@ export function App() { dirtyPage.current = value; }, []); const setView = (next: typeof view) => { - if (dirtyPage.current && !window.confirm('Leave your unsaved page draft?')) + if ( + dirtyPage.current && + !window.confirm(t('Leave your unsaved page draft?')) + ) return; dirtyPage.current = false; if (next !== 'space') @@ -89,7 +93,7 @@ export function App() { if (dirtyPage.current && location.hash === acceptedHash) return; if ( dirtyPage.current && - !window.confirm('Leave your unsaved page draft?') + !window.confirm(t('Leave your unsaved page draft?')) ) { history.replaceState(null, '', acceptedHash || location.pathname); return; @@ -147,7 +151,7 @@ export function App() { setNotices((current) => applyRefreshResult( current, - describeFailure(e, 'Could not connect to the server.'), + describeFailure(e, t('Could not connect to the server.')), ), ); } @@ -173,7 +177,7 @@ export function App() { setNotices((current) => applyCaptureResult( current, - describeFailure(e, 'Could not connect to the server.'), + describeFailure(e, t('Could not connect to the server.')), ), ); }); @@ -193,7 +197,7 @@ export function App() { setTaskDetail(await api(`/tasks/${taskDetail.task.id}`)); return true; } catch (e) { - setError(e instanceof Error ? e.message : 'Could not save.'); + setError(e instanceof Error ? e.message : t('Could not save.')); return false; } }; @@ -231,7 +235,7 @@ export function App() { try { const next = await api('/conversations', 'POST', { dotId: dot.id, - title: text?.slice(0, 80) || 'A new thought', + title: text?.slice(0, 80) || t('A new thought'), }); await refresh(); setSelectedThread(next.id); @@ -241,7 +245,9 @@ export function App() { setMobile(false); } catch (e) { setError( - e instanceof Error ? e.message : 'Could not create the conversation.', + e instanceof Error + ? e.message + : t('Could not create the conversation.'), ); } finally { setBusy(false); @@ -251,9 +257,11 @@ export function App() { return (
-

Your own little corner.

+

{t('Your own little corner.')}

- Enter the owner access token configured on this template’s server. + {t( + 'Enter the owner access token configured on this template’s server.', + )}

{ @@ -267,48 +275,50 @@ export function App() { setError( err instanceof Error ? err.message - : 'Access token was not accepted.', + : t('Access token was not accepted.'), ); } }} > setAuth(e.target.value)} required /> - +
{error && (

{error}

)} -

The token stays in this tab’s session storage.

+

+ {t('The token stays in this tab’s session storage.')} +

); if (!state || !workspace || !dot) return (
-

Finding your dots…

+

{t('Finding your dots…')}

{error && ( <>

{error}

- + )}
); const content = (
-
- {configured ? 'SELF-HOSTED' : 'SETUP REQUIRED'} + {configured ? t('SELF-HOSTED') : t('SETUP REQUIRED')}
{!configured && ( @@ -660,20 +681,22 @@ export function App() {
- Connect your Dot + {t('Connect your Dot')}

- Connect your model and conversation service in - Settings to start chatting. Your Spaces and Dot - preferences are ready to use. + {t( + 'Connect your model and conversation service in Settings to start chatting. Your Spaces and Dot preferences are ready to use.', + )}

- Setup and usage metadata is collected by default.{' '} + {t( + 'Setup and usage metadata is collected by default.', + )}{' '} - Tracking and opt-out details + {t('Tracking and opt-out details')}

- Open the setup guide + {t('Open the setup guide')}
@@ -694,11 +717,11 @@ export function App() { }} >