Repository navigation
feat(i18n): add multilingual interface localization (pt-BR, en, es, fr) - #121
KalyelNLaurindo wants to merge 3 commits into
Conversation
paoValle
left a comment
There was a problem hiding this comment.
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: testingerror instanceof TypeErrorbefore the genericErrorbranch also swallows programming TypeErrors, not only thefetchnetwork 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$1inside 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` |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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', { |
There was a problem hiding this comment.
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')}.{' '} |
There was a problem hiding this comment.
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')} |
| status: error instanceof ApiError ? error.status : undefined, | ||
| message: error instanceof Error ? error.message : fallback, | ||
| message: | ||
| error instanceof TypeError |
There was a problem hiding this comment.
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.
Summary
Adds interface localization to OpenDots with out-of-the-box support for:
en) (default)pt-BR)es)fr)Workflow & Features
localStorage(opendots-language). When no preference is saved, it automatically detects the browser language (navigator.language) and defaults cleanly to English (en).useSyncExternalStorewithout requiring a full page refresh, and sets<html lang="...">.src/client/i18n/with typed lookup viat(...)and placeholder interpolation.Documentation
docs/LOCALIZATION.mdwith guidelines for maintaining dictionaries and adding new translations.Verification
npm run check-format(Prettier) passednpm run lint(ESLint) passednpm run typecheck(tsc) passednpm test(Vitest: 302/302 tests passing across all 48 test suites)npm run build(Vite + tsc server) passed