Follow-up to #285. Three corrections that rode in with the Japanese locale.
1. The ja key-parity check failed the build whenever en.js had a key ja.js
lacked. i18n.js lookup() is already
`registry[lang]?.[key] ?? fallback[key] ?? key`, so an untranslated string
renders in English and nothing is broken by a gap - the only effect was
that adding any English string blocked CI until a Japanese translation
existed. It also singled out one locale; es/fr/de/pt/hi/it were never held
to it, and hi.js is a deliberate skeleton whose own header explains that
every key falls back to English on purpose.
Replaced with two checks over EVERY locale: a locale may not define a key
that English does not (dead weight after a rename, and fixable by whoever
touched the file, whatever language they speak), and coverage is printed
rather than gated. Help tips still have to exist everywhere - that test is
unchanged and still fails.
Current coverage: ja 100%, es 65.5%, fr/de/pt 63.7%, it 59.5%, hi 0%.
2. Applying the strict half to all locales immediately found
add_display.smart_tv_note living in fr, pt, it and de but not in en.js and
referenced by no view - a string dropped from English that left four
translations behind. Removed.
3. The new timezone test restored process.env.TZ by assigning the saved value
back. When TZ was not set to begin with - which is the case in CI - that
assigns undefined, which writes the STRING "undefined"; Node cannot parse
it and silently falls back to UTC for the rest of the process. Every test
after it in that file is date arithmetic. It now deletes the key when it
was previously unset.
4. package-lock.json removed from .gitignore. server/package-lock.json is
tracked, so the rule was inert, but it would silently prevent a future
lockfile and works against the SBOM and reproducible-install setup added
in #282.
Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56
Co-authored-by: Dan Walters <dan.walters@bytetinker.net>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A title= is a tooltip the user reads and an aria-label is what a screen reader
says, but fourteen of them were hardcoded English. They were invisible to the
key checks added earlier precisely because they never call t() — so a French
user hovering the only route to workspace members read "Manage members", and a
German screen reader announced every modal's close button as "Close".
The user-visible ones matter most: the workspace switcher's Manage members and
Rename, the video wall's rename and remove, and the dashboard's select-for-wall.
All are translated into every active locale, along with the close buttons.
A test now rejects a capitalised literal in a title or aria-label, since that is
the shape this takes and nothing else catches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
An audit of every view turned up two problems with the in-product help.
The tips only appeared on :hover. On a tablet or a phone there is no hover, so
the entire explanation layer was invisible to touch users — a large share of
the people administering signage — and unreachable from a keyboard. Tapping a
marker now opens it, Escape or a tap elsewhere closes it, and the marker is
focusable so Tab reaches it and a screen reader announces it. Bound once at the
document level and applied by observing the DOM, because views render from
about twenty call sites and modals appear later still; hooking each one would
have left the next new route silently unreachable again.
Four views had no tip at all. Playlists is the important one: a playlist is the
concept the reported confusion was actually about, and the page said nothing
about what one is or how it reaches a screen. Activity and Settings now have
one too. Help does not, because it is the help.
The schedule tip described a product that no longer exists — it said to click
Add Schedule, predating the drag, resize and right-click gestures. Rewritten.
All four are translated into every active locale rather than left to fall back
to English, since a tip falling back is a non-English user being handed an
English paragraph at the moment they are confused. hi.js stays deliberately
empty per the note in that file. Tests now check that every tip is translated
everywhere, and that a tip marker never names a string that does not exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
Driving the app in a real browser showed a context menu whose only item read
"schedule.ctx_new". t() returns the KEY when a string is missing — it never
returns undefined — so a missing key renders literally, and the common
`t('x') || 'A readable default'` guard is dead code: the key is truthy, the
default can never fire, and the pattern hides the problem instead of covering
it. Every occurrence of it in the app was doing exactly that.
Nineteen strings were affected, most of them predating this work: fifteen in
the self-hosted update panel and four in video walls, all of which have been
showing raw keys to users. The intended text was recovered from the dead
defaults, so the wording is the authors' own, and the defaults are removed
rather than left to imply a safety net that does not exist.
A test now walks the views for the keys they actually ask for and fails on any
that English does not define, and separately rejects the `|| default` pattern.
Neither problem is visible to a syntax check, a unit test, or review — only to
someone looking at the screen — so the guard is the only thing that keeps them
from coming back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL