Skip to content

feat: add shared localization infrastructure (xtask generate-strings)#703

Open
Vineet1101 wants to merge 4 commits intobitcoinppl:masterfrom
Vineet1101:feature/shared-localization
Open

feat: add shared localization infrastructure (xtask generate-strings)#703
Vineet1101 wants to merge 4 commits intobitcoinppl:masterfrom
Vineet1101:feature/shared-localization

Conversation

@Vineet1101
Copy link
Copy Markdown

@Vineet1101 Vineet1101 commented Apr 23, 2026

Add a centralized localization system that generates platform-specific string files from a single JSON source of truth.

New files:

  • localization/strings.json: shared source with 162 strings organized by feature area (common, wallet, send, transaction, etc.)
  • rust/xtask/src/localization.rs: generator that produces iOS Localizable.strings and Android generated_strings.xml
  • ios/Cove/Resources/en.lproj/Localizable.strings: generated iOS strings
  • android/app/src/main/res/values/generated_strings.xml: generated Android strings

Modified:

  • rust/xtask/src/main.rs: wire GenerateStrings command
  • rust/xtask/Cargo.toml: add serde_json dependency
  • justfile: add 'generate-strings' recipe (alias: gs)

Includes 7 unit tests covering iOS/Android flattening, XML/quote escaping, nested keys, and $schema key filtering.

Closes #538 (Phase 1)

Summary

Testing

Platform Coverage

  • Tested on iOS device
  • Tested on Android device
  • Tested on iOS simulator
  • Tested on Android simulator
  • Not tested

Checklist

Summary by CodeRabbit

  • New Features
    • English UI text added across the app: wallet/account flows, import/recovery, send/receive, transactions, UTXO management, settings, alerts, and common actions (Back/Cancel/Copy/Save).
  • Chores
    • Added tooling and a build task to generate and keep platform localizations in sync.
    • Removed a few obsolete UTXO-related labels/strings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a single-source JSON for localization and an xtask generator plus just recipe to produce platform-native localization files; generates ios/.../Localizable.strings and android/.../generated_strings.xml, and removes a few legacy Android string keys.

Changes

Cohort / File(s) Summary
Localization Source
localization/strings.json
Adds the hierarchical JSON single source of truth containing all user-facing strings and schema link.
Xtask generator & CLI
rust/xtask/src/localization.rs, rust/xtask/src/main.rs, rust/xtask/Cargo.toml
New generator module and CLI subcommand generate-strings; adds serde_json dependency and integrates generation into xtask.
Build integration
justfile
Always rebuilds xtask before running; adds generate-strings recipe and private alias gs.
Generated iOS resources
ios/Cove/Resources/en.lproj/Localizable.strings
New generated English .strings file (dot-separated keys) produced from JSON.
Generated Android resources
android/app/src/main/res/values/generated_strings.xml
New generated Android strings.xml (underscore-separated keys) with many new entries.
Android cleanup
android/app/src/main/res/values/strings.xml
Removed legacy keys: sort_date, sort_name, sort_amount, sort_change, and utxo_description.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as "Dev / CI"
    participant Just as "just (gs)"
    participant Xtask as "xtask (generate-strings)"
    participant JSON as "localization/strings.json"
    participant FS as "File System"
    participant iOS as "iOS Localizable.strings"
    participant Android as "Android generated_strings.xml"

    Dev->>Just: run gs / just generate-strings
    Just->>Xtask: invoke generate-strings
    Xtask->>JSON: read & validate strings.json
    Xtask->>Xtask: flatten keys & escape platform chars
    Xtask->>FS: write iOS Localizable.strings
    FS-->>iOS: file created
    Xtask->>FS: write Android generated_strings.xml
    FS-->>Android: file created
    Xtask->>Dev: report emitted string count
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

android

Poem

🐰 I nibble JSON leaves at break of day,
I hop and scatter keys in neat array.
xtask hums a tune and spins them into light,
iOS and Android wear the words just right.
Hooray — strings sprout across the code’s bright hay! 🎋

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: introducing a shared localization infrastructure with xtask command for generating platform-specific strings.
Description check ✅ Passed The description is comprehensive, covering all major changes, new files, modified files, tests included, and related issue reference; author completed required checklists.
Linked Issues check ✅ Passed The PR meets all core coding objectives from #538: centralized JSON source (localization/strings.json), xtask generator for iOS/Android platform files, justfile recipe, unit tests, and incremental migration support.
Out of Scope Changes check ✅ Passed All changes are scoped to localization infrastructure: JSON source, generator logic, platform file generation, xtask integration, and cleanup of deprecated Android strings; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds a centralized localization system: a shared localization/strings.json source of truth and a Rust xtask generate-strings command that produces Localizable.strings (iOS) and generated_strings.xml (Android) from it.

  • The header comment written by the generator (/* Generated from localization/strings.json*/) does not match the header in the committed generated files (/* Generated from localization/strings.json — DO NOT EDIT */), so re-running the tool immediately produces a diff — any freshness check in CI would fail.
  • The referenced strings.schema.json is not committed.

Confidence Score: 4/5

Safe to merge after fixing the two header-string mismatches; the generator will produce dirty diffs otherwise.

Two P1 findings: the iOS and Android header strings in localization.rs don't match the committed generated files. Running generate-strings would immediately produce a diff from HEAD, breaking any CI freshness gate. The fix is one-line each. All other findings are P2.

rust/xtask/src/localization.rs lines 660 and 720–721 (header string literals)

Important Files Changed

Filename Overview
rust/xtask/src/localization.rs New localization generator — two P1 header mismatches cause re-running the tool to produce diffs from committed files; also missing % escaping for Android format specifiers
localization/strings.json New source-of-truth JSON with 162 strings; references a strings.schema.json that is not committed
ios/Cove/Resources/en.lproj/Localizable.strings Generated iOS strings file; header differs from what the generator code would produce today (missing "— DO NOT EDIT")
android/app/src/main/res/values/generated_strings.xml Generated Android strings file; header differs from generator code output; apostrophes correctly escaped with \'
rust/xtask/src/main.rs Wires the new GenerateStrings subcommand — straightforward addition, no issues
justfile Adds generate-strings recipe and gs alias — looks correct
rust/xtask/Cargo.toml Adds serde_json = "1" dependency — appropriate, no issues

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[localization/strings.json\nsingle source of truth] --> B[cargo xtask generate-strings\nrust/xtask/src/localization.rs]
    B --> C[flatten_to_ios\ndot-separated keys]
    B --> D[flatten_to_android\nunderscore-separated keys]
    C --> E[ios/Cove/Resources/en.lproj/\nLocalizable.strings]
    D --> F[android/app/src/main/res/values/\ngenerated_strings.xml]
    G[justfile: generate-strings / gs] --> B
Loading

Comments Outside Diff (3)

  1. rust/xtask/src/localization.rs, line 660 (link)

    P1 Generated header doesn't match committed files

    The header string written by the code (/* Generated from localization/strings.json*/) differs from the header in the committed Localizable.strings (/* Generated from localization/strings.json — DO NOT EDIT */). Running generate-strings today would produce a diff from the committed files, so any "generated files are up-to-date" CI check would immediately fail.

  2. rust/xtask/src/localization.rs, line 720-721 (link)

    P1 Android header mismatch too

    Same issue as the iOS header: the code emits <!-- Generated from localization/strings.json--> but the committed generated_strings.xml has <!-- Generated from localization/strings.json — DO NOT EDIT -->. Re-running the generator will produce a diff.

  3. rust/xtask/src/localization.rs, line 756-763 (link)

    P2 Android XML: > doesn't need escaping in text nodes, and % may need it

    Escaping > in XML text content is not required (only < and & are mandatory), so &gt; adds noise without benefit. More practically, Android's resource compiler treats % as a format specifier placeholder — strings containing a literal % (e.g. "50% complete") should have it escaped as %% to prevent String.format errors at runtime. Neither % nor its escaped form is currently handled. Consider adding .replace('%', "%%") before the other replacements.

Reviews (1): Last reviewed commit: "feat: add shared localization infrastruc..." | Re-trigger Greptile

Comment thread localization/strings.json Outdated
@@ -0,0 +1,207 @@
{
"$schema": "./strings.schema.json",
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 Referenced schema file is not committed

strings.json declares "$schema": "./strings.schema.json", but no strings.schema.json exists in the repository. IDE validation will either error or silently skip schema enforcement. Either commit the schema file or remove the reference until it's ready.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@localization/strings.json`:
- Line 132: Update the localization entry for the "body" key to remove the comma
after "recovery words" so the sentence reads "Whoever has your recovery words
controls your Bitcoin." — locate the "body" string in strings.json and replace
the current sentence fragment "Whoever has your recovery words, controls your
Bitcoin." with the corrected punctuation and wording.
- Line 2: The "$schema" reference in localization/strings.json points to a
missing file ("./strings.schema.json"); fix by either committing/adding the
referenced schema file to the repo (strings.schema.json) or removing the
"$schema" entry from localization/strings.json so the JSON no longer references
a non-existent schema; update localization/strings.json's "$schema" key or add
the schema file accordingly and ensure the name matches exactly.

In `@rust/xtask/src/localization.rs`:
- Around line 55-57: The generated file header written into the mutable String
(see variable output and the output.push_str calls in localization generator)
must be made deterministic and match the committed “— DO NOT EDIT” header;
replace the current push_str content at the header sites with a single constant
header string (no timestamps, no random data) that exactly matches the committed
header (including the “— DO NOT EDIT” line), and ensure the same constant is
reused for the second header location (the other push_str block around the &str
generation) so running just generate-strings won’t change the files.
- Around line 14-16: Replace runtime lookups of CARGO_MANIFEST_DIR with the
compile-time env! macro and centralize it in a small helper: create a fn
manifest_dir() -> &'static str { env!("CARGO_MANIFEST_DIR") } and use
Path::new(manifest_dir()) wherever std::env::var("CARGO_MANIFEST_DIR") is
currently used (e.g., the code that builds json_path and the other occurrences
referenced around lines 45-48 and 103-107), removing the expect() panic call and
anchoring paths with the compile-time string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6d17d5f1-1162-4425-8d61-56bbe0a9a40e

📥 Commits

Reviewing files that changed from the base of the PR and between fc4f6d3 and 2a6bcf2.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • android/app/src/main/res/values/generated_strings.xml
  • ios/Cove/Resources/en.lproj/Localizable.strings
  • justfile
  • localization/strings.json
  • rust/xtask/Cargo.toml
  • rust/xtask/src/localization.rs
  • rust/xtask/src/main.rs

Comment thread localization/strings.json Outdated
Comment thread localization/strings.json
"recoveryWords": {
"title": "Recovery Words",
"secureNote": "Please save these words in a secure location.",
"body": "Your secret recovery words are the only way to recover your wallet if you lose your phone or switch to a different wallet. Whoever has your recovery words, controls your Bitcoin.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the comma in the recovery warning.

This should read more naturally as “Whoever has your recovery words controls your Bitcoin.”

✏️ Proposed copy fix
-    "body": "Your secret recovery words are the only way to recover your wallet if you lose your phone or switch to a different wallet. Whoever has your recovery words, controls your Bitcoin.",
+    "body": "Your secret recovery words are the only way to recover your wallet if you lose your phone or switch to a different wallet. Whoever has your recovery words controls your Bitcoin.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"body": "Your secret recovery words are the only way to recover your wallet if you lose your phone or switch to a different wallet. Whoever has your recovery words, controls your Bitcoin.",
"body": "Your secret recovery words are the only way to recover your wallet if you lose your phone or switch to a different wallet. Whoever has your recovery words controls your Bitcoin.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@localization/strings.json` at line 132, Update the localization entry for the
"body" key to remove the comma after "recovery words" so the sentence reads
"Whoever has your recovery words controls your Bitcoin." — locate the "body"
string in strings.json and replace the current sentence fragment "Whoever has
your recovery words, controls your Bitcoin." with the corrected punctuation and
wording.

Comment thread rust/xtask/src/localization.rs Outdated
Comment thread rust/xtask/src/localization.rs
Add a centralized localization system that generates platform-specific
string files from a single JSON source of truth.

New files:
- localization/strings.json: shared source with 162 strings organized
  by feature area (common, wallet, send, transaction, etc.)
- rust/xtask/src/localization.rs: generator that produces iOS
  Localizable.strings and Android generated_strings.xml
- ios/Cove/Resources/en.lproj/Localizable.strings: generated iOS strings
- android/app/src/main/res/values/generated_strings.xml: generated
  Android strings

Modified:
- rust/xtask/src/main.rs: wire GenerateStrings command
- rust/xtask/Cargo.toml: add serde_json dependency
- justfile: add 'generate-strings' recipe (alias: gs)

Includes 7 unit tests covering iOS/Android flattening, XML/quote
escaping, nested keys, and $schema key filtering.

Closes bitcoinppl#538 (Phase 1)
@Vineet1101 Vineet1101 force-pushed the feature/shared-localization branch from 2a6bcf2 to 0dd7f4c Compare April 23, 2026 17:54
Vineet1101 and others added 3 commits April 24, 2026 06:00
Move sort_date, sort_name, sort_amount, sort_change, and
utxo_description from the hand-written strings.xml into the
generated file. These are now managed by localization/strings.json
as the single source of truth.
The justfile recipe used 'test -f' to skip rebuilding, which ran
stale binaries when source was modified. Now always runs cargo build
(which is a sub-second no-op when nothing changed).
@Vineet1101
Copy link
Copy Markdown
Author

hey @praveenperera can you please review it

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
rust/xtask/src/localization.rs (1)

68-68: Nit: inconsistent em-dash encoding across headers.

Line 68 uses \u{2014} while line 126 uses the literal . Output bytes are identical, but making the source consistent (either both literal or both escaped) avoids future confusion.

♻️ Proposed tweak
-    output.push_str("/* Generated from localization/strings.json \u{2014} DO NOT EDIT */\n\n");
+    output.push_str("/* Generated from localization/strings.json — DO NOT EDIT */\n\n");

Also applies to: 126-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/xtask/src/localization.rs` at line 68, Two header string literals in
localization.rs are inconsistent: one uses the escaped em-dash (\u{2014}) and
another uses the literal ‘—’; update both occurrences of the generated-file
comment (the output.push_str calls that start with "/* Generated from
localization/strings.json") to use the same encoding—either replace \u{2014}
with the literal — in the first occurrence or replace the literal — with
\u{2014} in the other—so both header strings match exactly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/xtask/src/localization.rs`:
- Around line 162-168: The blanket percent-escaping in the localized string
pipeline (the let escaped = s.replace(...).replace('%', '%%') chain) prevents
Android/Java format placeholders like "%1$s" from surviving; change the logic to
only double '%' when the string is not intended to be a format string — e.g. add
a per-key boolean flag (formatted or raw) in the source metadata and, in the
code that computes `escaped`, conditionally apply the `replace('%','%%')` step
only when formatted == false (or alternatively detect format placeholders with a
regex like /%(\d+\$)?[sdf]/ and skip escaping if matched). Update the code that
constructs/reads keys so callers can mark keys that expect String.format-style
placeholders.
- Around line 160-175: The Android escaping routine in the Value::String arm
must first escape backslashes so later replacements (like apostrophe and
newline) don't double-count introduced backslashes; in the function handling
string serialization (the Value::String branch that builds `escaped` and pushes
to `output`), add a `.replace('\\', "\\\\")` as the first replace in the chain
(before .replace('\'', "\\'") and .replace('\n', "\\n")), and add a unit test
(next to test_flatten_android_escapes_apostrophe) that supplies a raw backslash
(e.g., "C:\\path") and asserts the produced Android string contains `\\` to
verify correct escaping.

---

Nitpick comments:
In `@rust/xtask/src/localization.rs`:
- Line 68: Two header string literals in localization.rs are inconsistent: one
uses the escaped em-dash (\u{2014}) and another uses the literal ‘—’; update
both occurrences of the generated-file comment (the output.push_str calls that
start with "/* Generated from localization/strings.json") to use the same
encoding—either replace \u{2014} with the literal — in the first occurrence or
replace the literal — with \u{2014} in the other—so both header strings match
exactly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8fc27d0-c75a-4d15-bdc9-ec3866d15108

📥 Commits

Reviewing files that changed from the base of the PR and between 5bcb20f and 579552d.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • justfile
  • rust/xtask/src/localization.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • justfile

Comment on lines +160 to +175
Value::String(s) => {
// escape XML special characters and Android format specifiers
let escaped = s
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('"', "&quot;")
.replace('\'', "\\'")
.replace('%', "%%")
.replace('\n', "\\n");

output.push_str(&format!(" <string name=\"{prefix}\">{escaped}</string>\n"));
*count += 1;
}
_ => {}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Android escaping is missing backslash handling.

In Android string resources, \ is the escape prefix, so a literal backslash in the source JSON must be emitted as \\. The current chain never escapes \, and then introduces backslashes itself via .replace('\'', "\\'") and .replace('\n', "\\n"). Consequences:

  • A source string containing C:\path emits <string>...C:\path</string> — Android will silently drop the \ (or interpret \p as an unknown escape).
  • If you ever prepend a \\ escape step after the apostrophe/newline steps, the introduced \' / \n tokens would get doubled. So the fix must be the first replacement.
🐛 Proposed fix
             let escaped = s
-                .replace('&', "&amp;")
+                .replace('\\', "\\\\")
+                .replace('&', "&amp;")
                 .replace('<', "&lt;")
                 .replace('"', "&quot;")
                 .replace('\'', "\\'")
                 .replace('%', "%%")
                 .replace('\n', "\\n");

Worth adding a test alongside test_flatten_android_escapes_apostrophe that feeds a raw \ and asserts the output contains \\.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/xtask/src/localization.rs` around lines 160 - 175, The Android escaping
routine in the Value::String arm must first escape backslashes so later
replacements (like apostrophe and newline) don't double-count introduced
backslashes; in the function handling string serialization (the Value::String
branch that builds `escaped` and pushes to `output`), add a `.replace('\\',
"\\\\")` as the first replace in the chain (before .replace('\'', "\\'") and
.replace('\n', "\\n")), and add a unit test (next to
test_flatten_android_escapes_apostrophe) that supplies a raw backslash (e.g.,
"C:\\path") and asserts the produced Android string contains `\\` to verify
correct escaping.

Comment on lines +162 to +168
let escaped = s
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('"', "&quot;")
.replace('\'', "\\'")
.replace('%', "%%")
.replace('\n', "\\n");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Heads-up: blanket %%% blocks Android format strings.

This escapes every % unconditionally, which is safe for non-formatted strings but means keys intended for getString(id, args...) / String.format (e.g. "%1$s sent") can't be expressed in the JSON source — they'll be emitted as %%1$s and printed literally. If phase 1 doesn't need format args this is fine; flagging so the limitation is documented or addressed when you migrate platform strings that do use formatting (e.g. via a formatted="false" attribute or a per-key opt-out).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/xtask/src/localization.rs` around lines 162 - 168, The blanket
percent-escaping in the localized string pipeline (the let escaped =
s.replace(...).replace('%', '%%') chain) prevents Android/Java format
placeholders like "%1$s" from surviving; change the logic to only double '%'
when the string is not intended to be a format string — e.g. add a per-key
boolean flag (formatted or raw) in the source metadata and, in the code that
computes `escaped`, conditionally apply the `replace('%','%%')` step only when
formatted == false (or alternatively detect format placeholders with a regex
like /%(\d+\$)?[sdf]/ and skip escaping if matched). Update the code that
constructs/reads keys so callers can mark keys that expect String.format-style
placeholders.

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.

Better Shared Localization

1 participant