diff --git a/README.md b/README.md
index 340e3be..298cbb1 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,9 @@
> **iOS counterpart:** the sibling [Quantum Coin iOS wallet](https://github.com/quantumcoinproject/quantum-coin-wallet-ios)
> is kept feature-parity with this Android client. Both share the same
> JavaScript SDK bundle byte-for-byte and the same `en_us.json`
-> localization catalog (parity-tested on every build).
+> localization catalog as the canonical reference; the Android-side
+> `JsonInteractParityTest` keeps the key set and accessor surface in
+> lockstep with the iOS catalog.
Native Android client for the [Quantum Coin](https://quantumcoin.org)
post-quantum blockchain. Quantum Coin is a Layer-1 quantum-resistant
@@ -918,11 +920,11 @@ the one shipped by the iOS wallet at
regenerates `GeneratedBundleHash.java` so the constant inside
`classes.dex` stays in lockstep with the shipping bundle bytes.
Wired as a `preBuild` dependency.
-- **`syncIosLocale` Gradle task** — pulls a snapshot of the iOS
- `en_us.json` into `app/src/test/resources/locale-snapshots/`
- before unit tests run, so the
- [`EnUsParityTest`](app/src/test/java/com/quantumcoinwallet/app/locale/EnUsParityTest.java)
- byte-compares both files in CI.
+- **`syncIosImpersonatorFilter` Gradle task** — pulls a snapshot of
+ the iOS `StablecoinImpersonatorFilter.swift` into
+ `app/src/test/resources/code-snapshots/` before unit tests run so
+ the Android-side parity test can byte-compare the two pattern
+ lists in CI. Skip-on-missing for fresh checkouts.
---
@@ -997,7 +999,7 @@ contract documented in
│ ├── webpack.config.js
│ └── src/ Re-export glue around the upstream SDKs
└── app/
- ├── build.gradle App module + embedBundleHash + syncIosLocale tasks
+ ├── build.gradle App module + embedBundleHash + syncIosImpersonatorFilter tasks
├── proguard-rules.pro
└── src/
├── main/
@@ -1172,7 +1174,6 @@ It contains 29 test classes and 183 unit tests:
| `interact/JsonInteractParityTest` | Localization-key presence, accessor wiring, OS-specific divergence wording (root vs jailbreak, Play Store vs App Store, Android Auto Backup vs iCloud) |
| `keystorage/AddNetworkPersistsToStrongboxTest` | Pins that adding a custom network goes through the strongbox `customNetworks` field (not `SharedPreferences`) and survives a relock/unlock round-trip |
| `keystorage/MacUtilTest` | HMAC + HKDF primitives used by the generation-counter, brute-force-lockout binders, and the v=3 file MAC / inner checksum |
-| `locale/EnUsParityTest` | Cross-file byte-comparison between `app/src/main/res/raw/en_us.json` and the iOS snapshot (synced by `:app:syncIosLocale` before tests run) |
| `networking/UrlBuilderHostInvariantTest` | Post-substitution URL host MUST equal the configured `BlockchainNetwork.blockExplorerDomain` (anti-host-pivot) |
| `networking/UrlBuilderLockdownTest` | CI grep guard — fails if a naive `replace("{address}"…)` or `replace("{txhash}"…)` call site reappears anywhere |
| `networking/UrlBuilderTest` | Strict regex acceptance + percent-encoding behavior |
diff --git a/app/build.gradle b/app/build.gradle
index f1adbb4..ad0968a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -267,51 +267,6 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}
-// =================================================================
-// Cross-platform localization snapshot sync
-// =================================================================
-// The :app:syncIosLocale task copies the iOS en_us.json file into
-// app/src/test/resources/locale-snapshots/ios_en_us.json so the
-// Android EnUsParityTest can byte-compare both files at CI time.
-// Source-of-truth for the iOS file is the sibling repo at
-// ../quantum-coin-wallet-ios. The path is configurable via the
-// -PiosRepo=... gradle property for non-default checkouts.
-// Workflow:
-// 1. Open the Android repo and the iOS repo side-by-side.
-// 2. Make a localization change on whichever side is the
-// change source.
-// 3. Run `./gradlew :app:syncIosLocale` from the Android repo.
-// 4. Commit BOTH the iOS edit (in the iOS repo) AND the
-// regenerated snapshot (in the Android repo) so the parity
-// test sees the new state.
-// The task also runs implicitly before unit tests (`testDebugUnitTest`
-// dependsOn) so a stale snapshot does NOT silently pass parity in CI:
-// the snapshot is regenerated from the iOS sibling on every test
-// run when the sibling repo is present, and the parity test fails
-// loudly if it is not.
-// CI hook: a CI job that mounts both repos can run
-// ./gradlew :app:syncIosLocale && ./gradlew :app:testDebugUnitTest
-// and fail the build on any drift.
-tasks.register('syncIosLocale') {
- description = 'Copy iOS en_us.json snapshot into Android test resources for parity test'
- group = 'verification'
-
- doLast {
- def iosRepo = project.findProperty('iosRepo')
- ?: file("${rootDir}/../quantum-coin-wallet-ios").absolutePath
- def src = file("${iosRepo}/QuantumCoinWallet/Resources/en_us.json")
- def dst = file("${projectDir}/src/test/resources/locale-snapshots/ios_en_us.json")
- if (!src.exists()) {
- logger.warn("syncIosLocale: source file not found at ${src.absolutePath}; "
- + "skipping. Pass -PiosRepo=/path/to/quantum-coin-wallet-ios to override.")
- return
- }
- dst.parentFile.mkdirs()
- dst.bytes = src.bytes
- logger.lifecycle("syncIosLocale: snapshot updated -> ${dst.absolutePath}")
- }
-}
-
// =================================================================
// Cross-platform impersonator-filter snapshot sync
// =================================================================
@@ -319,11 +274,14 @@ tasks.register('syncIosLocale') {
// for StablecoinImpersonatorFilter into
// app/src/test/resources/code-snapshots/StablecoinImpersonatorFilter.swift
// so the Android StablecoinImpersonatorFilterTest parity check can
-// byte-compare the two pattern lists at CI time. Same skip-on-missing
-// + auto-runs-before-unit-tests semantics as syncIosLocale; same
-// reasoning. If you change the Android filter PATTERNS list, walk the
-// Swift file too and run :app:syncIosImpersonatorFilter to regenerate
-// the snapshot.
+// byte-compare the two pattern lists at CI time. Source-of-truth for
+// the iOS file is the sibling repo at ../quantum-coin-wallet-ios; the
+// path is configurable via the -PiosRepo=... Gradle property for
+// non-default checkouts. The task is skip-on-missing so a developer
+// without the sibling repo checked out still gets a clean test run.
+// If you change the Android filter PATTERNS list, walk the Swift file
+// too and run :app:syncIosImpersonatorFilter to regenerate the
+// snapshot.
tasks.register('syncIosImpersonatorFilter') {
description = 'Copy iOS StablecoinImpersonatorFilter.swift snapshot into Android test resources for parity test'
group = 'verification'
@@ -346,11 +304,11 @@ tasks.register('syncIosImpersonatorFilter') {
afterEvaluate {
tasks.matching { it.name.startsWith('test') && it.name.endsWith('UnitTest') }.configureEach {
- // Best-effort: pull fresh iOS snapshots before unit tests
- // so the parity checks see current state. The tasks are
- // skip-on-missing (see above), so a developer without the
- // sibling repo checked out still gets a clean test run.
- dependsOn 'syncIosLocale'
+ // Best-effort: pull a fresh impersonator-filter snapshot
+ // before unit tests so the parity check sees current state.
+ // The task is skip-on-missing (see above), so a developer
+ // without the sibling repo checked out still gets a clean
+ // test run.
dependsOn 'syncIosImpersonatorFilter'
}
}
diff --git a/app/src/main/res/raw/en_us.json b/app/src/main/res/raw/en_us.json
index 0068b8f..285d798 100644
--- a/app/src/main/res/raw/en_us.json
+++ b/app/src/main/res/raw/en_us.json
@@ -183,7 +183,7 @@
"no-tokens": "No tokens for this address",
"contract": "Contract",
"symbol": "Symbol",
- "asset-to-send": "Asset to send",
+ "asset-to-send": "What to send?",
"what-is-being-sent": "Which item to send?",
"from-address": "From Address",
"to-address": "To Address",
diff --git a/app/src/test/java/com/quantumcoinwallet/app/locale/EnUsParityTest.java b/app/src/test/java/com/quantumcoinwallet/app/locale/EnUsParityTest.java
deleted file mode 100644
index 6678b42..0000000
--- a/app/src/test/java/com/quantumcoinwallet/app/locale/EnUsParityTest.java
+++ /dev/null
@@ -1,276 +0,0 @@
-package com.quantumcoinwallet.app.locale;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Cross-platform localization parity test (Android side).
- *
Mirrors iOS {@code QuantumCoinWalletTests/EnUsParityTests.swift}.
- * Both sides ship the IDENTICAL English string catalog ({@code en_us.json})
- * except for an explicit, audited allow-list of OS-specific divergences.
- * Drift here is a phishing risk (an attacker can craft a screenshot that
- * looks plausibly cross-platform if the strings disagree) and a test/QA
- * regression vector (acceptance scripts that grep for "Backup to iCloud"
- * suddenly do not match the Android string and tests silently pass).
- *
How this test works:
- *
- * - Both files are parsed by a small line-oriented parser (no Android
- * {@code org.json} mocking required, same approach as
- * {@code JsonInteractParityTest}). We collect the {@code "": ""}
- * pairs inside the {@code langValues} block.
- * - For every key in iOS-langValues, we assert the Android file has
- * the same key. Missing keys = drift.
- * - For every key in Android-langValues, we assert the iOS file has
- * the same key. Same direction matters: an Android-only string is
- * unreachable from any iOS QA path.
- * - For each shared key, we assert the values are byte-identical
- * UNLESS the key appears in {@link #OS_SPECIFIC_DIVERGENCES} (the
- * audited allow-list).
- *
- * The iOS file is snapshotted at
- * {@code app/src/test/resources/locale-snapshots/ios_en_us.json}. The
- * snapshot is regenerated by the {@code :app:syncIosLocale} Gradle task
- * (see {@code app/build.gradle}).
- *
Skip-if-missing semantics: tests assume the snapshot is present.
- * In a fresh checkout the snapshot is committed to the repo so the
- * tests run unconditionally; if a developer deletes the snapshot
- * locally the suite fails fast with a clear message rather than
- * silently passing.
- */
-public class EnUsParityTest {
-
- /**
- * Audited OS-specific divergences. Adding to this list requires
- * a security-review tag in the PR.: each
- * entry is paired with the rationale for why the divergence is
- * permanent rather than a TODO.
- */
- private static final Map OS_SPECIFIC_DIVERGENCES = new HashMap<>();
- static {
- // "Jailbroken" (iOS) vs "Rooted" (Android): platform-specific
- // jailbreak/root vocabulary is the user-facing term of art on
- // each platform and has no cross-platform equivalent.
- OS_SPECIFIC_DIVERGENCES.put("tamper-jailbreak-banner", "rooted-device-vocabulary");
- OS_SPECIFIC_DIVERGENCES.put("tamper-jailbreak-message", "rooted-device-vocabulary");
- // "App Store" (iOS) vs "Play Store" (Android): platform-store
- // names are user-recognizable. Reinstall guidance must point
- // at the right store widget.
- OS_SPECIFIC_DIVERGENCES.put("tamper-runtime-message", "platform-store-name");
- // "iCloud Backup" + "Finder/iTunes" (iOS) vs "Android Auto
- // Backup" + "Smart Switch / device-to-device transfer"
- // (Android): the on-platform backup facilities are different
- // products with different user-visible names. Both messages
- // are accurate for their platform.
- OS_SPECIFIC_DIVERGENCES.put("backup-description", "platform-cloud-backup-name");
- OS_SPECIFIC_DIVERGENCES.put("backup-encrypted-warning", "platform-cloud-backup-name");
- OS_SPECIFIC_DIVERGENCES.put("backup-submitted-cloud-title", "platform-cloud-backup-name");
- OS_SPECIFIC_DIVERGENCES.put("backup-submitted-cloud-message", "platform-cloud-backup-name");
- OS_SPECIFIC_DIVERGENCES.put("cloud-backup-info", "platform-cloud-backup-name");
- }
-
- @Test
- public void everyIosKeyExistsOnAndroid() throws Exception {
- Map ios = readLangValues(snapshot());
- Map android = readLangValues(androidLocale());
- Set missing = new TreeSet<>(ios.keySet());
- missing.removeAll(android.keySet());
- if (!missing.isEmpty()) {
- fail("Android en_us.json is missing iOS langValues keys: "
- + missing
- + ". Add them with the Android-flavored translation OR "
- + "extend the OS_SPECIFIC_DIVERGENCES allow-list.");
- }
- }
-
- @Test
- public void everyAndroidKeyExistsOnIos() throws Exception {
- Map ios = readLangValues(snapshot());
- Map android = readLangValues(androidLocale());
- Set missing = new TreeSet<>(android.keySet());
- missing.removeAll(ios.keySet());
- if (!missing.isEmpty()) {
- fail("iOS en_us.json (snapshot) is missing Android langValues keys: "
- + missing
- + ". Either add them to iOS or remove them from Android. "
- + "If the divergence is permanent, extend the "
- + "OS_SPECIFIC_DIVERGENCES allow-list AND open a follow-up "
- + "ticket to either de-divergence or document the rationale.");
- }
- }
-
- @Test
- public void sharedKeysHaveByteIdenticalValuesOrAreOnAllowlist() throws Exception {
- Map ios = readLangValues(snapshot());
- Map android = readLangValues(androidLocale());
- Set shared = new HashSet<>(ios.keySet());
- shared.retainAll(android.keySet());
- List drifted = new ArrayList<>();
- for (String key : new TreeSet<>(shared)) {
- String i = ios.get(key);
- String a = android.get(key);
- if (i.equals(a)) continue;
- if (OS_SPECIFIC_DIVERGENCES.containsKey(key)) continue;
- drifted.add(key + ": iOS=\"" + i + "\" / Android=\"" + a + "\"");
- }
- if (!drifted.isEmpty()) {
- fail("Cross-platform string drift on " + drifted.size()
- + " key(s):\n - " + String.join("\n - ", drifted)
- + "\nFix the drift OR extend OS_SPECIFIC_DIVERGENCES "
- + "with a documented rationale.");
- }
- }
-
- @Test
- public void iosSnapshotIsPresent() {
- File snap = snapshotPath();
- assertTrue("iOS en_us.json snapshot is missing at " + snap.getAbsolutePath()
- + ". Run `./gradlew :app:syncIosLocale` to regenerate.",
- snap.exists());
- }
-
- @Test
- public void allowlistEntriesAreCovered() throws Exception {
- // Each entry in the allow-list MUST exist in at least one of
- // the two files; a stale allow-list entry is a smell that
- // hides actual drift.
- Map ios = readLangValues(snapshot());
- Map android = readLangValues(androidLocale());
- Set known = new HashSet<>();
- known.addAll(ios.keySet());
- known.addAll(android.keySet());
- List stale = new ArrayList<>();
- for (String key : OS_SPECIFIC_DIVERGENCES.keySet()) {
- if (!known.contains(key)) stale.add(key);
- }
- if (!stale.isEmpty()) {
- fail("Stale entries in OS_SPECIFIC_DIVERGENCES (key not present "
- + "on either platform): " + stale + ". Remove them.");
- }
- }
-
- // -------- file IO + parser ---------
-
- private static String snapshot() throws IOException {
- File f = snapshotPath();
- org.junit.Assume.assumeTrue(
- "iOS en_us.json snapshot missing; run `./gradlew :app:syncIosLocale`.",
- f.exists());
- return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
- }
-
- private static File snapshotPath() {
- return new File("src/test/resources/locale-snapshots/ios_en_us.json");
- }
-
- private static String androidLocale() throws IOException {
- File f = new File("src/main/res/raw/en_us.json");
- org.junit.Assume.assumeTrue("Android en_us.json missing.", f.exists());
- return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
- }
-
- /**
- * Slice the {@code langValues: { ... }} block out of the JSON
- * file with a brace-matching state machine, then extract every
- * top-level {@code "key": "value"} pair. Tolerates trailing
- * commas and nested arrays/objects (which are skipped because
- * we only care about scalar string entries -- arrays / objects
- * are NOT cross-checked for parity at this stage; that is a
- * follow-up).
- */
- static Map readLangValues(String src) {
- int start = src.indexOf("\"langValues\"");
- if (start < 0) return new LinkedHashMap<>();
- int braceOpen = src.indexOf('{', start);
- if (braceOpen < 0) return new LinkedHashMap<>();
- int depth = 0;
- int braceClose = -1;
- for (int i = braceOpen; i < src.length(); i++) {
- char c = src.charAt(i);
- if (c == '{') depth++;
- else if (c == '}') {
- depth--;
- if (depth == 0) { braceClose = i; break; }
- }
- }
- if (braceClose < 0) return new LinkedHashMap<>();
- String body = src.substring(braceOpen + 1, braceClose);
- Map out = new LinkedHashMap<>();
- // Match top-level "key": "value" pairs. Allow escaped
- // double quotes inside the value via the (?:\\"|[^"])* run.
- Pattern p = Pattern.compile(
- "\"([^\"\\\\]+)\"\\s*:\\s*\"((?:\\\\\"|\\\\\\\\|[^\"])*)\"");
- Matcher m = p.matcher(body);
- while (m.find()) {
- out.put(m.group(1), unescape(m.group(2)));
- }
- return out;
- }
-
- private static String unescape(String s) {
- StringBuilder b = new StringBuilder(s.length());
- for (int i = 0; i < s.length(); i++) {
- char c = s.charAt(i);
- if (c == '\\' && i + 1 < s.length()) {
- char n = s.charAt(++i);
- switch (n) {
- case 'n': b.append('\n'); break;
- case 't': b.append('\t'); break;
- case 'r': b.append('\r'); break;
- case '"': b.append('"'); break;
- case '\\': b.append('\\'); break;
- case '/': b.append('/'); break;
- default: b.append('\\').append(n); break;
- }
- } else {
- b.append(c);
- }
- }
- return b.toString();
- }
-
- @Test
- public void parser_canRoundTripBasicShapes() {
- // Sanity-check the parser so a regression in this test is
- // independently observable.
- String src = "{\n \"langValues\": {\n"
- + " \"hello\": \"world\",\n"
- + " \"escape\": \"a\\\"b\"\n"
- + " }\n}";
- Map kv = readLangValues(src);
- assertEquals(2, kv.size());
- assertEquals("world", kv.get("hello"));
- assertEquals("a\"b", kv.get("escape"));
- }
-
- /** Exposed for ad-hoc debugging. */
- static List sortedKeys(Map m) {
- return new ArrayList<>(new TreeSet<>(m.keySet()));
- }
-
- @SuppressWarnings("unused")
- private static String csv(Set s) {
- return String.join(", ", new TreeSet<>(s));
- }
-
- @SuppressWarnings("unused")
- private static String join(String[] arr) { return Arrays.toString(arr); }
-}
diff --git a/app/src/test/resources/locale-audit/HARDCODED_INVENTORY.md b/app/src/test/resources/locale-audit/HARDCODED_INVENTORY.md
deleted file mode 100644
index f34ade7..0000000
--- a/app/src/test/resources/locale-audit/HARDCODED_INVENTORY.md
+++ /dev/null
@@ -1,165 +0,0 @@
-# Hardcoded user-facing strings — follow-up inventory
-
-This document tracks user-facing English literals that remain in
-Java code after the "move error-messages to `en_us.json`" sweep. It
-is a follow-up backlog, not a list of bugs: every entry below has a
-deliberate reason it was deferred (cross-platform parity coupling,
-defense-in-depth fallback, or a non-translatable token).
-
-The runtime localization source of truth is
-[`app/src/main/res/raw/en_us.json`](../../../main/res/raw/en_us.json).
-Strings in its `langValues` block are **byte-compared with the iOS
-sibling repo** by
-[`EnUsParityTest`](../../java/com/quantumcoinwallet/app/locale/EnUsParityTest.java);
-the `errors` block is Android-only and was the target of the
-preceding sweep. Adding any new `langValues` key REQUIRES a
-matching edit in the iOS repo (or an entry in
-`OS_SPECIFIC_DIVERGENCES`).
-
-The audit was last run against the tree containing the lockout /
-network-add / reveal-wallet localizations. Line numbers may drift;
-treat each row as a starting point, not a precise pointer.
-
----
-
-## Group A — pure literals (deferred)
-
-Each requires either an iOS-side coordinated `langValues` add OR a
-permanent allow-list entry. Filed here so a future pass can land
-them in one batch rather than as ad-hoc one-off changes.
-
-### Tamper / security gate
-- [`TamperGatePolicy.java`](../../../main/java/com/quantumcoinwallet/app/security/TamperGatePolicy.java)
- — `title = "Security check"`, `continueLabel = "Continue at my own risk"`, and the
- three body paragraphs in `describe(Severity)` (ROOT_SUSPECTED,
- DEBUGGER_ATTACHED_IN_RELEASE, RUNTIME_TAMPER_DETECTED, default).
- The catalog already has `tamper-*-banner / -message / -title`
- keys but the dialog body is hand-rolled; the catalog wording is
- shorter than the in-Java prose. Choice for the future pass:
- either (a) shorten the in-Java prose to match the catalog and
- drop the literals or (b) add a parallel set of
- `tamper-*-long-message` keys on both platforms.
-
-### Permission prompt defaults
-- [`GlobalMethods.java`](../../../main/java/com/quantumcoinwallet/app/utils/GlobalMethods.java)
- — `Permission required` (default dialog title) and `Open Settings`
- (default positive button). Not in the catalog on either platform.
- Both are Android-flavored ("Settings" is the Android in-app
- navigation target); a future pass should add
- `permission-required-title` / `open-settings-button` as
- `langValues` keys on both platforms, OR allow-list them as
- Android-only.
-
-### Backup executor fallbacks
-- [`BackupExecutor.java`](../../../main/java/com/quantumcoinwallet/app/backup/BackupExecutor.java)
- — `safe()`-shaped fallbacks for `Backup failed: …`,
- `Wallet backed up`, `Backup submitted`,
- `Backup submitted to cloud`, `Wallet submitted to cloud
- destination. Upload may take a moment.`, `Wallet exported`,
- `Export failed: …`, `Press OK to dismiss.`, and the
- `cloud-backup-info` fallback at line 142. The catalog already
- exposes `backup-saved`, `backup-failed`, `backup-submitted-cloud-*`
- and `cloud-backup-info`; these fallbacks fire only on a missing
- key — they are defense-in-depth. Low-priority follow-up: audit
- that all six template keys are present on iOS, then collapse
- these fallbacks to a single tight English string.
-
-### Error-title literals on exception paths
-- [`HomeWalletFragment.java`](../../../main/java/com/quantumcoinwallet/app/view/fragment/HomeWalletFragment.java)
- — `"Error"` literal title at lines 880, 1039, 1671 (passed
- through `ExceptionError`), 2021, etc. The catalog already has
- `errorTitle`. Each is a one-line `safe(vm.getErrorTitleByLangValues(),
- "Error")` substitution; deferred because there are ~10 sites and
- they all funnel through the same dialog helper. A future pass
- could either (a) replace the literals in place or (b) move the
- fallback logic into `GlobalMethods.ShowErrorDialog` so callers
- pass `null` and the helper substitutes.
-
-### Restore-summary column labels
-- [`HomeWalletFragment.java`](../../../main/java/com/quantumcoinwallet/app/view/fragment/HomeWalletFragment.java)
- lines ~3444–3461 — `Status`, `Address`, `Restored`,
- `Already exists`, `Skipped`. These match the
- `restore-summary-status-column` / `-address-column` /
- `-status-restored` / `-status-skipped` / `-status-already-exists`
- keys already in the catalog; they fire only on a catalog miss.
- Same defense-in-depth pattern; no functional bug.
-
-### Send-success and restore-progress fallbacks
-- [`HomeWalletFragment.java`](../../../main/java/com/quantumcoinwallet/app/view/fragment/HomeWalletFragment.java)
- lines ~3387, ~3640, ~3655, ~3659 — fallbacks for
- `[COUNT] wallet(s) were restored. Enter password for the
- remaining.`, `Unable to decrypt. Enter a different password or
- skip this file.`, `The wallet with following address already
- exists:\n[ADDRESS]`, `OK`. All keys exist in the catalog; same
- defense-in-depth pattern.
-
-### Accessibility / brand literals (intentionally English-only)
-- [`BackupPasswordDialog.java:45`](../../../main/java/com/quantumcoinwallet/app/view/dialog/BackupPasswordDialog.java)
- — `Show or hide password` content description. Documented as
- intentionally English-only; mirrors the rationale on
- `strings.xml password_toggle_content_description` (Android
- `TextInputLayout` does not support runtime relabeling of the
- static `passwordToggleContentDescription` attribute).
-- [`TransactionReviewDialog.java:246`](../../../main/java/com/quantumcoinwallet/app/view/dialog/TransactionReviewDialog.java)
- — `i agree` literal English fallback for the agree-gate
- validation. The dialog accepts EITHER the localized
- `i-agree-literal` OR the English literal so a partially
- translated bundle never permanently blocks the user; deliberate.
-- [`SendFragment.java:1360`](../../../main/java/com/quantumcoinwallet/app/view/fragment/SendFragment.java)
- — `QuantumCoin` brand literal (asset name). Non-translatable.
-- [`AccountTransactionsFragment.java:161-162`](../../../main/java/com/quantumcoinwallet/app/view/fragment/AccountTransactionsFragment.java)
- — `<` and `>` pagination glyphs. Non-translatable.
-
----
-
-## Group B — `safe(vm.get*ByLangValues(), "English fallback")`
-
-These are defense-in-depth fallbacks: the catalog key DOES exist
-on both platforms, but the call site keeps a hardcoded English
-literal as the catalog-miss fallback. Acceptable as-is; flagged
-only so a future pass can decide whether to collapse them.
-
-Approximate sites (line numbers may drift):
-- [`BackupPasswordDialog.java`](../../../main/java/com/quantumcoinwallet/app/view/dialog/BackupPasswordDialog.java)
- — ~25 `safe()` calls covering `Backup password`, `Password`,
- `Confirm password`, `OK`, `Cancel`, `Error`,
- `Enter a password`, `Wallets to restore:`, etc.
-- [`TransactionReviewDialog.java`](../../../main/java/com/quantumcoinwallet/app/view/dialog/TransactionReviewDialog.java)
- — `Please review your transaction request to be sent:`,
- `What is being sent?`, `Contract address:`, `From Address`,
- `To Address`, `Send quantity`, `chain`, `Network`, `Type `,
- `I agree`, ` to confirm:`, `Cancel`, `OK`, `Error`,
- `Please type "…" to confirm.` (compound interpolation).
-- [`TamperGatePolicy.java:84`](../../../main/java/com/quantumcoinwallet/app/security/TamperGatePolicy.java)
- — `safe(vm.getCloseByLangValues(), "Close app")`.
-- [`SettingsFragment.java`](../../../main/java/com/quantumcoinwallet/app/view/fragment/SettingsFragment.java)
- — `Phone Backup`, `Enabled`, `Disabled`.
-
----
-
-## Group C — dynamic / non-localizable
-
-- `e.getMessage()` exception passthroughs (every
- `GlobalMethods.ExceptionError` call). Localization is not
- meaningful; the surfaced string is whatever the underlying
- library produced.
-- `R.string.*` toasts in
- [`SendFragment.java`](../../../main/java/com/quantumcoinwallet/app/view/fragment/SendFragment.java)
- (lines 376, 396, 1126, 1185, 1548, 1595) and
- [`HomeActivity.java`](../../../main/java/com/quantumcoinwallet/app/view/activities/HomeActivity.java)
- (lines 410, 413, 416, 419, 422, 443, 444, 449, 450, 1126,
- 1386, 1426, 1427, 1430, 1439, 1440) — these go through
- Android resources, not `en_us.json`. The two pipelines are
- intentional: notification-channel labels, retry-layout error
- strings, network-error toasts (HTTP `4xx`) use `strings.xml`,
- while UI screens use `en_us.json` (parity-checked with iOS).
- See README "Source of truth for strings" notes.
-
----
-
-## Out-of-scope: bridge identifiers
-
-Strings like `INVALID_ADDRESS` / `BAD_AMOUNT` returned by
-`QuantumCoinJSBridge` to the WebView are protocol identifiers, not
-user-facing UI. They are translated to user copy inside the
-JavaScript bundle. Do NOT localize these on the Java side.
diff --git a/app/src/test/resources/locale-snapshots/ios_en_us.json b/app/src/test/resources/locale-snapshots/ios_en_us.json
deleted file mode 100644
index ed28512..0000000
--- a/app/src/test/resources/locale-snapshots/ios_en_us.json
+++ /dev/null
@@ -1,245 +0,0 @@
- {
- "infoStep": "Welcome, info [STEP] OF [TOTAL_STEPS]",
- "info": [
- {
- "title": "Important, Please Read!",
- "desc": "You are about to create a quantum resistant wallet. Do not send Ethereum or other coins or tokens to this wallet address!"
- },
- {
- "title": "Passwords",
- "desc": "Make sure that you use a strong, long password and do not forget it!"
- },
- {
- "title": "Backups",
- "desc": "Backup your wallet and keep it safe offline. Keep at least three copies of your wallets in different places offline."
- },
- {
- "title": "Wallet Safety",
- "desc": "Do not share your wallet or passwords with anyone. Scammers often offer to help but will steal your wallet!"
- },
- {
- "title": "Email Safety",
- "desc": "Do not reply to or open attachments, links or images in emails that talks about Quantum Coin, tokens or wallets. They are most likely scams that can result in your coins and tokens getting stolen!"
- },
- {
- "title": "Social Media and Messaging Apps Safety",
- "desc": "Do not respond to social media and messaging app direct-messages from community members or anyone in general talking about Quantum Coin. They are most likely scammers who can trick you and steal your coins or tokens!"
- }
- ],
- "quizStep": "Safety Quiz [STEP] OF [TOTAL_STEPS]",
- "quizWrongAnswer": "Your answer is wrong! Please read the question carefully and try again.",
- "quizNoChoice": "Please select an option.",
- "quiz": [
- {
- "title": "Coin Type",
- "question": "What coins or tokens can you send to this wallet?",
- "choices": [
- "Ethereum",
- "DogeP ERC20 Tokens",
- "Quantum Coin Mainnet",
- "All the above"
- ],
- "correctChoice": 3,
- "afterQuizInfo": "This is correct. You should be send only Quantum Coins to this wallet."
- },
- {
- "title": "Lost wallet or password",
- "question": "If you lost your wallet or password, who can help recover?",
- "choices": [
- "Quantum Coin Community",
- "Customer Support Team",
- "Dev",
- "No one"
- ],
- "correctChoice": 4,
- "afterQuizInfo": "This is correct. No one can help you recover your wallet or password."
- }
- ],
- "langValues": {
- "title": "Quantum Coin (Q)",
- "next": "Next",
- "ok": "Ok",
- "cancel": "Cancel",
- "close": "Close",
- "send": "Send",
- "receive": "Receive",
- "transactions": "Transactions",
- "copy": "Copy",
- "balance": "Balance",
- "completed-transactions": "Completed Transactions",
- "pending-transactions": "Pending Transactions",
- "wallets": "Wallets",
- "settings": "Settings",
- "unlock": "Unlock",
- "unlock-wallet": "Unlock Wallet",
- "select-network": "Select Network",
- "enter-a-password": "Enter a password",
- "password": "Password",
- "set-wallet-passowrd": "Set Wallet Password",
- "use-strong-password": "Use a strong and long password. And do not forget it!",
- "retype-password": "Retype Password",
- "retype-the-password": "Retype the password",
- "create-restore-wallet": "Create or Restore Quantum Wallet",
- "select-an-option": "Select an option",
- "create-new-wallet": "Create New Quantum Wallet",
- "restore-wallet-from-seed": "Restore A Quantum Wallet From Seed Words",
- "seed-words": "Seed Words",
- "seed-words-info-1": "1. Ensure that no one is looking at the screen other than you.",
- "seed-words-info-2": "2. Ensure that there is no camera pointed at this screen, including from your phone.",
- "seed-words-info-3": "3. You should save the seed words safely offline and keep multiple copies in a trustworthy and safe location.",
- "seed-words-info-4": "4. If these seed words are stolen or someone else gets access to them, your wallet is compromised.",
- "seed-words-show": "5. Click here to reveal the seed words.",
- "verify-seed-words": "Verify Seed Words",
- "waitWalletSave": "Please wait while your wallet is being saved with strong encryption. This can take upto a minute or so to complete...",
- "waitWalletOpen": "Please wait while your wallet is being decrypted and opened. This can take upto a minute...",
- "waitUnlock": "Please wait while your wallet is being decrypted and unlocked. This can take upto a minute.",
- "wait-opening-picker": "Please wait, opening picker...",
- "status-verifying": "Verifying...",
- "strongbox-degraded-banner": "Wallet integrity check recovered from a backup slot. Please create a fresh backup soon.",
- "dpscan": "Block Explorer",
- "address": "Address",
- "coins": "Coins",
- "reveal-seed": "Reveal Seed",
- "networks": "Networks",
- "id": "Network ID",
- "name": "Name",
- "scan-api-url": "Scan API URL",
- "rpc-endpoint": "RPC Endpoint",
- "block-explorer-url": "Block Explorer URL",
- "add-network": "Add Network",
- "no-active-network": "There is no active network. Add and select a network from Settings.",
- "help": "Help",
- "block-explorer-title": "Block Explorer",
- "add": "Add",
- "enter-network-json": "Enter Blockchain Network JSON",
- "enter-quantum-wallet-password": "Enter your quantum wallet password",
- "network": "Network",
- "address-to-send": "To address",
- "quantity-to-send": "Quantity",
- "receive-coins": "Receive Coins",
- "send-only": "Send only Quantum coins to this address!",
- "inout": "In/Out",
- "no-more-transactions": "There are no more transactions to show.",
- "from": "From",
- "to": "To",
- "hash": "Hash",
- "select-wallet-type": "Select Wallet Type",
- "wallet-type-default": "Default",
- "wallet-type-advanced": "Advanced (20 times higher gas cost)",
- "select-seed-word-length": "How many seed words do you have?",
- "seed-length-32": "32 words (A1 to H4)",
- "seed-length-36": "36 words (A1 to I4)",
- "seed-length-48": "48 words (A1 to L4)",
- "copied": "Copied",
- "back": "Back",
- "confirm-wallet": "Confirm Wallet",
- "confirm-wallet-description": "Check your wallet address. If this is not the correct address, you may press back to review and edit the seed words.",
- "enter-seed-words": "Enter Seed Words",
- "skip": "Skip",
- "skip-verify-confirm": "Are you sure you want to skip verification? It is recommended that you verify.",
- "yes": "Yes",
- "no": "No",
- "errorOccurred": "An error occurred: ",
- "errorTitle": "Error",
- "signing": "Advanced Signing",
- "advanced-signing-option": "Advanced signing",
- "advanced-signing-description": "Applicable wallets will incur 30 times higher gas price if this setting is enabled",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "backup": "Backup",
- "backup-prompt": "Do you want to allow wallets to be backed up as part of phone backups?",
- "backup-description": "When enabled, encrypted wallet data will be included in iCloud Backup and unencrypted Finder backups, so you can restore on a new device using your password. When disabled, the wallet file is excluded from these backups; note that ENCRYPTED Finder/iTunes backups still include all app data due to platform behavior. This setting does not affect wallet files you explicitly export or save to iCloud Drive.",
- "backup-encrypted-warning": "Important: choosing \"No\" excludes the wallet file from iCloud Backup and unencrypted Finder backups, but ENCRYPTED Finder/iTunes backups always include all app data. The wallet file remains encrypted with your password regardless of this setting.",
- "seed-accessibility-summary": "Seed phrase is displayed on screen. Use the Copy button to copy it.",
- "seed-hidden-for-capture": "Seed phrase hidden because the screen is being recorded or mirrored. Stop screen recording or mirroring to view the seed.",
- "address-checksum-warning": "This address does not match its expected checksum form. Double-check that the address is correct before sending.",
- "phone-backup": "Phone Backup",
- "backup-saved": "Wallet exported to [FOLDER]/[FILENAME]",
- "backup-submitted-cloud-title": "Backup submitted to iCloud",
- "backup-submitted-cloud-message": "Your wallet has been written to [FOLDER]/[FILENAME] and submitted to iCloud for upload. iCloud uploads finish in the background and may take time depending on your network. The backup is NOT yet fully durable in the cloud — keep this device powered on and connected until iCloud finishes the upload. You can check upload status in the Files app.",
- "backup-failed": "Failed to export wallet: [ERROR]",
- "backup-password": "Backup password",
- "confirm-backup-password": "Confirm password",
- "restore-from-cloud": "Restore from Cloud",
- "restore-from-file": "Restore from File",
- "restore-decrypt-failed": "Unable to decrypt. Enter a different password or skip this file.",
- "restore-enter-different-password": "Enter a different password",
- "restore-no-backups-found": "No backup files were found in the selected folder.",
- "restore-password-prompt-remaining": "Wallets to restore:",
- "restore-summary-status-column": "Status",
- "restore-summary-address-column": "Address",
- "restore-summary-status-restored": "Restored",
- "restore-summary-status-skipped": "Skipped",
- "restore-summary-status-already-exists": "Already exists",
- "restore-try-different-password": "Unable to decrypt any wallet with that password. Try with a different password.",
- "restore-strongbox-write-failed": "The backup file decrypted successfully, but it could not be saved to your device's wallet store. The wallet password you entered earlier does not match the password protecting this device. Cancel and start the restore again, then enter the device wallet password when asked.",
- "restore-progress-of": "[CURRENT] of [TOTAL]",
- "restore-partial-progress": "[COUNT] wallet(s) were restored. Enter password for the remaining.",
- "restore-wallets-decrypting": "Please wait while your wallet(s) are being decrypted and restored; this can take many minutes.",
- "camera-permission-denied": "Camera access has been blocked. Open Settings and grant the Camera permission to scan QR codes.",
- "backup-to-cloud": "Backup to cloud",
- "cloud-backup-info": "Pick a folder in iCloud Drive (or any cloud-synced folder) to use for wallet backups. Tap Open after selecting the folder; future backups will be saved there automatically. iCloud Drive is available from the picker sidebar.",
- "backup-to-file": "Backup to a file",
- "backup-done": "Done",
- "backup-saved-short": "Saved",
- "backup-options-title": "Backup your wallet",
- "backup-options-description": "Save an encrypted backup now. You can save to cloud and to a file (you can do both, one after the other). Tap Done when you're finished.",
- "enter-backup-password-title": "Enter password of the backup",
- "wallet-already-exists-detailed": "The wallet with following address already exists:\n[ADDRESS]",
- "no-transactions": "No transactions yet",
- "tokens": "Tokens",
- "tokens-tab": "Tokens",
- "unrecognized-tokens-tab": "Unrecognized Tokens",
- "show-unrecognized-tokens": "Show Unrecognized Tokens",
- "contract-address": "Contract address:",
- "no-tokens": "No tokens for this address",
- "contract": "Contract",
- "symbol": "Symbol",
- "decimals": "Decimals",
- "asset-to-send": "Asset to send",
- "what-is-being-sent": "Which item to send?",
- "from-address": "From Address",
- "to-address": "To Address",
- "send-quantity": "Quantity",
- "chain-id-suffix": "chain",
- "tamper-jailbreak-title": "Reduced device protection",
- "tamper-jailbreak-message": "This device shows signs of jailbreak. The OS-level isolation that protects your wallet is bypassed - apps you trust can be modified by other apps you have installed. Continue at your own risk, or quit?",
- "tamper-continue-at-risk": "Continue at my own risk",
- "tamper-quit": "Quit",
- "tamper-ignore-and-resume": "Ignore and resume",
- "tamper-jailbreak-banner": "Jailbroken device - reduced protection",
- "tamper-debugger-title": "Debugger detected",
- "tamper-debugger-message": "A debugger is attached to this app. We strongly recommend you exit. If you understand the risk, you can ignore this warning and continue.",
- "tamper-debugger-banner": "Debugger detected - reduced protection",
- "tamper-runtime-title": "Tampering detected",
- "tamper-runtime-message": "This wallet's signing module has been modified. We strongly recommend you exit and reinstall from the App Store. If you understand the risk, you can ignore this warning and continue.",
- "tamper-runtime-banner": "Tampering detected - reduced protection",
- "review-transaction-prompt": "Please review your transaction request to be sent:",
- "type-i-agree-to-confirm": "Type ",
- "type-i-agree-to-confirm-suffix": " to confirm:",
- "i-agree-literal": "i agree",
- "must-agree-to-submit": "You have to agree to submit the transaction.",
- "decrypting-wallet": "Please wait while decrypting wallet...",
- "submitting-transaction": "Please wait while your transaction is being submitted.",
- "transaction-sent": "Your transaction request has been sent.",
- "transaction-id": "Transaction ID",
- "transaction-message-exits": "A transaction request is already in progress.",
- "show-password": "Show password",
- "hide-password": "Hide password"
- },
- "errors": {
- "selectOption": "Please select an option.",
- "retypePasswordMismatch": "Retype the password correctly",
- "passwordSpec": "Enter a minimum of 12 characters for password",
- "passwordSpace": "Password cannot start or end with spaces",
- "walletPasswordMismatch": "The password does not match what you entered earlier.",
- "invalidNetworkJson": "The JSON is invalid.",
- "enterAmount": "Please enter the amount to send correctly.",
- "quantumAddr": "Please enter a valid address.",
- "wallet-password-not-set": "Wallet password is not set.",
- "emptyPassword": "Please enter password",
- "seed-word-empty": "Please enter the seed word in [LABEL].",
- "seed-word-invalid": "The word in [LABEL] is not a valid seed word.",
- "seed-word-mismatch": "The word in [LABEL] does not match the original seed word."
- }
-}