Translate the remaining hard-coded interface texts (#129)

* Translate the interface texts that stage 9 left in English

The GUI tab checkboxes, the file chooser and dropdown placeholders, the
profile dialog's Apply, the settings notice's OK, the chat clear button,
the selected-station map button and "all", six column titles, the monitor
window's title and heading, and the refusal and hint texts of the profiles,
beacon and notification tab states now come from the translation files.

Column titles go through CurrentStrings in a lambda like the existing ones,
so the id stays untranslated and the stored layout keeps its keys. The tab
states read CurrentStrings as StatusIndicatorState already does. The
English values are unchanged, so the screen tests still find every control
by its old label; a new test switches to German and checks one sample per
path: a composed label, a column title and a tab-state refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Translate the settings notices, profile labels and alert button too

Wires the settings.spot* and settings.write* keys that existed but were
never used, translates the settings window title and its notice titles,
the profile tab's "unknown", "Copy of" and database-mode labels, and lets
the shared alert use alert.ok instead of a literal. The German for
messages.qsoOfTheOther becomes "Fremd-QSOs", the word the menu already uses.

"Copy of" is only the suggested display name; the profile id is derived
from the entered name once at creation and stored, so a later language
switch does not touch it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rsclub2_2andClaude Opus 5.5 authored and GitHub committed 2026-10-16 11:11:39 +02:00
1 parent a9d8c013ef
commit c9b838e946
21 files changed
+278 -82

No files matched your search

@@ -181,7 +181,7 @@ fun ChatInputRow(
)
Form.button(text = "TX", onClick = { state.send() })
Form.button(text = "clear", onClick = { state.clear() })
Form.button(text = kst4contest.view.i18n.LocalStrings.current.chatClear, onClick = { state.clear() })
VerticalRule()
@@ -274,7 +274,7 @@ object ComposeAlert {
AlertContent(
header = header,
body = body,
buttons = alertButtons("OK", null, ConfirmKind.OK_DONE, BUTTON_ORDER_LINUX),
buttons = alertButtons(CurrentStrings.get().alertOk, null, ConfirmKind.OK_DONE, BUTTON_ORDER_LINUX),
onPressed = { dialog.dispose() },
link = linkText to linkTarget,
)
@@ -54,7 +54,7 @@ object DirectedMessageColumns {
),
DataColumn(
id = "airscout",
title = "AP [minutes / pot%]",
title = { kst4contest.view.i18n.CurrentStrings.get().columnAirplane },
value = { AirplaneCell.format(it.sender?.airPlaneReflectInfo) },
accent = { _, rendered -> AirplaneCell.accentFor(rendered) },
comparator = compareBy(AirplaneCell.order) { it.sender?.airPlaneReflectInfo },
@@ -62,7 +62,7 @@ object DirectedMessageColumns {
),
DataColumn(
id = "category",
title = "Category",
title = { kst4contest.view.i18n.CurrentStrings.get().columnCategory },
value = { message ->
message.chatCategory
?.let { it.getChatCategoryName(it.categoryNumber) }
@@ -223,7 +223,7 @@ object Form {
.heightIn(min = Density.FIELD_MIN_HEIGHT),
) {
Text(
draft.value?.let(describe) ?: "not selected",
draft.value?.let(describe) ?: kst4contest.view.i18n.LocalStrings.current.formNotSelected,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.fillMaxWidth(),
)
@@ -267,7 +267,7 @@ object Form {
modifier = Modifier.heightIn(min = Density.FIELD_MIN_HEIGHT),
) {
Text(
draft.value?.let(describe) ?: "not selected",
draft.value?.let(describe) ?: kst4contest.view.i18n.LocalStrings.current.formNotSelected,
style = MaterialTheme.typography.bodyMedium,
)
}
@@ -51,7 +51,7 @@ object MonitorColumns {
*/
DataColumn(
id = "category",
title = "Category",
title = { kst4contest.view.i18n.CurrentStrings.get().columnCategory },
value = { message ->
message.sender?.chatCategory
?.let { it.getChatCategoryName(it.categoryNumber) }
@@ -61,7 +61,7 @@ object MonitorWindow {
onResized: (Float, Float) -> Unit,
) {
host.show(
title = { "Cluster & QSO of the other" },
title = { kst4contest.view.i18n.CurrentStrings.get().monitorTitle },
darkMode = darkMode,
baseFontSizeSp = baseFontSizeSp,
paletteStore = paletteStore,
@@ -97,7 +97,7 @@ internal fun MonitorContent(
HorizontalDivider(thickness = Density.HAIRLINE)
Text(
"QSO of the other",
kst4contest.view.i18n.LocalStrings.current.messagesQsoOfTheOther,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top = 4.dp),
)
@@ -26,7 +26,7 @@ object PublicMessageColumns {
DataColumn(id = "name", title = { kst4contest.view.i18n.CurrentStrings.get().columnName }, value = { it.sender?.name.orEmpty() }),
DataColumn(
id = "last-qrg",
title = "Last QRG",
title = { kst4contest.view.i18n.CurrentStrings.get().columnLastQrg },
value = { MessageFormats.paddedQrg(MonitorColumns.frequencyOf(it.sender)) },
weight = 0.8f,
),
@@ -38,7 +38,7 @@ object PublicMessageColumns {
),
DataColumn(
id = "category",
title = "Category",
title = { kst4contest.view.i18n.CurrentStrings.get().columnCategory },
value = { message ->
message.sender?.chatCategory
?.let { it.getChatCategoryName(it.categoryNumber) }
@@ -16,13 +16,13 @@ object SelectedStationMessageColumns {
),
DataColumn(
id = "call-tx",
title = "Sender",
title = { kst4contest.view.i18n.CurrentStrings.get().columnSender },
value = { MonitorColumns.callSignOf(it.sender) },
weight = 0.8f,
),
DataColumn(
id = "call-rx",
title = "Receiver",
title = { kst4contest.view.i18n.CurrentStrings.get().columnReceiver },
value = { MonitorColumns.callSignOf(it.receiver) },
weight = 0.8f,
),
@@ -127,7 +127,7 @@ fun SelectedStationPanel(
}
}
Form.check("all", activeNotQrvBands.all { state.isTaggedNotQrv(it) }) {
Form.check(strings.selectedNotQrvAll, activeNotQrvBands.all { state.isTaggedNotQrv(it) }) {
state.tagNotQrvAll(it)
}
}
@@ -183,7 +183,7 @@ fun SelectedStationPanel(
Form.section(strings.selectedLookUpAndShow) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(Density.BUTTON_GAP)) {
Form.button(strings.selectedShowPathInAs, onClick = onShowPathInAirScout)
Form.button("🗺 Show on map", onClick = onShowOnMap)
Form.button(strings.selectedShowOnMap, onClick = onShowOnMap)
Form.button(
strings.selectedTurnAntenna(state.selected?.callSignRaw.orEmpty()),
enabled = state.canTurnAntenna,
@@ -264,18 +264,18 @@ fun buildSettingsTabs(
*/
private fun deliverTestSpot(controller: ChatController, spot: ChatMember): String? {
if (!controller.isConnectedAndLoggedIn || !controller.chatPreferences.isNotify_dxClusterServerEnabled()) {
return "Connect KST4Contest and enable the local DX Cluster server first."
return kst4contest.view.i18n.CurrentStrings.get().settingsSpotNeedsConnection
}
val server = controller.dxClusterServer
?: return "Connect KST4Contest and enable the local DX Cluster server first."
?: return kst4contest.view.i18n.CurrentStrings.get().settingsSpotNeedsConnection
if (!server.hasConnectedClients()) {
return "No DX Cluster client is connected to KST4Contest."
return kst4contest.view.i18n.CurrentStrings.get().settingsSpotNoClient
}
if (!server.broadcastSingleDXClusterEntryToLoggers(spot)) {
return "The test spot could not be delivered."
return kst4contest.view.i18n.CurrentStrings.get().settingsSpotNotDelivered
}
return null
@@ -318,14 +318,9 @@ fun settingsButtons(
onSave = {
val target = host.savePreferencesFromSettings()
if (target == null) {
notices.problem(
"The configuration file could not be written. The settings stay " +
"active for this session."
)
notices.problem(kst4contest.view.i18n.CurrentStrings.get().settingsWriteFailed)
} else {
notices.info(
"Settings are stored as default to the xml config file:\n" + target
)
notices.info(kst4contest.view.i18n.CurrentStrings.get().settingsWrittenTo(target))
}
},
onApplyAndClose = close,
@@ -165,7 +165,7 @@ object SettingsWindow {
buttons: @Composable (close: () -> Unit) -> Unit,
) {
host.show(
title = { "Change Client Settings" },
title = { kst4contest.view.i18n.CurrentStrings.get().settingsWindowTitle },
darkMode = darkMode,
baseFontSizeSp = baseFontSizeSp,
paletteStore = paletteStore,
@@ -241,13 +241,13 @@ internal fun SettingsContent(
title = {
Text(
when (notice) {
is SettingsNotice.Info -> "Info"
is SettingsNotice.Problem -> "Settings"
is SettingsNotice.Info -> kst4contest.view.i18n.LocalStrings.current.settingsNoticeInfo
is SettingsNotice.Problem -> kst4contest.view.i18n.LocalStrings.current.settingsNoticeProblem
}
)
},
text = { Text(notice.message) },
confirmButton = { TextButton(onClick = notices::dismiss) { Text("OK") } },
confirmButton = { TextButton(onClick = notices::dismiss) { Text(kst4contest.view.i18n.LocalStrings.current.dialogOk) } },
)
}
}
@@ -65,7 +65,7 @@ object StationColumns {
add(DataColumn(id = "qrg", title = "QRG", value = { it.frequency?.get().orEmpty() }, weight = 0.8f))
add(DataColumn(
id = "airscout",
title = "AP [minutes / pot%]",
title = { kst4contest.view.i18n.CurrentStrings.get().columnAirplane },
value = { AirplaneCell.format(it.airPlaneReflectInfo) },
accent = { _, rendered -> AirplaneCell.accentFor(rendered) },
comparator = compareBy(AirplaneCell.order) { it.airPlaneReflectInfo },
@@ -74,7 +74,7 @@ object StationColumns {
add(DataColumn(id = "tropo", title = "Tropo", value = tropoOf, weight = 0.7f))
add(DataColumn(
id = "score",
title = "Score",
title = { kst4contest.view.i18n.CurrentStrings.get().columnScore },
value = { priorityScoreOf(it).toInt().toString() },
comparator = compareBy { priorityScoreOf(it) },
weight = 0.6f,
@@ -113,7 +113,7 @@ object StationColumns {
add(DataColumn(id = "not-qrv", title = "NOT QRV @", value = { qrvBands(it, activeBands) }, weight = 1.6f))
add(DataColumn(
id = "category",
title = "Category",
title = { kst4contest.view.i18n.CurrentStrings.get().columnCategory },
value = { member ->
member.chatCategory?.let { it.getChatCategoryName(it.categoryNumber) }.orEmpty()
},
@@ -2,6 +2,7 @@ package kst4contest.view.compose.tabs
import kst4contest.controller.ChatController
import kst4contest.model.ChatPreferences
import kst4contest.view.i18n.CurrentStrings
/**
* State of the beacon tab: the two CQ templates and the one interval they share.
@@ -69,7 +70,7 @@ class BeaconTabState(
* so a refused template that replaced a working one would silently stop
* the beacon instead of reporting anything.
*/
return "The beacon message is invalid: " + refusal.message
return CurrentStrings.get().beaconInvalidMessage(refusal.message.orEmpty())
}
store(template)
@@ -85,8 +86,7 @@ class BeaconTabState(
val minutes = entered.trim().toIntOrNull()
if (minutes == null || minutes < ChatController.MIN_BEACON_INTERVAL_MINUTES) {
return "Enter a whole beacon interval of at least " +
ChatController.MIN_BEACON_INTERVAL_MINUTES + " minute."
return CurrentStrings.get().beaconIntervalTooShort(ChatController.MIN_BEACON_INTERVAL_MINUTES.toString())
}
prefs.setBcn_beaconIntervalInMinutesMainCat(minutes)
@@ -103,7 +103,7 @@ internal fun FilePathRow(
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
)
Form.button("Choose...") { choosing = true }
Form.button(kst4contest.view.i18n.LocalStrings.current.formChoose) { choosing = true }
}
}
@@ -25,31 +25,31 @@ fun GuiOptionsTab(state: GuiOptionsTabState) {
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Form.section(strings.guiStartupDesign) {
Form.check("Start in the evening design", state.gUI_darkModeActiveByDefault) {
Form.check(strings.guiStartInEveningDesign, state.gUI_darkModeActiveByDefault) {
state.gUI_darkModeActiveByDefault = it
}
}
Form.flowingSection(strings.guiStartupFilters) {
Form.check("Public messages", state.guiOptions_defaultFilterPublicMsgs) {
Form.check(strings.guiFilterPublic, state.guiOptions_defaultFilterPublicMsgs) {
state.guiOptions_defaultFilterPublicMsgs = it
}
Form.check("Private messages to me", state.guiOptions_defaultFilterPmToMe) {
Form.check(strings.guiFilterPmToMe, state.guiOptions_defaultFilterPmToMe) {
state.guiOptions_defaultFilterPmToMe = it
}
Form.check("Private messages to others", state.guiOptions_defaultFilterPmToOther) {
Form.check(strings.guiFilterPmToOthers, state.guiOptions_defaultFilterPmToOther) {
state.guiOptions_defaultFilterPmToOther = it
}
Form.check("No filter", state.guiOptions_defaultFilterNothing) {
Form.check(strings.guiFilterNothing, state.guiOptions_defaultFilterNothing) {
state.guiOptions_defaultFilterNothing = it
}
}
Form.flowingSection(strings.guiBandColumnHints) {
Form.check("Mark a callsign seen for the first time", state.guiOptions_showFreshCallHintInBandColumns) {
Form.check(strings.guiHintFreshCall, state.guiOptions_showFreshCallHintInBandColumns) {
state.guiOptions_showFreshCallHintInBandColumns = it
}
Form.check("Mark a worked large grid field", state.guiOptions_showGrossFieldWorkedHintInBandColumns) {
Form.check(strings.guiHintLargeGridWorked, state.guiOptions_showGrossFieldWorkedHintInBandColumns) {
state.guiOptions_showGrossFieldWorkedHintInBandColumns = it
}
}
@@ -9,6 +9,7 @@ import kst4contest.view.GuiUtils
import kst4contest.view.compose.CommitOutcome
import kst4contest.view.compose.EditableListState
import kst4contest.view.compose.EntryRule
import kst4contest.view.i18n.CurrentStrings
import java.util.Locale
/**
@@ -106,8 +107,7 @@ class NotificationTabState(
val port = entered.trim().toIntOrNull()
if (port == null || port < 1 || port > 65535) {
return "\"" + entered.trim() + "\" is not a valid TCP port. " +
"Enter a value from 1 to 65535."
return CurrentStrings.get().notificationInvalidPort(entered.trim())
}
prefs.setNotify_dxclusterServerPort(port)
@@ -130,7 +130,7 @@ class NotificationTabState(
val spotterCallSign = entered.trim().uppercase(Locale.ROOT)
if (!GuiUtils.isCallSignSyntax(spotterCallSign)) {
return "\"" + spotterCallSign + "\" is not a valid spotter callsign."
return CurrentStrings.get().notificationInvalidSpotter(spotterCallSign)
}
prefs.setNotify_DXCSrv_SpottersCallSign(spotterCallSign)
@@ -151,7 +151,7 @@ class NotificationTabState(
}
if (verdict !is CommitOutcome.Stored) {
return "Please enter a valid callsign."
return CurrentStrings.get().notificationInvalidCallsign
}
/*
@@ -202,11 +202,11 @@ class NotificationTabState(
)
if (baseCall == null || !GuiUtils.isCallSignSyntax(baseCall)) {
return CommitOutcome.Refused("Please enter a valid callsign.")
return CommitOutcome.Refused(CurrentStrings.get().notificationInvalidCallsign)
}
if (others.any { it != null && it.equals(baseCall, ignoreCase = true) }) {
return CommitOutcome.Refused("This base callsign is already in the monitoring list.")
return CommitOutcome.Refused(CurrentStrings.get().notificationAlreadyMonitored)
}
return CommitOutcome.Stored(baseCall)
@@ -341,7 +341,7 @@ private fun ProfileDialogs(
)
}
},
confirmButton = { TextButton(onClick = { onWorkedData(shared) }) { Text("Apply") } },
confirmButton = { TextButton(onClick = { onWorkedData(shared) }) { Text(strings.dialogApply) } },
dismissButton = { TextButton(onClick = onDismiss) { Text(strings.dialogCancel) } },
)
}
@@ -6,6 +6,7 @@ import androidx.compose.runtime.setValue
import kst4contest.controller.OperatorProfileManagementService
import kst4contest.model.OperatorProfile
import kst4contest.model.OperatorProfileSelection
import kst4contest.view.i18n.CurrentStrings
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -57,7 +58,7 @@ class ProfilesTabState(
}
val activeProfileName: String
get() = activeSelection()?.profile?.displayName ?: "unknown"
get() = activeSelection()?.profile?.displayName ?: CurrentStrings.get().profilesUnknown
val preferencesPath: String
get() = activeSelection()?.preferencesAbsolutePath ?: ""
@@ -90,9 +91,7 @@ class ProfilesTabState(
}
val created = service.createProfile(name.trim(), sharedWorkedDatabase)
?: return ProfileNotice.Error(
"The profile could not be created. The profile registry could not be written."
)
?: return ProfileNotice.Error(CurrentStrings.get().profilesCreateFailed)
refresh()
selected = created
@@ -101,10 +100,7 @@ class ProfilesTabState(
* Said explicitly because a new profile that cannot log in looks broken
* otherwise: callsign and password are deliberately not copied or invented.
*/
return ProfileNotice.Info(
"The profile \"" + created.displayName + "\" was created without callsign and " +
"password. Enter them on the Station tab after switching to it."
)
return ProfileNotice.Info(CurrentStrings.get().profilesCreatedWithoutCredentials(created.displayName))
}
fun duplicateSelected(name: String): ProfileNotice? {
@@ -115,7 +111,7 @@ class ProfilesTabState(
}
val duplicate = service.duplicateProfile(profile, name)
?: return ProfileNotice.Error("The profile could not be duplicated.")
?: return ProfileNotice.Error(CurrentStrings.get().profilesDuplicateFailed)
refresh()
selected = duplicate
@@ -124,7 +120,7 @@ class ProfilesTabState(
/** The suggested name of a duplicate, the one the JavaFX dialog pre-filled. */
fun duplicateNameSuggestion(): String =
selected?.let { "Copy of " + it.displayName } ?: ""
selected?.let { CurrentStrings.get().profilesCopyOf(it.displayName) } ?: ""
fun renameSelected(name: String): ProfileNotice? {
val profile = selected ?: return notASelection()
@@ -142,20 +138,16 @@ class ProfilesTabState(
val profile = selected ?: return notASelection()
if (profile.isRootProfile) {
return ProfileNotice.Error(
"The default profile uses the files of the installation itself and cannot be deleted."
)
return ProfileNotice.Error(CurrentStrings.get().profilesRootNotDeletable)
}
if (isActive(profile)) {
return ProfileNotice.Error(
"The profile currently in use cannot be deleted. Switch to another profile first."
)
return ProfileNotice.Error(CurrentStrings.get().profilesActiveNotDeletable)
}
if (!service.deleteProfile(profile)) {
refresh()
return ProfileNotice.Error("The profile could not be deleted.")
return ProfileNotice.Error(CurrentStrings.get().profilesDeleteFailed)
}
refresh()
@@ -164,22 +156,18 @@ class ProfilesTabState(
/** The folder the deletion confirmation names, and what happens to worked stations. */
fun describeDeletion(profile: OperatorProfile): String =
"The following folder is removed permanently:\n" +
service.getProfileDirectory(profile) + "\n\n" +
CurrentStrings.get().profilesDeletionFolder(service.getProfileDirectory(profile).toString()) + "\n\n" +
if (profile.isSharedWorkedDatabase) {
"The common station worked stations are not touched."
CurrentStrings.get().profilesDeletionKeepsShared
} else {
"The worked stations of this profile are deleted as well."
CurrentStrings.get().profilesDeletionRemovesOwn
}
fun changeWorkedDataModeOfSelected(sharedWorkedDatabase: Boolean): ProfileNotice? {
val profile = selected ?: return notASelection()
if (profile.isRootProfile) {
return ProfileNotice.Error(
"The default profile always uses the common station worked stations, because " +
"that database is the one of the installation itself."
)
return ProfileNotice.Error(CurrentStrings.get().profilesRootAlwaysShared)
}
if (sharedWorkedDatabase == profile.isSharedWorkedDatabase) {
@@ -190,7 +178,7 @@ class ProfilesTabState(
refresh()
if (isActive(profile)) {
return ProfileNotice.Info("The change takes effect after switching to this profile again.")
return ProfileNotice.Info(CurrentStrings.get().profilesChangeAfterSwitch)
}
return null
@@ -200,7 +188,7 @@ class ProfilesTabState(
val profile = selected ?: return notASelection()
if (isActive(profile)) {
return ProfileNotice.Info("This profile is already active.")
return ProfileNotice.Info(CurrentStrings.get().profilesAlreadyActive)
}
requestActivation(profile)
@@ -217,7 +205,7 @@ class ProfilesTabState(
}
private fun notASelection(): ProfileNotice =
ProfileNotice.Info("Select a profile in the table first.")
ProfileNotice.Info(CurrentStrings.get().profilesSelectFirst)
companion object {
@@ -230,9 +218,9 @@ class ProfilesTabState(
*/
fun describeWorkedDataMode(profile: OperatorProfile): String =
if (profile.isRootProfile || profile.isSharedWorkedDatabase) {
"common station database"
CurrentStrings.get().profilesSharedDatabase
} else {
"own database"
CurrentStrings.get().profilesOwnDatabase
}
/** Blank for a profile never used, rather than an epoch date. */