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
+
+