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 0e98e8053d..351aa1effa 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/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") + } +} 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. diff --git a/journeys/README.md b/journeys/README.md index 0862fbbbae..6e4148541a 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -126,6 +126,7 @@ fixtures, push notifications) live in each suite's README. | [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 | | [security](security) | 1 | PIN result sheet layout at a long locale and font scale; no README | +| [shop](shop) | 1 | Shop Discover category titles and web view handoff; needs Bitrefill reachable; no README | | [subscriptions](subscriptions) | 4 | Paykit subscription lifecycle across two wallets, plus the Payments tab | | [tags](tags) | 1 | Tag input length cap on an activity; no backend, no README | | [transfers](transfers) | 1 | Transfer to Spending settling after the LSP closes the channel; no README | @@ -154,6 +155,7 @@ Known differences in the corpus, as of the iOS port (synonymdev/bitkit-ios#691): | `transfers/closed-channel-transfer-settles.xml` | not ported — the closed-channel and order-closure settle rules are an iOS follow-up | | `deeplinks/*` | not ported — iOS registers the `bitkit` scheme but has no screen or sheet router | | `backup-restore/restore-keeps-tags-and-closed-channels.xml` | not ported yet — iOS already gates uploads across the whole restore (`AppScene.restoreFromMostRecentBackup` sets `BackupService.setRestoring(true)` before the timestamp probe), but still applies the three activity slices in one block (`BackupService.performFullRestoreFromLatestBackup`), which is the half this journey pins; port it with the iOS slice fix | +| `shop/gift-card-category-titles.xml` | not ported — iOS still hardcodes the category names, and its route in has no screen deeplink | | `home/pull-to-refresh-rates.xml` | not ported — iOS does not refresh exchange rates on pull to refresh | | `security/pin-result-long-label.xml` | not ported — the toggle exists on the iOS security success screen, but the overlap check is a follow-up | | `tags/activity-tag-length-cap.xml` | not ported — iOS has no 20-character cap on tag input | diff --git a/journeys/shop/gift-card-category-titles.xml b/journeys/shop/gift-card-category-titles.xml new file mode 100644 index 0000000000..f94b01fd5c --- /dev/null +++ b/journeys/shop/gift-card-category-titles.xml @@ -0,0 +1,36 @@ + + + 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. + + + 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 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 + +