i18n(backup): make the restore notes and preview details translatable (#2656)

A German user got a translated modal with "Not present in this backup commit" in
the middle of it. Every tally note and preview caveat was a server-built English
sentence rendered verbatim.

Follows the backup.pathCheck contract already in use one card down in the same
component, deliberately rather than inventing a second convention: the server
sends a `code` plus typed `params` and carries the English along as the
fallback, and the client renders
`t(`...${code}`, { ...params, defaultValue: message })`. The defaultValue arm is
what keeps a newer backend's unfamiliar code readable instead of printing the
raw key — covered by its own test.

Shapes:
- notes: list[str] -> list[GitHubRestoreNote] {code, params, message}. Breaking,
  but the field is unreleased in this same PR.
- GitHubRestorePreviewCategory gains detail_code / detail_params; `detail` stays
  as the English fallback.
- _CategoryTally.note(code, message, **params), deduped on (code, params) rather
  than on the rendered text, so two offline printers both keep their names. The
  20-note cap is unchanged.

28 new leaves across 13 locales: 20 notes.* and 8 details.*. `noData` collapses
the four per-category "No X data in this backup" strings into one, since the
category heading already renders beside it. Counts use single-form {{count}} in
the existing "N record(s)" style rather than i18next plural suffixes — nothing in
this block uses _one/_other and the parity script has extra rules for them.
Parity holds at 5737 leaves in all 13 locales.

Deliberately out of scope, and worth saying so rather than leaving it to look
like an oversight: result.message, the commit-picker subject lines and the HTTP
error strings stay English. Those also originate in the provider backends, so
code-ifying them widens the diff well past the restore service.

spoolTagKept is added here with the rest of the locale churn but is not emitted
until the next commit, so the 13-locale change lands once.
This commit is contained in:
jmoore-skild
2026-08-04 08:57:33 -04:00
parent 6ea2daf9d0
commit af8d14d796
20 changed files with 826 additions and 104 deletions
@@ -169,7 +169,18 @@ class TestRestoreEndpoint:
"ref": "aaa1111",
"results": {
"spools": {"restored": 4, "skipped": 1, "failed": 0, "notes": []},
"settings": {"restored": 1, "skipped": 2, "failed": 0, "notes": ["1 credential-like key(s) skipped"]},
"settings": {
"restored": 1,
"skipped": 2,
"failed": 0,
"notes": [
{
"code": "settingsCredentialsSkipped",
"params": {"count": 1},
"message": "1 credential-like key(s) skipped",
}
],
},
},
}
with patch(
@@ -184,7 +195,15 @@ class TestRestoreEndpoint:
assert response.status_code == 200
body = response.json()
assert body["results"]["spools"]["restored"] == 4
assert body["results"]["settings"]["notes"] == ["1 credential-like key(s) skipped"]
# Notes cross the wire as code + params + English fallback, so a
# non-English client can translate them (#2656).
assert body["results"]["settings"]["notes"] == [
{
"code": "settingsCredentialsSkipped",
"params": {"count": 1},
"message": "1 credential-like key(s) skipped",
}
]
assert mock.await_args.kwargs["overwrite_existing"] is True
assert mock.await_args.kwargs["ref"] == "aaa1111"
@@ -391,7 +410,7 @@ class TestRestoreDoesNotOpenTheMetricsEndpoint:
response = await async_client.get("/api/v1/metrics")
assert response.status_code == 404, "a settings restore opened the metrics endpoint"
assert "bambuddy_build_info" not in response.text
assert any("switched off" in note for note in tally.notes)
assert any(note["code"] == "settingsCompanionSkipped" for note in tally.notes)
@pytest.mark.asyncio
@pytest.mark.integration