From c9be3ec3341f873f39260abe40242c837b3bbced Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 07:23:34 -0300 Subject: [PATCH 1/4] fix: localize shop category titles Co-Authored-By: Claude Opus 5 (1M context) --- .../to/bitkit/models/BitrefillCategory.kt | 54 ++++++++++--------- .../shop/shopDiscover/ShopDiscoverScreen.kt | 5 +- app/src/main/res/values/strings.xml | 22 ++++++++ changelog.d/next/715.fixed.md | 1 + 4 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 changelog.d/next/715.fixed.md diff --git a/app/src/main/java/to/bitkit/models/BitrefillCategory.kt b/app/src/main/java/to/bitkit/models/BitrefillCategory.kt index b1007ad89d..45dc29e16d 100644 --- a/app/src/main/java/to/bitkit/models/BitrefillCategory.kt +++ b/app/src/main/java/to/bitkit/models/BitrefillCategory.kt @@ -1,5 +1,6 @@ package to.bitkit.models +import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CardGiftcard import androidx.compose.material.icons.filled.Checkroom @@ -22,39 +23,44 @@ import androidx.compose.material.icons.filled.Storefront import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.icons.filled.VideogameAsset import androidx.compose.ui.graphics.vector.ImageVector +import to.bitkit.R /** * Represents the categories for purchases. * - * @property title The display name of the category. + * @property titleRes The string resource of the category display name. * @property route The navigation route associated with the category. * @property icon The visual icon for the category. */ enum class BitrefillCategory( - val title: String, + @StringRes val titleRes: Int, val route: String, val icon: ImageVector ) { - APPAREL("Apparel", "buy/apparel", Icons.Filled.Checkroom), - AUTOMOBILES("Automobiles", "buy/automobiles", Icons.Filled.DirectionsCar), - CRUISES("Cruises", "buy/cruises", Icons.Filled.DirectionsBoat), - ECOMMERCE("Ecommerce", "buy/ecommerce", Icons.Filled.ShoppingCart), - ELECTRONICS("Electronics", "buy/electronics", Icons.Filled.Print), - ENTERTAINMENT("Entertainment", "buy/entertainment", Icons.Filled.Headphones), - EXPERIENCES("Experiences", "buy/experiences", Icons.Filled.Public), - FLIGHTS("Flights", "buy/flights", Icons.Filled.Flight), - FOOD("Food", "buy/food", Icons.Filled.Storefront), - FOOD_DELIVERY("Food Delivery", "buy/food-delivery", Icons.Filled.DeliveryDining), - GAMES("Games", "buy/games", Icons.Filled.VideogameAsset), - GIFTS("Gifts", "buy/gifts", Icons.Filled.CardGiftcard), - GROCERIES("Groceries", "buy/groceries", Icons.Filled.ShoppingBag), - HEALTH_AND_BEAUTY("Health & Beauty", "buy/health-beauty", Icons.Filled.FavoriteBorder), - HOME("Home", "buy/home", Icons.Filled.Home), - MULTI_BRAND("Multi-Brand", "buy/multi-brand", Icons.Filled.Layers), - PETS("Pets", "buy/pets", Icons.Filled.Pets), - RESTAURANTS("Restaurants", "buy/restaurants", Icons.Filled.Restaurant), - RETAIL("Retail", "buy/retail", Icons.Filled.Storefront), - STREAMING("Streaming", "buy/streaming", Icons.Filled.Videocam), - TRAVEL("Travel", "buy/travel", Icons.Filled.Flight), - VOIP("VoIP", "buy/voip", Icons.Filled.Phone) + APPAREL(R.string.other__shop__categories__apparel, "buy/apparel", Icons.Filled.Checkroom), + AUTOMOBILES(R.string.other__shop__categories__automobiles, "buy/automobiles", Icons.Filled.DirectionsCar), + CRUISES(R.string.other__shop__categories__cruises, "buy/cruises", Icons.Filled.DirectionsBoat), + ECOMMERCE(R.string.other__shop__categories__ecommerce, "buy/ecommerce", Icons.Filled.ShoppingCart), + ELECTRONICS(R.string.other__shop__categories__electronics, "buy/electronics", Icons.Filled.Print), + ENTERTAINMENT(R.string.other__shop__categories__entertainment, "buy/entertainment", Icons.Filled.Headphones), + EXPERIENCES(R.string.other__shop__categories__experiences, "buy/experiences", Icons.Filled.Public), + FLIGHTS(R.string.other__shop__categories__flights, "buy/flights", Icons.Filled.Flight), + FOOD(R.string.other__shop__categories__food, "buy/food", Icons.Filled.Storefront), + FOOD_DELIVERY(R.string.other__shop__categories__food_delivery, "buy/food-delivery", Icons.Filled.DeliveryDining), + GAMES(R.string.other__shop__categories__games, "buy/games", Icons.Filled.VideogameAsset), + GIFTS(R.string.other__shop__categories__gifts, "buy/gifts", Icons.Filled.CardGiftcard), + GROCERIES(R.string.other__shop__categories__groceries, "buy/groceries", Icons.Filled.ShoppingBag), + HEALTH_AND_BEAUTY( + R.string.other__shop__categories__health_and_beauty, + "buy/health-beauty", + Icons.Filled.FavoriteBorder, + ), + HOME(R.string.other__shop__categories__home, "buy/home", Icons.Filled.Home), + MULTI_BRAND(R.string.other__shop__categories__multi_brand, "buy/multi-brand", Icons.Filled.Layers), + PETS(R.string.other__shop__categories__pets, "buy/pets", Icons.Filled.Pets), + RESTAURANTS(R.string.other__shop__categories__restaurants, "buy/restaurants", Icons.Filled.Restaurant), + RETAIL(R.string.other__shop__categories__retail, "buy/retail", Icons.Filled.Storefront), + STREAMING(R.string.other__shop__categories__streaming, "buy/streaming", Icons.Filled.Videocam), + TRAVEL(R.string.other__shop__categories__travel, "buy/travel", Icons.Filled.Flight), + VOIP(R.string.other__shop__categories__voip, "buy/voip", Icons.Filled.Phone) } diff --git a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt index 54336975eb..6a243fe7a5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/shop/shopDiscover/ShopDiscoverScreen.kt @@ -194,12 +194,13 @@ private fun ShopTabContent( } items(items = BitrefillCategory.entries.toList(), key = { it.name }) { item -> + val title = stringResource(item.titleRes) Column { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .clickableAlpha { - navigateWebView(item.route, item.title) + navigateWebView(item.route, title) } .padding(top = 8.5.dp, bottom = 10.5.dp) ) { @@ -218,7 +219,7 @@ private fun ShopTabContent( ) } BodyM( - text = item.title, + text = title, modifier = Modifier .weight(1f) .padding(horizontal = 8.dp) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80e2fbddc7..b51acb18f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -569,6 +569,28 @@ Decoding Error Unable To Interpret Provided Data Share QR Code Via + Apparel + Automobiles + Cruises + Ecommerce + Electronics + Entertainment + Experiences + Flights + Food + Food Delivery + Games + Gifts + Groceries + Health & Beauty + Home + Multi-Brand + Pets + Restaurants + Retail + Streaming + Travel + VoIP Go borderless ESims Shop with Bitcoin diff --git a/changelog.d/next/715.fixed.md b/changelog.d/next/715.fixed.md new file mode 100644 index 0000000000..304bc1cc7f --- /dev/null +++ b/changelog.d/next/715.fixed.md @@ -0,0 +1 @@ +Shop gift card category names can now be translated. From f2663e4bf38c2b21ce9b58f2d80a2469559bb3b6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 10:26:17 -0300 Subject: [PATCH 2/4] docs: add shop gift card category titles journey Co-Authored-By: Claude Opus 5 (1M context) --- journeys/README.md | 2 ++ journeys/shop/gift-card-category-titles.xml | 26 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 journeys/shop/gift-card-category-titles.xml diff --git a/journeys/README.md b/journeys/README.md index 403c5e2309..c79327be73 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -121,6 +121,7 @@ fixtures, push notifications) live in each suite's README. | [notification-permission](notification-permission) | 4 | Background-setup toggles | | [payment-requests](payment-requests) | 2 | Requires a linked fixture issuer; rejected shapes are unit fixtures | | [pubky-marketplace](pubky-marketplace) | 1 | Two-wallet Paykit marketplace payment; integration fixture required | +| [shop](shop) | 1 | Shop Discover category titles and web view handoff; needs Bitrefill reachable; no README | | [widgets](widgets) | 2 | Needs no backend — the quickest way to see the loop work; no README | ## Cross-platform @@ -142,6 +143,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `hardware-wallet/receive-onchain.xml`, `hardware-wallet/send-onchain.xml` | not ported | | `payment-requests/requested-resolution-failure.xml` | not ported | | `deeplinks/*` | not ported — iOS registers the `bitkit` scheme but has no screen or sheet router | +| `shop/gift-card-category-titles.xml` | not ported — iOS still hardcodes the category names, and its route in has no screen deeplink | | — | `hardware-wallet/transfer-to-spending-over-max.xml` exists only on iOS | ### Running one on iOS diff --git a/journeys/shop/gift-card-category-titles.xml b/journeys/shop/gift-card-category-titles.xml new file mode 100644 index 0000000000..2bf02c157e --- /dev/null +++ b/journeys/shop/gift-card-category-titles.xml @@ -0,0 +1,26 @@ + + + Proves the Shop Discover gift card category rows render their titles from string resources and + pass the same localized title to the gift card web view. Precondition: onboarded debug build, + dev mode on (the screen deeplink is debug-only and gated on it), device and app locale English, + network access to Bitrefill. + Category titles fall back to English in a locale whose Transifex strings do not yet carry the + other__shop__categories__* keys; "VoIP" is not translatable and stays "VoIP" in every locale. + The screen deeplink skips the Shop intro without marking it seen; iOS has no screen router, so a + port reaches Shop Discover through the drawer's Shop entry instead. + + + Run `adb shell am start -W -a android.intent.action.VIEW -d "bitkit://screen/shop-discover" to.bitkit.dev` + Verify that the Shop Discover screen is visible with the "Tab-shop" and "Tab-map" tabs and a "GIFT CARD CATEGORIES" header + Scroll the category list to the end and verify these 22 rows appear in order: "Apparel", "Automobiles", "Cruises", "Ecommerce", "Electronics", "Entertainment", "Experiences", "Flights", "Food", "Food Delivery", "Games", "Gifts", "Groceries", "Health & Beauty", "Home", "Multi-Brand", "Pets", "Restaurants", "Retail", "Streaming", "Travel", "VoIP" + Tap "Food Delivery" + Verify that the top bar title reads "Shop Food Delivery" + Wait for the web view to load, then take a screenshot and verify the Bitrefill Food Delivery listing is visible + Press the device back button + Run `adb shell cmd locale set-app-locales to.bitkit.dev --locales de`, then `adb shell am force-stop to.bitkit.dev` and relaunch the app + Run `adb shell am start -W -a android.intent.action.VIEW -d "bitkit://screen/shop-discover" to.bitkit.dev` + Verify that the app did not crash, the Map tab reads "Karte", and every category row shows either its German translation or its English title + Run `adb shell cmd locale set-app-locales to.bitkit.dev --locales ''`, then `adb shell am force-stop to.bitkit.dev` and relaunch the app + Verify that the wallet overview is visible in English + + From 6646dde27bbd7995ba3bd5cd0f69e89d8aef4416 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 11:36:25 -0300 Subject: [PATCH 3/4] docs: note shop journey does not prove localization Co-Authored-By: Claude Opus 5 (1M context) --- journeys/shop/gift-card-category-titles.xml | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/journeys/shop/gift-card-category-titles.xml b/journeys/shop/gift-card-category-titles.xml index 2bf02c157e..f94b01fd5c 100644 --- a/journeys/shop/gift-card-category-titles.xml +++ b/journeys/shop/gift-card-category-titles.xml @@ -1,11 +1,21 @@ - Proves the Shop Discover gift card category rows render their titles from string resources and - pass the same localized title to the gift card web view. Precondition: onboarded debug build, - dev mode on (the screen deeplink is debug-only and gated on it), device and app locale English, - network access to Bitrefill. - Category titles fall back to English in a locale whose Transifex strings do not yet carry the - other__shop__categories__* keys; "VoIP" is not translatable and stays "VoIP" in every locale. + Proves the Shop Discover gift card category rows render all 22 titles in order, survive an app + locale change, and pass the title shown on the row to the gift card web view. Precondition: + onboarded debug build, dev mode on (the screen deeplink is debug-only and gated on it), device + and app locale English, network access to Bitrefill. + + This journey does not yet distinguish resource-backed titles from the hardcoded enum they + replace, and a pass is not proof of localization. The English values added in + `values/strings.xml:572-593` are the same text the enum carried, and no `values-*` file carries + an `other__shop__categories__*` key yet, so every row reads English in every shipped locale + either way. A pseudolocale cannot close the gap: `androidResources.localeFilters` + (`app/build.gradle.kts:276`) keeps only the 16 shipped locales, so `en-XA` resources are + stripped. Once the Transifex pull lands the German strings, replace the German step below with + assertions on concrete translated titles ("Food Delivery" and "Health & Beauty" rows plus + the web view top bar) and drop the English fallback allowance; that is what makes this journey + discriminate. "VoIP" is not translatable and stays "VoIP" in every locale. + The screen deeplink skips the Shop intro without marking it seen; iOS has no screen router, so a port reaches Shop Discover through the drawer's Shop entry instead. @@ -19,7 +29,7 @@ Press the device back button Run `adb shell cmd locale set-app-locales to.bitkit.dev --locales de`, then `adb shell am force-stop to.bitkit.dev` and relaunch the app Run `adb shell am start -W -a android.intent.action.VIEW -d "bitkit://screen/shop-discover" to.bitkit.dev` - Verify that the app did not crash, the Map tab reads "Karte", and every category row shows either its German translation or its English title + Verify that the app did not crash, the Map tab reads "Karte", and all 22 category rows are still listed in the same order; until the German strings land they all read their English titles, which is the expected state and not a failure Run `adb shell cmd locale set-app-locales to.bitkit.dev --locales ''`, then `adb shell am force-stop to.bitkit.dev` and relaunch the app Verify that the wallet overview is visible in English From 124c2959e54e66be30508fb6b6a6f7140cf867f5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 12:17:14 -0300 Subject: [PATCH 4/4] test: cover shop category string resource pairing Co-Authored-By: Claude Opus 5 (1M context) --- .../to/bitkit/models/BitrefillCategoryTest.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/test/java/to/bitkit/models/BitrefillCategoryTest.kt diff --git a/app/src/test/java/to/bitkit/models/BitrefillCategoryTest.kt b/app/src/test/java/to/bitkit/models/BitrefillCategoryTest.kt new file mode 100644 index 0000000000..8c2d668f15 --- /dev/null +++ b/app/src/test/java/to/bitkit/models/BitrefillCategoryTest.kt @@ -0,0 +1,51 @@ +package to.bitkit.models + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@Config(sdk = [34], qualifiers = "en-rUS") +@RunWith(RobolectricTestRunner::class) +class BitrefillCategoryTest { + + private val context = ApplicationProvider.getApplicationContext() + + @Test + fun `every entry points at the string resource named after it`() { + BitrefillCategory.entries.forEach { category -> + val expected = "other__shop__categories__${category.name.lowercase()}" + val actual = context.resources.getResourceEntryName(category.titleRes) + assertEquals(expected, actual, "'${category.name}' resolves to the wrong string resource") + } + } + + @Test + fun `every title resolves to a distinct non-blank label`() { + val titles = BitrefillCategory.entries.associateWith { context.getString(it.titleRes) } + + titles.forEach { (category, title) -> + assertTrue(title.isNotBlank(), "'${category.name}' resolves to a blank label") + } + assertEquals( + BitrefillCategory.entries.size, + titles.values.toSet().size, + "categories share a label: ${titles.values.groupBy { it }.filterValues { it.size > 1 }.keys}", + ) + } + + @Test + fun `every route is a distinct buy path`() { + val routes = BitrefillCategory.entries.map { it.route } + + routes.forEach { route -> + assertTrue(route.startsWith("buy/"), "route '$route' is not a buy path") + assertTrue(route.removePrefix("buy/").isNotBlank(), "route '$route' has no slug") + } + assertEquals(BitrefillCategory.entries.size, routes.toSet().size, "categories share a route") + } +}