Fix strings missing from translations (#959)

## Description
Weblate's Android string parser (aka
https://translate.codeberg.org/projects/wholphin/) doesn't support
direct translations of string arrays.

Many of Wholphin's preference options are defined in string arrays and
therefore could not be translated. So this PR implements the [suggested
workaround](https://docs.weblate.org/en/latest/formats/android.html) to
move the string into individual entries and reference them in arrays.

Also moves some hardcoded strings into `strings.xml` so they can be
translated. Note: virtually all error messages are still hardcoded, but
they are only for exceptional cases, so it's probably not a huge
problem.

A minor bug where the backdrop would show briefly after switching users
is fixed.

### Related issues
Fixes #957

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-23 11:17:57 -05:00 committed by GitHub
parent a44fad890f
commit 2a371e7d6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 156 additions and 93 deletions

View file

@ -123,6 +123,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var suggestionsSchedulerService: SuggestionsSchedulerService
@Inject
lateinit var backdropService: BackdropService
// Note: unused but injected to ensure it is created
@Inject
lateinit var serverEventListener: ServerEventListener
@ -247,6 +250,9 @@ class MainActivity : AppCompatActivity() {
}
is SetupDestination.AppContent -> {
LaunchedEffect(Unit) {
backdropService.clearBackdrop()
}
val current = key.current
ProvideLocalClock {
if (UpdateChecker.ACTIVE && appPreferences.autoCheckForUpdates) {

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.WholphinApplication
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSegmentType
import timber.log.Timber
@ -76,7 +77,7 @@ val BaseItemDto.seriesProductionYears: String?
append(productionYear.toString())
if (status == "Continuing") {
append(" - ")
append("Present")
append(WholphinApplication.instance.getString(R.string.series_continueing))
} else if (status == "Ended") {
endDate?.let {
if (it.year != productionYear) {

View file

@ -857,7 +857,7 @@ fun CollectionFolderGridContent(
DataLoadingState.Loading,
-> {
// This shouldn't happen, so just show placeholder
Text("Loading")
Text(stringResource(R.string.loading))
}
is DataLoadingState.Error -> {

View file

@ -102,7 +102,7 @@ fun DownloadSubtitlesContent(
.padding(PaddingValues(24.dp)),
) {
Text(
text = "Search & download subtitles",
text = stringResource(R.string.search_and_download_subtitles),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
@ -113,7 +113,7 @@ fun DownloadSubtitlesContent(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Language",
text = stringResource(R.string.language),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
)
@ -137,7 +137,7 @@ fun DownloadSubtitlesContent(
}
if (dialogItems.isEmpty()) {
Text(
text = "No remote subtitles were found",
text = stringResource(R.string.no_subtitles_found),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.playback
import androidx.compose.ui.layout.ContentScale
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.PrefContentScale
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
@ -9,13 +10,13 @@ val playbackSpeedOptions = listOf(".25", ".5", ".75", "1.0", "1.25", "1.5", "1.7
val playbackScaleOptions =
mapOf(
ContentScale.Fit to "Fit",
ContentScale.None to "None",
ContentScale.Crop to "Crop",
ContentScale.Fit to R.string.content_scale_fit,
ContentScale.None to R.string.none,
ContentScale.Crop to R.string.content_scale_crop,
// ContentScale.Inside to "Inside",
ContentScale.FillBounds to "Fill",
ContentScale.FillWidth to "Fill Width",
ContentScale.FillHeight to "Fill Height",
ContentScale.FillBounds to R.string.content_scale_fill,
ContentScale.FillWidth to R.string.content_scale_fill_width,
ContentScale.FillHeight to R.string.content_scale_fill_height,
)
val PrefContentScale.scale: ContentScale

View file

@ -144,7 +144,9 @@ fun PlaybackDialog(
BottomDialogItem(
data = PlaybackDialogType.VIDEO_SCALE,
headline = stringResource(R.string.video_scale),
supporting = playbackScaleOptions[settings.contentScale],
supporting =
playbackScaleOptions[settings.contentScale]
?.let { stringResource(it) },
),
)
}
@ -220,7 +222,7 @@ fun PlaybackDialog(
playbackScaleOptions.map { (scale, name) ->
BottomDialogItem(
data = scale,
headline = name,
headline = stringResource(name),
supporting = null,
)
}

View file

@ -62,7 +62,7 @@ fun AddSeerrServerApiKey(
val passwordFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text(
text = "Enter URL & API Key",
text = stringResource(R.string.enter_url_api_key),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
@ -73,7 +73,7 @@ fun AddSeerrServerApiKey(
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text(
text = "URL",
text = stringResource(R.string.url),
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(
@ -104,7 +104,7 @@ fun AddSeerrServerApiKey(
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text(
text = "API Key",
text = stringResource(R.string.api_key),
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(

View file

@ -6,6 +6,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.DialogItem
@ -71,27 +73,26 @@ fun ChooseSeerrLoginType(
onChoose: (SeerrAuthMethod) -> Unit,
) {
val params =
remember {
DialogParams(
fromLongClick = false,
title = "Login to Seerr server",
items =
listOf(
DialogItem(
text = "API Key",
onClick = { onChoose.invoke(SeerrAuthMethod.API_KEY) },
),
DialogItem(
text = "Jellyfin user",
onClick = { onChoose.invoke(SeerrAuthMethod.JELLYFIN) },
),
DialogItem(
text = "Local user",
onClick = { onChoose.invoke(SeerrAuthMethod.LOCAL) },
),
DialogParams(
fromLongClick = false,
title = stringResource(R.string.seerr_login),
items =
listOf(
DialogItem(
text = stringResource(R.string.api_key),
onClick = { onChoose.invoke(SeerrAuthMethod.API_KEY) },
),
)
}
DialogItem(
text = stringResource(R.string.seerr_jellyfin_user),
onClick = { onChoose.invoke(SeerrAuthMethod.JELLYFIN) },
),
DialogItem(
text = stringResource(R.string.seerr_local_user),
onClick = { onChoose.invoke(SeerrAuthMethod.LOCAL) },
),
),
)
DialogPopup(
params = params,
onDismissRequest = onDismissRequest,