Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions iosApp/VitruvianPhoenix/VitruvianPhoenix/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
</dict>
<key>CFBundleDisplayName</key>
<string>Project Phoenix</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>de</string>
<string>es</string>
<string>fr</string>
<string>nl</string>
<string>it</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Project Phoenix needs Bluetooth to connect to your Vitruvian Trainer machine.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devil.phoenixproject.domain.model

import java.util.Locale

/**
* Android actual for [currentLanguageCode]. Returns the JVM `Locale`'s
* lowercased language code, e.g. `"en"` / `"it"`. Returns `""` if the
* default locale is `ROOT` or otherwise has no language component.
*/
actual fun currentLanguageCode(): String =
Locale.getDefault().language.orEmpty()
33 changes: 33 additions & 0 deletions shared/src/commonMain/composeResources/values-it/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Italian (italiano) — Beta AI-generated translation -->
<!-- Fitness terminology context:
"set" = serie (un gruppo di ripetizioni)
"rep" = ripetizione (un singolo movimento)
"1RM" = 1RM (one-rep max, universale)
"AMRAP" = AMRAP (as many reps as possible, universale)
"TUT" = TUT (time under tension, universale)
"PR" = PR (record personale)
Technical terms like "Echo", "TUT", "AMRAP", "1RM" stay in English.
-->
<resources>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose Italian in the language picker

Adding this values-it locale and advertising it in CFBundleLocalizations makes Italian a shipped locale, but the app’s own language dropdown still constructs only en, nl, de, es, and fr options in SettingsTab.kt (checked the languageOptions list at lines 1123-1128). In contexts where the user does not have system Italian selected, or has previously chosen another in-app language that is persisted via the existing locale helpers, these new Italian strings and the NBSP percent fix are not reachable from the app’s language setting. Please add the it option and its display string alongside this locale.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Testing

  • ./gradlew -Pskip.supabase.check=true :shared:compileKotlinIosArm64
  • python3 - <<'PY' from pathlib import Path import xml.etree.ElementTree as ET for p in Path('shared/src/commonMain/composeResources').glob('values*/strings.xml'): ET.parse(p) print('parsed compose resource strings.xml files') PY
  • git diff --check
  • ⚠️ ./gradlew :shared:compileKotlinIosArm64 :shared:compileAndroidHostTest (warning due to missing Supabase credentials in this environment)
  • ⚠️ ./gradlew -Pskip.supabase.check=true :shared:compileKotlinIosArm64 :shared:compileAndroidHostTest (warning due to missing Android SDK / ANDROID_HOME in this environment)

View task →

<string name="app_name">Phoenix</string>

<!-- ==================== Workout Modes ==================== -->
<string name="echo_level">Livello Echo</string>
<string name="echo_level_hard">Difficile</string>
<string name="echo_level_harder">Più difficile</string>
<string name="echo_level_hardest">Difficilissimo</string>
<string name="echo_level_epic">Epico</string>
<string name="eccentric_load">Carico eccentrico</string>
<string name="eccentric_load_helper">Carico durante la fase eccentrica (discesa)</string>
<string name="rep_count_timing">Tempismo conteggio ripetizioni</string>
<string name="rep_count_timing_top">Conta in cima (picco concentrico)</string>
<string name="rep_count_timing_bottom">Conta in basso (fase eccentrica)</string>

<!-- ==================== Rest Timer ==================== -->
<string name="rest_eccentric_load">CARICO ECCENTRICO</string>
<string name="rest_echo_level">LIVELLO ECHO</string>

<!-- ==================== Configuration ==================== -->
<string name="config_echo_level">LIVELLO ECHO</string>
</resources>
8 changes: 8 additions & 0 deletions shared/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,15 @@
<string name="echo_mode">Echo Mode</string>
<string name="echo_mode_configuration">Echo Mode Configuration</string>
<string name="echo_level">Echo Level</string>
<string name="echo_level_hard">Hard</string>
<string name="echo_level_harder">Harder</string>
<string name="echo_level_hardest">Hardest</string>
<string name="echo_level_epic">Epic</string>
<string name="eccentric_load">Eccentric Load</string>
<string name="eccentric_load_helper">Load during eccentric (lowering) phase</string>
<string name="rep_count_timing">Rep Count Timing</string>
<string name="rep_count_timing_top">Count at top of lift (concentric peak)</string>
<string name="rep_count_timing_bottom">Count at bottom (eccentric valley)</string>
<string name="select_tut_variant">Select TUT Variant</string>

<!-- ==================== Workout Setup ==================== -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.devil.phoenixproject.domain.model

/**
* Returns the BCP-47 language subtag of the active app locale, lowercased
* (e.g. `"en"`, `"it"`, `"de"`). Returns `""` when the language cannot be
* determined.
*
* This is a multiplatform-friendly alternative to reading
* `LocalConfiguration.current.locales`, which is not exposed in the
* Compose Multiplatform iOS klib (CMP 1.10.3) and would not compile for
* `iosArm64`. We delegate to platform-native APIs:
*
* - Android: `java.util.Locale.getDefault().language`
* - iOS: `NSUserDefaults.standardUserDefaults` first entry of `AppleLanguages`
* (the user-selected preferred language list, which is exactly the
* Italian iPhone setting from issue #540).
*
* The returned value is the *language* subtag only — no region, no script.
* Tests for the Italian NBSP branch check `equals("it", ignoreCase = true)`.
*/
expect fun currentLanguageCode(): String
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.devil.phoenixproject.domain.model

import androidx.compose.runtime.Composable
import org.jetbrains.compose.resources.stringResource
import vitruvianprojectphoenix.shared.generated.resources.Res
import vitruvianprojectphoenix.shared.generated.resources.echo_level_epic
import vitruvianprojectphoenix.shared.generated.resources.echo_level_hard
import vitruvianprojectphoenix.shared.generated.resources.echo_level_harder
import vitruvianprojectphoenix.shared.generated.resources.echo_level_hardest

/**
* Locale-aware label helpers for the Echo-mode UI block in
* [com.devil.phoenixproject.presentation.screen.JustLiftScreen].
*
* Background (issue #540): on Italian iPhone the system font (SF Pro) renders
* the literal sequence `110%` with effectively zero left-advance on the `%`
* glyph, so the EccentricLoad dropdown value reads as `11U%` / `11 U%`.
* Italian typography inserts a hard non-breaking space (U+00A0) between the
* digits and the percent sign — `110 %` — and iOS SF Pro renders that form
* without a glyph collision. This file emits that form for any `it-*`
* language and the back-compat ASCII `110%` form for every other locale.
*
* The pure formatter [formatEccentricLoad] is `internal` so the unit test in
* `commonTest` can exercise the per-locale behaviour without needing a
* Compose runtime or a platform-specific locale implementation. The
* `@Composable` [eccentricLoadLabel] wrapper uses the expect/actual
* [currentLanguageCode] helper to get the active app locale without pulling
* in any Compose-Multiplatform-specific locale API.
*/

/**
* Non-Composable pure formatter used by the `@Composable` [eccentricLoadLabel]
* wrapper. Exposed as `internal` for unit testing.
*
* Behaviour:
* * For Italian (the language code `it`, case-insensitive) this returns the
* digits followed by U+00A0 NBSP and `%` — e.g. `"110\u00A0%"`. This is
* the canonical Italian typographic form and resolves the iOS SF Pro
* `%`-glyph collision.
* * For every other locale this returns the digits followed by `%` — e.g.
* `"110%"` (byte-identical to the legacy `EccentricLoad.displayName` so
* the existing English UI is unchanged).
*
* @param load the [EccentricLoad] enum entry whose
* [EccentricLoad.percentage] is formatted.
* @param language the lowercased language code of the active locale, e.g.
* `"en"`, `"it"`, `"de"`. Pass `""` or a non-`"it"` value to
* get the ASCII form.
*/
internal fun formatEccentricLoad(load: EccentricLoad, language: String): String {
return if (language.equals("it", ignoreCase = true)) {
"${load.percentage}\u00A0%"
} else {
"${load.percentage}%"
}
}

/**
* Composable wrapper that resolves the active language code via
* [currentLanguageCode] and delegates to [formatEccentricLoad]. No
* Compose-Multiplatform-specific locale API is required, so this helper
* compiles cleanly for both the Android and iOS targets.
*
* Returns the locale-formatted percentage for the dropdown value cell in
* [com.devil.phoenixproject.presentation.screen.JustLiftScreen] (line ~528)
* and the matching dropdown item text (line ~544).
*/
@Composable
fun eccentricLoadLabel(load: EccentricLoad): String {
return formatEccentricLoad(load, currentLanguageCode())
}

/**
* Returns the [EchoLevel] label via the [stringResource] lookup so the
* FilterChip and SegmentedButton rows pick up the per-locale translation.
*
* Resource key convention: `echo_level_<lowercase name>` → `echo_level_hard`,
* `echo_level_harder`, `echo_level_hardest`, `echo_level_epic`.
*/
@Composable
fun echoLevelLabel(level: EchoLevel): String = when (level) {
EchoLevel.HARD -> stringResource(Res.string.echo_level_hard)
EchoLevel.HARDER -> stringResource(Res.string.echo_level_harder)
EchoLevel.HARDEST -> stringResource(Res.string.echo_level_hardest)
EchoLevel.EPIC -> stringResource(Res.string.echo_level_epic)
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ import com.devil.phoenixproject.domain.model.WeightUnit
import com.devil.phoenixproject.domain.model.WorkoutMetric
import com.devil.phoenixproject.domain.model.WorkoutMode
import com.devil.phoenixproject.domain.model.WorkoutState
import com.devil.phoenixproject.domain.model.eccentricLoadLabel
import com.devil.phoenixproject.domain.model.echoLevelLabel
import com.devil.phoenixproject.domain.model.toWorkoutMode
import com.devil.phoenixproject.presentation.components.AddProfileDialog
import com.devil.phoenixproject.presentation.components.CompactNumberPicker
Expand All @@ -110,7 +112,13 @@ import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
import vitruvianprojectphoenix.shared.generated.resources.Res
import vitruvianprojectphoenix.shared.generated.resources.cd_close_workout
import vitruvianprojectphoenix.shared.generated.resources.eccentric_load
import vitruvianprojectphoenix.shared.generated.resources.eccentric_load_helper
import vitruvianprojectphoenix.shared.generated.resources.echo_level
import vitruvianprojectphoenix.shared.generated.resources.label_live
import vitruvianprojectphoenix.shared.generated.resources.rep_count_timing
import vitruvianprojectphoenix.shared.generated.resources.rep_count_timing_bottom
import vitruvianprojectphoenix.shared.generated.resources.rep_count_timing_top

/**
* Just Lift screen - quick workout configuration.
Expand Down Expand Up @@ -512,7 +520,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
},
) {
Text(
"Eccentric Load",
stringResource(Res.string.eccentric_load),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
Expand All @@ -525,7 +533,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
onExpandedChange = { expanded = it },
) {
OutlinedTextField(
value = eccentricLoad.displayName,
value = eccentricLoadLabel(eccentricLoad),
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
Expand All @@ -541,7 +549,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
) {
EccentricLoad.entries.forEach { load ->
DropdownMenuItem(
text = { Text(load.displayName) },
text = { Text(eccentricLoadLabel(load)) },
onClick = {
eccentricLoad = load
expanded = false
Expand All @@ -553,7 +561,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
}

Text(
"Load during eccentric (lowering) phase",
stringResource(Res.string.eccentric_load_helper),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Expand Down Expand Up @@ -581,7 +589,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
},
) {
Text(
"Echo Level",
stringResource(Res.string.echo_level),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
Expand All @@ -600,7 +608,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
echoLevel = level
selectedMode = WorkoutMode.Echo(level)
},
label = { Text(level.displayName) },
label = { Text(echoLevelLabel(level)) },
)
}
}
Expand All @@ -617,7 +625,7 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
},
selected = echoLevel == level,
) {
Text(level.displayName, maxLines = 1)
Text(echoLevelLabel(level), maxLines = 1)
}
}
}
Expand All @@ -642,16 +650,16 @@ fun JustLiftScreen(navController: NavController, viewModel: MainViewModel, theme
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Rep Count Timing",
text = stringResource(Res.string.rep_count_timing),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = if (repCountTiming == RepCountTiming.TOP) {
"Count at top of lift (concentric peak)"
stringResource(Res.string.rep_count_timing_top)
} else {
"Count at bottom (eccentric valley)"
stringResource(Res.string.rep_count_timing_bottom)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
Expand Down
Loading
Loading