From 0b1229de6f346cd48ef454feb203247785d741fe Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 28 Mar 2026 20:52:50 -0300 Subject: [PATCH 1/2] feat: android and ios translations --- .../worn/ui/components/AddItemFields.kt | 160 +++++++++-- .../worn/ui/components/AiLockedSheet.kt | 8 +- .../worn/ui/components/CategoryFilterChips.kt | 15 +- .../github/worn/ui/components/ClothingCard.kt | 8 - .../github/worn/ui/components/OutfitCard.kt | 3 +- .../worn/ui/components/WornBottomBar.kt | 23 +- .../com/github/worn/ui/screen/AddItemSheet.kt | 14 +- .../worn/ui/screen/CreateOutfitSheet.kt | 25 +- .../com/github/worn/ui/screen/GapsScreen.kt | 69 +++-- .../github/worn/ui/screen/ItemDetailSheet.kt | 49 +++- .../worn/ui/screen/OutfitDetailSheet.kt | 52 +++- .../github/worn/ui/screen/OutfitsScreen.kt | 41 ++- .../github/worn/ui/screen/SettingsScreen.kt | 139 ++++++---- .../com/github/worn/ui/screen/TryItScreen.kt | 30 +- .../github/worn/ui/screen/WardrobeScreen.kt | 46 +++- .../androidMain/res/values-pt-rBR/strings.xml | 256 ++++++++++++++++++ .../src/androidMain/res/values/strings.xml | 255 ++++++++++++++++- iosApp/iosApp.xcodeproj/project.pbxproj | 1 + iosApp/iosApp/Components/AiLockedSheet.swift | 6 +- .../Components/CategoryFilterChips.swift | 18 +- iosApp/iosApp/Components/ClothingCard.swift | 10 +- iosApp/iosApp/Components/WornBottomBar.swift | 22 +- iosApp/iosApp/Screens/AddItemSheet.swift | 78 +++--- iosApp/iosApp/Screens/CreateOutfitSheet.swift | 12 +- iosApp/iosApp/Screens/GapsScreen.swift | 48 ++-- iosApp/iosApp/Screens/ItemDetailSheet.swift | 56 ++-- iosApp/iosApp/Screens/OutfitDetailSheet.swift | 28 +- iosApp/iosApp/Screens/OutfitsScreen.swift | 28 +- iosApp/iosApp/Screens/SettingsScreen.swift | 98 +++---- iosApp/iosApp/Screens/TryItScreen.swift | 28 +- iosApp/iosApp/Screens/WardrobeScreen.swift | 30 +- iosApp/iosApp/en.lproj/InfoPlist.strings | 1 + iosApp/iosApp/en.lproj/Localizable.strings | 234 ++++++++++++++++ .../iosApp/en.lproj/Localizable.stringsdict | 70 +++++ iosApp/iosApp/pt-BR.lproj/InfoPlist.strings | 1 + iosApp/iosApp/pt-BR.lproj/Localizable.strings | 234 ++++++++++++++++ .../pt-BR.lproj/Localizable.stringsdict | 70 +++++ 37 files changed, 1825 insertions(+), 441 deletions(-) create mode 100644 composeApp/src/androidMain/res/values-pt-rBR/strings.xml create mode 100644 iosApp/iosApp/en.lproj/InfoPlist.strings create mode 100644 iosApp/iosApp/en.lproj/Localizable.strings create mode 100644 iosApp/iosApp/en.lproj/Localizable.stringsdict create mode 100644 iosApp/iosApp/pt-BR.lproj/InfoPlist.strings create mode 100644 iosApp/iosApp/pt-BR.lproj/Localizable.strings create mode 100644 iosApp/iosApp/pt-BR.lproj/Localizable.stringsdict diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AddItemFields.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AddItemFields.kt index 8736948..f0a9ced 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AddItemFields.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AddItemFields.kt @@ -50,6 +50,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -109,7 +111,7 @@ fun PhotoUploadZone(bitmap: ImageBitmap?, onClick: () -> Unit) { ) Spacer(Modifier.height(8.dp)) Text( - text = "Tap to add photo", + text = stringResource(R.string.add_item_photo_hint), color = WornColors.TextSecondary, fontSize = 14.sp, fontWeight = FontWeight.Medium, @@ -128,7 +130,12 @@ fun AiBadge(onClick: () -> Unit = {}) { ) { Row(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) { Text("✦ ", color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) - Text("Auto-tag with AI", color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) + Text( + stringResource(R.string.add_item_ai_badge), + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + ) } } } @@ -138,7 +145,13 @@ fun ItemNameField(value: String, onValueChange: (String) -> Unit) { TextField( value = value, onValueChange = onValueChange, - placeholder = { Text("Item name", color = WornColors.IconMuted, fontSize = 15.sp) }, + placeholder = { + Text( + stringResource(R.string.add_item_name_hint), + color = WornColors.IconMuted, + fontSize = 15.sp, + ) + }, colors = TextFieldDefaults.colors( focusedContainerColor = WornColors.BgCard, unfocusedContainerColor = WornColors.BgCard, @@ -178,7 +191,7 @@ fun CategoryDropdown(selected: Category?, onSelected: (Category) -> Unit) { Spacer(Modifier.size(12.dp)) } Text( - text = selected?.displayName() ?: "Category", + text = selected?.displayName() ?: stringResource(R.string.label_category), color = if (selected != null) WornColors.TextPrimary else WornColors.IconMuted, fontSize = 15.sp, modifier = Modifier.weight(1f), @@ -245,7 +258,12 @@ private fun CategoryOptionList(onSelected: (Category) -> Unit) { @Composable fun ColorSection(selectedColors: Set, onToggle: (String) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Color", color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + stringResource(R.string.label_color), + color = WornColors.TextPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) FlowRow( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), @@ -283,7 +301,12 @@ private fun contrastColor(background: Color): Color { @Composable fun SeasonSection(selectedSeasons: Set, onToggle: (Season) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Season", color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + stringResource(R.string.label_season), + color = WornColors.TextPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Season.entries.forEach { season -> val isActive = season in selectedSeasons @@ -333,7 +356,11 @@ fun SaveButton( .background(if (enabled) gradient else disabledGradient), ) { Text( - text = if (isSaving) "Saving…" else (label ?: "Save to wardrobe"), + text = if (isSaving) { + stringResource(R.string.common_saving) + } else { + label ?: stringResource(R.string.add_item_save_to_wardrobe) + }, color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, @@ -351,12 +378,13 @@ internal fun Category.iconRes(): Int = when (this) { Category.ACCESSORY -> R.drawable.ic_glasses } +@Composable private fun Category.displayName(): String = when (this) { - Category.TOP -> "Tops" - Category.BOTTOM -> "Bottoms" - Category.OUTERWEAR -> "Outerwear" - Category.SHOES -> "Shoes" - Category.ACCESSORY -> "Accessories" + Category.TOP -> stringResource(R.string.category_tops) + Category.BOTTOM -> stringResource(R.string.category_bottoms) + Category.OUTERWEAR -> stringResource(R.string.category_outerwear) + Category.SHOES -> stringResource(R.string.category_shoes) + Category.ACCESSORY -> stringResource(R.string.category_accessories) } @Composable @@ -375,7 +403,7 @@ fun SubcategoryDropdown(category: Category, selected: Subcategory?, onSelected: color = Color.Transparent, ) { DropdownHeader( - text = selected?.displayName() ?: "Subcategory", + text = selected?.displayName() ?: stringResource(R.string.label_subcategory), hasSelection = selected != null, expanded = expanded, ) @@ -443,7 +471,12 @@ private fun SubcategoryOptionList(options: List, onSelected: (Subca @Composable fun FitSection(selected: Fit?, onSelected: (Fit?) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Fit", color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + stringResource(R.string.label_fit), + color = WornColors.TextPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Fit.entries.forEach { fit -> val isActive = fit == selected @@ -470,7 +503,12 @@ fun FitSection(selected: Fit?, onSelected: (Fit?) -> Unit) { @Composable fun MaterialSection(selected: Material?, onSelected: (Material?) -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Material", color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) + Text( + stringResource(R.string.label_material), + color = WornColors.TextPrimary, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -496,22 +534,84 @@ fun MaterialSection(selected: Material?, onSelected: (Material?) -> Unit) { } } -internal fun Subcategory.displayName(): String = name.lowercase() - .replace('_', ' ') - .replaceFirstChar { it.uppercase() } +@Suppress("CyclomaticComplexMethod") +@Composable +internal fun Subcategory.displayName(): String = stringResource( + when (this) { + Subcategory.T_SHIRT -> R.string.subcategory_t_shirt + Subcategory.POLO -> R.string.subcategory_polo + Subcategory.DRESS_SHIRT -> R.string.subcategory_dress_shirt + Subcategory.HENLEY -> R.string.subcategory_henley + Subcategory.SWEATER -> R.string.subcategory_sweater + Subcategory.HOODIE -> R.string.subcategory_hoodie + Subcategory.JEANS -> R.string.subcategory_jeans + Subcategory.CHINOS -> R.string.subcategory_chinos + Subcategory.TAILORED_PANTS -> R.string.subcategory_tailored_pants + Subcategory.SHORTS -> R.string.subcategory_shorts + Subcategory.CARGO_PANTS -> R.string.subcategory_cargo_pants + Subcategory.SWEATPANTS -> R.string.subcategory_sweatpants + Subcategory.BOMBER -> R.string.subcategory_bomber + Subcategory.TRUCKER -> R.string.subcategory_trucker + Subcategory.PUFFER -> R.string.subcategory_puffer + Subcategory.BLAZER -> R.string.subcategory_blazer + Subcategory.COAT -> R.string.subcategory_coat + Subcategory.WINDBREAKER -> R.string.subcategory_windbreaker + Subcategory.SNEAKERS -> R.string.subcategory_sneakers + Subcategory.BOOTS_MILITARY -> R.string.subcategory_boots_military + Subcategory.BOOTS_CHELSEA -> R.string.subcategory_boots_chelsea + Subcategory.DERBY -> R.string.subcategory_derby + Subcategory.OXFORD -> R.string.subcategory_oxford + Subcategory.LOAFER -> R.string.subcategory_loafer + Subcategory.SANDALS -> R.string.subcategory_sandals + Subcategory.WATCH -> R.string.subcategory_watch + Subcategory.BELT -> R.string.subcategory_belt + Subcategory.SUNGLASSES -> R.string.subcategory_sunglasses + Subcategory.HAT_CAP -> R.string.subcategory_hat_cap + Subcategory.SCARF -> R.string.subcategory_scarf + Subcategory.BAG_BACKPACK -> R.string.subcategory_bag_backpack + }, +) -internal fun Fit.displayName(): String = when (this) { - Fit.SLIM_FIT -> "Slim Fit" - Fit.REGULAR -> "Regular" - Fit.RELAXED -> "Relaxed" - Fit.OVERSIZED -> "Oversized" -} +@Composable +internal fun Fit.displayName(): String = stringResource( + when (this) { + Fit.SLIM_FIT -> R.string.fit_slim + Fit.REGULAR -> R.string.fit_regular + Fit.RELAXED -> R.string.fit_relaxed + Fit.OVERSIZED -> R.string.fit_oversized + }, +) -internal fun Material.displayName(): String = name.lowercase().replaceFirstChar { it.uppercase() } +@Composable +internal fun Material.displayName(): String = stringResource( + when (this) { + Material.COTTON -> R.string.material_cotton + Material.LINEN -> R.string.material_linen + Material.DENIM -> R.string.material_denim + Material.WOOL -> R.string.material_wool + Material.SYNTHETIC -> R.string.material_synthetic + Material.LEATHER -> R.string.material_leather + Material.SILK -> R.string.material_silk + Material.KNIT -> R.string.material_knit + }, +) + +@Composable +internal fun Season.displayName(): String = stringResource( + when (this) { + Season.SPRING -> R.string.season_spring + Season.SUMMER -> R.string.season_summer + Season.FALL -> R.string.season_fall + Season.WINTER -> R.string.season_winter + }, +) -internal fun Season.displayName(): String = when (this) { - Season.SPRING -> "Spring" - Season.SUMMER -> "Summer" - Season.FALL -> "Fall" - Season.WINTER -> "Winter" +@Composable +internal fun Category.displayLabel(): String = when (this) { + Category.TOP -> stringResource(R.string.category_tops) + Category.BOTTOM -> stringResource(R.string.category_bottoms) + Category.OUTERWEAR -> stringResource(R.string.category_outerwear) + Category.SHOES -> stringResource(R.string.category_shoes) + Category.ACCESSORY -> stringResource(R.string.category_accessories) } + diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AiLockedSheet.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AiLockedSheet.kt index 1b8e33b..ce432e2 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AiLockedSheet.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/AiLockedSheet.kt @@ -30,8 +30,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.github.worn.R import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.WornColors @@ -94,14 +96,14 @@ internal fun AiLockedContent( } Text( - text = "Unlock AI features", + text = stringResource(R.string.ai_locked_title), color = WornColors.TextPrimary, fontSize = 22.sp, fontWeight = FontWeight.Medium, ) Text( - text = "Add your Claude API key in Settings to enable this.", + text = stringResource(R.string.ai_locked_description), color = WornColors.TextSecondary, fontSize = 14.sp, textAlign = TextAlign.Center, @@ -132,7 +134,7 @@ private fun SettingsCta(onClick: () -> Unit) { .padding(horizontal = 40.dp, vertical = 14.dp), ) { Text( - text = "Go to Settings", + text = stringResource(R.string.ai_locked_cta), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt index 8843bda..a7902cf 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/CategoryFilterChips.kt @@ -12,8 +12,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.ui.theme.WornColors @@ -25,7 +27,7 @@ fun CategoryFilterChips( onCategorySelected: (Category?) -> Unit, modifier: Modifier = Modifier, ) { - val allChips = listOf>(null to "All") + + val allChips = listOf>(null to stringResource(R.string.filter_all)) + Category.entries.map { it to it.displayName() } LazyRow( @@ -66,10 +68,11 @@ private fun CategoryChip( } } +@Composable private fun Category.displayName(): String = when (this) { - Category.TOP -> "Tops" - Category.BOTTOM -> "Bottoms" - Category.OUTERWEAR -> "Outerwear" - Category.SHOES -> "Shoes" - Category.ACCESSORY -> "Accessories" + Category.TOP -> stringResource(R.string.category_tops) + Category.BOTTOM -> stringResource(R.string.category_bottoms) + Category.OUTERWEAR -> stringResource(R.string.category_outerwear) + Category.SHOES -> stringResource(R.string.category_shoes) + Category.ACCESSORY -> stringResource(R.string.category_accessories) } diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/ClothingCard.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/ClothingCard.kt index b8eb09f..08295d0 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/ClothingCard.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/ClothingCard.kt @@ -170,11 +170,3 @@ internal fun Category.dotColor(): Color = when (this) { Category.SHOES -> WornColors.CategoryDotShoes Category.ACCESSORY -> WornColors.CategoryDotAccessory } - -internal fun Category.displayLabel(): String = when (this) { - Category.TOP -> "Tops" - Category.BOTTOM -> "Bottoms" - Category.OUTERWEAR -> "Outerwear" - Category.SHOES -> "Shoes" - Category.ACCESSORY -> "Accessories" -} diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/OutfitCard.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/OutfitCard.kt index 10b65a0..7257e3f 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/OutfitCard.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/OutfitCard.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -129,7 +130,7 @@ private fun ItemCountBadge(outfit: Outfit) { val badgeColor = badgeColors[outfit.id.hashCode().mod(badgeColors.size)] Surface(shape = badgeShape, color = badgeColor) { Text( - text = "${outfit.itemIds.size} items", + text = stringResource(R.string.outfit_detail_items_count, outfit.itemIds.size), color = WornColors.TextOnColor, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/WornBottomBar.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/WornBottomBar.kt index 1e8037a..86bad7c 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/WornBottomBar.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/components/WornBottomBar.kt @@ -1,5 +1,7 @@ package com.github.worn.ui.components +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -12,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.annotation.DrawableRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Extension import androidx.compose.material.icons.outlined.Layers @@ -26,6 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -33,15 +35,15 @@ import com.github.worn.R import com.github.worn.ui.theme.WornColors enum class Tab( - val label: String, + @StringRes val labelRes: Int, val icon: ImageVector? = null, @DrawableRes val iconRes: Int? = null, ) { - WARDROBE("WARDROBE", iconRes = R.drawable.ic_shirt), - OUTFITS("OUTFITS", icon = Icons.Outlined.Layers), - GAPS("GAPS", icon = Icons.Outlined.Extension), - TRY_IT("TRY IT", icon = Icons.Outlined.QrCodeScanner), - SETTINGS("SETTINGS", icon = Icons.Outlined.Settings), + WARDROBE(R.string.tab_wardrobe, iconRes = R.drawable.ic_shirt), + OUTFITS(R.string.tab_outfits, icon = Icons.Outlined.Layers), + GAPS(R.string.tab_gaps, icon = Icons.Outlined.Extension), + TRY_IT(R.string.tab_try_it, icon = Icons.Outlined.QrCodeScanner), + SETTINGS(R.string.tab_settings, icon = Icons.Outlined.Settings), } @Composable @@ -109,23 +111,24 @@ private fun TabItem( modifier = Modifier.fillMaxHeight(), ) { val tint = if (isActive) WornColors.TextOnColor else WornColors.TextSecondary + val label = stringResource(tab.labelRes) if (tab.iconRes != null) { Icon( painter = painterResource(id = tab.iconRes), - contentDescription = tab.label, + contentDescription = label, tint = tint, modifier = Modifier.size(18.dp), ) } else if (tab.icon != null) { Icon( imageVector = tab.icon, - contentDescription = tab.label, + contentDescription = label, tint = tint, modifier = Modifier.size(18.dp), ) } Text( - text = tab.label, + text = label, color = if (isActive) WornColors.TextOnColor else WornColors.TextSecondary, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/AddItemSheet.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/AddItemSheet.kt index f8dd40c..0aa5d86 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/AddItemSheet.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/AddItemSheet.kt @@ -40,10 +40,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Fit @@ -278,7 +280,7 @@ private fun AddItemFormContent( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = if (isEditing) "Edit item" else "Add new item", + text = stringResource(if (isEditing) R.string.add_item_title_edit else R.string.add_item_title), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, @@ -303,7 +305,7 @@ private fun AddItemFormContent( enabled = canSave && !isSaving, isSaving = isSaving, onClick = onSave, - label = if (isEditing) "Save Changes" else null, + label = if (isEditing) stringResource(R.string.common_save_changes) else null, ) } } @@ -316,7 +318,7 @@ private fun PhotoSourceDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Add photo") }, + title = { Text(stringResource(R.string.add_item_photo_dialog_title)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { TextButton(onClick = onCamera, modifier = Modifier.fillMaxWidth()) { @@ -327,7 +329,7 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text("Take photo", fontSize = 16.sp) + Text(stringResource(R.string.add_item_take_photo), fontSize = 16.sp) } } TextButton(onClick = onGallery, modifier = Modifier.fillMaxWidth()) { @@ -338,14 +340,14 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.PhotoLibrary, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text("Choose from gallery", fontSize = 16.sp) + Text(stringResource(R.string.add_item_choose_gallery), fontSize = 16.sp) } } } }, confirmButton = {}, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt index 7d3152a..4358924 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/CreateOutfitSheet.kt @@ -47,11 +47,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Outfit @@ -134,7 +137,7 @@ internal fun CreateOutfitForm( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = if (isEditing) "Edit outfit" else "Create outfit", + text = stringResource(if (isEditing) R.string.create_outfit_title_edit else R.string.create_outfit_title), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, @@ -151,7 +154,7 @@ internal fun CreateOutfitForm( SaveOutfitButton( enabled = canSave, isSaving = isSaving, - label = if (isEditing) "Save Changes" else null, + label = if (isEditing) stringResource(R.string.common_save_changes) else null, onClick = { onSave(name) }, ) } @@ -162,7 +165,13 @@ private fun OutfitNameField(name: String, onNameChange: (String) -> Unit) { TextField( value = name, onValueChange = onNameChange, - placeholder = { Text("Outfit name", color = WornColors.IconMuted, fontSize = 15.sp) }, + placeholder = { + Text( + stringResource(R.string.create_outfit_name_hint), + color = WornColors.IconMuted, + fontSize = 15.sp, + ) + }, colors = TextFieldDefaults.colors( focusedContainerColor = WornColors.BgCard, unfocusedContainerColor = WornColors.BgCard, @@ -184,14 +193,14 @@ private fun SelectItemsHeader(selectedCount: Int) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "Select items", + text = stringResource(R.string.create_outfit_select_items), color = WornColors.TextPrimary, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) if (selectedCount > 0) { Text( - text = "$selectedCount selected", + text = pluralStringResource(R.plurals.selected_count, selectedCount, selectedCount), color = WornColors.AccentGreen, fontSize = 13.sp, fontWeight = FontWeight.Medium, @@ -323,7 +332,11 @@ private fun SaveOutfitButton(enabled: Boolean, isSaving: Boolean, label: String? .background(if (enabled) gradient else disabledGradient), ) { Text( - text = if (isSaving) "Saving…" else (label ?: "Save outfit"), + text = if (isSaving) { + stringResource(R.string.common_saving) + } else { + label ?: stringResource(R.string.create_outfit_save) + }, color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/GapsScreen.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/GapsScreen.kt index c8e06f9..1a9b6a6 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/GapsScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/GapsScreen.kt @@ -38,11 +38,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.annotation.StringRes import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -52,11 +55,14 @@ import androidx.window.core.layout.WindowWidthSizeClass import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.GapRecommendation +import com.github.worn.domain.model.Season import com.github.worn.presentation.viewmodel.GapsState import com.github.worn.presentation.viewmodel.GapsViewModel import com.github.worn.ui.components.AiLockedSheet import com.github.worn.ui.components.Tab import com.github.worn.ui.components.WornBottomBar +import com.github.worn.ui.components.displayLabel +import com.github.worn.ui.components.displayName import com.github.worn.ui.components.iconRes import com.github.worn.ui.theme.WornColors import com.github.worn.ui.theme.WornTheme @@ -146,14 +152,14 @@ private fun GapsScaffold( ) { Spacer(Modifier.height(24.dp)) Text( - text = "What's missing", + text = stringResource(R.string.gaps_title), color = WornColors.TextPrimary, fontSize = 28.sp, fontWeight = FontWeight.SemiBold, letterSpacing = (-0.5).sp, ) Text( - text = "Items that would expand your combinations most", + text = stringResource(R.string.gaps_subtitle), color = WornColors.TextSecondary, fontSize = 14.sp, ) @@ -206,14 +212,14 @@ private fun CompleteContent() { } Spacer(Modifier.height(24.dp)) Text( - text = "Your wardrobe looks complete!", + text = stringResource(R.string.gaps_complete_title), color = WornColors.TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, ) Spacer(Modifier.height(8.dp)) Text( - text = "We couldn't find any gaps.\nYou have great coverage across categories.", + text = stringResource(R.string.gaps_complete_description), color = WornColors.TextSecondary, fontSize = 14.sp, lineHeight = 20.sp, @@ -259,18 +265,18 @@ private fun GapsBanner(isAiMode: Boolean, onClick: () -> Unit) { modifier = Modifier.fillMaxWidth().padding(16.dp), ) { Column(modifier = Modifier.weight(1f)) { + val titleRes = if (isAiMode) R.string.gaps_banner_ai_title + else R.string.gaps_banner_common_title + val subtitleRes = if (isAiMode) R.string.gaps_banner_ai_subtitle + else R.string.gaps_banner_common_subtitle Text( - text = if (isAiMode) "AI Recommendations" else "Common Suggestions", + text = stringResource(titleRes), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) Text( - text = if (isAiMode) { - "Personalized suggestions based on your wardrobe" - } else { - "Connect Claude AI for personalized picks" - }, + text = stringResource(subtitleRes), color = Color.White.copy(alpha = 0.8f), fontSize = 13.sp, ) @@ -323,9 +329,9 @@ private fun GapCard( ) Text( text = if (isAiMode) { - "Would pair with ${recommendation.pairingCount} of your items" + stringResource(R.string.gaps_pairing_ai, recommendation.pairingCount) } else { - "Common wardrobe essential" + stringResource(R.string.gaps_pairing_common) }, color = WornColors.TextSecondary, fontSize = 12.sp, @@ -494,9 +500,9 @@ private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boole Spacer(Modifier.width(8.dp)) Text( text = if (isAiMode) { - "Would pair with ${recommendation.pairingCount} of your items" + stringResource(R.string.gaps_pairing_ai, recommendation.pairingCount) } else { - "Common wardrobe essential" + stringResource(R.string.gaps_pairing_common) }, color = WornColors.TextSecondary, fontSize = 13.sp, @@ -509,24 +515,27 @@ private fun DetailPairingInfo(recommendation: GapRecommendation, isAiMode: Boole private fun DetailRows(recommendation: GapRecommendation) { Column { recommendation.subcategory?.let { - DetailRow("Subcategory", it.name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)) + DetailRow(stringResource(R.string.label_subcategory), it.displayName()) } if (recommendation.colors.isNotEmpty()) { - DetailRow("Color", recommendation.colors.joinToString(", ")) + DetailRow(stringResource(R.string.label_color), recommendation.colors.joinToString(", ")) } if (recommendation.seasons.isNotEmpty()) { - val seasonsText = if (recommendation.seasons.size == 4) "All seasons" else { - recommendation.seasons.joinToString(", ") { - it.name.lowercase().replaceFirstChar(Char::uppercase) + val seasonsText = if (recommendation.seasons.size == 4) { + stringResource(R.string.common_all_seasons) + } else { + val context = LocalContext.current + recommendation.seasons.joinToString(", ") { season -> + context.getString(season.stringRes()) } } - DetailRow("Season", seasonsText) + DetailRow(stringResource(R.string.label_season), seasonsText) } recommendation.fit?.let { - DetailRow("Fit", it.name.lowercase().replace('_', ' ').replaceFirstChar(Char::uppercase)) + DetailRow(stringResource(R.string.label_fit), it.displayName()) } recommendation.material?.let { - DetailRow("Material", it.name.lowercase().replaceFirstChar(Char::uppercase)) + DetailRow(stringResource(R.string.label_material), it.displayName()) } } } @@ -561,7 +570,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { .background(gradient), ) { Text( - "Add to Wardrobe", + stringResource(R.string.gaps_add_to_wardrobe), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, @@ -580,7 +589,7 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { modifier = Modifier.fillMaxWidth().height(48.dp), ) { Text( - "Dismiss", + stringResource(R.string.gaps_dismiss), color = WornColors.TextSecondary, fontSize = 15.sp, fontWeight = FontWeight.Medium, @@ -591,12 +600,12 @@ private fun DetailActions(onAddToWardrobe: () -> Unit, onDismiss: () -> Unit) { // endregion -private fun Category.displayLabel(): String = when (this) { - Category.TOP -> "Tops" - Category.BOTTOM -> "Bottoms" - Category.OUTERWEAR -> "Outerwear" - Category.SHOES -> "Shoes" - Category.ACCESSORY -> "Accessories" +@StringRes +private fun Season.stringRes(): Int = when (this) { + Season.SPRING -> R.string.season_spring + Season.SUMMER -> R.string.season_summer + Season.FALL -> R.string.season_fall + Season.WINTER -> R.string.season_winter } private fun GapRecommendation.toPreFilledItem() = diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt index 102a6b9..d4cd21c 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/ItemDetailSheet.kt @@ -53,6 +53,8 @@ import com.github.worn.domain.model.Fit import com.github.worn.domain.model.Material import com.github.worn.domain.model.Season import com.github.worn.domain.model.Subcategory +import androidx.compose.ui.res.stringResource +import com.github.worn.R import com.github.worn.ui.components.addItemColorPalette import com.github.worn.ui.components.displayLabel import com.github.worn.ui.components.displayName @@ -131,8 +133,8 @@ internal fun ItemDetailContent( ItemProperties(item = item, fontSize = dims.propFontSize, gap = dims.propGap) if (showActions) { DetailActionButtons( - editLabel = "Edit Item", - deleteLabel = "Delete Item", + editLabel = stringResource(R.string.item_detail_edit), + deleteLabel = stringResource(R.string.item_detail_delete), buttonHeight = dims.buttonHeight, buttonFontSize = dims.buttonFontSize, onEdit = { onEdit(item) }, @@ -238,15 +240,25 @@ private fun ItemProperties(item: ClothingItem, fontSize: TextUnit, gap: Dp) { } if (item.seasons.isNotEmpty()) { val seasonText = if (item.seasons.size == Season.entries.size) { - "All seasons" + stringResource(R.string.common_all_seasons) } else { - item.seasons.joinToString(", ") { it.displayName() } + item.seasons.map { it.displayName() }.joinToString(", ") } - PropertyRow(label = "Season", value = seasonText, fontSize = fontSize) + PropertyRow(label = stringResource(R.string.label_season), value = seasonText, fontSize = fontSize) + } + item.fit?.let { + PropertyRow(label = stringResource(R.string.label_fit), value = it.displayName(), fontSize = fontSize) + } + item.subcategory?.let { + PropertyRow( + label = stringResource(R.string.label_subcategory), + value = it.displayName(), + fontSize = fontSize, + ) + } + item.material?.let { + PropertyRow(label = stringResource(R.string.label_material), value = it.displayName(), fontSize = fontSize) } - item.fit?.let { PropertyRow(label = "Fit", value = it.displayName(), fontSize = fontSize) } - item.subcategory?.let { PropertyRow(label = "Subcategory", value = it.displayName(), fontSize = fontSize) } - item.material?.let { PropertyRow(label = "Material", value = it.displayName(), fontSize = fontSize) } } } @@ -257,7 +269,12 @@ private fun ColorPropertyRow(item: ClothingItem, fontSize: TextUnit) { horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text("Color", color = WornColors.TextSecondary, fontSize = fontSize, fontWeight = FontWeight.Medium) + Text( + stringResource(R.string.label_color), + color = WornColors.TextSecondary, + fontSize = fontSize, + fontWeight = FontWeight.Medium, + ) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Surface( shape = CircleShape, @@ -328,10 +345,16 @@ internal fun DetailActionButtons( private fun DeleteItemDialog(itemName: String, onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete item?", fontWeight = FontWeight.SemiBold, fontSize = 22.sp) }, + title = { + Text( + stringResource(R.string.item_detail_delete_dialog_title), + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ) + }, text = { Text( - "This action cannot be undone. \"$itemName\" will be permanently removed from your wardrobe.", + stringResource(R.string.item_detail_delete_dialog_message, itemName), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, ) }, @@ -340,9 +363,9 @@ private fun DeleteItemDialog(itemName: String, onConfirm: () -> Unit, onDismiss: onClick = onConfirm, colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), shape = RoundedCornerShape(24.dp), - ) { Text("Delete", fontWeight = FontWeight.SemiBold) } + ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, - dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }, + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt index a26b6e8..86ac29b 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitDetailSheet.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight @@ -46,11 +47,12 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import androidx.compose.ui.res.stringResource +import com.github.worn.R import com.github.worn.domain.model.Category import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.Outfit import com.github.worn.domain.model.Season -import com.github.worn.ui.components.displayName import com.github.worn.ui.theme.SheetPreview import com.github.worn.ui.theme.WornColors import java.io.File @@ -133,8 +135,8 @@ internal fun OutfitDetailContent( OutfitProperties(outfit = outfit, items = outfitItems, isCompact = isCompact, padding = contentPadding) Box(modifier = Modifier.padding(horizontal = contentPadding)) { DetailActionButtons( - editLabel = "Edit Outfit", - deleteLabel = "Delete Outfit", + editLabel = stringResource(R.string.outfit_detail_edit), + deleteLabel = stringResource(R.string.outfit_detail_delete), buttonHeight = if (isCompact) 48.dp else 52.dp, buttonFontSize = if (isCompact) 15.sp else 16.sp, onEdit = { onEdit(outfit) }, @@ -188,8 +190,16 @@ private fun OutfitProperties(outfit: Outfit, items: List, isCompac modifier = Modifier.padding(horizontal = padding), verticalArrangement = Arrangement.spacedBy(propGap), ) { - OutfitPropertyRow(label = "Items", value = "${outfit.itemIds.size} items", fontSize = propFontSize) - OutfitPropertyRow(label = "Season", value = deriveSeasonText(items), fontSize = propFontSize) + OutfitPropertyRow( + label = stringResource(R.string.label_items), + value = stringResource(R.string.outfit_detail_items_count, outfit.itemIds.size), + fontSize = propFontSize, + ) + OutfitPropertyRow( + label = stringResource(R.string.label_season), + value = deriveSeasonText(items), + fontSize = propFontSize, + ) } } @@ -197,10 +207,16 @@ private fun OutfitProperties(outfit: Outfit, items: List, isCompac private fun DeleteOutfitDialog(outfitName: String, onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Delete outfit?", fontWeight = FontWeight.SemiBold, fontSize = 22.sp) }, + title = { + Text( + stringResource(R.string.outfit_detail_delete_dialog_title), + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + ) + }, text = { Text( - "This action cannot be undone. \"$outfitName\" will be permanently removed.", + stringResource(R.string.outfit_detail_delete_dialog_message, outfitName), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, ) }, @@ -209,9 +225,9 @@ private fun DeleteOutfitDialog(outfitName: String, onConfirm: () -> Unit, onDism onClick = onConfirm, colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), shape = RoundedCornerShape(24.dp), - ) { Text("Delete", fontWeight = FontWeight.SemiBold) } + ) { Text(stringResource(R.string.common_delete), fontWeight = FontWeight.SemiBold) } }, - dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }, + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } @@ -281,15 +297,27 @@ private fun OutfitPropertyRow(label: String, value: String, fontSize: TextUnit) } } +@Composable private fun deriveSeasonText(items: List): String { + val context = LocalContext.current val allSeasons = items.flatMap { it.seasons }.toSet() return when { - allSeasons.isEmpty() -> "Not specified" - allSeasons.size == Season.entries.size -> "All seasons" - else -> allSeasons.joinToString("/") { it.displayName() } + allSeasons.isEmpty() -> stringResource(R.string.common_not_specified) + allSeasons.size == Season.entries.size -> stringResource(R.string.common_all_seasons) + else -> allSeasons.joinToString("/") { season -> + context.getString(season.stringRes()) + } } } +@androidx.annotation.StringRes +private fun Season.stringRes(): Int = when (this) { + Season.SPRING -> R.string.season_spring + Season.SUMMER -> R.string.season_summer + Season.FALL -> R.string.season_fall + Season.WINTER -> R.string.season_winter +} + private val previewItems = listOf( ClothingItem("i1", "Black T-Shirt", Category.TOP, listOf("Black"), photoPath = "", createdAt = 0), ClothingItem("i2", "Navy Jeans", Category.BOTTOM, listOf("Navy"), photoPath = "", createdAt = 0), diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt index e63ac3d..80dfede 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/OutfitsScreen.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -49,6 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowWidthSizeClass +import com.github.worn.R import com.github.worn.domain.model.Outfit import com.github.worn.presentation.viewmodel.OutfitEffect import com.github.worn.presentation.viewmodel.OutfitIntent @@ -232,7 +235,7 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "Your outfits", + text = stringResource(R.string.outfits_title), color = WornColors.TextPrimary, fontSize = if (outfitCount == 0) 22.sp else 28.sp, fontWeight = FontWeight.SemiBold, @@ -246,14 +249,14 @@ private fun OutfitsHeader(outfitCount: Int, onCreateClick: () -> Unit = {}) { ) { Icon(Icons.Default.Add, contentDescription = null, Modifier.size(16.dp)) Spacer(Modifier.width(4.dp)) - Text("Create", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) + Text(stringResource(R.string.outfits_button_create), fontWeight = FontWeight.SemiBold, fontSize = 14.sp) } } } if (outfitCount > 0) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "$outfitCount saved combination${if (outfitCount != 1) "s" else ""}", + text = pluralStringResource(R.plurals.saved_combinations, outfitCount, outfitCount), color = WornColors.TextSecondary, fontSize = 14.sp, fontWeight = FontWeight.Medium, @@ -270,7 +273,7 @@ private fun SelectionHeader(count: Int, onCancel: () -> Unit, onDelete: () -> Un verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "$count selected", + text = pluralStringResource(R.plurals.selected_count, count, count), color = WornColors.TextPrimary, fontSize = 28.sp, fontWeight = FontWeight.Medium, @@ -283,12 +286,17 @@ private fun SelectionHeader(count: Int, onCancel: () -> Unit, onDelete: () -> Un ) { Icon(Icons.Outlined.Delete, contentDescription = null, tint = Color.White) Spacer(Modifier.width(6.dp)) - Text("Delete", color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 15.sp) + Text( + stringResource(R.string.common_delete), + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp, + ) } } Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Cancel", + text = stringResource(R.string.common_cancel), color = WornColors.TextSecondary, fontSize = 15.sp, fontWeight = FontWeight.Medium, @@ -350,7 +358,7 @@ private fun EmptyState(onCreateClick: () -> Unit = {}) { } Spacer(Modifier.height(24.dp)) Text( - "No outfits yet", + stringResource(R.string.outfits_empty_title), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, @@ -358,7 +366,7 @@ private fun EmptyState(onCreateClick: () -> Unit = {}) { ) Spacer(Modifier.height(24.dp)) Text( - "Create your first look by combining\nitems from your wardrobe", + stringResource(R.string.outfits_empty_description), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, @@ -376,7 +384,7 @@ private fun EmptyState(onCreateClick: () -> Unit = {}) { ) { Icon(Icons.Default.Add, contentDescription = null, Modifier.size(18.dp), WornColors.BgPage) Text( - "Create your first outfit", + stringResource(R.string.outfits_empty_cta), color = WornColors.TextOnColor, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) } @@ -394,14 +402,14 @@ private fun DeleteConfirmationDialog( onDismissRequest = onDismiss, title = { Text( - "Delete $count outfit${if (count != 1) "s" else ""}?", + pluralStringResource(R.plurals.delete_outfits_title, count, count), fontWeight = FontWeight.SemiBold, fontSize = 22.sp, ) }, text = { Text( - "This action cannot be undone. The selected outfits will be permanently removed.", + stringResource(R.string.outfits_delete_dialog_message), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, @@ -414,11 +422,18 @@ private fun DeleteConfirmationDialog( colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), shape = RoundedCornerShape(24.dp), ) { - Text(if (isDeleting) "Deleting…" else "Delete", fontWeight = FontWeight.SemiBold) + Text( + text = if (isDeleting) { + stringResource(R.string.common_deleting) + } else { + stringResource(R.string.common_delete) + }, + fontWeight = FontWeight.SemiBold, + ) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/SettingsScreen.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/SettingsScreen.kt index f899bb5..543fe35 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/SettingsScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/SettingsScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -58,6 +59,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.window.core.layout.WindowWidthSizeClass +import com.github.worn.R import com.github.worn.domain.model.AgeRange import com.github.worn.domain.model.BodyType import com.github.worn.domain.model.Climate @@ -141,7 +143,7 @@ private fun SettingsScaffold( ) { Spacer(Modifier.height(24.dp)) Text( - text = "Settings", + text = stringResource(R.string.settings_title), color = WornColors.TextPrimary, fontSize = 28.sp, fontWeight = FontWeight.SemiBold, @@ -149,27 +151,29 @@ private fun SettingsScaffold( ) Spacer(Modifier.height(28.dp)) - SectionLabel("YOUR PROFILE") + SectionLabel(stringResource(R.string.settings_section_profile)) Spacer(Modifier.height(10.dp)) SettingsCard( icon = { SettingsIcon(color = WornColors.AccentGreen, icon = Icons.Outlined.Person) }, - title = "Your Profile", + title = stringResource(R.string.settings_your_profile), subtitle = state.userProfile.summaryText(), onClick = onProfileClick, ) Spacer(Modifier.height(24.dp)) - SectionLabel("AI FEATURES") + SectionLabel(stringResource(R.string.settings_section_ai)) Spacer(Modifier.height(10.dp)) SettingsCard( icon = { SettingsIcon(color = WornColors.AccentIndigo, icon = Icons.Outlined.AutoAwesome) }, - title = "Claude API Key", - subtitle = if (state.hasApiKey) "Connected" else "Required for AI features", + title = stringResource(R.string.settings_api_key_title), + subtitle = stringResource( + if (state.hasApiKey) R.string.settings_api_key_connected else R.string.settings_api_key_required, + ), onClick = onApiKeyClick, ) Spacer(Modifier.height(24.dp)) - SectionLabel("ABOUT") + SectionLabel(stringResource(R.string.settings_section_about)) Spacer(Modifier.height(10.dp)) AboutCard() @@ -258,7 +262,12 @@ private fun AboutCard() { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(16.dp), ) { - Text("Version", color = WornColors.TextPrimary, fontSize = 15.sp, modifier = Modifier.weight(1f)) + Text( + stringResource(R.string.settings_version), + color = WornColors.TextPrimary, + fontSize = 15.sp, + modifier = Modifier.weight(1f), + ) Text(versionName ?: "1.0", color = WornColors.TextSecondary, fontSize = 15.sp) } HorizontalDivider(color = WornColors.BorderSubtle.copy(alpha = 0.5f)) @@ -270,7 +279,12 @@ private fun AboutCard() { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(16.dp), ) { - Text("Licenses", color = WornColors.TextPrimary, fontSize = 15.sp, modifier = Modifier.weight(1f)) + Text( + stringResource(R.string.settings_licenses), + color = WornColors.TextPrimary, + fontSize = 15.sp, + modifier = Modifier.weight(1f), + ) Icon( imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null, @@ -327,6 +341,12 @@ private fun ProfileSheet( @Composable private fun ProfileSheetContent(state: SettingsState, onIntent: (SettingsIntent) -> Unit, onSave: () -> Unit) { + val bodyTypeOptions = BodyType.entries.map { it to it.displayName() } + val styleProfileOptions = StyleProfile.entries.map { it to it.displayName() } + val ageRangeOptions = AgeRange.entries.map { it to it.displayName() } + val climateOptions = Climate.entries.map { it to it.displayName() } + val lifestyleOptions = Lifestyle.entries.map { it to it.displayName() } + Column( modifier = Modifier .fillMaxWidth() @@ -335,47 +355,47 @@ private fun ProfileSheetContent(state: SettingsState, onIntent: (SettingsIntent) verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = "Your Profile", + text = stringResource(R.string.settings_your_profile), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, ) Text( - text = "Help AI give better suggestions", + text = stringResource(R.string.settings_profile_help), color = WornColors.TextSecondary, fontSize = 14.sp, ) ChipGroup( - title = "Body Type", - options = BodyType.entries.map { it to it.displayName() }, + title = stringResource(R.string.label_body_type), + options = bodyTypeOptions, selected = state.userProfile.bodyType, onSelected = { onIntent(SettingsIntent.SelectBodyType(it)) }, ) ChipGroup( - title = "Style Profile", - options = StyleProfile.entries.map { it to it.displayName() }, + title = stringResource(R.string.label_style_profile), + options = styleProfileOptions, selected = state.userProfile.styleProfile, onSelected = { onIntent(SettingsIntent.SelectStyleProfile(it)) }, ) ChipGroup( - title = "Age Range", - options = AgeRange.entries.map { it to it.displayName() }, + title = stringResource(R.string.label_age_range), + options = ageRangeOptions, selected = state.userProfile.ageRange, onSelected = { onIntent(SettingsIntent.SelectAgeRange(it)) }, ) ChipGroup( - title = "Climate / Region", - options = Climate.entries.map { it to it.displayName() }, + title = stringResource(R.string.label_climate), + options = climateOptions, selected = state.userProfile.climate, onSelected = { onIntent(SettingsIntent.SelectClimate(it)) }, ) MultiChipGroup( - title = "Lifestyle / Occasions", - options = Lifestyle.entries.map { it to it.displayName() }, + title = stringResource(R.string.label_lifestyle), + options = lifestyleOptions, selected = state.userProfile.lifestyles, onToggle = { onIntent(SettingsIntent.ToggleLifestyle(it)) }, ) - SaveGradientButton(text = "Save", onClick = onSave) + SaveGradientButton(text = stringResource(R.string.common_save), onClick = onSave) } } @@ -427,7 +447,7 @@ private fun ApiKeySheetContent( onToggleVisibility = { passwordVisible = !passwordVisible }, ) SaveGradientButton( - text = "Save & Connect", + text = stringResource(R.string.settings_save_connect), enabled = !hasApiKey && keyInput.isNotBlank(), onClick = { onSave(keyInput) @@ -438,7 +458,7 @@ private fun ApiKeySheetContent( Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { Surface(onClick = onClear, color = Color.Transparent) { Text( - text = "Remove key", + text = stringResource(R.string.settings_remove_key), color = WornColors.TextSecondary, fontSize = 14.sp, fontWeight = FontWeight.Medium, @@ -453,19 +473,18 @@ private fun ApiKeySheetContent( @Composable private fun ApiKeySheetHeader() { Text( - text = "Connect Claude AI", + text = stringResource(R.string.settings_connect_claude), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, ) Text( - text = "Paste your Anthropic API key to unlock AI-powered " + - "features like auto-tagging clothes and outfit analysis.", + text = stringResource(R.string.settings_api_description), color = WornColors.TextSecondary, fontSize = 14.sp, ) Text( - text = "Get a free key at console.anthropic.com →", + text = stringResource(R.string.settings_api_get_key), color = WornColors.AccentGreen, fontSize = 13.sp, fontWeight = FontWeight.Medium, @@ -498,7 +517,9 @@ private fun ApiKeyTextField( } else { Icons.Outlined.VisibilityOff }, - contentDescription = if (passwordVisible) "Hide" else "Show", + contentDescription = stringResource( + if (passwordVisible) R.string.settings_api_hide else R.string.settings_api_show, + ), tint = WornColors.IconMuted, ) } @@ -591,7 +612,7 @@ private fun MultiChipGroup( Row { Text(title, color = WornColors.TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold) Spacer(Modifier.width(6.dp)) - Text("(multi-select)", color = WornColors.TextMuted, fontSize = 12.sp) + Text(stringResource(R.string.settings_multi_select), color = WornColors.TextMuted, fontSize = 12.sp) } FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -622,53 +643,59 @@ private fun MultiChipGroup( // region Display names +@Composable private fun UserProfile.summaryText(): String { val parts = listOfNotNull( bodyType?.displayName(), styleProfile?.displayName(), ageRange?.displayName(), ) - return if (parts.isEmpty()) "Tap to set up" else parts.joinToString(" · ") + return if (parts.isEmpty()) stringResource(R.string.settings_profile_subtitle_empty) else parts.joinToString(" · ") } +@Composable private fun BodyType.displayName(): String = when (this) { - BodyType.SLIM -> "Slim" - BodyType.ATHLETIC -> "Athletic" - BodyType.AVERAGE -> "Average" - BodyType.STOCKY -> "Stocky" - BodyType.SHORT -> "Short" - BodyType.TALL_AND_SLIM -> "Tall & Slim" - BodyType.BIG_AND_TALL -> "Big & Tall" + BodyType.SLIM -> stringResource(R.string.body_type_slim) + BodyType.ATHLETIC -> stringResource(R.string.body_type_athletic) + BodyType.AVERAGE -> stringResource(R.string.body_type_average) + BodyType.STOCKY -> stringResource(R.string.body_type_stocky) + BodyType.SHORT -> stringResource(R.string.body_type_short) + BodyType.TALL_AND_SLIM -> stringResource(R.string.body_type_tall_and_slim) + BodyType.BIG_AND_TALL -> stringResource(R.string.body_type_big_and_tall) } +@Composable private fun StyleProfile.displayName(): String = when (this) { - StyleProfile.CLASSIC -> "Classic" - StyleProfile.CASUAL -> "Casual" - StyleProfile.STREETWEAR -> "Streetwear" - StyleProfile.SMART_CASUAL -> "Smart Casual" - StyleProfile.MINIMALIST -> "Minimalist" + StyleProfile.CLASSIC -> stringResource(R.string.style_classic) + StyleProfile.CASUAL -> stringResource(R.string.style_casual) + StyleProfile.STREETWEAR -> stringResource(R.string.style_streetwear) + StyleProfile.SMART_CASUAL -> stringResource(R.string.style_smart_casual) + StyleProfile.MINIMALIST -> stringResource(R.string.style_minimalist) } +@Composable private fun AgeRange.displayName(): String = when (this) { - AgeRange.AGE_18_25 -> "18-25" - AgeRange.AGE_26_35 -> "26-35" - AgeRange.AGE_36_45 -> "36-45" - AgeRange.AGE_46_PLUS -> "46+" + AgeRange.AGE_18_25 -> stringResource(R.string.age_18_25) + AgeRange.AGE_26_35 -> stringResource(R.string.age_26_35) + AgeRange.AGE_36_45 -> stringResource(R.string.age_36_45) + AgeRange.AGE_46_PLUS -> stringResource(R.string.age_46_plus) } +@Composable private fun Climate.displayName(): String = when (this) { - Climate.TROPICAL -> "Tropical" - Climate.TEMPERATE -> "Temperate" - Climate.COLD -> "Cold" - Climate.MIXED -> "Mixed" + Climate.TROPICAL -> stringResource(R.string.climate_tropical) + Climate.TEMPERATE -> stringResource(R.string.climate_temperate) + Climate.COLD -> stringResource(R.string.climate_cold) + Climate.MIXED -> stringResource(R.string.climate_mixed) } +@Composable private fun Lifestyle.displayName(): String = when (this) { - Lifestyle.WORK_OFFICE -> "Work (Office)" - Lifestyle.WORK_MANUAL -> "Work (Manual)" - Lifestyle.SOCIAL -> "Social" - Lifestyle.SPORTS -> "Sports" - Lifestyle.FORMAL_EVENTS -> "Formal Events" + Lifestyle.WORK_OFFICE -> stringResource(R.string.lifestyle_work_office) + Lifestyle.WORK_MANUAL -> stringResource(R.string.lifestyle_work_manual) + Lifestyle.SOCIAL -> stringResource(R.string.lifestyle_social) + Lifestyle.SPORTS -> stringResource(R.string.lifestyle_sports) + Lifestyle.FORMAL_EVENTS -> stringResource(R.string.lifestyle_formal_events) } // endregion diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/TryItScreen.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/TryItScreen.kt index cf48bda..c1e61d3 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/TryItScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/TryItScreen.kt @@ -65,6 +65,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -73,6 +74,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.window.core.layout.WindowWidthSizeClass import coil3.compose.AsyncImage +import com.github.worn.R import com.github.worn.domain.model.ClothingItem import com.github.worn.domain.model.TryItResult import com.github.worn.presentation.viewmodel.TryItEffect @@ -204,7 +206,7 @@ private fun PhotoSourceDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("Add photo") }, + title = { Text(stringResource(R.string.add_item_photo_dialog_title)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { TextButton(onClick = onCamera, modifier = Modifier.fillMaxWidth()) { @@ -215,7 +217,7 @@ private fun PhotoSourceDialog( ) { Icon(Icons.Outlined.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(12.dp)) - Text("Take photo", fontSize = 16.sp) + Text(stringResource(R.string.add_item_take_photo), fontSize = 16.sp) } } TextButton(onClick = onGallery, modifier = Modifier.fillMaxWidth()) { @@ -230,13 +232,13 @@ private fun PhotoSourceDialog( modifier = Modifier.size(24.dp), ) Spacer(Modifier.width(12.dp)) - Text("Choose from gallery", fontSize = 16.sp) + Text(stringResource(R.string.add_item_choose_gallery), fontSize = 16.sp) } } } }, confirmButton = {}, - dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } }, + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } @@ -327,7 +329,7 @@ private fun AiEmptyContent( } Spacer(Modifier.height(24.dp)) Text( - text = "AI powered analysis", + text = stringResource(R.string.tryit_ai_empty_title), color = WornColors.TextPrimary, fontSize = titleSize, fontWeight = FontWeight.Medium, @@ -335,7 +337,7 @@ private fun AiEmptyContent( ) Spacer(Modifier.height(12.dp)) Text( - text = "Connect your Claude API key in Settings to analyze items against your wardrobe.", + text = stringResource(R.string.tryit_ai_empty_description), color = WornColors.TextSecondary, fontSize = if (isCompact) 15.sp else 16.sp, lineHeight = if (isCompact) 22.sp else 24.sp, @@ -343,7 +345,7 @@ private fun AiEmptyContent( modifier = Modifier.widthIn(max = descWidth), ) Spacer(Modifier.height(24.dp)) - IndigoCtaButton(text = "Connect Claude AI", onClick = onGoToSettings) + IndigoCtaButton(text = stringResource(R.string.tryit_connect_cta), onClick = onGoToSettings) } } @@ -479,7 +481,7 @@ private fun TryItTabletContent( @Composable private fun TryItTitle(fontSize: androidx.compose.ui.unit.TextUnit) { Text( - text = "Would it fit your wardrobe?", + text = stringResource(R.string.tryit_title), color = WornColors.TextPrimary, fontSize = fontSize, fontWeight = FontWeight.SemiBold, @@ -522,7 +524,7 @@ private fun UploadZone( ) Spacer(Modifier.height(12.dp)) Text( - text = "Upload a photo of the item\nyou're considering", + text = stringResource(R.string.tryit_upload_hint), color = WornColors.TextSecondary, fontSize = 13.sp, fontWeight = FontWeight.Medium, @@ -564,7 +566,7 @@ private fun AnalyzeButton(onClick: () -> Unit) { modifier = Modifier.size(20.dp), ) Text( - "Analyze with Claude", + stringResource(R.string.tryit_analyze), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, @@ -623,7 +625,7 @@ private fun PairsSection( Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "It would pair with...", + text = stringResource(R.string.tryit_pairs_with), color = WornColors.TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, @@ -692,7 +694,7 @@ private fun CombinationsCard(count: Int, isCompact: Boolean) { .padding(horizontal = 20.dp, vertical = 16.dp), ) { Text( - text = "Combinations unlocked", + text = stringResource(R.string.tryit_combinations_unlocked), color = WornColors.TextSecondary, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, @@ -717,7 +719,7 @@ private fun GapsFilledSection(gaps: List, isCompact: Boolean) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = "Wardrobe gaps it fills", + text = stringResource(R.string.tryit_gaps_filled), color = WornColors.TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, @@ -754,7 +756,7 @@ private fun DecisionBanner(worthAdding: Boolean, isCompact: Boolean) { Brush.verticalGradient(listOf(Color(0xFF8B7D7D), Color(0xFF6B5E5E))) } val icon = if (worthAdding) Icons.Outlined.CheckCircle else Icons.Outlined.Cancel - val text = if (worthAdding) "Worth adding" else "Skip this one" + val text = stringResource(if (worthAdding) R.string.tryit_worth_adding else R.string.tryit_skip) Box( contentAlignment = Alignment.Center, diff --git a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt index 0cbbe5d..cc9b0d8 100644 --- a/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt +++ b/composeApp/src/androidMain/kotlin/com/github/worn/ui/screen/WardrobeScreen.kt @@ -44,6 +44,8 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.window.core.layout.WindowWidthSizeClass @@ -244,7 +246,11 @@ private fun WardrobeScaffold( private fun WardrobeHeader(itemCount: Int) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = if (itemCount == 0) "Your wardrobe" else "Worn", + text = if (itemCount == 0) { + stringResource(R.string.wardrobe_title_empty) + } else { + stringResource(R.string.wardrobe_title) + }, color = WornColors.TextPrimary, fontSize = if (itemCount == 0) 22.sp else 28.sp, fontWeight = FontWeight.SemiBold, @@ -253,7 +259,7 @@ private fun WardrobeHeader(itemCount: Int) { if (itemCount > 0) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Your capsule wardrobe \u00B7 $itemCount items", + text = stringResource(R.string.wardrobe_subtitle, itemCount), color = WornColors.TextSecondary, fontSize = 14.sp, fontWeight = FontWeight.Medium, @@ -270,7 +276,7 @@ private fun SelectionHeader(count: Int, onCancel: () -> Unit, onDelete: () -> Un verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "$count selected", + text = pluralStringResource(R.plurals.selected_count, count, count), color = WornColors.TextPrimary, fontSize = 28.sp, fontWeight = FontWeight.Medium, @@ -283,12 +289,17 @@ private fun SelectionHeader(count: Int, onCancel: () -> Unit, onDelete: () -> Un ) { Icon(Icons.Outlined.Delete, contentDescription = null, tint = Color.White) Spacer(Modifier.width(6.dp)) - Text("Delete", color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 15.sp) + Text( + stringResource(R.string.common_delete), + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp, + ) } } Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Cancel", + text = stringResource(R.string.common_cancel), color = WornColors.TextSecondary, fontSize = 15.sp, fontWeight = FontWeight.Medium, @@ -353,7 +364,7 @@ private fun CategoryEmptyState() { ) Spacer(Modifier.height(16.dp)) Text( - "No items in this category", + stringResource(R.string.wardrobe_category_empty), color = WornColors.TextSecondary, fontSize = 16.sp, fontWeight = FontWeight.Medium, @@ -384,7 +395,7 @@ private fun EmptyState(onAddItemClick: () -> Unit) { } Spacer(Modifier.height(24.dp)) Text( - "No items yet", + stringResource(R.string.wardrobe_empty_title), color = WornColors.TextPrimary, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, @@ -392,7 +403,7 @@ private fun EmptyState(onAddItemClick: () -> Unit) { ) Spacer(Modifier.height(24.dp)) Text( - "Add your first piece to start\nbuilding your wardrobe", + stringResource(R.string.wardrobe_empty_description), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, @@ -410,7 +421,7 @@ private fun EmptyState(onAddItemClick: () -> Unit) { ) { Icon(Icons.Default.Add, contentDescription = null, Modifier.size(18.dp), WornColors.BgPage) Text( - "Add your first item", + stringResource(R.string.wardrobe_empty_cta), color = WornColors.TextOnColor, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) } @@ -427,7 +438,7 @@ private fun AddItemFab(onClick: () -> Unit) { ) { Icon(Icons.Default.Add, contentDescription = null) Spacer(Modifier.width(8.dp)) - Text(text = "Add item", fontWeight = FontWeight.SemiBold, fontSize = 15.sp) + Text(text = stringResource(R.string.wardrobe_fab_add), fontWeight = FontWeight.SemiBold, fontSize = 15.sp) } } @@ -442,14 +453,14 @@ private fun DeleteConfirmationDialog( onDismissRequest = onDismiss, title = { Text( - "Delete $count item${if (count != 1) "s" else ""}?", + pluralStringResource(R.plurals.delete_items_title, count, count), fontWeight = FontWeight.SemiBold, fontSize = 22.sp, ) }, text = { Text( - "This action cannot be undone. The selected items will be permanently removed from your wardrobe.", + stringResource(R.string.wardrobe_delete_dialog_message), color = WornColors.TextSecondary, fontSize = 15.sp, lineHeight = 22.sp, @@ -462,11 +473,18 @@ private fun DeleteConfirmationDialog( colors = ButtonDefaults.buttonColors(containerColor = WornColors.DeleteRed), shape = RoundedCornerShape(24.dp), ) { - Text(if (isDeleting) "Deleting…" else "Delete", fontWeight = FontWeight.SemiBold) + Text( + text = if (isDeleting) { + stringResource(R.string.common_deleting) + } else { + stringResource(R.string.common_delete) + }, + fontWeight = FontWeight.SemiBold, + ) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } diff --git a/composeApp/src/androidMain/res/values-pt-rBR/strings.xml b/composeApp/src/androidMain/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..e13507c --- /dev/null +++ b/composeApp/src/androidMain/res/values-pt-rBR/strings.xml @@ -0,0 +1,256 @@ + + Worn + + + Excluir + Cancelar + Salvar + Excluindo… + Salvando… + Salvar alterações + Todas as estações + Não especificado + + + GUARDA-ROUPA + LOOKS + LACUNAS + TESTAR + AJUSTES + + + Seu guarda-roupa + Worn + Seu guarda-roupa cápsula · %d itens + Nenhum item ainda + Adicione sua primeira peça para\ncomeçar a montar seu guarda-roupa + Adicione seu primeiro item + Nenhum item nesta categoria + Adicionar item + Esta ação não pode ser desfeita. Os itens selecionados serão removidos permanentemente do seu guarda-roupa. + + + Seus looks + Criar + Nenhum look ainda + Crie seu primeiro look combinando\nitens do seu guarda-roupa + Crie seu primeiro look + Esta ação não pode ser desfeita. Os looks selecionados serão removidos permanentemente. + + + Adicionar novo item + Editar item + Adicionar foto + Tirar foto + Escolher da galeria + Toque para adicionar foto + Auto-tag com IA + Nome do item + Salvar no guarda-roupa + + + Criar look + Editar look + Nome do look + Selecionar itens + Salvar look + + + Editar Item + Excluir Item + Excluir item? + Esta ação não pode ser desfeita. \"%s\" será removido permanentemente do seu guarda-roupa. + + + Editar Look + Excluir Look + %d itens + Excluir look? + Esta ação não pode ser desfeita. \"%s\" será removido permanentemente. + + + Ajustes + SEU PERFIL + FUNÇÕES DE IA + SOBRE + Seu Perfil + Toque para configurar + Ajude a IA a dar melhores sugestões + Chave da API Claude + Conectado + Necessária para funções de IA + Conectar Claude IA + Cole sua chave da API Anthropic para desbloquear funções de IA como auto-tag de roupas e análise de looks. + Obtenha uma chave gratuita em console.anthropic.com → + Salvar e conectar + Remover chave + Versão + Licenças + (seleção múltipla) + Ocultar + Mostrar + + + O que está faltando + Itens que mais expandiriam suas combinações + Seu guarda-roupa parece completo! + Não encontramos nenhuma lacuna.\nVocê tem ótima cobertura em todas as categorias. + Recomendações com IA + Sugestões Comuns + Sugestões personalizadas baseadas no seu guarda-roupa + Conecte a Claude IA para sugestões personalizadas + Combinaria com %d dos seus itens + Item essencial do guarda-roupa + Adicionar ao guarda-roupa + Dispensar + + + Combinaria com seu guarda-roupa? + Envie uma foto do item\nque você está considerando + Analisar com Claude + Análise com inteligência artificial + Conecte sua chave da API Claude nos Ajustes para analisar itens com seu guarda-roupa. + Conectar Claude IA + Combinaria com… + Combinações desbloqueadas + Lacunas que preenche + Vale adicionar + Pule esse + + + Desbloquear funções de IA + Adicione sua chave da API Claude nos Ajustes para habilitar. + Ir para Ajustes + + + Todos + + + Cor + Estação + Caimento + Subcategoria + Material + Categoria + Itens + Biotipo + Perfil de Estilo + Faixa Etária + Clima / Região + Estilo de Vida / Ocasiões + + + Partes de Cima + Partes de Baixo + Agasalhos + Calçados + Acessórios + + + Primavera + Verão + Outono + Inverno + + + Slim + Regular + Relaxado + Oversized + + + Algodão + Linho + Jeans + + Sintético + Couro + Seda + Malha + + + Camiseta + Polo + Camisa social + Henley + Suéter + Moletom + Jeans + Chinos + Calça social + Bermuda + Calça cargo + Calça de moletom + Bomber + Trucker + Puffer + Blazer + Casaco + Corta-vento + Tênis + Coturno + Bota chelsea + Derby + Oxford + Mocassim + Sandálias + Relógio + Cinto + Óculos de sol + Boné + Cachecol + Mochila + + + Magro + Atlético + Médio + Robusto + Baixo + Alto e Magro + Grande e Alto + + + Clássico + Casual + Streetwear + Smart Casual + Minimalista + + + 18–25 + 26–35 + 36–45 + 46+ + + + Tropical + Temperado + Frio + Misto + + + Trabalho (Escritório) + Trabalho (Manual) + Social + Esportes + Eventos Formais + + + + %d selecionado + %d selecionados + + + Excluir %d item? + Excluir %d itens? + + + Excluir %d look? + Excluir %d looks? + + + %d combinação salva + %d combinações salvas + + diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml index e24f5c8..eec6371 100644 --- a/composeApp/src/androidMain/res/values/strings.xml +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -1,3 +1,256 @@ Worn - \ No newline at end of file + + + Delete + Cancel + Save + Deleting… + Saving… + Save Changes + All seasons + Not specified + + + WARDROBE + OUTFITS + GAPS + TRY IT + SETTINGS + + + Your wardrobe + Worn + Your capsule wardrobe · %d items + No items yet + Add your first piece to start\nbuilding your wardrobe + Add your first item + No items in this category + Add item + This action cannot be undone. The selected items will be permanently removed from your wardrobe. + + + Your outfits + Create + No outfits yet + Create your first look by combining\nitems from your wardrobe + Create your first outfit + This action cannot be undone. The selected outfits will be permanently removed. + + + Add new item + Edit item + Add photo + Take photo + Choose from gallery + Tap to add photo + Auto-tag with AI + Item name + Save to wardrobe + + + Create outfit + Edit outfit + Outfit name + Select items + Save outfit + + + Edit Item + Delete Item + Delete item? + This action cannot be undone. \"%s\" will be permanently removed from your wardrobe. + + + Edit Outfit + Delete Outfit + %d items + Delete outfit? + This action cannot be undone. \"%s\" will be permanently removed. + + + Settings + YOUR PROFILE + AI FEATURES + ABOUT + Your Profile + Tap to set up + Help AI give better suggestions + Claude API Key + Connected + Required for AI features + Connect Claude AI + Paste your Anthropic API key to unlock AI-powered features like auto-tagging clothes and outfit analysis. + Get a free key at console.anthropic.com → + Save & Connect + Remove key + Version + Licenses + (multi-select) + Hide + Show + + + What\'s missing + Items that would expand your combinations most + Your wardrobe looks complete! + We couldn\'t find any gaps.\nYou have great coverage across categories. + AI Recommendations + Common Suggestions + Personalized suggestions based on your wardrobe + Connect Claude AI for personalized picks + Would pair with %d of your items + Common wardrobe essential + Add to Wardrobe + Dismiss + + + Would it fit your wardrobe? + Upload a photo of the item\nyou\'re considering + Analyze with Claude + AI powered analysis + Connect your Claude API key in Settings to analyze items against your wardrobe. + Connect Claude AI + It would pair with… + Combinations unlocked + Wardrobe gaps it fills + Worth adding + Skip this one + + + Unlock AI features + Add your Claude API key in Settings to enable this. + Go to Settings + + + All + + + Color + Season + Fit + Subcategory + Material + Category + Items + Body Type + Style Profile + Age Range + Climate / Region + Lifestyle / Occasions + + + Tops + Bottoms + Outerwear + Shoes + Accessories + + + Spring + Summer + Fall + Winter + + + Slim Fit + Regular + Relaxed + Oversized + + + Cotton + Linen + Denim + Wool + Synthetic + Leather + Silk + Knit + + + T shirt + Polo + Dress shirt + Henley + Sweater + Hoodie + Jeans + Chinos + Tailored pants + Shorts + Cargo pants + Sweatpants + Bomber + Trucker + Puffer + Blazer + Coat + Windbreaker + Sneakers + Boots military + Boots chelsea + Derby + Oxford + Loafer + Sandals + Watch + Belt + Sunglasses + Hat cap + Scarf + Bag backpack + + + Slim + Athletic + Average + Stocky + Short + Tall & Slim + Big & Tall + + + Classic + Casual + Streetwear + Smart Casual + Minimalist + + + 18–25 + 26–35 + 36–45 + 46+ + + + Tropical + Temperate + Cold + Mixed + + + Work (Office) + Work (Manual) + Social + Sports + Formal Events + + + + %d selected + %d selected + + + Delete %d item? + Delete %d items? + + + Delete %d outfit? + Delete %d outfits? + + + %d saved combination + %d saved combinations + + diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 07d3b53..c7f3380 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ knownRegions = ( en, Base, + "pt-BR", ); mainGroup = D51AC824190772C80F46ACD7; minimizedProjectReferenceProxies = 1; diff --git a/iosApp/iosApp/Components/AiLockedSheet.swift b/iosApp/iosApp/Components/AiLockedSheet.swift index 72162ac..5d51137 100644 --- a/iosApp/iosApp/Components/AiLockedSheet.swift +++ b/iosApp/iosApp/Components/AiLockedSheet.swift @@ -25,11 +25,11 @@ struct AiLockedSheet: View { .foregroundColor(.white) } - Text("Unlock AI features") + Text(String(localized: "ai_locked_title")) .font(.system(size: 22, weight: .medium)) .foregroundColor(WornColors.textPrimary) - Text("Add your Claude API key in Settings to enable this.") + Text(String(localized: "ai_locked_description")) .font(.system(size: 14)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) @@ -40,7 +40,7 @@ struct AiLockedSheet: View { onGoToSettings() onDismiss() } label: { - Text("Go to Settings") + Text(String(localized: "ai_locked_cta")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) .padding(.horizontal, 40) diff --git a/iosApp/iosApp/Components/CategoryFilterChips.swift b/iosApp/iosApp/Components/CategoryFilterChips.swift index 60f99b9..baad33c 100644 --- a/iosApp/iosApp/Components/CategoryFilterChips.swift +++ b/iosApp/iosApp/Components/CategoryFilterChips.swift @@ -5,14 +5,16 @@ struct CategoryFilterChips: View { let activeCategory: Category? let onCategorySelected: (Category?) -> Void - private let allChips: [(category: Category?, label: String)] = [ - (nil, "All"), - (.top, "Tops"), - (.bottom, "Bottoms"), - (.outerwear, "Outerwear"), - (.shoes, "Shoes"), - (.accessory, "Accessories"), - ] + private var allChips: [(category: Category?, label: String)] { + [ + (nil, String(localized: "filter_all")), + (.top, String(localized: "category_tops")), + (.bottom, String(localized: "category_bottoms")), + (.outerwear, String(localized: "category_outerwear")), + (.shoes, String(localized: "category_shoes")), + (.accessory, String(localized: "category_accessories")), + ] + } var body: some View { ScrollView(.horizontal, showsIndicators: false) { diff --git a/iosApp/iosApp/Components/ClothingCard.swift b/iosApp/iosApp/Components/ClothingCard.swift index 459c8b2..bdc5e7e 100644 --- a/iosApp/iosApp/Components/ClothingCard.swift +++ b/iosApp/iosApp/Components/ClothingCard.swift @@ -106,11 +106,11 @@ struct ClothingCard: View { private func displayLabel(for category: Category) -> String { switch category { - case .top: return "Tops" - case .bottom: return "Bottoms" - case .outerwear: return "Outerwear" - case .shoes: return "Shoes" - case .accessory: return "Accessories" + case .top: return String(localized: "category_tops") + case .bottom: return String(localized: "category_bottoms") + case .outerwear: return String(localized: "category_outerwear") + case .shoes: return String(localized: "category_shoes") + case .accessory: return String(localized: "category_accessories") default: return "" } } diff --git a/iosApp/iosApp/Components/WornBottomBar.swift b/iosApp/iosApp/Components/WornBottomBar.swift index 6cba57b..42ded28 100644 --- a/iosApp/iosApp/Components/WornBottomBar.swift +++ b/iosApp/iosApp/Components/WornBottomBar.swift @@ -1,11 +1,11 @@ import SwiftUI enum WornTab: String, CaseIterable { - case wardrobe = "WARDROBE" - case outfits = "OUTFITS" - case gaps = "GAPS" - case tryIt = "TRY IT" - case settings = "SETTINGS" + case wardrobe + case outfits + case gaps + case tryIt + case settings var icon: String { switch self { @@ -16,6 +16,16 @@ enum WornTab: String, CaseIterable { case .settings: return "gearshape" } } + + var label: String { + switch self { + case .wardrobe: return String(localized: "tab_wardrobe") + case .outfits: return String(localized: "tab_outfits") + case .gaps: return String(localized: "tab_gaps") + case .tryIt: return String(localized: "tab_try_it") + case .settings: return String(localized: "tab_settings") + } + } } struct WornBottomBar: View { @@ -62,7 +72,7 @@ private struct TabItem: View { VStack(spacing: 4) { Image(systemName: tab.icon) .font(.system(size: 18)) - Text(tab.rawValue) + Text(tab.label) .font(.system(size: 10, weight: .semibold)) .tracking(0.5) } diff --git a/iosApp/iosApp/Screens/AddItemSheet.swift b/iosApp/iosApp/Screens/AddItemSheet.swift index f3b0cc4..7d84de3 100644 --- a/iosApp/iosApp/Screens/AddItemSheet.swift +++ b/iosApp/iosApp/Screens/AddItemSheet.swift @@ -70,17 +70,17 @@ struct AddItemSheet: View { .padding(.bottom, 24) } .background(WornColors.bgElevated) - .navigationTitle(isEditing ? "Edit item" : "Add new item") + .navigationTitle(isEditing ? String(localized: "add_item_title_edit") : String(localized: "add_item_title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel", action: onDismiss) + Button(String(localized: "common_cancel"), action: onDismiss) } } - .confirmationDialog("Add photo", isPresented: $showSourceChooser) { - Button("Take Photo") { showCamera = true } - Button("Choose from Library") { showPhotoPicker = true } - Button("Cancel", role: .cancel) {} + .confirmationDialog(String(localized: "add_item_photo_dialog_title"), isPresented: $showSourceChooser) { + Button(String(localized: "add_item_take_photo")) { showCamera = true } + Button(String(localized: "add_item_choose_gallery")) { showPhotoPicker = true } + Button(String(localized: "common_cancel"), role: .cancel) {} } .photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images) .fullScreenCover(isPresented: $showCamera) { @@ -134,7 +134,7 @@ struct AddItemSheet: View { Image(systemName: "camera") .font(.system(size: 32)) .foregroundColor(WornColors.iconMuted) - Text("Tap to add photo") + Text(String(localized: "add_item_photo_hint")) .font(.system(size: 14, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -159,7 +159,7 @@ struct AddItemSheet: View { Text("✦") .font(.system(size: 12, weight: .semibold)) .foregroundColor(.white) - Text("Auto-tag with AI") + Text(String(localized: "add_item_ai_badge")) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.white) } @@ -176,7 +176,7 @@ struct AddItemSheet: View { } private var nameField: some View { - TextField("Item name", text: $name) + TextField(String(localized: "add_item_name_hint"), text: $name) .font(.system(size: 15)) .padding(16) .background(WornColors.bgCard) @@ -199,7 +199,7 @@ struct AddItemSheet: View { .foregroundColor(WornColors.textSecondary) .frame(width: 20, height: 20) } - Text(selectedCategory.map { displayName(for: $0) } ?? "Category") + Text(selectedCategory.map { displayName(for: $0) } ?? String(localized: "label_category")) .font(.system(size: 15)) .foregroundColor(selectedCategory != nil ? WornColors.textPrimary : WornColors.iconMuted) Spacer() @@ -250,7 +250,7 @@ struct AddItemSheet: View { private var colorSection: some View { VStack(alignment: .leading, spacing: 10) { - Text("Color") + Text(String(localized: "label_color")) .font(.system(size: 14, weight: .semibold)) .foregroundColor(WornColors.textPrimary) @@ -287,7 +287,7 @@ struct AddItemSheet: View { private var seasonSection: some View { VStack(alignment: .leading, spacing: 10) { - Text("Season") + Text(String(localized: "label_season")) .font(.system(size: 14, weight: .semibold)) .foregroundColor(WornColors.textPrimary) @@ -326,7 +326,7 @@ struct AddItemSheet: View { onSave(data, name, cat, Array(selectedColors), Array(selectedSeasons), selectedSubcategory, selectedFit, selectedMaterial) } label: { - Text(isSaving ? "Saving…" : (isEditing ? "Save Changes" : "Save to wardrobe")) + Text(isSaving ? String(localized: "common_saving") : (isEditing ? String(localized: "common_save_changes") : String(localized: "add_item_save_to_wardrobe"))) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -348,13 +348,13 @@ struct AddItemSheet: View { private var categoryOptions: [(Category, String)] { [ - (.top, "Tops"), (.bottom, "Bottoms"), - (.outerwear, "Outerwear"), (.shoes, "Shoes"), (.accessory, "Accessories"), + (.top, String(localized: "category_tops")), (.bottom, String(localized: "category_bottoms")), + (.outerwear, String(localized: "category_outerwear")), (.shoes, String(localized: "category_shoes")), (.accessory, String(localized: "category_accessories")), ] } private var seasonOptions: [(Season, String)] { - [(.spring, "Spring"), (.summer, "Summer"), (.fall, "Fall"), (.winter, "Winter")] + [(.spring, String(localized: "season_spring")), (.summer, String(localized: "season_summer")), (.fall, String(localized: "season_fall")), (.winter, String(localized: "season_winter"))] } private func iconName(for category: Category) -> String { @@ -370,11 +370,11 @@ struct AddItemSheet: View { private func displayName(for category: Category) -> String { switch category { - case .top: return "Tops" - case .bottom: return "Bottoms" - case .outerwear: return "Outerwear" - case .shoes: return "Shoes" - case .accessory: return "Accessories" + case .top: return String(localized: "category_tops") + case .bottom: return String(localized: "category_bottoms") + case .outerwear: return String(localized: "category_outerwear") + case .shoes: return String(localized: "category_shoes") + case .accessory: return String(localized: "category_accessories") default: return "" } } @@ -386,17 +386,22 @@ struct AddItemSheet: View { private var subcategoryOptions: [(Subcategory, String)] { guard let cat = selectedCategory else { return [] } return SubcategoryKt.subcategoriesFor(category: cat).map { sub in - (sub, sub.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized) + (sub, localizedSubcategoryName(sub)) } } + private func localizedSubcategoryName(_ subcategory: Subcategory) -> String { + let key = "subcategory_\(subcategory.name.lowercased())" + return String(localized: String.LocalizationValue(key)) + } + private var subcategoryField: some View { VStack(spacing: 0) { Button { withAnimation { subcategoryExpanded.toggle() } } label: { HStack { Text(selectedSubcategory.map { - $0.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized - } ?? "Subcategory") + localizedSubcategoryName($0) + } ?? String(localized: "label_subcategory")) .font(.system(size: 15)) .foregroundColor(selectedSubcategory != nil ? WornColors.textPrimary : WornColors.iconMuted) Spacer() @@ -441,13 +446,16 @@ struct AddItemSheet: View { // MARK: - Fit - private let fitOptions: [(Fit, String)] = [ - (.slimFit, "Slim Fit"), (.regular, "Regular"), (.relaxed, "Relaxed"), (.oversized, "Oversized"), - ] + private var fitOptions: [(Fit, String)] { + [ + (.slimFit, String(localized: "fit_slim")), (.regular, String(localized: "fit_regular")), + (.relaxed, String(localized: "fit_relaxed")), (.oversized, String(localized: "fit_oversized")), + ] + } private var fitSection: some View { VStack(alignment: .leading, spacing: 10) { - Text("Fit") + Text(String(localized: "label_fit")) .font(.system(size: 14, weight: .semibold)) .foregroundColor(WornColors.textPrimary) @@ -477,14 +485,18 @@ struct AddItemSheet: View { // MARK: - Material - private let materialOptions: [(Material, String)] = [ - (.cotton, "Cotton"), (.linen, "Linen"), (.denim, "Denim"), (.wool, "Wool"), - (.synthetic, "Synthetic"), (.leather, "Leather"), (.silk, "Silk"), (.knit, "Knit"), - ] + private var materialOptions: [(Material, String)] { + [ + (.cotton, String(localized: "material_cotton")), (.linen, String(localized: "material_linen")), + (.denim, String(localized: "material_denim")), (.wool, String(localized: "material_wool")), + (.synthetic, String(localized: "material_synthetic")), (.leather, String(localized: "material_leather")), + (.silk, String(localized: "material_silk")), (.knit, String(localized: "material_knit")), + ] + } private var materialSection: some View { VStack(alignment: .leading, spacing: 10) { - Text("Material") + Text(String(localized: "label_material")) .font(.system(size: 14, weight: .semibold)) .foregroundColor(WornColors.textPrimary) diff --git a/iosApp/iosApp/Screens/CreateOutfitSheet.swift b/iosApp/iosApp/Screens/CreateOutfitSheet.swift index 738fcd0..e71354c 100644 --- a/iosApp/iosApp/Screens/CreateOutfitSheet.swift +++ b/iosApp/iosApp/Screens/CreateOutfitSheet.swift @@ -40,7 +40,7 @@ struct CreateOutfitSheet: View { .padding(.bottom, 24) } .background(WornColors.bgElevated) - .navigationTitle(existingOutfit != nil ? "Edit outfit" : "Create outfit") + .navigationTitle(existingOutfit != nil ? String(localized: "create_outfit_title_edit") : String(localized: "create_outfit_title")) .onAppear { if let outfit = existingOutfit, !didInitFromExisting { didInitFromExisting = true @@ -50,14 +50,14 @@ struct CreateOutfitSheet: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel", action: onDismiss) + Button(String(localized: "common_cancel"), action: onDismiss) } } } } private var nameField: some View { - TextField("Outfit name", text: $name) + TextField(String(localized: "create_outfit_name_hint"), text: $name) .font(.system(size: 15)) .padding(16) .background(WornColors.bgCard) @@ -70,12 +70,12 @@ struct CreateOutfitSheet: View { private var selectItemsHeader: some View { HStack { - Text("Select items") + Text(String(localized: "create_outfit_select_items")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(WornColors.textPrimary) Spacer() if !selectedItemIds.isEmpty { - Text("\(selectedItemIds.count) selected") + Text(String(format: String(localized: "selected_count"), selectedItemIds.count)) .font(.system(size: 13, weight: .medium)) .foregroundColor(WornColors.accentGreen) } @@ -103,7 +103,7 @@ struct CreateOutfitSheet: View { Button { onSave(name) } label: { - Text(isSaving ? "Saving…" : (existingOutfit != nil ? "Save Changes" : "Save outfit")) + Text(isSaving ? String(localized: "common_saving") : (existingOutfit != nil ? String(localized: "common_save_changes") : String(localized: "create_outfit_save"))) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) diff --git a/iosApp/iosApp/Screens/GapsScreen.swift b/iosApp/iosApp/Screens/GapsScreen.swift index 5e68524..0fa7e2d 100644 --- a/iosApp/iosApp/Screens/GapsScreen.swift +++ b/iosApp/iosApp/Screens/GapsScreen.swift @@ -14,11 +14,11 @@ struct GapsScreen: View { VStack(spacing: 0) { ScrollView { VStack(alignment: .leading, spacing: 0) { - Text("What's missing") + Text(String(localized: "gaps_title")) .font(.system(size: 28, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) - Text("Items that would expand your combinations most") + Text(String(localized: "gaps_subtitle")) .font(.system(size: 14)) .foregroundColor(WornColors.textSecondary) .padding(.top, 4) @@ -102,13 +102,13 @@ struct GapsScreen: View { } .frame(maxWidth: .infinity) - Text("Your wardrobe looks complete!") + Text(String(localized: "gaps_complete_title")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) .multilineTextAlignment(.center) - Text("We couldn't find any gaps.\nYou have great coverage across categories.") + Text(String(localized: "gaps_complete_description")) .font(.system(size: 14)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) @@ -151,12 +151,12 @@ struct GapsScreen: View { } label: { HStack { VStack(alignment: .leading, spacing: 2) { - Text(viewModel.state.isAiMode ? "AI Recommendations" : "Common Suggestions") + Text(viewModel.state.isAiMode ? String(localized: "gaps_banner_ai_title") : String(localized: "gaps_banner_common_title")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) Text(viewModel.state.isAiMode - ? "Personalized suggestions based on your wardrobe" - : "Connect Claude AI for personalized picks") + ? String(localized: "gaps_banner_ai_subtitle") + : String(localized: "gaps_banner_common_subtitle")) .font(.system(size: 13)) .foregroundColor(.white.opacity(0.8)) } @@ -190,8 +190,8 @@ struct GapsScreen: View { .font(.system(size: 15, weight: .medium)) .foregroundColor(WornColors.textPrimary) Text(viewModel.state.isAiMode - ? "Would pair with \(recommendation.pairingCount) of your items" - : "Common wardrobe essential") + ? String(format: String(localized: "gaps_pairing_ai"), recommendation.pairingCount) + : String(localized: "gaps_pairing_common")) .font(.system(size: 12)) .foregroundColor(WornColors.textSecondary) } @@ -301,8 +301,8 @@ private struct GapDetailSheet: View { .font(.system(size: 14)) .foregroundColor(WornColors.accentGreen) Text(isAiMode - ? "Would pair with \(recommendation.pairingCount) of your items" - : "Common wardrobe essential") + ? String(format: String(localized: "gaps_pairing_ai"), recommendation.pairingCount) + : String(localized: "gaps_pairing_common")) .font(.system(size: 13)) .foregroundColor(WornColors.textSecondary) } @@ -315,22 +315,22 @@ private struct GapDetailSheet: View { private var detailRows: some View { VStack(spacing: 0) { if let sub = recommendation.subcategory { - detailRow("Subcategory", sub.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized) + detailRow(String(localized: "label_subcategory"), sub.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized) } if !recommendation.colors.isEmpty { - detailRow("Color", recommendation.colors.joined(separator: ", ")) + detailRow(String(localized: "label_color"), recommendation.colors.joined(separator: ", ")) } if !recommendation.seasons.isEmpty { let text = recommendation.seasons.count == 4 - ? "All seasons" + ? String(localized: "common_all_seasons") : recommendation.seasons.map { $0.name.lowercased().capitalized }.joined(separator: ", ") - detailRow("Season", text) + detailRow(String(localized: "label_season"), text) } if let fit = recommendation.fit { - detailRow("Fit", fit.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized) + detailRow(String(localized: "label_fit"), fit.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized) } if let material = recommendation.material { - detailRow("Material", material.name.lowercased().capitalized) + detailRow(String(localized: "label_material"), material.name.lowercased().capitalized) } } } @@ -351,7 +351,7 @@ private struct GapDetailSheet: View { private var detailActions: some View { VStack(spacing: 8) { Button(action: onAddToWardrobe) { - Text("Add to Wardrobe") + Text(String(localized: "gaps_add_to_wardrobe")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -366,7 +366,7 @@ private struct GapDetailSheet: View { } Button(action: onDismiss) { - Text("Dismiss") + Text(String(localized: "gaps_dismiss")) .font(.system(size: 15, weight: .medium)) .foregroundColor(WornColors.textSecondary) .frame(maxWidth: .infinity) @@ -401,11 +401,11 @@ private struct GapDetailSheet: View { private func displayLabel(for category: Category) -> String { switch category { - case .top: return "Tops" - case .bottom: return "Bottoms" - case .outerwear: return "Outerwear" - case .shoes: return "Shoes" - case .accessory: return "Accessories" + case .top: return String(localized: "category_tops") + case .bottom: return String(localized: "category_bottoms") + case .outerwear: return String(localized: "category_outerwear") + case .shoes: return String(localized: "category_shoes") + case .accessory: return String(localized: "category_accessories") default: return "" } } diff --git a/iosApp/iosApp/Screens/ItemDetailSheet.swift b/iosApp/iosApp/Screens/ItemDetailSheet.swift index bd2f093..be4f11e 100644 --- a/iosApp/iosApp/Screens/ItemDetailSheet.swift +++ b/iosApp/iosApp/Screens/ItemDetailSheet.swift @@ -36,11 +36,11 @@ struct ItemDetailSheet: View { .padding(.bottom, 36) } .background(WornColors.bgElevated) - .alert("Delete item?", isPresented: $showDeleteAlert) { - Button("Cancel", role: .cancel) {} - Button("Delete", role: .destructive) { onDelete(item.id) } + .alert(String(localized: "item_detail_delete_dialog_title"), isPresented: $showDeleteAlert) { + Button(String(localized: "common_cancel"), role: .cancel) {} + Button(String(localized: "common_delete"), role: .destructive) { onDelete(item.id) } } message: { - Text("This action cannot be undone. \"\(item.name)\" will be permanently removed from your wardrobe.") + Text(String(format: String(localized: "item_detail_delete_dialog_message"), item.name)) } } @@ -107,7 +107,7 @@ struct ItemDetailSheet: View { VStack(spacing: propGap) { if !item.colors.isEmpty { HStack { - Text("Color") + Text(String(localized: "label_color")) .font(.system(size: propFontSize, weight: .medium)) .foregroundColor(WornColors.textSecondary) Spacer() @@ -127,21 +127,21 @@ struct ItemDetailSheet: View { if !item.seasons.isEmpty { let seasonText = item.seasons.count == Season.entries.count - ? "All seasons" + ? String(localized: "common_all_seasons") : item.seasons.map { seasonDisplayName($0) }.joined(separator: ", ") - propertyRow(label: "Season", value: seasonText) + propertyRow(label: String(localized: "label_season"), value: seasonText) } if let fit = item.fit { - propertyRow(label: "Fit", value: fitDisplayName(fit)) + propertyRow(label: String(localized: "label_fit"), value: fitDisplayName(fit)) } if let subcategory = item.subcategory { - propertyRow(label: "Subcategory", value: subcategoryDisplayName(subcategory)) + propertyRow(label: String(localized: "label_subcategory"), value: subcategoryDisplayName(subcategory)) } if let material = item.material { - propertyRow(label: "Material", value: materialDisplayName(material)) + propertyRow(label: String(localized: "label_material"), value: materialDisplayName(material)) } } } @@ -161,7 +161,7 @@ struct ItemDetailSheet: View { private var buttons: some View { VStack(spacing: 12) { Button { onEdit(item) } label: { - Text("Edit Item") + Text(String(localized: "item_detail_edit")) .font(.system(size: buttonFontSize, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity) @@ -175,7 +175,7 @@ struct ItemDetailSheet: View { } Button { showDeleteAlert = true } label: { - Text("Delete Item") + Text(String(localized: "item_detail_delete")) .font(.system(size: buttonFontSize, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -199,41 +199,43 @@ struct ItemDetailSheet: View { private func displayLabel(for category: Category) -> String { switch category { - case .top: return "Tops" - case .bottom: return "Bottoms" - case .outerwear: return "Outerwear" - case .shoes: return "Shoes" - case .accessory: return "Accessories" + case .top: return String(localized: "category_tops") + case .bottom: return String(localized: "category_bottoms") + case .outerwear: return String(localized: "category_outerwear") + case .shoes: return String(localized: "category_shoes") + case .accessory: return String(localized: "category_accessories") default: return "" } } private func seasonDisplayName(_ season: Season) -> String { switch season { - case .spring: return "Spring" - case .summer: return "Summer" - case .fall: return "Fall" - case .winter: return "Winter" + case .spring: return String(localized: "season_spring") + case .summer: return String(localized: "season_summer") + case .fall: return String(localized: "season_fall") + case .winter: return String(localized: "season_winter") default: return "" } } private func fitDisplayName(_ fit: Fit) -> String { switch fit { - case .slimFit: return "Slim Fit" - case .regular: return "Regular" - case .relaxed: return "Relaxed" - case .oversized: return "Oversized" + case .slimFit: return String(localized: "fit_slim_fit") + case .regular: return String(localized: "fit_regular") + case .relaxed: return String(localized: "fit_relaxed") + case .oversized: return String(localized: "fit_oversized") default: return "" } } private func subcategoryDisplayName(_ sub: Subcategory) -> String { - sub.name.lowercased().replacingOccurrences(of: "_", with: " ").capitalized + let key = "subcategory_\(sub.name.lowercased())" + return String(localized: String.LocalizationValue(key)) } private func materialDisplayName(_ material: Material) -> String { - material.name.lowercased().capitalized + let key = "material_\(material.name.lowercased())" + return String(localized: String.LocalizationValue(key)) } private let colorPalette: [(name: String, color: Color)] = [ diff --git a/iosApp/iosApp/Screens/OutfitDetailSheet.swift b/iosApp/iosApp/Screens/OutfitDetailSheet.swift index 766d6a0..c821683 100644 --- a/iosApp/iosApp/Screens/OutfitDetailSheet.swift +++ b/iosApp/iosApp/Screens/OutfitDetailSheet.swift @@ -57,15 +57,15 @@ struct OutfitDetailSheet: View { // Properties VStack(spacing: propGap) { - propertyRow(label: "Items", value: "\(outfit.itemIds.count) items") - propertyRow(label: "Season", value: deriveSeasonText()) + propertyRow(label: String(localized: "label_items"), value: String(format: String(localized: "outfit_detail_items_count"), outfit.itemIds.count)) + propertyRow(label: String(localized: "label_season"), value: deriveSeasonText()) } .padding(.horizontal, contentPadding) // Buttons VStack(spacing: 12) { Button { onEdit(outfit) } label: { - Text("Edit Outfit") + Text(String(localized: "outfit_detail_edit")) .font(.system(size: buttonFontSize, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .frame(maxWidth: .infinity) @@ -79,7 +79,7 @@ struct OutfitDetailSheet: View { } Button { showDeleteAlert = true } label: { - Text("Delete Outfit") + Text(String(localized: "outfit_detail_delete")) .font(.system(size: buttonFontSize, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -93,11 +93,11 @@ struct OutfitDetailSheet: View { .padding(.bottom, 36) } .background(WornColors.bgElevated) - .alert("Delete outfit?", isPresented: $showDeleteAlert) { - Button("Cancel", role: .cancel) {} - Button("Delete", role: .destructive) { onDelete(outfit.id) } + .alert(String(localized: "outfit_detail_delete_dialog_title"), isPresented: $showDeleteAlert) { + Button(String(localized: "common_cancel"), role: .cancel) {} + Button(String(localized: "common_delete"), role: .destructive) { onDelete(outfit.id) } } message: { - Text("This action cannot be undone. \"\(outfit.name)\" will be permanently removed.") + Text(String(format: String(localized: "outfit_detail_delete_dialog_message"), outfit.name)) } } @@ -155,17 +155,17 @@ struct OutfitDetailSheet: View { private func deriveSeasonText() -> String { let allSeasons = Set(outfitItems.flatMap { $0.seasons }) - if allSeasons.isEmpty { return "Not specified" } - if allSeasons.count == Season.entries.count { return "All seasons" } + if allSeasons.isEmpty { return String(localized: "common_not_specified") } + if allSeasons.count == Season.entries.count { return String(localized: "common_all_seasons") } return allSeasons.map { seasonName($0) }.joined(separator: "/") } private func seasonName(_ season: Season) -> String { switch season { - case .spring: return "Spring" - case .summer: return "Summer" - case .fall: return "Fall" - case .winter: return "Winter" + case .spring: return String(localized: "season_spring") + case .summer: return String(localized: "season_summer") + case .fall: return String(localized: "season_fall") + case .winter: return String(localized: "season_winter") default: return "" } } diff --git a/iosApp/iosApp/Screens/OutfitsScreen.swift b/iosApp/iosApp/Screens/OutfitsScreen.swift index 33b8b5f..4d6f863 100644 --- a/iosApp/iosApp/Screens/OutfitsScreen.swift +++ b/iosApp/iosApp/Screens/OutfitsScreen.swift @@ -108,13 +108,13 @@ struct OutfitsContent: View { } .background(WornColors.bgPage) .alert( - "Delete \(state.selectedIds.count) outfit\(state.selectedIds.count != 1 ? "s" : "")?", + String(format: String(localized: "delete_outfits_title"), state.selectedIds.count), isPresented: $showDeleteDialog ) { - Button("Cancel", role: .cancel) {} - Button("Delete", role: .destructive) { onDeleteSelected() } + Button(String(localized: "common_cancel"), role: .cancel) {} + Button(String(localized: "common_delete"), role: .destructive) { onDeleteSelected() } } message: { - Text("This action cannot be undone. The selected outfits will be permanently removed.") + Text(String(localized: "outfits_delete_dialog_message")) } } @@ -150,7 +150,7 @@ struct OutfitsContent: View { private var normalHeader: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("Your outfits") + Text(String(localized: "outfits_title")) .font(.system(size: state.outfits.isEmpty ? 22 : 28, weight: .semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) @@ -160,7 +160,7 @@ struct OutfitsContent: View { HStack(spacing: 4) { Image(systemName: "plus") .font(.system(size: 12, weight: .semibold)) - Text("Create") + Text(String(localized: "outfits_button_create")) .font(.system(size: 14, weight: .semibold)) } .foregroundColor(.white) @@ -173,7 +173,7 @@ struct OutfitsContent: View { } if !state.outfits.isEmpty { - Text("\(state.outfits.count) saved combination\(state.outfits.count != 1 ? "s" : "")") + Text(String(format: String(localized: "saved_combinations"), state.outfits.count)) .font(.system(size: 14, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -183,7 +183,7 @@ struct OutfitsContent: View { private var selectionHeader: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("\(state.selectedIds.count) selected") + Text(String(format: String(localized: "selected_count"), state.selectedIds.count)) .font(.system(size: 28, weight: .medium)) .tracking(-0.8) .foregroundColor(WornColors.textPrimary) @@ -194,7 +194,7 @@ struct OutfitsContent: View { HStack(spacing: 6) { Image(systemName: "trash") .font(.system(size: 15)) - Text("Delete") + Text(String(localized: "common_delete")) .font(.system(size: 15, weight: .semibold)) } .foregroundColor(.white) @@ -204,7 +204,7 @@ struct OutfitsContent: View { .clipShape(Capsule()) } } - Button("Cancel") { onClearSelection() } + Button(String(localized: "common_cancel")) { onClearSelection() } .font(.system(size: 15, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -254,12 +254,12 @@ struct OutfitsContent: View { .foregroundColor(WornColors.textSecondary) } - Text("No outfits yet") + Text(String(localized: "outfits_empty_title")) .font(.system(size: 24, weight: .semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) - Text("Create your first look by combining\nitems from your wardrobe") + Text(String(localized: "outfits_empty_description")) .font(.system(size: 15)) .lineSpacing(4) .multilineTextAlignment(.center) @@ -270,7 +270,7 @@ struct OutfitsContent: View { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) .foregroundColor(WornColors.bgPage) - Text("Create your first outfit") + Text(String(localized: "outfits_empty_cta")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(WornColors.textOnColor) } @@ -368,7 +368,7 @@ private struct OutfitCardView: View { } private var itemCountBadge: some View { - Text("\(outfit.itemIds.count) items") + Text(String(format: String(localized: "outfit_detail_items_count"), outfit.itemIds.count)) .font(.system(size: 11, weight: .semibold)) .foregroundColor(.white) .padding(.horizontal, 10) diff --git a/iosApp/iosApp/Screens/SettingsScreen.swift b/iosApp/iosApp/Screens/SettingsScreen.swift index 8eb9a6f..88e56b8 100644 --- a/iosApp/iosApp/Screens/SettingsScreen.swift +++ b/iosApp/iosApp/Screens/SettingsScreen.swift @@ -12,34 +12,34 @@ struct SettingsScreen: View { VStack(spacing: 0) { ScrollView { VStack(alignment: .leading, spacing: 0) { - Text("Settings") + Text(String(localized: "settings_title")) .font(.system(size: 28, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .padding(.top, 24) .padding(.bottom, 28) - sectionLabel("YOUR PROFILE") + sectionLabel(String(localized: "settings_section_profile")) settingsCard( iconColor: WornColors.accentGreen, iconName: "person.fill", - title: "Your Profile", + title: String(localized: "settings_your_profile"), subtitle: profileSummary, action: { showProfileSheet = true } ) .padding(.top, 10) - sectionLabel("AI FEATURES") + sectionLabel(String(localized: "settings_section_ai")) .padding(.top, 24) settingsCard( iconColor: WornColors.accentIndigo, iconName: "sparkles", - title: "Claude API Key", - subtitle: viewModel.state.hasApiKey ? "Connected" : "Required for AI features", + title: String(localized: "settings_api_key_title"), + subtitle: viewModel.state.hasApiKey ? String(localized: "settings_api_key_connected") : String(localized: "settings_api_key_required"), action: { showApiKeySheet = true } ) .padding(.top, 10) - sectionLabel("ABOUT") + sectionLabel(String(localized: "settings_section_about")) .padding(.top, 24) aboutCard .padding(.top, 10) @@ -73,7 +73,7 @@ struct SettingsScreen: View { (profile.styleProfile as? StyleProfile)?.displayName, (profile.ageRange as? AgeRange)?.displayName, ].compactMap { $0 } - return parts.isEmpty ? "Tap to set up" : parts.joined(separator: " · ") + return parts.isEmpty ? String(localized: "settings_profile_subtitle_empty") : parts.joined(separator: " · ") } // MARK: - Components @@ -123,7 +123,7 @@ struct SettingsScreen: View { private var aboutCard: some View { VStack(spacing: 0) { HStack { - Text("Version") + Text(String(localized: "settings_version")) .font(.system(size: 15)) .foregroundColor(WornColors.textPrimary) Spacer() @@ -141,7 +141,7 @@ struct SettingsScreen: View { } } label: { HStack { - Text("Licenses") + Text(String(localized: "settings_licenses")) .font(.system(size: 15)) .foregroundColor(WornColors.textPrimary) Spacer() @@ -170,35 +170,35 @@ private struct ProfileSheet: View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 20) { - Text("Your Profile") + Text(String(localized: "settings_your_profile")) .font(.system(size: 24, weight: .semibold)) .foregroundColor(WornColors.textPrimary) - Text("Help AI give better suggestions") + Text(String(localized: "settings_profile_help")) .font(.system(size: 14)) .foregroundColor(WornColors.textSecondary) - chipGroup(title: "Body Type", options: bodyTypeOptions, + chipGroup(title: String(localized: "label_body_type"), options: bodyTypeOptions, selected: viewModel.state.userProfile.bodyType as? BodyType) { viewModel.selectBodyType($0) } - chipGroup(title: "Style Profile", options: styleOptions, + chipGroup(title: String(localized: "label_style_profile"), options: styleOptions, selected: viewModel.state.userProfile.styleProfile as? StyleProfile) { viewModel.selectStyleProfile($0) } - chipGroup(title: "Age Range", options: ageOptions, + chipGroup(title: String(localized: "label_age_range"), options: ageOptions, selected: viewModel.state.userProfile.ageRange as? AgeRange) { viewModel.selectAgeRange($0) } - chipGroup(title: "Climate / Region", options: climateOptions, + chipGroup(title: String(localized: "label_climate"), options: climateOptions, selected: viewModel.state.userProfile.climate as? Climate) { viewModel.selectClimate($0) } - multiChipGroup(title: "Lifestyle / Occasions", options: lifestyleOptions, + multiChipGroup(title: String(localized: "label_lifestyle"), options: lifestyleOptions, selected: Set((viewModel.state.userProfile.lifestyles as? Set) ?? [])) { viewModel.toggleLifestyle($0) } - saveGradientButton(text: "Save") { dismiss() } + saveGradientButton(text: String(localized: "common_save")) { dismiss() } } .padding(.horizontal, 24) .padding(.bottom, 24) @@ -208,23 +208,23 @@ private struct ProfileSheet: View { } private var bodyTypeOptions: [(BodyType, String)] { - [(.slim, "Slim"), (.athletic, "Athletic"), (.average, "Average"), - (.stocky, "Stocky"), (.short_, "Short"), (.tallAndSlim, "Tall & Slim"), - (.bigAndTall, "Big & Tall")] + [(.slim, String(localized: "body_type_slim")), (.athletic, String(localized: "body_type_athletic")), (.average, String(localized: "body_type_average")), + (.stocky, String(localized: "body_type_stocky")), (.short_, String(localized: "body_type_short")), (.tallAndSlim, String(localized: "body_type_tall_slim")), + (.bigAndTall, String(localized: "body_type_big_tall"))] } private var styleOptions: [(StyleProfile, String)] { - [(.classic, "Classic"), (.casual, "Casual"), (.streetwear, "Streetwear"), - (.smartCasual, "Smart Casual"), (.minimalist, "Minimalist")] + [(.classic, String(localized: "style_classic")), (.casual, String(localized: "style_casual")), (.streetwear, String(localized: "style_streetwear")), + (.smartCasual, String(localized: "style_smart_casual")), (.minimalist, String(localized: "style_minimalist"))] } private var ageOptions: [(AgeRange, String)] { - [(.age1825, "18-25"), (.age2635, "26-35"), (.age3645, "36-45"), (.age46Plus, "46+")] + [(.age1825, String(localized: "age_18_25")), (.age2635, String(localized: "age_26_35")), (.age3645, String(localized: "age_36_45")), (.age46Plus, String(localized: "age_46_plus"))] } private var climateOptions: [(Climate, String)] { - [(.tropical, "Tropical"), (.temperate, "Temperate"), (.cold, "Cold"), (.mixed, "Mixed")] + [(.tropical, String(localized: "climate_tropical")), (.temperate, String(localized: "climate_temperate")), (.cold, String(localized: "climate_cold")), (.mixed, String(localized: "climate_mixed"))] } private var lifestyleOptions: [(Lifestyle, String)] { - [(.workOffice, "Work (Office)"), (.workManual, "Work (Manual)"), - (.social, "Social"), (.sports, "Sports"), (.formalEvents, "Formal Events")] + [(.workOffice, String(localized: "lifestyle_work_office")), (.workManual, String(localized: "lifestyle_work_manual")), + (.social, String(localized: "lifestyle_social")), (.sports, String(localized: "lifestyle_sports")), (.formalEvents, String(localized: "lifestyle_formal_events"))] } } @@ -241,13 +241,13 @@ private struct ApiKeySheet: View { var body: some View { VStack(alignment: .leading, spacing: 16) { - Text("Connect Claude AI") + Text(String(localized: "settings_connect_claude")) .font(.system(size: 24, weight: .semibold)) .foregroundColor(WornColors.textPrimary) - Text("Paste your Anthropic API key to unlock AI-powered features like auto-tagging clothes and outfit analysis.") + Text(String(localized: "settings_api_description")) .font(.system(size: 14)) .foregroundColor(WornColors.textSecondary) - Text("Get a free key at console.anthropic.com →") + Text(String(localized: "settings_api_get_key")) .font(.system(size: 13, weight: .medium)) .foregroundColor(WornColors.accentGreen) @@ -275,7 +275,7 @@ private struct ApiKeySheet: View { .stroke(WornColors.borderSubtle, lineWidth: 1) ) - saveGradientButton(text: "Save & Connect", enabled: !hasApiKey && !keyInput.isEmpty) { + saveGradientButton(text: String(localized: "settings_save_connect"), enabled: !hasApiKey && !keyInput.isEmpty) { onSave(keyInput) keyInput = "" dismiss() @@ -288,7 +288,7 @@ private struct ApiKeySheet: View { onClear() dismiss() } label: { - Text("Remove key") + Text(String(localized: "settings_remove_key")) .font(.system(size: 14, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -338,7 +338,7 @@ private func multiChipGroup( Text(title) .font(.system(size: 14, weight: .semibold)) .foregroundColor(WornColors.textPrimary) - Text("(multi-select)") + Text(String(localized: "settings_multi_select")) .font(.system(size: 12)) .foregroundColor(WornColors.textMuted) } @@ -388,13 +388,13 @@ private func saveGradientButton(text: String, enabled: Bool = true, action: @esc private extension BodyType { var displayName: String { switch self { - case .slim: return "Slim" - case .athletic: return "Athletic" - case .average: return "Average" - case .stocky: return "Stocky" - case .short_: return "Short" - case .tallAndSlim: return "Tall & Slim" - case .bigAndTall: return "Big & Tall" + case .slim: return String(localized: "body_type_slim") + case .athletic: return String(localized: "body_type_athletic") + case .average: return String(localized: "body_type_average") + case .stocky: return String(localized: "body_type_stocky") + case .short_: return String(localized: "body_type_short") + case .tallAndSlim: return String(localized: "body_type_tall_slim") + case .bigAndTall: return String(localized: "body_type_big_tall") default: return "" } } @@ -403,11 +403,11 @@ private extension BodyType { private extension StyleProfile { var displayName: String { switch self { - case .classic: return "Classic" - case .casual: return "Casual" - case .streetwear: return "Streetwear" - case .smartCasual: return "Smart Casual" - case .minimalist: return "Minimalist" + case .classic: return String(localized: "style_classic") + case .casual: return String(localized: "style_casual") + case .streetwear: return String(localized: "style_streetwear") + case .smartCasual: return String(localized: "style_smart_casual") + case .minimalist: return String(localized: "style_minimalist") default: return "" } } @@ -416,10 +416,10 @@ private extension StyleProfile { private extension AgeRange { var displayName: String { switch self { - case .age1825: return "18-25" - case .age2635: return "26-35" - case .age3645: return "36-45" - case .age46Plus: return "46+" + case .age1825: return String(localized: "age_18_25") + case .age2635: return String(localized: "age_26_35") + case .age3645: return String(localized: "age_36_45") + case .age46Plus: return String(localized: "age_46_plus") default: return "" } } diff --git a/iosApp/iosApp/Screens/TryItScreen.swift b/iosApp/iosApp/Screens/TryItScreen.swift index 21ce742..c71fac1 100644 --- a/iosApp/iosApp/Screens/TryItScreen.swift +++ b/iosApp/iosApp/Screens/TryItScreen.swift @@ -29,10 +29,10 @@ struct TryItScreen: View { WornBottomBar(activeTab: .tryIt, onTabSelected: onTabSelected, isCompact: isCompact) } .background(WornColors.bgPage) - .confirmationDialog("Add photo", isPresented: $showSourceChooser) { - Button("Take Photo") { showCamera = true } - Button("Choose from Library") { showPhotoPicker = true } - Button("Cancel", role: .cancel) {} + .confirmationDialog(String(localized: "add_item_photo_dialog_title"), isPresented: $showSourceChooser) { + Button(String(localized: "add_item_take_photo")) { showCamera = true } + Button(String(localized: "add_item_choose_gallery")) { showPhotoPicker = true } + Button(String(localized: "common_cancel"), role: .cancel) {} } .photosPicker(isPresented: $showPhotoPicker, selection: $selectedPhotoItem, matching: .images) .fullScreenCover(isPresented: $showCamera) { @@ -88,19 +88,19 @@ struct TryItScreen: View { .foregroundColor(WornColors.accentIndigo) } - Text("AI powered analysis") + Text(String(localized: "tryit_ai_empty_title")) .font(.system(size: isCompact ? 24 : 26, weight: .medium)) .foregroundColor(WornColors.textPrimary) .multilineTextAlignment(.center) - Text("Connect your Claude API key in Settings to analyze items against your wardrobe.") + Text(String(localized: "tryit_ai_empty_description")) .font(.system(size: isCompact ? 15 : 16)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) .lineSpacing(4) .frame(maxWidth: isCompact ? 280 : 380) - indigoCtaButton(text: "Connect Claude AI") { + indigoCtaButton(text: String(localized: "tryit_connect_cta")) { onTabSelected(.settings) } @@ -194,7 +194,7 @@ struct TryItScreen: View { // MARK: - Components private func tryItTitle(fontSize: CGFloat) -> some View { - Text("Would it fit your wardrobe?") + Text(String(localized: "tryit_title")) .font(.system(size: fontSize, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.8) @@ -214,7 +214,7 @@ struct TryItScreen: View { Image(systemName: "camera") .font(.system(size: 44)) .foregroundColor(WornColors.iconMuted) - Text("Upload a photo of the item\nyou're considering") + Text(String(localized: "tryit_upload_hint")) .font(.system(size: 13, weight: .medium)) .foregroundColor(WornColors.textSecondary) .multilineTextAlignment(.center) @@ -242,7 +242,7 @@ struct TryItScreen: View { HStack(spacing: 8) { Image(systemName: "cpu") .font(.system(size: 18)) - Text("Analyze with Claude") + Text(String(localized: "tryit_analyze")) .font(.system(size: 16, weight: .semibold)) } .foregroundColor(.white) @@ -294,7 +294,7 @@ struct TryItScreen: View { Group { if !matchingItems.isEmpty { VStack(alignment: .leading, spacing: 12) { - Text("It would pair with...") + Text(String(localized: "tryit_pairs_with")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -349,7 +349,7 @@ struct TryItScreen: View { let valueSize: CGFloat = isCompact ? 40 : 44 return VStack(alignment: .leading, spacing: 4) { - Text("Combinations unlocked") + Text(String(localized: "tryit_combinations_unlocked")) .font(.system(size: 12, weight: .semibold)) .foregroundColor(WornColors.textSecondary) .tracking(0.5) @@ -375,7 +375,7 @@ struct TryItScreen: View { Group { if !gaps.isEmpty { VStack(alignment: .leading, spacing: 12) { - Text("Wardrobe gaps it fills") + Text(String(localized: "tryit_gaps_filled")) .font(.system(size: 18, weight: .semibold)) .foregroundColor(WornColors.textPrimary) .tracking(-0.2) @@ -401,7 +401,7 @@ struct TryItScreen: View { ? [WornColors.accentGreen, WornColors.accentGreenDark] : [Color(hex: "8B7D7D"), Color(hex: "6B5E5E")] let iconName = worthAdding ? "checkmark.circle" : "xmark.circle" - let text = worthAdding ? "Worth adding" : "Skip this one" + let text = worthAdding ? String(localized: "tryit_worth_adding") : String(localized: "tryit_skip") return HStack(spacing: 10) { Image(systemName: iconName) diff --git a/iosApp/iosApp/Screens/WardrobeScreen.swift b/iosApp/iosApp/Screens/WardrobeScreen.swift index 17815dc..b4946ba 100644 --- a/iosApp/iosApp/Screens/WardrobeScreen.swift +++ b/iosApp/iosApp/Screens/WardrobeScreen.swift @@ -100,11 +100,11 @@ struct WardrobeContent: View { } } .background(WornColors.bgPage) - .alert("Delete \(state.selectedIds.count) item\(state.selectedIds.count != 1 ? "s" : "")?", isPresented: $showDeleteDialog) { - Button("Cancel", role: .cancel) {} - Button("Delete", role: .destructive) { onDeleteSelected() } + .alert(String(format: String(localized: "delete_items_title"), state.selectedIds.count), isPresented: $showDeleteDialog) { + Button(String(localized: "common_cancel"), role: .cancel) {} + Button(String(localized: "common_delete"), role: .destructive) { onDeleteSelected() } } message: { - Text("This action cannot be undone. The selected items will be permanently removed from your wardrobe.") + Text(String(localized: "wardrobe_delete_dialog_message")) } } @@ -143,17 +143,17 @@ struct WardrobeContent: View { private var normalHeader: some View { VStack(alignment: .leading, spacing: 8) { if state.totalItemCount == 0 { - Text("Your wardrobe") + Text(String(localized: "wardrobe_title_empty")) .font(.system(size: 22, weight: .semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) } else { - Text("Worn") + Text(String(localized: "wardrobe_title")) .font(.system(size: 28, weight: .semibold)) .tracking(-0.8) .foregroundColor(WornColors.textPrimary) - Text("Your capsule wardrobe · \(state.totalItemCount) items") + Text(String(format: String(localized: "wardrobe_subtitle"), state.totalItemCount)) .font(.system(size: 14, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -163,7 +163,7 @@ struct WardrobeContent: View { private var selectionHeader: some View { VStack(alignment: .leading, spacing: 8) { HStack { - Text("\(state.selectedIds.count) selected") + Text(String(format: String(localized: "selected_count"), state.selectedIds.count)) .font(.system(size: 28, weight: .medium)) .tracking(-0.8) .foregroundColor(WornColors.textPrimary) @@ -174,7 +174,7 @@ struct WardrobeContent: View { HStack(spacing: 6) { Image(systemName: "trash") .font(.system(size: 15)) - Text("Delete") + Text(String(localized: "common_delete")) .font(.system(size: 15, weight: .semibold)) } .foregroundColor(.white) @@ -184,7 +184,7 @@ struct WardrobeContent: View { .clipShape(Capsule()) } } - Button("Cancel") { onClearSelection() } + Button(String(localized: "common_cancel")) { onClearSelection() } .font(.system(size: 15, weight: .medium)) .foregroundColor(WornColors.textSecondary) } @@ -237,7 +237,7 @@ struct WardrobeContent: View { .font(.system(size: 36, weight: .regular)) .foregroundColor(WornColors.textSecondary.opacity(0.5)) - Text("No items in this category") + Text(String(localized: "wardrobe_category_empty")) .font(.system(size: 16, weight: .medium)) .foregroundColor(WornColors.textSecondary) @@ -264,12 +264,12 @@ struct WardrobeContent: View { .foregroundColor(WornColors.textSecondary) } - Text("No items yet") + Text(String(localized: "wardrobe_empty_title")) .font(.system(size: 24, weight: .semibold)) .tracking(-0.5) .foregroundColor(WornColors.textPrimary) - Text("Add your first piece to start\nbuilding your wardrobe") + Text(String(localized: "wardrobe_empty_description")) .font(.system(size: 15)) .lineSpacing(4) .multilineTextAlignment(.center) @@ -280,7 +280,7 @@ struct WardrobeContent: View { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) .foregroundColor(WornColors.bgPage) - Text("Add your first item") + Text(String(localized: "wardrobe_empty_cta")) .font(.system(size: 16, weight: .semibold)) .foregroundColor(WornColors.textOnColor) } @@ -306,7 +306,7 @@ struct WardrobeContent: View { HStack(spacing: 8) { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) - Text("Add item") + Text(String(localized: "wardrobe_fab_add")) .font(.system(size: 15, weight: .semibold)) } .foregroundColor(WornColors.textOnColor) diff --git a/iosApp/iosApp/en.lproj/InfoPlist.strings b/iosApp/iosApp/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..14ba320 --- /dev/null +++ b/iosApp/iosApp/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"NSCameraUsageDescription" = "Worn needs camera access to photograph your clothing items"; diff --git a/iosApp/iosApp/en.lproj/Localizable.strings b/iosApp/iosApp/en.lproj/Localizable.strings new file mode 100644 index 0000000..98b30ac --- /dev/null +++ b/iosApp/iosApp/en.lproj/Localizable.strings @@ -0,0 +1,234 @@ +/* Common */ +"common_delete" = "Delete"; +"common_cancel" = "Cancel"; +"common_save" = "Save"; +"common_deleting" = "Deleting…"; +"common_saving" = "Saving…"; +"common_save_changes" = "Save Changes"; +"common_all_seasons" = "All seasons"; +"common_not_specified" = "Not specified"; + +/* Tabs */ +"tab_wardrobe" = "WARDROBE"; +"tab_outfits" = "OUTFITS"; +"tab_gaps" = "GAPS"; +"tab_try_it" = "TRY IT"; +"tab_settings" = "SETTINGS"; + +/* Wardrobe Screen */ +"wardrobe_title_empty" = "Your wardrobe"; +"wardrobe_title" = "Worn"; +"wardrobe_subtitle" = "Your capsule wardrobe · %d items"; +"wardrobe_empty_title" = "No items yet"; +"wardrobe_empty_description" = "Add your first piece to start\nbuilding your wardrobe"; +"wardrobe_empty_cta" = "Add your first item"; +"wardrobe_category_empty" = "No items in this category"; +"wardrobe_fab_add" = "Add item"; +"wardrobe_delete_dialog_message" = "This action cannot be undone. The selected items will be permanently removed from your wardrobe."; + +/* Outfits Screen */ +"outfits_title" = "Your outfits"; +"outfits_button_create" = "Create"; +"outfits_empty_title" = "No outfits yet"; +"outfits_empty_description" = "Create your first look by combining\nitems from your wardrobe"; +"outfits_empty_cta" = "Create your first outfit"; +"outfits_delete_dialog_message" = "This action cannot be undone. The selected outfits will be permanently removed."; + +/* Add Item Sheet */ +"add_item_title" = "Add new item"; +"add_item_title_edit" = "Edit item"; +"add_item_photo_dialog_title" = "Add photo"; +"add_item_take_photo" = "Take photo"; +"add_item_choose_gallery" = "Choose from gallery"; +"add_item_photo_hint" = "Tap to add photo"; +"add_item_ai_badge" = "Auto-tag with AI"; +"add_item_name_hint" = "Item name"; +"add_item_save_to_wardrobe" = "Save to wardrobe"; + +/* Create Outfit Sheet */ +"create_outfit_title" = "Create outfit"; +"create_outfit_title_edit" = "Edit outfit"; +"create_outfit_name_hint" = "Outfit name"; +"create_outfit_select_items" = "Select items"; +"create_outfit_save" = "Save outfit"; + +/* Item Detail Sheet */ +"item_detail_edit" = "Edit Item"; +"item_detail_delete" = "Delete Item"; +"item_detail_delete_dialog_title" = "Delete item?"; +"item_detail_delete_dialog_message" = "This action cannot be undone. \"%@\" will be permanently removed from your wardrobe."; + +/* Outfit Detail Sheet */ +"outfit_detail_edit" = "Edit Outfit"; +"outfit_detail_delete" = "Delete Outfit"; +"outfit_detail_items_count" = "%d items"; +"outfit_detail_delete_dialog_title" = "Delete outfit?"; +"outfit_detail_delete_dialog_message" = "This action cannot be undone. \"%@\" will be permanently removed."; + +/* Settings Screen */ +"settings_title" = "Settings"; +"settings_section_profile" = "YOUR PROFILE"; +"settings_section_ai" = "AI FEATURES"; +"settings_section_about" = "ABOUT"; +"settings_your_profile" = "Your Profile"; +"settings_profile_subtitle_empty" = "Tap to set up"; +"settings_profile_help" = "Help AI give better suggestions"; +"settings_api_key_title" = "Claude API Key"; +"settings_api_key_connected" = "Connected"; +"settings_api_key_required" = "Required for AI features"; +"settings_connect_claude" = "Connect Claude AI"; +"settings_api_description" = "Paste your Anthropic API key to unlock AI-powered features like auto-tagging clothes and outfit analysis."; +"settings_api_get_key" = "Get a free key at console.anthropic.com →"; +"settings_save_connect" = "Save & Connect"; +"settings_remove_key" = "Remove key"; +"settings_version" = "Version"; +"settings_licenses" = "Licenses"; +"settings_multi_select" = "(multi-select)"; +"settings_api_hide" = "Hide"; +"settings_api_show" = "Show"; + +/* Gaps Screen */ +"gaps_title" = "What's missing"; +"gaps_subtitle" = "Items that would expand your combinations most"; +"gaps_complete_title" = "Your wardrobe looks complete!"; +"gaps_complete_description" = "We couldn't find any gaps.\nYou have great coverage across categories."; +"gaps_banner_ai_title" = "AI Recommendations"; +"gaps_banner_common_title" = "Common Suggestions"; +"gaps_banner_ai_subtitle" = "Personalized suggestions based on your wardrobe"; +"gaps_banner_common_subtitle" = "Connect Claude AI for personalized picks"; +"gaps_pairing_ai" = "Would pair with %d of your items"; +"gaps_pairing_common" = "Common wardrobe essential"; +"gaps_add_to_wardrobe" = "Add to Wardrobe"; +"gaps_dismiss" = "Dismiss"; + +/* Try It Screen */ +"tryit_title" = "Would it fit your wardrobe?"; +"tryit_upload_hint" = "Upload a photo of the item\nyou're considering"; +"tryit_analyze" = "Analyze with Claude"; +"tryit_ai_empty_title" = "AI powered analysis"; +"tryit_ai_empty_description" = "Connect your Claude API key in Settings to analyze items against your wardrobe."; +"tryit_connect_cta" = "Connect Claude AI"; +"tryit_pairs_with" = "It would pair with…"; +"tryit_combinations_unlocked" = "Combinations unlocked"; +"tryit_gaps_filled" = "Wardrobe gaps it fills"; +"tryit_worth_adding" = "Worth adding"; +"tryit_skip" = "Skip this one"; + +/* AI Locked Sheet */ +"ai_locked_title" = "Unlock AI features"; +"ai_locked_description" = "Add your Claude API key in Settings to enable this."; +"ai_locked_cta" = "Go to Settings"; + +/* Filter */ +"filter_all" = "All"; + +/* Form Labels */ +"label_color" = "Color"; +"label_season" = "Season"; +"label_fit" = "Fit"; +"label_subcategory" = "Subcategory"; +"label_material" = "Material"; +"label_category" = "Category"; +"label_items" = "Items"; +"label_body_type" = "Body Type"; +"label_style_profile" = "Style Profile"; +"label_age_range" = "Age Range"; +"label_climate" = "Climate / Region"; +"label_lifestyle" = "Lifestyle / Occasions"; + +/* Category */ +"category_tops" = "Tops"; +"category_bottoms" = "Bottoms"; +"category_outerwear" = "Outerwear"; +"category_shoes" = "Shoes"; +"category_accessories" = "Accessories"; + +/* Season */ +"season_spring" = "Spring"; +"season_summer" = "Summer"; +"season_fall" = "Fall"; +"season_winter" = "Winter"; + +/* Fit */ +"fit_slim" = "Slim Fit"; +"fit_regular" = "Regular"; +"fit_relaxed" = "Relaxed"; +"fit_oversized" = "Oversized"; + +/* Material */ +"material_cotton" = "Cotton"; +"material_linen" = "Linen"; +"material_denim" = "Denim"; +"material_wool" = "Wool"; +"material_synthetic" = "Synthetic"; +"material_leather" = "Leather"; +"material_silk" = "Silk"; +"material_knit" = "Knit"; + +/* Subcategory */ +"subcategory_t_shirt" = "T shirt"; +"subcategory_polo" = "Polo"; +"subcategory_dress_shirt" = "Dress shirt"; +"subcategory_henley" = "Henley"; +"subcategory_sweater" = "Sweater"; +"subcategory_hoodie" = "Hoodie"; +"subcategory_jeans" = "Jeans"; +"subcategory_chinos" = "Chinos"; +"subcategory_tailored_pants" = "Tailored pants"; +"subcategory_shorts" = "Shorts"; +"subcategory_cargo_pants" = "Cargo pants"; +"subcategory_sweatpants" = "Sweatpants"; +"subcategory_bomber" = "Bomber"; +"subcategory_trucker" = "Trucker"; +"subcategory_puffer" = "Puffer"; +"subcategory_blazer" = "Blazer"; +"subcategory_coat" = "Coat"; +"subcategory_windbreaker" = "Windbreaker"; +"subcategory_sneakers" = "Sneakers"; +"subcategory_boots_military" = "Boots military"; +"subcategory_boots_chelsea" = "Boots chelsea"; +"subcategory_derby" = "Derby"; +"subcategory_oxford" = "Oxford"; +"subcategory_loafer" = "Loafer"; +"subcategory_sandals" = "Sandals"; +"subcategory_watch" = "Watch"; +"subcategory_belt" = "Belt"; +"subcategory_sunglasses" = "Sunglasses"; +"subcategory_hat_cap" = "Hat cap"; +"subcategory_scarf" = "Scarf"; +"subcategory_bag_backpack" = "Bag backpack"; + +/* Body Type */ +"body_type_slim" = "Slim"; +"body_type_athletic" = "Athletic"; +"body_type_average" = "Average"; +"body_type_stocky" = "Stocky"; +"body_type_short" = "Short"; +"body_type_tall_and_slim" = "Tall & Slim"; +"body_type_big_and_tall" = "Big & Tall"; + +/* Style Profile */ +"style_classic" = "Classic"; +"style_casual" = "Casual"; +"style_streetwear" = "Streetwear"; +"style_smart_casual" = "Smart Casual"; +"style_minimalist" = "Minimalist"; + +/* Age Range */ +"age_18_25" = "18–25"; +"age_26_35" = "26–35"; +"age_36_45" = "36–45"; +"age_46_plus" = "46+"; + +/* Climate */ +"climate_tropical" = "Tropical"; +"climate_temperate" = "Temperate"; +"climate_cold" = "Cold"; +"climate_mixed" = "Mixed"; + +/* Lifestyle */ +"lifestyle_work_office" = "Work (Office)"; +"lifestyle_work_manual" = "Work (Manual)"; +"lifestyle_social" = "Social"; +"lifestyle_sports" = "Sports"; +"lifestyle_formal_events" = "Formal Events"; diff --git a/iosApp/iosApp/en.lproj/Localizable.stringsdict b/iosApp/iosApp/en.lproj/Localizable.stringsdict new file mode 100644 index 0000000..db13698 --- /dev/null +++ b/iosApp/iosApp/en.lproj/Localizable.stringsdict @@ -0,0 +1,70 @@ + + + + + selected_count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d selected + other + %d selected + + + delete_items_title + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Delete %d item? + other + Delete %d items? + + + delete_outfits_title + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Delete %d outfit? + other + Delete %d outfits? + + + saved_combinations + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d saved combination + other + %d saved combinations + + + + diff --git a/iosApp/iosApp/pt-BR.lproj/InfoPlist.strings b/iosApp/iosApp/pt-BR.lproj/InfoPlist.strings new file mode 100644 index 0000000..96f4f21 --- /dev/null +++ b/iosApp/iosApp/pt-BR.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"NSCameraUsageDescription" = "Worn precisa de acesso à câmera para fotografar suas peças de roupa"; diff --git a/iosApp/iosApp/pt-BR.lproj/Localizable.strings b/iosApp/iosApp/pt-BR.lproj/Localizable.strings new file mode 100644 index 0000000..e803f19 --- /dev/null +++ b/iosApp/iosApp/pt-BR.lproj/Localizable.strings @@ -0,0 +1,234 @@ +/* Common */ +"common_delete" = "Excluir"; +"common_cancel" = "Cancelar"; +"common_save" = "Salvar"; +"common_deleting" = "Excluindo…"; +"common_saving" = "Salvando…"; +"common_save_changes" = "Salvar alterações"; +"common_all_seasons" = "Todas as estações"; +"common_not_specified" = "Não especificado"; + +/* Tabs */ +"tab_wardrobe" = "GUARDA-ROUPA"; +"tab_outfits" = "LOOKS"; +"tab_gaps" = "LACUNAS"; +"tab_try_it" = "TESTAR"; +"tab_settings" = "AJUSTES"; + +/* Wardrobe Screen */ +"wardrobe_title_empty" = "Seu guarda-roupa"; +"wardrobe_title" = "Worn"; +"wardrobe_subtitle" = "Seu guarda-roupa cápsula · %d itens"; +"wardrobe_empty_title" = "Nenhum item ainda"; +"wardrobe_empty_description" = "Adicione sua primeira peça para\ncomeçar a montar seu guarda-roupa"; +"wardrobe_empty_cta" = "Adicione seu primeiro item"; +"wardrobe_category_empty" = "Nenhum item nesta categoria"; +"wardrobe_fab_add" = "Adicionar item"; +"wardrobe_delete_dialog_message" = "Esta ação não pode ser desfeita. Os itens selecionados serão removidos permanentemente do seu guarda-roupa."; + +/* Outfits Screen */ +"outfits_title" = "Seus looks"; +"outfits_button_create" = "Criar"; +"outfits_empty_title" = "Nenhum look ainda"; +"outfits_empty_description" = "Crie seu primeiro look combinando\nitens do seu guarda-roupa"; +"outfits_empty_cta" = "Crie seu primeiro look"; +"outfits_delete_dialog_message" = "Esta ação não pode ser desfeita. Os looks selecionados serão removidos permanentemente."; + +/* Add Item Sheet */ +"add_item_title" = "Adicionar novo item"; +"add_item_title_edit" = "Editar item"; +"add_item_photo_dialog_title" = "Adicionar foto"; +"add_item_take_photo" = "Tirar foto"; +"add_item_choose_gallery" = "Escolher da galeria"; +"add_item_photo_hint" = "Toque para adicionar foto"; +"add_item_ai_badge" = "Auto-tag com IA"; +"add_item_name_hint" = "Nome do item"; +"add_item_save_to_wardrobe" = "Salvar no guarda-roupa"; + +/* Create Outfit Sheet */ +"create_outfit_title" = "Criar look"; +"create_outfit_title_edit" = "Editar look"; +"create_outfit_name_hint" = "Nome do look"; +"create_outfit_select_items" = "Selecionar itens"; +"create_outfit_save" = "Salvar look"; + +/* Item Detail Sheet */ +"item_detail_edit" = "Editar Item"; +"item_detail_delete" = "Excluir Item"; +"item_detail_delete_dialog_title" = "Excluir item?"; +"item_detail_delete_dialog_message" = "Esta ação não pode ser desfeita. \"%@\" será removido permanentemente do seu guarda-roupa."; + +/* Outfit Detail Sheet */ +"outfit_detail_edit" = "Editar Look"; +"outfit_detail_delete" = "Excluir Look"; +"outfit_detail_items_count" = "%d itens"; +"outfit_detail_delete_dialog_title" = "Excluir look?"; +"outfit_detail_delete_dialog_message" = "Esta ação não pode ser desfeita. \"%@\" será removido permanentemente."; + +/* Settings Screen */ +"settings_title" = "Ajustes"; +"settings_section_profile" = "SEU PERFIL"; +"settings_section_ai" = "FUNÇÕES DE IA"; +"settings_section_about" = "SOBRE"; +"settings_your_profile" = "Seu Perfil"; +"settings_profile_subtitle_empty" = "Toque para configurar"; +"settings_profile_help" = "Ajude a IA a dar melhores sugestões"; +"settings_api_key_title" = "Chave da API Claude"; +"settings_api_key_connected" = "Conectado"; +"settings_api_key_required" = "Necessária para funções de IA"; +"settings_connect_claude" = "Conectar Claude IA"; +"settings_api_description" = "Cole sua chave da API Anthropic para desbloquear funções de IA como auto-tag de roupas e análise de looks."; +"settings_api_get_key" = "Obtenha uma chave gratuita em console.anthropic.com →"; +"settings_save_connect" = "Salvar e conectar"; +"settings_remove_key" = "Remover chave"; +"settings_version" = "Versão"; +"settings_licenses" = "Licenças"; +"settings_multi_select" = "(seleção múltipla)"; +"settings_api_hide" = "Ocultar"; +"settings_api_show" = "Mostrar"; + +/* Gaps Screen */ +"gaps_title" = "O que está faltando"; +"gaps_subtitle" = "Itens que mais expandiriam suas combinações"; +"gaps_complete_title" = "Seu guarda-roupa parece completo!"; +"gaps_complete_description" = "Não encontramos nenhuma lacuna.\nVocê tem ótima cobertura em todas as categorias."; +"gaps_banner_ai_title" = "Recomendações com IA"; +"gaps_banner_common_title" = "Sugestões Comuns"; +"gaps_banner_ai_subtitle" = "Sugestões personalizadas baseadas no seu guarda-roupa"; +"gaps_banner_common_subtitle" = "Conecte a Claude IA para sugestões personalizadas"; +"gaps_pairing_ai" = "Combinaria com %d dos seus itens"; +"gaps_pairing_common" = "Item essencial do guarda-roupa"; +"gaps_add_to_wardrobe" = "Adicionar ao guarda-roupa"; +"gaps_dismiss" = "Dispensar"; + +/* Try It Screen */ +"tryit_title" = "Combinaria com seu guarda-roupa?"; +"tryit_upload_hint" = "Envie uma foto do item\nque você está considerando"; +"tryit_analyze" = "Analisar com Claude"; +"tryit_ai_empty_title" = "Análise com inteligência artificial"; +"tryit_ai_empty_description" = "Conecte sua chave da API Claude nos Ajustes para analisar itens com seu guarda-roupa."; +"tryit_connect_cta" = "Conectar Claude IA"; +"tryit_pairs_with" = "Combinaria com…"; +"tryit_combinations_unlocked" = "Combinações desbloqueadas"; +"tryit_gaps_filled" = "Lacunas que preenche"; +"tryit_worth_adding" = "Vale adicionar"; +"tryit_skip" = "Pule esse"; + +/* AI Locked Sheet */ +"ai_locked_title" = "Desbloquear funções de IA"; +"ai_locked_description" = "Adicione sua chave da API Claude nos Ajustes para habilitar."; +"ai_locked_cta" = "Ir para Ajustes"; + +/* Filter */ +"filter_all" = "Todos"; + +/* Form Labels */ +"label_color" = "Cor"; +"label_season" = "Estação"; +"label_fit" = "Caimento"; +"label_subcategory" = "Subcategoria"; +"label_material" = "Material"; +"label_category" = "Categoria"; +"label_items" = "Itens"; +"label_body_type" = "Biotipo"; +"label_style_profile" = "Perfil de Estilo"; +"label_age_range" = "Faixa Etária"; +"label_climate" = "Clima / Região"; +"label_lifestyle" = "Estilo de Vida / Ocasiões"; + +/* Category */ +"category_tops" = "Partes de Cima"; +"category_bottoms" = "Partes de Baixo"; +"category_outerwear" = "Agasalhos"; +"category_shoes" = "Calçados"; +"category_accessories" = "Acessórios"; + +/* Season */ +"season_spring" = "Primavera"; +"season_summer" = "Verão"; +"season_fall" = "Outono"; +"season_winter" = "Inverno"; + +/* Fit */ +"fit_slim" = "Slim"; +"fit_regular" = "Regular"; +"fit_relaxed" = "Relaxado"; +"fit_oversized" = "Oversized"; + +/* Material */ +"material_cotton" = "Algodão"; +"material_linen" = "Linho"; +"material_denim" = "Jeans"; +"material_wool" = "Lã"; +"material_synthetic" = "Sintético"; +"material_leather" = "Couro"; +"material_silk" = "Seda"; +"material_knit" = "Malha"; + +/* Subcategory */ +"subcategory_t_shirt" = "Camiseta"; +"subcategory_polo" = "Polo"; +"subcategory_dress_shirt" = "Camisa social"; +"subcategory_henley" = "Henley"; +"subcategory_sweater" = "Suéter"; +"subcategory_hoodie" = "Moletom"; +"subcategory_jeans" = "Jeans"; +"subcategory_chinos" = "Chinos"; +"subcategory_tailored_pants" = "Calça social"; +"subcategory_shorts" = "Bermuda"; +"subcategory_cargo_pants" = "Calça cargo"; +"subcategory_sweatpants" = "Calça de moletom"; +"subcategory_bomber" = "Bomber"; +"subcategory_trucker" = "Trucker"; +"subcategory_puffer" = "Puffer"; +"subcategory_blazer" = "Blazer"; +"subcategory_coat" = "Casaco"; +"subcategory_windbreaker" = "Corta-vento"; +"subcategory_sneakers" = "Tênis"; +"subcategory_boots_military" = "Coturno"; +"subcategory_boots_chelsea" = "Bota chelsea"; +"subcategory_derby" = "Derby"; +"subcategory_oxford" = "Oxford"; +"subcategory_loafer" = "Mocassim"; +"subcategory_sandals" = "Sandálias"; +"subcategory_watch" = "Relógio"; +"subcategory_belt" = "Cinto"; +"subcategory_sunglasses" = "Óculos de sol"; +"subcategory_hat_cap" = "Boné"; +"subcategory_scarf" = "Cachecol"; +"subcategory_bag_backpack" = "Mochila"; + +/* Body Type */ +"body_type_slim" = "Magro"; +"body_type_athletic" = "Atlético"; +"body_type_average" = "Médio"; +"body_type_stocky" = "Robusto"; +"body_type_short" = "Baixo"; +"body_type_tall_and_slim" = "Alto e Magro"; +"body_type_big_and_tall" = "Grande e Alto"; + +/* Style Profile */ +"style_classic" = "Clássico"; +"style_casual" = "Casual"; +"style_streetwear" = "Streetwear"; +"style_smart_casual" = "Smart Casual"; +"style_minimalist" = "Minimalista"; + +/* Age Range */ +"age_18_25" = "18–25"; +"age_26_35" = "26–35"; +"age_36_45" = "36–45"; +"age_46_plus" = "46+"; + +/* Climate */ +"climate_tropical" = "Tropical"; +"climate_temperate" = "Temperado"; +"climate_cold" = "Frio"; +"climate_mixed" = "Misto"; + +/* Lifestyle */ +"lifestyle_work_office" = "Trabalho (Escritório)"; +"lifestyle_work_manual" = "Trabalho (Manual)"; +"lifestyle_social" = "Social"; +"lifestyle_sports" = "Esportes"; +"lifestyle_formal_events" = "Eventos Formais"; diff --git a/iosApp/iosApp/pt-BR.lproj/Localizable.stringsdict b/iosApp/iosApp/pt-BR.lproj/Localizable.stringsdict new file mode 100644 index 0000000..29af6e3 --- /dev/null +++ b/iosApp/iosApp/pt-BR.lproj/Localizable.stringsdict @@ -0,0 +1,70 @@ + + + + + selected_count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d selecionado + other + %d selecionados + + + delete_items_title + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Excluir %d item? + other + Excluir %d itens? + + + delete_outfits_title + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Excluir %d look? + other + Excluir %d looks? + + + saved_combinations + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d combinação salva + other + %d combinações salvas + + + + From 0b1943c5bcdb992d91538f562c48e6ce5a370774 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Sat, 28 Mar 2026 21:00:31 -0300 Subject: [PATCH 2/2] chore: update CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index c9b7b99..c804bff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,7 @@ Requires JDK 17+ (SQLDelight 2.0.2 requirement). iOS builds require Xcode — op - Always add previews to screens: on Android, include `@Preview` for phone and tablet (portrait) with `showSystemUi = true`; on iOS, include SwiftUI `#Preview` for iPhone and iPad (portrait). - Use native/framework components (e.g., Material3 `Button`, `FilledTonalButton`, `ElevatedButton`) instead of building custom equivalents from `Box`/`clickable`. Only go custom when the framework component genuinely cannot match the design. - Always update both Android (Compose) and iOS (SwiftUI) when making UI changes. Every screen, component, or visual behavior change must be applied to both platforms in the same task. +- Never hardcode user-facing strings in UI code. Always use platform string resources: `stringResource(R.string.xxx)` / `pluralStringResource()` on Android, `String(localized:)` / `String(format:)` on iOS. Add every new string to both `values/strings.xml` (en), `values-pt-rBR/strings.xml` (pt-BR), `en.lproj/Localizable.strings`, and `pt-BR.lproj/Localizable.strings`. For plurals, also update `values/strings.xml` `` entries and the `.stringsdict` files in both `.lproj` directories. ## Dependencies