diff --git a/docs/LOCALIZATION.md b/docs/LOCALIZATION.md new file mode 100644 index 00000000..74b787b7 --- /dev/null +++ b/docs/LOCALIZATION.md @@ -0,0 +1,5 @@ +# Interface localization + +The interface ships in Brazilian Portuguese (`pt-BR`), English (`en`), Spanish (`es`), and French (`fr`). Keep user-facing strings in the matching locale files under `src/client/i18n/`, and access them through the typed `t()` helper in `src/client/i18n/index.ts`. + +Translation keys are grouped by interface area. Keep all dictionaries aligned when adding keys, preserve product names and technical identifiers, and keep interpolation placeholders such as `{name}` identical. The selected language is saved in the browser. diff --git a/src/client/App.tsx b/src/client/App.tsx index e5cc6466..3a9a89ed 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -48,16 +48,23 @@ import { TaskRow } from './TaskPresentation'; import { TaskActions } from './TaskActions'; import { WorkspaceDialog, type Dialog } from './WorkspaceDialog'; import { submitComposerOnEnter } from './chat-composer'; +import { localizedSlackStatus, t, useLanguage } from './i18n'; function describeFailure(error: unknown, fallback: string) { return { ok: false as const, status: error instanceof ApiError ? error.status : undefined, - message: error instanceof Error ? error.message : fallback, + message: + error instanceof TypeError + ? fallback + : error instanceof Error + ? error.message + : fallback, }; } export function App() { + const language = useLanguage(); const [state, setState] = useState(); const [workspace, setWorkspace] = useState(); const [selectedDot, setSelectedDot] = useState(''); @@ -72,7 +79,7 @@ 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('workspace.unsavedPage'))) return; dirtyPage.current = false; if (next !== 'space') @@ -87,10 +94,7 @@ export function App() { ); if (!match) return; if (dirtyPage.current && location.hash === acceptedHash) return; - if ( - dirtyPage.current && - !window.confirm('Leave your unsaved page draft?') - ) { + if (dirtyPage.current && !window.confirm(t('workspace.unsavedPage'))) { 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('notices.serverConnection')), ), ); } @@ -173,7 +177,7 @@ export function App() { setNotices((current) => applyCaptureResult( current, - describeFailure(e, 'Could not connect to the server.'), + describeFailure(e, t('notices.serverConnection')), ), ); }); @@ -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('app.saveError')); 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('workspace.newIdea'), }); await refresh(); setSelectedThread(next.id); @@ -240,9 +244,7 @@ export function App() { setView('chat'); setMobile(false); } catch (e) { - setError( - e instanceof Error ? e.message : 'Could not create the conversation.', - ); + setError(e instanceof Error ? e.message : t('app.conversationError')); } finally { setBusy(false); } @@ -251,10 +253,8 @@ export function App() { return (
-

Your own little corner.

-

- Enter the owner access token configured on this template’s server. -

+

{t('workspace.yourCorner')}

+

{t('workspace.ownerToken')}

{ e.preventDefault(); @@ -264,51 +264,52 @@ export function App() { setError(''); await refresh(); } catch (err) { - setError( - err instanceof Error - ? err.message - : 'Access token was not accepted.', - ); + setError(err instanceof Error ? err.message : t('app.badToken')); } }} > setAuth(e.target.value)} required /> - +
{error && (

{error}

)} -

The token stays in this tab’s session storage.

+

{t('workspace.tokenSession')}

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

Finding your dots…

+

{t('workspace.findingAgents')}

{error && ( <>

{error}

- + )}
); const content = ( -
-
{!configured && ( @@ -660,20 +666,16 @@ export function App() {
- Connect your Dot + {t('app.connectAgent')} +

{t('app.setupIncomplete')}

- 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('app.telemetryNotice')}{' '} - Tracking and opt-out details + {t('app.tracking')}

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