Skip to content

Commit 0914eec

Browse files
committed
feat: added English and German translations of the App, a language switcher and more tests
1 parent 9841fa8 commit 0914eec

43 files changed

Lines changed: 2416 additions & 177 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎mobile/androidApp/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ android {
6666
dependencies {
6767
implementation(project(":shared"))
6868

69+
implementation(libs.androidx.appcompat)
6970
implementation(libs.androidx.core.ktx)
7071
implementation(libs.androidx.activity.compose)
7172
implementation(libs.androidx.lifecycle.runtime.ktx)

‎mobile/androidApp/src/main/AndroidManifest.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
<category android:name="android.intent.category.LAUNCHER" />
1717
</intent-filter>
1818
</activity>
19+
20+
<!-- Per-app locale storage backport for API < 33. -->
21+
<service
22+
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
23+
android:enabled="false"
24+
android:exported="false">
25+
<meta-data
26+
android:name="autoStoreLocales"
27+
android:value="true" />
28+
</service>
1929
</application>
2030

2131
</manifest>

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/Navigation.kt‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState
1616
import androidx.compose.runtime.getValue
1717
import androidx.compose.ui.Modifier
1818
import androidx.compose.ui.graphics.vector.ImageVector
19+
import androidx.compose.ui.res.stringResource
1920
import androidx.navigation.NavGraph.Companion.findStartDestination
2021
import androidx.navigation.NavType
2122
import androidx.navigation.compose.NavHost
@@ -71,13 +72,13 @@ object Routes {
7172

7273
enum class BottomTab(
7374
val route: String,
74-
val label: String,
75+
val labelRes: Int,
7576
val icon: ImageVector,
7677
) {
77-
Home(Routes.HOME, "Home", Icons.Default.Home),
78-
Categories(Routes.CATEGORIES, "Categories", Icons.Default.List),
79-
Search(Routes.SEARCH, "Search", Icons.Default.Search),
80-
Settings(Routes.SETTINGS, "Settings", Icons.Default.Settings),
78+
Home(Routes.HOME, R.string.nav_home, Icons.Default.Home),
79+
Categories(Routes.CATEGORIES, R.string.nav_categories, Icons.Default.List),
80+
Search(Routes.SEARCH, R.string.nav_search, Icons.Default.Search),
81+
Settings(Routes.SETTINGS, R.string.nav_settings, Icons.Default.Settings),
8182
}
8283

8384
// ── Root scaffold with NavHost ─────────────────────────────────────
@@ -99,9 +100,10 @@ fun MyFaqNavHost(aim: ActiveInstanceManager = koinInject()) {
99100
if (showBottomBar) {
100101
NavigationBar {
101102
BottomTab.entries.forEach { tab ->
103+
val tabLabel = stringResource(tab.labelRes)
102104
NavigationBarItem(
103-
icon = { Icon(tab.icon, contentDescription = tab.label) },
104-
label = { Text(tab.label) },
105+
icon = { Icon(tab.icon, contentDescription = tabLabel) },
106+
label = { Text(tabLabel) },
105107
selected = currentRoute == tab.route,
106108
onClick = {
107109
navController.navigate(tab.route) {

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/Theme.kt‎

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,12 @@ private val DarkColors = darkColorScheme()
1616
private val LightColors = lightColorScheme()
1717

1818
/** User-selectable theme mode, persisted via SharedPreferences. */
19-
enum class ThemeMode {
20-
SYSTEM,
21-
LIGHT,
22-
DARK,
23-
;
24-
25-
val label: String
26-
get() =
27-
when (this) {
28-
SYSTEM -> "System"
29-
LIGHT -> "Light"
30-
DARK -> "Dark"
31-
}
19+
enum class ThemeMode(
20+
val labelRes: Int,
21+
) {
22+
SYSTEM(R.string.theme_system),
23+
LIGHT(R.string.theme_light),
24+
DARK(R.string.theme_dark),
3225
}
3326

3427
/** Observable holder for the current theme preference. */

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/AddInstanceSheet.kt‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ import androidx.compose.runtime.remember
2828
import androidx.compose.runtime.setValue
2929
import androidx.compose.ui.Alignment
3030
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.res.stringResource
3132
import androidx.compose.ui.unit.dp
33+
import app.myfaq.android.R
3234
import app.myfaq.shared.data.ActiveInstanceManager
3335
import app.myfaq.shared.ui.AddInstanceState
3436
import app.myfaq.shared.ui.WorkspacesViewModel
@@ -53,10 +55,10 @@ fun AddInstanceSheet(
5355
Scaffold(
5456
topBar = {
5557
TopAppBar(
56-
title = { Text("Add Instance") },
58+
title = { Text(stringResource(R.string.add_instance)) },
5759
navigationIcon = {
5860
IconButton(onClick = onDismiss) {
59-
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
61+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
6062
}
6163
},
6264
)
@@ -72,8 +74,8 @@ fun AddInstanceSheet(
7274
OutlinedTextField(
7375
value = url,
7476
onValueChange = { url = it },
75-
label = { Text("phpMyFAQ URL") },
76-
placeholder = { Text("https://faq.example.com") },
77+
label = { Text(stringResource(R.string.url_label)) },
78+
placeholder = { Text(stringResource(R.string.url_placeholder)) },
7779
singleLine = true,
7880
modifier = Modifier.fillMaxWidth(),
7981
)
@@ -87,7 +89,7 @@ fun AddInstanceSheet(
8789
enabled = url.isNotBlank(),
8890
modifier = Modifier.fillMaxWidth(),
8991
) {
90-
Text("Connect")
92+
Text(stringResource(R.string.connect))
9193
}
9294
}
9395

@@ -119,7 +121,7 @@ fun AddInstanceSheet(
119121
onClick = { vm.probeInstance(url) },
120122
modifier = Modifier.fillMaxWidth(),
121123
) {
122-
Text("Retry")
124+
Text(stringResource(R.string.retry))
123125
}
124126
}
125127
}
@@ -146,7 +148,7 @@ private fun ConfirmationCard(
146148
style = MaterialTheme.typography.titleMedium,
147149
)
148150
Text(
149-
text = "phpMyFAQ $version",
151+
text = stringResource(R.string.phpmyfaq_version, version),
150152
style = MaterialTheme.typography.bodySmall,
151153
color = MaterialTheme.colorScheme.onSecondaryContainer,
152154
)
@@ -155,7 +157,7 @@ private fun ConfirmationCard(
155157
onClick = onConfirm,
156158
modifier = Modifier.fillMaxWidth(),
157159
) {
158-
Text("Add Instance")
160+
Text(stringResource(R.string.add_instance))
159161
}
160162
}
161163
}

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/CategoriesScreen.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import androidx.compose.runtime.getValue
2424
import androidx.compose.runtime.remember
2525
import androidx.compose.ui.Alignment
2626
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.res.stringResource
2728
import androidx.compose.ui.unit.dp
29+
import app.myfaq.android.R
2830
import app.myfaq.android.screens.components.ErrorRetry
2931
import app.myfaq.android.screens.components.LoadingIndicator
3032
import app.myfaq.shared.api.dto.Category
@@ -46,7 +48,7 @@ fun CategoriesScreen(
4648

4749
Scaffold(
4850
topBar = {
49-
CenterAlignedTopAppBar(title = { Text("Categories") })
51+
CenterAlignedTopAppBar(title = { Text(stringResource(R.string.categories_title)) })
5052
},
5153
) { padding ->
5254
when (val s = state) {

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/FaqDetailScreen.kt‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ import androidx.compose.runtime.setValue
4444
import androidx.compose.ui.Modifier
4545
import androidx.compose.ui.graphics.toArgb
4646
import androidx.compose.ui.platform.LocalContext
47+
import androidx.compose.ui.res.stringResource
4748
import androidx.compose.ui.unit.dp
4849
import androidx.compose.ui.viewinterop.AndroidView
50+
import app.myfaq.android.R
4951
import app.myfaq.android.screens.components.ErrorRetry
5052
import app.myfaq.android.screens.components.LoadingIndicator
5153
import app.myfaq.shared.api.dto.Comment
@@ -75,25 +77,26 @@ fun FaqDetailScreen(
7577
Scaffold(
7678
topBar = {
7779
TopAppBar(
78-
title = { Text("FAQ") },
80+
title = { Text(stringResource(R.string.faq)) },
7981
navigationIcon = {
8082
IconButton(onClick = onBack) {
81-
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
83+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
8284
}
8385
},
8486
actions = {
8587
if (faqState is UiState.Success) {
8688
val context = LocalContext.current
8789
val faq = (faqState as UiState.Success<FaqDetail>).data
90+
val shareLabel = stringResource(R.string.share)
8891
IconButton(onClick = {
8992
val sendIntent =
9093
Intent(Intent.ACTION_SEND).apply {
9194
type = "text/plain"
9295
putExtra(Intent.EXTRA_TEXT, faq.question)
9396
}
94-
context.startActivity(Intent.createChooser(sendIntent, "Share"))
97+
context.startActivity(Intent.createChooser(sendIntent, shareLabel))
9598
}) {
96-
Icon(Icons.Default.Share, contentDescription = "Share")
99+
Icon(Icons.Default.Share, contentDescription = shareLabel)
97100
}
98101
}
99102
},
@@ -233,7 +236,7 @@ private fun FaqDetailContent(
233236
modifier = Modifier.fillMaxWidth(),
234237
) {
235238
Icon(Icons.Default.Star, contentDescription = null)
236-
Text(" Rate this FAQ", style = MaterialTheme.typography.labelLarge)
239+
Text(" " + stringResource(R.string.rate_faq), style = MaterialTheme.typography.labelLarge)
237240
}
238241

239242
Spacer(modifier = Modifier.height(16.dp))
@@ -247,7 +250,7 @@ private fun FaqDetailContent(
247250
@Composable
248251
private fun AttachmentsSection(attachments: List<app.myfaq.shared.api.dto.Attachment>) {
249252
val context = LocalContext.current
250-
Text("Attachments", style = MaterialTheme.typography.titleSmall)
253+
Text(stringResource(R.string.attachments), style = MaterialTheme.typography.titleSmall)
251254
Spacer(modifier = Modifier.height(4.dp))
252255
attachments.forEach { attachment ->
253256
TextButton(
@@ -270,14 +273,14 @@ private fun CommentsSection(state: UiState<List<Comment>>) {
270273
when (state) {
271274
is UiState.Loading -> {
272275
Text(
273-
"Loading comments...",
276+
stringResource(R.string.loading_comments),
274277
style = MaterialTheme.typography.bodySmall,
275278
modifier = Modifier.padding(top = 8.dp),
276279
)
277280
}
278281
is UiState.Error -> {
279282
Text(
280-
"Could not load comments",
283+
stringResource(R.string.could_not_load_comments),
281284
style = MaterialTheme.typography.bodySmall,
282285
color = MaterialTheme.colorScheme.error,
283286
modifier = Modifier.padding(top = 8.dp),
@@ -286,7 +289,7 @@ private fun CommentsSection(state: UiState<List<Comment>>) {
286289
is UiState.Success -> {
287290
if (state.data.isEmpty()) {
288291
Text(
289-
"No comments yet",
292+
stringResource(R.string.no_comments),
290293
style = MaterialTheme.typography.bodySmall,
291294
color = MaterialTheme.colorScheme.onSurfaceVariant,
292295
modifier = Modifier.padding(top = 8.dp),
@@ -295,9 +298,9 @@ private fun CommentsSection(state: UiState<List<Comment>>) {
295298
TextButton(onClick = { expanded = !expanded }) {
296299
Text(
297300
if (expanded) {
298-
"Hide comments (${state.data.size})"
301+
stringResource(R.string.hide_comments, state.data.size)
299302
} else {
300-
"Show comments (${state.data.size})"
303+
stringResource(R.string.show_comments, state.data.size)
301304
},
302305
)
303306
}
@@ -359,8 +362,14 @@ private fun buildHtml(
359362
margin: 0;
360363
word-wrap: break-word;
361364
}
365+
/* Override any inline color/background from server HTML so
366+
dark-mode content stays readable. */
367+
body, body *:not(a) {
368+
color: $fgColor !important;
369+
background-color: transparent !important;
370+
}
371+
a, a * { color: #64b5f6 !important; }
362372
img { max-width: 100%; height: auto; }
363-
a { color: #1a73e8; }
364373
pre, code { overflow-x: auto; }
365374
</style>
366375
</head>

‎mobile/androidApp/src/main/kotlin/app/myfaq/android/screens/FaqListScreen.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import androidx.compose.runtime.collectAsState
1919
import androidx.compose.runtime.getValue
2020
import androidx.compose.runtime.remember
2121
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.res.stringResource
2223
import androidx.compose.ui.unit.dp
24+
import app.myfaq.android.R
2325
import app.myfaq.android.screens.components.ErrorRetry
2426
import app.myfaq.android.screens.components.FaqCard
2527
import app.myfaq.android.screens.components.LoadingIndicator
@@ -48,7 +50,7 @@ fun FaqListScreen(
4850
title = { Text(categoryName) },
4951
navigationIcon = {
5052
IconButton(onClick = onBack) {
51-
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
53+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
5254
}
5355
},
5456
)

0 commit comments

Comments
 (0)