Skip to content

feat(i18n): add multilingual interface localization (pt-BR, en, es, fr) - #121

Open
KalyelNLaurindo wants to merge 3 commits into
CopilotKit:mainfrom
KalyelNLaurindo:feat/pt-br-localization
Open

KalyelNLaurindo wants to merge 3 commits into
CopilotKit:mainfrom
KalyelNLaurindo:feat/pt-br-localization

Conversation

@KalyelNLaurindo

Copy link
Copy Markdown

Summary

Adds interface localization to OpenDots with out-of-the-box support for:

  • English (en) (default)
  • Brazilian Portuguese (pt-BR)
  • Spanish (es)
  • French (fr)

Workflow & Features

  • Language Selector: Added in Settings dialog alongside the Appearance theme controls.
  • Persistence & Detection: The chosen language is stored in localStorage (opendots-language). When no preference is saved, it automatically detects the browser language (navigator.language) and defaults cleanly to English (en).
  • Dynamic Updates: Changing language updates the interface reactively via useSyncExternalStore without requiring a full page refresh, and sets <html lang="...">.
  • Type-Safe Translations: Keys are grouped by interface domain under src/client/i18n/ with typed lookup via t(...) and placeholder interpolation.
  • Database Neutrality: Server database defaults remain canonical and language-agnostic; default starter space titles and descriptions are localized dynamically on the client.

Documentation

  • Updated docs/LOCALIZATION.md with guidelines for maintaining dictionaries and adding new translations.

Verification

  • npm run check-format (Prettier) passed
  • npm run lint (ESLint) passed
  • npm run typecheck (tsc) passed
  • npm test (Vitest: 302/302 tests passing across all 48 test suites)
  • npm run build (Vite + tsc server) passed

@paoValle paoValle left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the whole diff and ran the branch locally (Node 22.23.3): npm run typecheck, npm run lint, npm run check-format, npm test → 48 files / 302 tests pass. The structure is right: one typed key list, all four dictionaries carry the same 556 keys (checked them against each other), and the switcher is wired through WorkspaceDialog. Two changes go beyond translation and affect the three non-Portuguese languages.

1. Dates and times are now always formatted for Brazil. toLocaleDateString(undefined, …) / toLocaleTimeString() used the browser locale; 'pt-BR' is now fixed at TaskPresentation.tsx:18, SpaceLibrary.tsx:144, ComputerPanel.tsx:351 and ComputerPanel.tsx:664:

$ TZ=UTC node -e "const d=new Date('2026-10-05T18:12:30Z'); for (const l of ['en','es','fr','pt-BR']) console.log(l.padEnd(6),'|',d.toLocaleDateString(l,{month:'short',day:'numeric'}),'|',d.toLocaleTimeString(l))"
en     | Oct 5     | 6:12:30 PM
es     | 5 oct     | 18:12:30
fr     | 5 oct.    | 18:12:30
pt-BR  | 5 de out. | 18:12:30

Passing the selected language (useLanguage() inside the components) or keeping undefined is a one-line change per site. SpaceLibrary.tsx:141 still uses the browser locale for the tooltip, so today the tooltip and the date next to it can disagree.

2. relative() is hardcoded Portuguese. há ${minutes} min / há … h at TaskPresentation.tsx:15,17 show to every language and are the only strings left untranslated in that function; Intl.RelativeTimeFormat(language, { numeric: 'auto' }) replaces the three branches without new keys.

Smaller notes:

  • App.tsx:57-61: testing error instanceof TypeError before the generic Error branch also swallows programming TypeErrors, not only the fetch network one, so a real defect is reported as "could not connect". If the target is the network rejection, narrow the check; either way this is outside the PR's scope and easier to review on its own.
  • tests/telemetry.test.ts: three timeouts raised to 15 s. Unrelated to localization — worth a line in the body saying why, or splitting out.
  • .replace('{name}', dot.name) (ComputerToolCard.tsx:148,159,186, ComputerPanel.tsx:681, SpaceLibrary.tsx:64, SpaceNav.tsx:69): $&, $' or $1 inside a Space or Dot name is interpreted as a replacement pattern. .replace('{name}', () => dot.name) avoids that.

Nothing blocking beyond the locale/Portuguese strings above.

? t('tasks.now')
: minutes < 60
? `${minutes}m ago`
? `há ${minutes} min`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two relative branches are the only strings left in this function that are not localized: the English, Spanish and French UI show há 5 min / há 3 h. Intl.RelativeTimeFormat(language, { numeric: 'auto' }) would cover both without adding keys.

? `${Math.floor(minutes / 60)}h ago`
: new Date(value).toLocaleDateString();
? `há ${Math.floor(minutes / 60)} h`
: new Date(value).toLocaleDateString('pt-BR');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the browser locale (toLocaleDateString()) with a fixed pt-BR: TZ=UTC node -e "const d=new Date('2026-10-05T18:12:30Z'); console.log(d.toLocaleDateString('en',{month:'short',day:'numeric'}), '|', d.toLocaleDateString('pt-BR',{month:'short',day:'numeric'}))" prints Oct 5 | 5 de out., so for en/es/fr the date is now Brazilian. Pass the selected language, or keep undefined to stay on the browser locale.

Edited{' '}
{new Date(page.updatedAt).toLocaleDateString(undefined, {
{t('pages.edited')}{' '}
{new Date(page.updatedAt).toLocaleDateString('pt-BR', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as TaskPresentation.tsx:18: 'pt-BR' here forces the Brazilian format for every language (main used undefined, i.e. the browser locale). The tooltip on line 141 still uses the browser locale, so the two can disagree.

{new Date(screen.capturedAt).toLocaleTimeString()}. Screen
updates while this panel is open.
{t('computer.refreshed')}{' '}
{new Date(screen.capturedAt).toLocaleTimeString('pt-BR')}.{' '}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue: toLocaleTimeString('pt-BR') drops the browser locale, so en/es/fr readers see Brazilian time formatting (24 h) instead of their own.

? t('computer.auditFailed')
: t('computer.auditPending')}{' '}
·{' '}
{new Date(entry.createdAt).toLocaleTimeString('pt-BR')}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fixed 'pt-BR' as line 351.

Comment thread src/client/App.tsx
status: error instanceof ApiError ? error.status : undefined,
message: error instanceof Error ? error.message : fallback,
message:
error instanceof TypeError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instanceof TypeError here catches every programming TypeError too, not just the fetch network rejection, so a real defect is now reported to the user as "could not connect". If the network rejection is the target, narrow this check (or keep the previous behaviour); it is also unrelated to localization and would be easier to review on its own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants