Skip to content

refactor: show the Settings window and menu bar through Fluent messages - #880

Merged
penso merged 29 commits into
mainfrom
i18n/phase-2-strings
Oct 29, 2026
Merged

penso merged 29 commits into
mainfrom
i18n/phase-2-strings

Conversation

@penso

@penso penso commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Summary

First PR of Phase 2 of docs/i18n.md (string migration), which ships as a series split by feature area. This one covers the Settings window and the menu bar, plus the tooling every later PR uses. English text is unchanged except for the two visible changes listed below.

Tooling

  • scripts/check-i18n.py: a module marked // i18n: migrated (the file and the directory of the same name) may hold no string literal that reads as English. Logs, panics, attributes such as #[error], write! in a Display, key contexts and bindings are exempt. A literal that is data (a theme or font name, a config value) takes // i18n: data. Covered by new Python tests.
  • The en-XA leak harness moves to i18n::leak_tests (under_en_xa, draw, assert_translated), so each window keeps its own leak test. Settings has one test per page, plus one per Cloud devices provider card. The menu bar has a test that builds every menu under en-XA, since macOS's native menus are not drawn by the app.
  • i18n::takes(id) (test only) checks tables that name messages as data.

Settings window (settings_window/ is marked migrated as a whole)

  • The frame, section list, groups, page leads, search, footer and every catalog row come from settings.ftl. Search matches both the translated and the English text, as the palette does. Page selectors are their message IDs, the same in every language.
  • Each page group has its own .ftl file: settings-general, -tabs, -notifications, -configuration, -theme, -sidebar, -indicators, -fonts, -status-bar, -code, -integrations, -plugins, -remote, -cloud, and one per cloud provider. Parallel PRs therefore do not share a catalog.
  • Hand-built plurals (theme and font counts, cloud setup line counts) use Fluent selectors.
  • Tables that held English labels now hold message IDs, each checked by a test: positions, faces, specimen lines, icon sets, status-bar choices, provider fields. Number fields take a Unit instead of an English string.
  • [features] flags, sidebar layouts, tab layouts and chat layouts are named by messages (features.ftl, layout.ftl, tab-layout.ftl, chat.ftl). The few outside helpers Settings draws are migrated with it: integrations.rs and cloud_setup::summary.

Menu bar: every menu and item, QA included, and the in-window bar's own labels on Linux and Windows (menus.ftl).

Visible changes

  • The Remote control certificate files card was titled with its ID ("REMOTE-FILES"); it now reads "Certificate files".
  • Azure VM size choices wrap instead of truncating.

Not in this PR (next PRs in the series)

  • Dialogs and popover menus (menu/, about 700 literals), then the remaining surfaces.
  • Localized error enums. Errors still reach the UI as $error placeables fed their English Display.
  • A few strings Settings shows from other crates: herdr-remote engine errors, Azure VM spec and price summaries, cloud job status, and remote::device_line.

Validation

  • just format
  • just ci on dba48610 (macOS arm64): passed. 14,492 Rust tests, 0 failed; check-i18n reports 1017 messages in 32 files, valid and used.
  • just lint-windows: passed
  • just lint-linux: passed
  • en-XA leak tests: every Settings page, every Cloud devices provider card, the palette, and the menu bar
  • Native check: open Settings under QA > Pseudo-Locale > Accented (en-XA) and Right to Left (en-XB), page through every section, and check for clipped labels.
  • Native check in English: Settings looks as before, including the Remote control "Certificate files" card and Azure's size list in Cloud devices.
  • macOS menu bar under en-XA: every menu shows bracketed, accented text.

Screenshots

No native screenshots: they were not captured in this session, and no desktop interaction was done. English rendering is meant to be unchanged apart from the two visible changes above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE

penso and others added 27 commits October 28, 2026 17:29
…t messages

First step of Phase 2 of docs/i18n.md.

- scripts/check-i18n.py: a module marked `// i18n: migrated` (the file and
  the directory of the same name) may hold no string literal that reads as
  English outside logs, panics, attributes, Display writes, and key
  bindings; `// i18n: data` exempts a literal that is data.
- The en-XA leak harness moves to `i18n::leak_tests` so each window keeps
  its own test; Settings gets one per page, ignored until the page is
  migrated.
- Settings: the window title, section list, groups, page leads, search,
  footer, and every catalog row's title and description come from
  settings.ftl; the `[features]` flags name messages in features.ftl.
  Search matches the translated and the English text. Page selectors are
  their message IDs, the same in every language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The layout enums under config keep their config names and gain a message
ID each (layout.ftl, tab-layout.ftl, chat.ftl); `label()` now formats it
in the current locale, so the View menu and Settings show them
translated. Settings' segmented control takes owned labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Every menu and item of the menu bar, the QA menu included, and the
in-window bar's own labels (Linux and Windows) come from menus.ftl. A
test builds the menus under en-XA and checks every name is a translated
message, since the leak test cannot see macOS's native menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The page's groups, rows, tab bar positions, and each SSH host's
scrollback note come from settings-tabs.ftl, and the rows search finds
from their settings.ftl messages. A host's failure keeps its error as an
$error placeable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Files group, the file rows' titles, Reveal, Open, Reload, and the
placeholder for an unreadable Herdr config come from messages; paths stay
data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…ssages

- settings.ftl gains the shared controls' text: the read-only and
  unavailable reasons, On/Off, notification positions, the number range,
  the swatch's "none", and the footer's save and reload outcomes, with
  errors as $error placeables.
- The "Use Herdr setting" dropdown entry is one message around Herdr's
  value; the button mutes the words around the value with highlights on
  a single line instead of matching an English prefix.
- Position tables name message IDs, checked by a test.
- General (with Windows' Launch at login) and Notifications draw from
  settings-general.ftl and settings-notifications.ftl, and their en-XA
  leak tests, with Tabs and Configuration, now run.
- Native live checks mark their fixture names and log lines as data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Remote control page's rows, notes, buttons, status lines, and errors
come from settings-remote.ftl, with plurals as Fluent selectors and the
port, fingerprint, and errors as placeables. Rows in the catalog reuse its
titles. The pane history reload failure on SSH hosts moves there too.
Form cards take a borrowed title, so a page can pass a formatted message,
and the certificate files card gets a title instead of its ID. The page's
en-XA leak test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Code page's VS Code mode, server state, license, connection test, and
px0 address come from settings-code.ftl; paths, addresses, and commits
are placeables, and example addresses stay data. The license text is a
message here; the browser's own copy of it still uses the launcher's
constant until the browser module is migrated. The page's en-XA leak test
now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Integrations page's search, its fallback without a session window, and
the list the daemon's host reports (crate::integrations) come from
settings-integrations.ftl and integrations.ftl; agent names, commands,
hosts, and the daemon's messages stay data. The chats' environment card on
the General page, about what agents start with, moves to
settings-integrations.ftl too. The page's en-XA leak test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Plugins page's notes, empty states, switches, and value details come
from settings-plugins.ftl, with the status labels count as a plural.
Searching finds the status labels row by its translated or English title.
Plugin tokens, the samples they report, host names, and the preview's
example workspace and agent stay data. The page's en-XA leak test now
runs, allowing the preview's names and their truncations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Theme page, its theme browser, the window backdrop, and terminal
padding take their text from settings-theme.ftl, and rows search finds
use their settings.ftl titles. Theme names stay data; FOLLOW stays the
config value it is compared as, and the page shows a message for it.
Load and preview errors pass the error as an argument. The page's en-XA
leak test now runs; the number control's unit and range are listed as
data until `ui::number` is migrated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Sidebar page and its host colour rows take their text from
settings-sidebar.ftl, and rows search finds use their settings.ftl
titles and descriptions. Host rows name their hint's message, and the
swatch row takes the formatted hint. Host names stay data. The page's
en-XA leak test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Each status icon set names its title and description by messages in
settings-indicators.ftl, as do the statuses its card previews; the edge
rail and Icons heading use their settings.ftl rows. Tests check every
such ID is an English message. The page's en-XA leak test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Fonts page, its specimen, and the family picker take their text from
settings-fonts.ftl, and each face its settings.ftl row title. The picker
selects which face it sets inside one message rather than joining a
translated face name. Specimen lines are a message each, except the
terminal's, which is code. Family names stay data. The page's en-XA leak
test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Status bar page takes its heading, choices, and note from
settings-status-bar.ftl, and its rows their settings.ftl titles and
descriptions where those match. The choice tables name messages, which
a test checks. The page's en-XA leak test now runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Number fields take a Unit (pixels, seconds, percent, or none) whose label
is a message, in place of an English string; the font faces, the font
search placeholder, and the missing config path's line are messages too.
controls.rs is marked migrated. The en-XA leak test no longer needs to
allow units and ranges as data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…nt messages

The Cloud devices page's own text, its New device setup card, and each
provider's Images section come from settings-cloud.ftl. The setup
summary under each provider's switch, also shown by the Coder menu, moves
to cloud-setup.ftl with plural line counts. Provider cards gain a
`cloud_card` helper that takes a translated title, and field labels take
owned text, ahead of migrating each provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Azure card's fields, sign-in status, size choices, device rows, and
delete confirmation come from settings-cloud-azure.ftl; text the
provider cards share (Save, Cancel, Remove from Herdr only, delete
outcomes) is in settings-cloud.ftl. The fields table names message IDs,
checked by a test. Whether a device is a VM or a sandbox selects within
each message rather than splicing in an English noun. Size specs and
prices still come formatted from herdr-remote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Boat card's fields, key status, sandbox count (now a plural), pending
creation recovery, and device rows come from settings-cloud-boat.ftl,
with the API key and creation text other cards share in
settings-cloud.ftl. The default API URL stays the URL field's
placeholder as data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
The Coder card's fields, sign-in state, client secret notes, and the
workspace destroy confirmation and progress come from
settings-cloud-coder.ftl. A shared `Hint` names a field's placeholder
as a message or as a value shown as it is, such as an example URL, and
Boat's fields use it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…ssages

The Container card's engine line, fields, new container box, container
rows and their states, and the unsaved list come from
settings-cloud-container.ftl. Each row's state is one message with the
engine as an argument; the default image, CPUs, and memory stay their
fields' placeholders as values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…ages

The Daytona card's fields, key status, sandbox count (now a plural
selector rather than an English suffix), and new sandbox box come from
settings-cloud-daytona.ftl, sharing the API key and creation text in
settings-cloud.ftl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…messages

The OpenSandbox card's fields, server status with a plural sandbox
count, key notes, and new sandbox box come from
settings-cloud-opensandbox.ftl. Its "(experimental)" title is a shared
message around the provider's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…ages

The Railway card's key choices, generated key location, file dialog
title, device rows, and status lines come from
settings-cloud-railway.ftl. The key name rule is a message of its own,
with a test that its English stays herdr-remote's NAME_RULE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
…ages

The Sprites card's API URL and token fields, token notes, sprite count
as a plural, and new sprite box come from settings-cloud-sprites.ftl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Cloud devices is marked migrated, and its en-XA page test runs, allowing
only the provider names on the tabs. That page test opens no card, so a
test of its own draws each provider's tab, with every account
unconfigured, under en-XA and allows only provider names and the values
fields show as placeholders.

It found the Azure sandbox sizes' "GiB disk" coming from herdr-remote,
now a message, and size choices cutting their lines short: they now
wrap, as a translation may run longer than English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Every Settings page now draws only messages, so the marker moves to
settings_window.rs and check-i18n rejects new English literals anywhere
under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
@penso

penso commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner Author

just ci ran locally and passed on 15732dcc (macOS arm64; 14,494 tests, 0 failed). Extra recipes run: just lint-linux (passed). just lint-windows last ran on dba48610; the two new commits add no platform cfg gates.

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The changes appear safe to merge; both previous findings are fixed, and no new actionable issues were found.

Summary

Moves Settings and menu-bar text into Fluent messages and adds checks for untranslated text.

  • Settings pages and cloud cards show text from the active language.
  • Menu bars show labels from the active language.
  • A checker and locale tests track English text left in migrated screens.

Reviews (2) · Last reviewed commit: "fix: show Settings search hints in the l..." · Reviewed by Greptile

Comment thread scripts/check-i18n.py Resolved
penso and others added 2 commits October 28, 2026 22:22
The tokenizer started a name on any `isalnum()` character but matched it
with an ASCII-only pattern, so `let café = 1;` tripped an assertion. The
name pattern is now Unicode-aware `\w`, matched in place rather than on a
slice of the rest of the source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
Settings formatted its field placeholders once, when each field was
built, so changing `language` or the QA pseudo-locale left the search,
theme, font, integration, plugin, px0, and cloud-provider hints in the
old language until the window reopened. `SearchInput::set_placeholder_with`
keeps how to format the hint and formats it again when `CurrentLocale`
changes, without touching what was typed. Example paths and tokens keep
`set_placeholder`, which stays as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh55PnUHvhsWwqaMNjE
@penso

penso commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

@greptileai review

@penso
penso merged commit a5b2df2 into main Oct 29, 2026
19 checks passed
@penso
penso deleted the i18n/phase-2-strings branch October 29, 2026 06:05
penso added a commit that referenced this pull request Oct 31, 2026
#941)

* fix(i18n): map the errors #905 added so the app builds again

#905 (iOS app, one pairing for every machine) and #926 (localized
errors) merged independently. #905 moved herdr-automate's binary,
checksum, and platform failures into host_build::Error and added
herdr_client::Error::TerminalAttach, remote control's app::Error::Enroll,
and the root Error::Nearby, none of which had a Localize mapping, so main
no longer compiled.

Each new enum gets its own mapping and .ftl file, as #926 set out:
host_build (error-host-build.ftl, which takes over the three automation
messages), terminal_attach (error-client-terminal-attach.ftl), nearby
(error-nearby.ftl), and enroll (in error-remote-control.ftl). Per-variant
tests cover each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh5CsF9aiVsJURUwioh

* style(i18n): keep the client error mapping sorted

* refactor(i18n): show the window's notices and tab faces through Fluent

Part of Phase 2 of docs/i18n.md, following #880. Toasts, flashes, the
config diagnostic cards, the agent skill offer, and the window's command
notices take their text from window-notices.ftl and notifications.ftl;
the tab faces' muted hint and close button from tab-layout.ftl.

- Marked migrated: notifications.rs, tab_layout.rs, and in window/ the
  toasts, flash, system notifications, config diagnostic, commands,
  monitors, zen, and Shift-select hint modules. D-Bus and registry names
  in the system notification backends are tagged as data.
- Errors reaching these notices use localize() instead of to_string():
  request failures, the agent skill install, keep-awake, checkpoint
  restores, and dismissing the announcement.
- The skill install's place count is a Fluent selector.
- notifications.rs's inline test module moves to notifications/tests.rs.
- A new en-XA leak test covers each toast preview, a hint and a flash,
  the status line's error, the config diagnostic card, and the agent
  skill offer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh5CsF9aiVsJURUwioh

* fix(i18n): keep preview direction marks and restore failure causes

Greptile on #941: Notice::new cleans daemon text with safe_text, which
also stripped the direction marks en-XB puts in the QA toast and system
notification previews; previews now set their title and body from the
messages afterwards. A failed checkpoint restore's flash lost the cause
checkpoint::describe used to append; a failed Git script's or worker's
error now follows the message. Also puts host_build.rs's keep-sorted end
marker back on its own line, which main's rustfmt had folded away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFavh5CsF9aiVsJURUwioh

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant