Skip to content

Feature/i18n - #138

Merged
jherforth merged 8 commits into
mainfrom
feature/i18n
Aug 17, 2026
Merged

Feature/i18n#138
jherforth merged 8 commits into
mainfrom
feature/i18n

Conversation

@jherforth

Copy link
Copy Markdown
Owner

No description provided.

jherforth and others added 8 commits August 13, 2026 16:07
CalendarWidget imported Calendar and built momentLocalizer(moment) but never
used either — the widget draws its own month, week, and day views through
MonthDayCell and getNext7Days. The import, the localizer, and the library's
stylesheet were pure weight, and the only other `Calendar` matches in the file
are unrelated identifiers (editingCalendar, handleSaveCalendar).

Found while sizing i18n (#137): a react-big-calendar instance would have needed
its own translated `messages` object, and the momentLocalizer is listed in #120
as a blocker for replacing moment. Neither is real.

  CalendarWidget chunk   237 KB -> 110 KB   (-54%)
  total client JS      1,803 KB -> 1,676 KB (-127 KB)
  bundled CSS             18 KB ->   7 KB   (react-big-calendar.css gone)
  node_modules           17 packages removed

Verified: client 79/79, build clean, and a browser check confirming the
calendar still renders its grid, weekday headers, and 31 demo event chips with
zero console errors, including after toggling between month and week views.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sh (#137)

First phase of multi-language support. Everything here is complete and
working; the remaining widgets still render English and are listed below.

Infrastructure
- i18next + react-i18next + language detection, set up in client/src/i18n.
  English is bundled (it is the fallback); other languages and their date
  locales load on demand, so an English household downloads nothing extra.
- Language is per display, stored in localStorage like theme and screensaver
  settings, so a kitchen tablet and a phone can differ. A display that has
  never chosen follows the `default_language` server setting if one is set.
- Missing keys log a warning in development naming the namespace and key.

Date seam (client/src/utils/dateUtils.js)
- Every user-visible date now formats through one module, so changing language
  re-formats the whole UI from a single place, and replacing moment later
  (#120) touches this file rather than every call site.
- Display helpers are separated from machine ones. toDateKey and friends
  deliberately stay unlocalized: those YYYY-MM-DD strings are map keys, API
  parameters, and event-matching identifiers, and localizing them would break
  calendar lookup silently.

Explicit week-start setting
- Chosen deliberately over deriving it from the language: households disagree
  with their locale often enough, and it reflows the whole calendar grid.

Translated so far
- Chore widget end to end: chores, bonus chores, prize store, split spending,
  quick spend, transfer, snooze, and the redemption celebration.
- Admin Panel language and week-start controls.
- Day-of-week checkboxes show translated labels while still storing the
  English key that crontab conversion and the API depend on.

Tooling
- npm run check:i18n fails if any English key is missing from another
  language, wired into CI for the frontend job.
- docs/guides/translations.md covers adding a language, adding strings, and
  the traps (machine formats, stored values, user data, server errors).

Not yet converted, still English in every language: the rest of the Admin
Panel, and the Calendar, Weather, and Photos widgets. Each is a self-contained
follow-up — add its namespace to NAMESPACES once its locale files exist.

Verified: client 79/79, build clean, translation check 115/115 keys at 100%,
and an 8/8 browser pass covering English default, switching to Spanish,
no leaked keys, persistence across reload, the html lang attribute, and zero
page errors. Spanish ships as separate 1.4-2.8 KB chunks per namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Weather widget
- All UI strings move to a weather namespace: the settings dialog, air quality
  levels, empty state, and the location errors.
- The three OpenWeatherMap URLs now pass lang. OWM localizes its own condition
  text ("clear sky", "few clouds"), so those strings translate server-side for
  free instead of needing a lookup table here. The demo snapshot stays English.

Tab icon picker
- Icon labels were stored alongside the icon list as English strings. The list
  now carries only the stable `name` identifier and the label is looked up at
  render, so what gets persisted on a tab is unchanged.
- The dialog's title and save-button defaults resolve from translations rather
  than inline English, so a caller passing nothing still gets localized copy.

Plugins
- Plugin iframes already received theme and device; lang now rides the same
  channel. A plugin that ships translations can follow the display's language,
  and one that ignores the parameter is unaffected.

Verified: client 79/79, build clean, translation check 172/172 keys at 100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves the photo widget's strings into a photos namespace: the settings
popover (sources, per-view count, size, transition, speed), the source
dialog for both Immich and HomeGlow Photos, the empty states, and the
confirm/error messages.

The size, speed, and transition options keep their stored values (880, 3000,
"fade") and translate only the visible label, so existing device settings
keep working untouched.

Verified: client 79/79, build clean, translation check 211/211 keys at 100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…locales

Calendar widget
- All 87 strings move to a calendar namespace: the toolbar, day dialog, event
  editor, source management for ICS/CalDAV/Apple/Google, and the whole
  settings panel.
- Weekday names in the grid header and in the week/month start pickers are now
  localized. The stored values stay the English keys ('monday'), so existing
  tab settings and the crontab conversion are untouched.
- Display dates route through the date seam; the machine formats it uses as
  map keys and API parameters (YYYY-MM-DD and friends) deliberately do not.

Dates now use Intl.DateTimeFormat
The seam originally drove moment's own locale, which did not work: Vite
pre-bundles moment as an optimized dependency, so `import('moment/locale/es')`
registered the locale on a different module instance than the one formatting,
and the UI silently stayed in English while `moment_locale_es.js` loaded fine.
Rather than fight the bundler, display formatting now uses the platform's
Intl.DateTimeFormat. That fixes it, drops a 61 KB locale chunk, gives every
language the browser knows for free, and formats patterns per locale rather
than translating names inside an English-shaped pattern — Spanish renders
"agosto de 2026", not "agosto 2026".

Formatters are cached per locale and options, since the month grid formats
hundreds of cells per render.

Dropped the global week-start setting added earlier in this branch
The calendar has had per-tab "Week View Start" and "Month View Start" controls
since #127. A second, global control would have fought them. The existing ones
are localized instead, which is what the setting was for.

Verified: client 79/79, build clean, translation check 278/278 keys at 100%,
and a browser pass in both languages — English shows "August 2026" with
Sun/Mon/Tue, Spanish shows "agosto de 2026" with dom/lun/mar/mié, both render
all 31 demo event chips with no page errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the chore-definitions table, the schedule list and its filters, and the
schedule editor: recurrence modes, sleep interval, due date and time, sound
and reminder settings, transferable/snooze toggles, and both delete dialogs.

Two constants moved off hardcoded English:
- Day checkboxes now take their labels from the locale (index 0 = Sunday, so
  the crontab values are unchanged).
- Crontab presets carry a key instead of a label; the value stays the crontab
  expression and only the visible text translates.

Not yet converted in this file: a dozen short table headers and buttons
(Title, Actions, Edit, Delete, Duplicate, Refresh, and similar) plus the two
composed strings for "Once Completed {interval}" and "Next occurrence: {when}".
They still render English in every language.

Also fixes six props this pass initially broke: the bulk replacement rewrote
plain-text values that turned out to sit inside quoted JSX props, producing
title="{t('key')}" — a literal string with braces rather than an expression.
All six are unwrapped, and the check for that pattern now reports zero.

Verified: client 79/79, build clean, translation check 354/354 keys at 100%,
and a byte-level check that the diff introduces no mangled characters (the
en-dash in "Weekdays (Mon–Fri)" and the Spanish accents both survive).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the last of the file: table headers (including the data-label
attributes CSS renders as headings on mobile), the dialog buttons, the
recurrence-mode radios and interval units, and the two composed strings for
'Once Completed (interval)' and 'Next occurrence: when', which now interpolate
instead of concatenating.

The delete-chore dialog's warning takes its schedule count as an interpolated
value, and its prompt is composed so the chore's own title stays bold and
untranslated.

ChoreSchedulesTab now reports zero remaining hardcoded strings.

Verified: client 79/79, build clean, translation check 359/359 keys at 100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last and largest surface: 239 strings across every tab — widgets and
plugin management, tabs, devices, colours, screensaver, vacation mode, auto
dark mode, users, chore rewards and sounds, prizes, security, connections, and
about — plus all the toast and confirm messages.

Stored values are untouched throughout. Refresh intervals keep their
millisecond values, prize costs their numbers, plugin ids their strings; only
the visible label translates. The data-label attributes get translated too,
since CSS renders them as the row headings on mobile.

Two composed strings now interpolate rather than concatenate: the prize cost
line (which varies on whether the prize is repeatable) and the screensaver's
tab count.

With this, every user-facing surface in the app is translated. Server error
strings and console logging stay English on purpose — the first are
diagnostics rather than user copy, the second keeps issue reports searchable.

The translations guide now records the two traps this work hit: PowerShell's
Set-Content mangles emoji (use Node for scripted edits), and a bulk replace
can produce prop="{t('key')}" — a literal string with braces that builds fine
and renders the key to the user. Both have a grep to catch them.

Verified: client 79/79, build clean, translation check 588/588 keys at 100%,
and a browser pass walking all eight admin tabs in both languages — no raw
keys leak, Spanish headings render, no English headings remain. Two React
prop warnings appear in the console; they are pre-existing, confirmed by
stashing this work and reproducing them at the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jherforth jherforth linked an issue Aug 17, 2026 that may be closed by this pull request
@jherforth
jherforth merged commit 320f5dc into main Aug 17, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Aug 17, 2026
@jherforth
jherforth deleted the feature/i18n branch August 17, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

multi language support

1 participant