Skip to content

feat(i18n): translate what git operations report - #92

Open
noahbclarkson wants to merge 14 commits into
mainfrom
feat/i18n-git-messages
Open

noahbclarkson wants to merge 14 commits into
mainfrom
feat/i18n-git-messages

Conversation

@noahbclarkson

Copy link
Copy Markdown
Owner

Stacked on #91 (which is on #57). It also carries #90's commit so the push code is current; that commit drops out when #90 merges and this is rebased.

Translates what git operations report: the progress, success and failure message for every operation, plus the user-facing errors #89 picked out, with @VedaChen's Chinese for those. #89 translated failures only, so a running operation, its success toast and its failure toast came out in different mixes of languages.

How it works

  • The git layer builds messages, the workspace translates them.
    • rgitui_git builds each summary and detail as an rgitui_i18n::Message with msg!/msgp!. The key and argument names are checked against en.rs at compile time, and messages can be built on the background executor.
    • GitOperationUpdate.summary and .details are Messages. The workspace calls .translated() where it shows them: toasts, status line, operation banner, output bar.
    • Display is English, so logs and anyhow chains are unchanged.
    • Raw git output is Message::verbatim.
  • Errors carry their message.
  • Messages can contain messages.
    • GitOperationKind::name(), RepoState::label() and RepoState::operation() return Messages.
    • So "{operation} requires a clean working tree" and "Aborting {operation}…" come out whole in Chinese instead of with "Checkout" or "merge" pasted in.
    • The output bar's operation label and the conflict banner use them too.

Bugs fixed on the way

  • Typed outcomes instead of string matching.
    • Merge, pull and rebase signalled a conflict by prefixing "CONFLICT:" to the message, and the UI thread checked for the prefix and stripped it.
    • Bisect skip detected that no candidates were left by searching its own English for "cannot continue".
    • All four now return a flag.
  • The "would overwrite" path list and its "and N more" are a message, so the list isn't English inside a Chinese sentence.
  • Counts take the singular at one: "1 conflict detected", "Rebased 1 commit", "Removed 1 untracked entry".

English changes

  • "Merging aborted" / "Aborting merging..." → "Aborted merge" / "Aborting merge...".
  • "Merging completed" → "Completed merge".
  • "Interactive rebase requires a clean working tree" is unchanged; it has its own operation name.
  • Everything else reads as before, apart from the singular forms.

Still English

Errors outside the set #89 picked: libgit2/filesystem failures, git clean and other internal CLI errors, and the worktree-patch errors that aren't user mistakes. These show as they are, so nothing goes blank, but they aren't translated.

Verified

  • cargo clippy --workspace --all-targets -D warnings: clean.
  • Tests: rgitui_git 417, rgitui_workspace 772, rgitui_i18n 56, rgitui_graph 37, rgitui_diff 99, rgitui_ui 66, rgitui_settings 31, all passing.
  • New tests:
    • error_message: a plain message, an outer context winning, and a non-message error.
    • Message: English rendering, plurals, nested messages, verbatim text, and values not being re-scanned.
  • Strings: 909 English entries and 869 Chinese messages. The 40 without Chinese are .one forms Chinese doesn't use.
  • Ran Stage All in the app under the perf-harness replay with "language": "zh-CN". The files staged, and the UI around the operation was in Chinese. The success toast itself fell outside the captured window area, so the toast text wasn't checked visually.

🤖 Generated with Claude Code

noahbclarkson and others added 5 commits October 15, 2026 21:17
Adds `rgitui_i18n` and wires it into settings, so a language can be chosen
and applied without a restart. This is the infrastructure only — the
Appearance settings panel is converted as the working example; the other
~1500 user-facing strings are still English literals.

English is the source of truth in `src/en.rs` rather than a JSON asset, for
the same reason the built-in themes are Rust: it is always present and
cannot drift, so any missing translation has something to fall back to.
Translations are JSON under `assets/locales/`, embedded at build time, with
user overrides from the config directory so a translator can iterate against
a release build.

The catalogue is a process-wide RwLock rather than a GPUI global, because
error strings are built on background threads and need translating too. The
tradeoff is that GPUI cannot know a language change dirties the frame, so
`set_language` must be followed by `cx.refresh_windows()`.

`t!` and `tp!` return `SharedString`, which every `rgitui_ui` builder already
accepts — converting a call site is a one-line change.

Locale files are validated by tests: unknown keys fail (they would silently
never render), placeholders must survive translation, and plural forms must
be declared in pairs. Missing keys are deliberately allowed so partial
translations are worth shipping.

Simplified Chinese ships as the first translation and needs native review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the loader

Keys: t!, tp! and the new key! take literal keys and evaluate const
assertions against the English table, so a key missing from en.rs, or named
arguments that do not match the English message's placeholders, fail the
build at the call site instead of rendering the raw key. tp! fills {count}
itself. en.rs is one `key => message` line per entry, and an unsorted or
duplicated table fails the build naming the first key out of place, which is
what lets the lookup be a binary search. A test reports English keys that no
code uses.

Language changes: set_language takes the App, refreshes every window and
replaces an ActiveLanguage global, so callers no longer have to remember
refresh_windows, and views that keep translated text outside render can
observe it. The settings view reads the selection from rgitui_i18n instead of
keeping its own copy.

Matching: Traditional Chinese systems (zh-TW, zh-HK, zh-MO, zh-Hant) matched
the shipped zh-CN on the primary subtag alone and got Simplified characters;
matching now requires the same script, stated or implied. POSIX tags such as
zh_CN.UTF-8 are accepted. The system language and per-locale missing counts
are read once at init rather than on every render, and the count no longer
includes plural forms a locale's rule never selects, so a complete zh-CN
stops reporting one missing string.

Loader: user locale files must be regular files under 1 MiB, are read in path
order, and are layered over a shipped locale with the same id instead of
replacing it; keys English does not define are logged. An unknown plural_rule
is an error rather than silently becoming one_other. Shipped locales must use
exactly the English placeholders and only the plural forms their rule selects.

The UI font gains Simplified Chinese fallback families when the UI is
Simplified Chinese, and only then, so Japanese and Traditional Chinese text in
an English UI keeps the platform's glyph choice. The cached font is keyed on
the locale as well as the family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports @VedaChen's translation of the interface from #89 onto rgitui_i18n.
Their call-site work is kept file by file; the TrKey table, the settings
language enum and the git-layer changes are not.

- 469 strings carried over as written, under keys named by the panel that
  shows them (toolbar.push, confirm.force_push, branch_health.merged).
- Every `tr(TrKey::X)` call is now a compile-checked t!("key"); keys held as
  data (graph menu rows, toolbar tooltips, the settings quick reference) use
  key!. Positional `{}` templates filled by chained replacen are named
  placeholders filled in one pass, so user text containing "{}" can no longer
  swallow the next value.
- Sentence fragments joined with format! are whole sentences: "Pending changes
  on {branch}", "HEAD is detached at {commit}", "Return to {branch}",
  "Inspecting worktree: {name} ({branch})", "Failed to load comments: {error}".
  Counts are plurals ("1 file staged", "1 result", "1 commit"), and the four
  DescCount variants are one message built from two plural pieces.
- English words no longer land in Chinese sentences: the PR and issue empty
  states have a message per filter (which also fixes "No all issues found"),
  and the AI section translates its credential store names, catalogue ages,
  "verified" suffix and model-list footer.
- Relative dates in the graph, blame, history and detail panels go through
  shared time.* messages.
- Pill groups select by value instead of comparing the clicked label against
  both languages. The confirm dialog returns its translated label directly.

Dropped from #89:
- The 27 observe_global::<SettingsState> redraw observers. A language change
  refreshes every window already, and those observers redrew the main window
  on every settings write, including each move of the settings window.
- The placeholder resets in render. TextInput::set_placeholder_message takes a
  key and translates it when drawn, so inputs follow the language with no
  observer and no per-frame work.
- The General-page language dropdown, which duplicated the Appearance picker.
- The four #[allow(dead_code)] English label functions; the label functions
  that render are the ones tested now.
- description_tr's hard-coded Chinese in the keymap registry; the settings
  quick reference has message keys instead, with a test that keeps their
  English equal to each command's doc comment.

Terminology, from review of #89: rebase "pick" is 使用 (拣选 is cherry-pick),
bisect good/bad is 正常/有问题 everywhere with the menu saying 二分查找, the
"Open" filter is 打开, a stash message is 储藏说明, and the interface uses 你.

The working-tree row's labels and badges and the diff header were English on
the main screen in #89 too and are translated here.

Co-authored-by: chenjunjie <zmiss2024@foxmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n English

Retrying a failed push decided whether to force by searching the failure
summary for "force push". The summary on failure is always "Push to '<remote>'
failed", so a failed force push was retried as a plain push. Force pushes now
carry their own GitOperationKind::ForcePush, the retry reads the kind, and the
toolbar's pushing state covers both.

Several outcomes are recognised by matching git's output: CONFLICT and
"Automatic merge failed" after a pull, "could not apply" during a rebase,
"Already up to date.", the SSH auth failures that trigger the HTTPS fallback,
and the "[ahead N, behind M]" of %(upstream:track). A localized git translates
all of them. git_command() now sets LC_MESSAGES=C. When LC_ALL is set it
overrides every category, so its value moves to LANG and the per-category
variables it was overriding are removed, which changes the message language
and nothing else. git's stderr shown in failure toasts is English as a result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operation summaries and details are rgitui_i18n::Message values instead of
English strings. The git layer builds them with msg!/msgp! — checked against
en.rs at compile time, and safe to build on the background executor — and the
workspace renders them in the active language where it shows them: toasts,
the status line, the operation banner and the output bar. Display is English,
so logs and anyhow chains are unchanged.

Every operation's progress, success and failure message is covered, plus the
user-facing errors #89 chose (about 80), with @VedaChen's Chinese for those.
#89 translated failures only, so a running operation, its success toast and its
failure read in three different mixes of languages.

- Errors: messages::error(msg!(…)) wraps a Message in an anyhow error.
  error_message() uses it only when it is the text error.to_string() would
  show, so an outer .context() still wins — #89 took the innermost message in
  the chain and dropped any context around it.
- Operation names inside messages are translated values rather than English
  words: GitOperationKind::name() and RepoState::label()/operation() return
  Messages, so "Checkout requires a clean working tree" and "Aborting merge…"
  come out whole in Chinese. ensure_clean_worktree and checkout_tree_safe
  take the operation instead of a &str.
- Merge, pull and rebase signalled a conflict by prefixing "CONFLICT:" to the
  message and stripping it on the UI thread, and bisect skip detected
  exhaustion by searching its own English for "cannot continue". Each now
  returns a flag.
- The path list in "would overwrite" errors, and its "and N more", is a
  message; plural counts are plurals ("1 conflict", "1 file", "1 commit").
- Raw git output stays verbatim.

English changes: "Merging aborted"/"Aborting merging..." are now "Aborted
merge"/"Aborting merge...", "Merging completed" is "Completed merge", and
counts take the singular at one.

Co-authored-by: chenjunjie <zmiss2024@foxmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T09:47:12.119575Z f76d334 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1265385170

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +603 to +605
pub fn is_push(&self) -> bool {
matches!(self, GitOperationKind::Push | GitOperationKind::ForcePush)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat both push variants as one failure category

When a force push fails and a later normal push succeeds, or vice versa, the success handler in workspace/events.rs compares operation kinds for exact equality, so it does not clear last_failed_git_operation. After the successful operation finishes, the UI falls back to the stale failure banner and can still offer a retry that force-pushes. Use the new is_push() equivalence when deciding whether a successful push clears a prior push failure.

Useful? React with 👍 / 👎.

format!("{}: {}", update.summary, details)
} else {
update.summary.clone()
let summary = update.summary.translated();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep operation messages untranslated until render

If the user changes languages after an operation event is handled, this eagerly translated value has already been copied into status_message, active_operations, and last_operation_output; changing the language only refreshes windows and does not rebuild those stored strings. Consequently, the status bar and operation output remain in the previous language even though the banner that retains the original Message updates correctly. Preserve the Message until rendering, or rebuild these caches when ActiveLanguage changes. CLAUDE.mdL59-L59

Useful? React with 👍 / 👎.

noahbclarkson and others added 3 commits October 19, 2026 21:40
The picker keeps the footer and status notes it was handed, and the
`ActiveLanguage` observer only re-translated the API-key placeholders.
Opening the picker, switching language on the Appearance page and coming
back left "updated 3 h ago" and the hidden-for-tools note in the old
language. The observer now re-syncs the picker as well; `set_rows` and
`set_chips` keep the query and chip, so an open picker is not reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operation handler translated each update as it arrived and stored
the text in the status line, the running-operation label and the output
bar. Switching language afterwards refreshed the windows but not those
strings, so they stayed in the old language while the banner, which
keeps the `GitOperationUpdate`, followed the switch.

`status_message`, `ActiveOperation::label` and `OperationOutput`'s
operation and output now hold the `Message` and translate in render.
The failure line is built with `msg!` and nests the summary and details
as messages. Only toasts are still translated when raised, since they
are gone in seconds. Status lines that come from outside git go in as
`Message::verbatim`, which is what they already were.

`OperationState::is_loading` and `loading_message` were written on
every update and never read, so they are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tle a failed push

- Windows reads environment names without regard to case, so a
  lowercase `lc_all` is `LC_ALL` to git there. The exact-case check
  missed it, `LC_MESSAGES=C` lost to it, and git kept answering in the
  user's language. Names now match case-insensitively where the platform
  does (`cfg!(windows)`), and the variable is removed under the name it
  was found with.
- Splitting `ForcePush` from `Push` meant a plain push succeeding after
  a failed force push (or the reverse) no longer cleared the failure:
  the success handler compared kinds for equality, so the stale banner
  came back and still offered a retry that force-pushes.
  `GitOperationKind::settles` treats both pushes as one for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@noahbclarkson

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 1ad6ddc509

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

noahbclarkson and others added 6 commits October 19, 2026 21:52
# Conflicts:
#	CHANGELOG.md
#	CLAUDE.md
#	assets/locales/zh-CN.json
#	crates/rgitui_i18n/src/en.rs
#	crates/rgitui_i18n/src/lib.rs
#	crates/rgitui_workspace/src/settings_window/view.rs
The confirm dialog drew whatever title and warning its caller passed,
and every caller passed English, so a force push or a branch deletion
asked in English under translated buttons. The dialog now builds both
from the `ConfirmAction` in render: the actions already carry what the
text names (the branch, tag, remote, path, stash index or commit), so
`show_visible` takes only the action, and the text follows a language
change while the dialog is open.

- `AbortMerge` was used for every operation in progress and is now
  `AbortOperation(RepoState)`, so the title says which one is being
  aborted ("Abort Rebase") instead of "Abort Rebasing (interactive)".
- The two copies of the hard-reset warning, and the "Reset to Commit"
  and "Reset (hard)" titles for the same action, are one message each.
- A worktree removal names the path; the worktree name was only in the
  caller's text.
- `ConfirmDialog::show` had no callers and is gone, as is the commit id
  `GraphViewEvent::ResetToCommit` carried beside the `Oid` it was
  formatted from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/rgitui_git/src/project/network.rs
#	crates/rgitui_git/src/types.rs
# Conflicts:
#	CHANGELOG.md
#	assets/locales/zh-CN.json
#	crates/rgitui_i18n/src/en.rs
#	crates/rgitui_workspace/src/workspace/layout.rs
@noahbclarkson
noahbclarkson changed the base branch from feat/i18n-zh-port to main October 19, 2026 09:43
@noahbclarkson

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f76d3342a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if undoable {
cx.emit(GitProjectEvent::WorktreePatchApplied {
label: summary,
label: summary.translated().to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Keep the worktree undo label untranslated

When a user applies or reverts a worktree patch, changes the language, closes the originating repository, and invokes Undo within its 60-second lifetime, this translated string remains cached in UndoEntry and is interpolated into the closed-repository error toast by workspace/undo.rs:220-224, so that toast mixes the previous language with the current UI. Carry the Message through WorktreePatchApplied and the undo entry, and translate it only when the label is displayed. CLAUDE.mdL59-L59

Useful? React with 👍 / 👎.

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