Skip to content

i18n: messages with literal braces match only their own catalog row - #7151

Merged
bflatastic merged 8 commits into
storytold:mainfrom
dotCooCoo:fix/i18n-literal-brace-messages
Nov 14, 2026
Merged

bflatastic merged 8 commits into
storytold:mainfrom
dotCooCoo:fix/i18n-literal-brace-messages

Conversation

@dotCooCoo

@dotCooCoo dotCooCoo commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The message scan in crates/ui-egui/src/i18n/tests.rs read every message as a format string, so a plain string literal such as the plug-in error expected {"dataBase64": "…"} (crates/plugins/src/export.rs) became the catalog key expected {"dataBase64"}. The catalog parses that key as the text expected followed by a placeholder, so the row matched any message that starts with "expected". In German, the plug-in output error plug-in output: expected value at line 1 column 1 was shown as Plug-in-Ausgabe: erwartet: value at line 1 column 1. The same fault was in the rows for expected {"objects": [...]}, the read-only object, stroke and effect errors of the plug-in host (22 catalogs), and 16 engine errors such as quotes must be {double?, single?} and give {key, value} or {values} (hi and sv). The fix for escaped braces in format! strings (#7047) did not cover plain literals.

  • The scan doubles every brace of a message that is not a format string (a plain literal or a &str constant). A format! string and an #[error(...)] string keep their {{, }} and placeholders as before. The engine's error( helper also occurs inside each #[error( attribute; the scan reads that one as a format string.
  • 150 @msg rows in 23 catalogs are keyed by the full message with its braces doubled. Their translations write those braces doubled, and the rows that had a shortened JSON example now carry the full one from the English ({{"dataBase64": "…"}}, {{"objects": [...]}}, {{"id": …}}, {{"wght": 650}}, {{"formats": [...]}}, {{current: new}}). Eight of the 150 rows are not an RTF file: it doesn't start with {{\rtf (crates/engine/src/cmd/place/rtf.rs), which is also a plain literal.
  • Catalog::parse (crates/ui-egui/src/i18n/catalog.rs) stores an @msg row that has braces and no placeholder as an exact message, under its text with {{ and }} read as single braces. Before, such a row went to the templates, where exact_message did not find it and messages over the template length limit were not matched. A JSON part inside a message that such a row translates is shown with single braces (exact_translation in crates/ui-egui/src/i18n.rs).
  • The Hindi row for give {key, value} or {values} now translates the whole message. Before, it kept the English "or" and left out "give".
  • docs/development.md and the catalog-format header in zh-hant.tsv say how a literal brace is written in a row, and docs/target-app-parity.md has one line for the change.

This changes existing behavior in one way: a status or error message that only starts like one of these rows, such as a JSON reason after plug-in output:, is no longer replaced by that row's translation and is shown in English.

How it was tested

New tests in crates/ui-egui/src/i18n/tests.rs, each failing without the change:

  • plain_message_keys_double_their_braces: a plain literal, a &str constant and the RTF message get keys with their braces doubled; a format! string with a real placeholder and an #[error(...)] string keep their keys.
  • literal_brace_rows_match_only_their_message: the row expected {{"dataBase64": "…"}} translates that error with single braces and does not match expected value at line 1 column 1 or expected {"dataBase64"}; a row of more than 3,000 characters without placeholders is found; and in the German catalog plug-in output: expected value at line 1 column 1 keeps its English reason.

Focused checks on the final tree, on Linux. The branch is one commit on main at e8c7421a3d and merges cleanly with it. It conflicts with open #7084 in docs/development.md. It conflicts with open #1532 in crates/ui-egui/src/i18n/catalog.rs.

  • cargo fmt --all -- --check
  • cargo clippy -p vectorcraft-ui-egui --all-targets -- -D warnings
  • cargo check --workspace --tests
  • cargo test -p vectorcraft-ui-egui --lib i18n: 103 passed, 0 failed.
  • cargo xtask brands, layers (30 crates) and assets (337 files).
  • cargo xtask ci runs in this pull request's CI.

cargo xtask ci passed in this pull request's CI run on eb5ecde44a: https://github.com/storytold/vectorcraft/actions/runs/43525568871/job/132230334868. The gate on 88012ed92b is pending.

Checklist

  • cargo xtask ci passes (fmt, clippy, tests, assets, brands, layers, wasm).
  • No panics in shipped code: no unwrap(), expect(), panic!, unreachable!, todo! or
    unimplemented! outside tests, and no unchecked indexing on data from files, parameters or
    messages. Errors are returned as Result and shown to the user
    (why and how).
  • Clean-room: nothing copied from Adobe products or GPL/AGPL code (AGENTS.md).
  • Every new asset has a row in ASSETS.md.

Summary by CodeRabbit

  • Bug Fixes
    • Improved translations for messages containing literal braces, including long messages. These messages now match exact catalog entries with escaped braces, while placeholder-based formatting is preserved.
    • Nested placeholders are translated when an exact catalog entry exists; otherwise, their original text is retained.
  • Documentation
    • Clarified how to represent literal braces in localization entries, including the distinction between plain message text and format strings. Also documented how exact matching applies to messages containing literal braces.

The message scan read every message as a format string, so a plain
literal such as `expected {"dataBase64": "…"}` became the catalog key
`expected {"dataBase64"}`, a template whose placeholder took the rest of
any message starting with "expected". The plug-in output error
`expected value at line 1 column 1` was then shown in German as
"erwartet: value at line 1 column 1".

- The scan doubles every brace of a message that is not a format string
  (a plain literal or a `&str` constant); `format!` strings and
  `#[error(...)]` strings keep their `{{`, `}}` and placeholders.
- 150 `@msg` rows in 23 catalogs are keyed by the full message with its
  braces doubled, and their translations write those braces doubled and
  carry the full JSON example of the English.
- An `@msg` row with no placeholder is an exact message under its text
  with `{{` and `}}` read as braces, so it is found at any length, and a
  nested JSON part translated by such a row shows single braces.
- The Hindi row for `give {key, value} or {values}` translates the whole
  message; it kept the English "or" and left out "give".

Docs: docs/development.md, the zh-hant.tsv header, docs/target-app-parity.md.
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Message extraction and catalog parsing now handle literal braces as escaped catalog-key characters. Nested exact translations process escaped braces once. Tests and documentation describe the extraction rules and exact matching.

Changes: Literal Brace Translation

Layer / File(s) Summary
Literal brace keys
crates/ui-egui/src/i18n/catalog.rs, crates/ui-egui/src/i18n/tests.rs, docs/development.md, docs/target-app-parity.md
Message extraction distinguishes format strings from ordinary literals and constants. Catalog parsing processes every @msg source and stores placeholder-free entries under parsed text. Tests and documentation describe doubled braces and exact matching.
Nested exact translation
crates/ui-egui/src/i18n.rs
Nested values beginning with { or [ use an exact catalog message. The translation is formatted once with no arguments.

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Suggested reviewers: bflatastic, aiongg, mockydevs

Merge Risk: Minimal · up to 82e09

This change makes messages containing literal braces match only their own catalog row. Messages that merely share a row's prefix now stay in English instead of being replaced by that row's translation. The supplied evidence shows no concrete merge-blocking risk, and the change is covered by new tests and CI runs reported by the author.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title "i18n: messages with literal braces match only their own catalog row" matches the main change: plain-literal messages get doubled-brace keys, and brace-containing rows without placeholders a…
Description check Passed The description follows the template. It has a 'What and why' section that explains the scanner fault, the escaped-brace fix, the catalog parsing change, and the linked context (#7047, #7084, #1532). …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "i18n: messages with literal braces match only their own catalog row" matches the main change: plain-literal messages get doubled-brace keys, and brace-containing rows without placeholders are stored as exact messages, so they match only their own message. The title is specific and concise. It omits the scanner change, but it names the primary behavior.

Full details: Description check

Explanation

The description follows the template. It has a 'What and why' section that explains the scanner fault, the escaped-brace fix, the catalog parsing change, and the linked context (#7047, #7084, #1532). It has a 'How it was tested' section with named tests, focused checks, and CI status. The checklist is present. The first checklist item is unchecked and the CI gate on the latest commit is pending, which the author states. The other two checklist items are checked. The template does not require an issue link beyond what is provided, so the description is mostly complete.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

LanguageTool

docs/target-app-parity.md

LanguageTool checks are incomplete because the per-file request limit of 5 was reached. Remaining text was skipped; findings from completed checks are retained.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 88012ed and 82e098d.

Files ignored due to path filters: 24

  • crates/ui-egui/src/i18n/bs.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ca.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/cs.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/de.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/el.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/es.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/fi.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/fr.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/hi.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/hu.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/it.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ja.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ka.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ko.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/nl.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/pl.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/pt-br.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ru.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/sv.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/tr.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/uk.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/vi.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/zh-hans.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/zh-hant.tsv is excluded by !**/*.tsv
Files selected for processing: 2

  • crates/ui-egui/src/i18n/tests.rs
  • docs/target-app-parity.md
Files skipped from review as they are similar to previous changes: 1

  • docs/target-app-parity.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@bflatastic

Copy link
Copy Markdown
Member

Thanks @dotCooCoo, this is approved and CI was green, but main has since changed files this PR touches, so that run no longer covers it. Could you merge main (or rebase) so CI runs on current main? I'll merge it once it's green.

@dotCooCoo

Copy link
Copy Markdown
Contributor Author

@bflatastic Merged main in (eb5ecde).

@bflatastic
bflatastic merged commit 73b9eee into storytold:main Nov 14, 2026
7 checks passed
@bflatastic

Copy link
Copy Markdown
Member

Merged, thanks @dotCooCoo! It passed its CI checks.

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.

2 participants