diff --git a/app-desktop/src/main/i18n/strings_de.properties b/app-desktop/src/main/i18n/strings_de.properties index c1d72074..e743e7b2 100644 --- a/app-desktop/src/main/i18n/strings_de.properties +++ b/app-desktop/src/main/i18n/strings_de.properties @@ -362,7 +362,7 @@ tooltip.unknownGrid=unbekanntes Feld tooltip.grid=Feld {0} messages.public=Öffentliche Nachrichten messages.dxCluster=DXCluster-Nachrichten -messages.qsoOfTheOther=QSO der Gegenstation +messages.qsoOfTheOther=Fremd-QSOs chat.ownQrgTooltip=Die eigene Frequenz in der Hauptkategorie chat.secondQrgTooltip=Die Frequenz für die zweite Chat-Kategorie von Hand eintragen chat.qtfTooltip=Die eigene Antennenrichtung (QTF); wird vom Rotor übernommen, wenn einer abgeglichen wird @@ -379,3 +379,49 @@ contrast.textOnWindowSurface=Text auf der Fensterfläche contrast.textInsideField=Text im Feld contrast.accentOnWindowSurface=Akzent auf der Fensterfläche contrast.textOnMenuStrip=Text auf der Menüzeile +gui.startInEveningDesign=Im Abenddesign starten +gui.filterPublic=Öffentliche Nachrichten +gui.filterPmToMe=Privatnachrichten an mich +gui.filterPmToOthers=Privatnachrichten an andere +gui.filterNothing=Kein Filter +gui.hintFreshCall=Ein zum ersten Mal gesehenes Rufzeichen markieren +gui.hintLargeGridWorked=Ein gearbeitetes Großfeld markieren +form.choose=Wählen... +form.notSelected=nicht gewählt +dialog.apply=Übernehmen +chat.clear=leeren +selected.showOnMap=🗺 Auf der Karte zeigen +selected.notQrvAll=alle +column.category=Kategorie +column.sender=Absender +column.receiver=Empfänger +column.lastQrg=Letzte QRG +column.airplane=AP [Minuten / Pot%] +column.score=Wertung +monitor.title=Cluster & Fremd-QSOs +profiles.createFailed=Das Profil konnte nicht angelegt werden. Die Profilliste konnte nicht geschrieben werden. +profiles.createdWithoutCredentials=Das Profil „{0}" wurde ohne Rufzeichen und Passwort angelegt. Nach dem Wechsel zu diesem Profil beides im Reiter „Station" eintragen. +profiles.duplicateFailed=Das Profil konnte nicht dupliziert werden. +profiles.rootNotDeletable=Das Standardprofil verwendet die Dateien der Installation selbst und kann nicht gelöscht werden. +profiles.activeNotDeletable=Das gerade benutzte Profil kann nicht gelöscht werden. Zuerst zu einem anderen Profil wechseln. +profiles.deleteFailed=Das Profil konnte nicht gelöscht werden. +profiles.deletionFolder=Der folgende Ordner wird endgültig entfernt:\n{0} +profiles.deletionKeepsShared=Die gemeinsamen gearbeiteten Stationen der Station werden nicht angefasst. +profiles.deletionRemovesOwn=Die gearbeiteten Stationen dieses Profils werden ebenfalls gelöscht. +profiles.rootAlwaysShared=Das Standardprofil verwendet immer die gemeinsamen gearbeiteten Stationen der Station, weil diese Datenbank die der Installation selbst ist. +profiles.changeAfterSwitch=Die Änderung wirkt erst nach einem erneuten Wechsel zu diesem Profil. +profiles.alreadyActive=Dieses Profil ist bereits aktiv. +profiles.selectFirst=Zuerst ein Profil in der Tabelle wählen. +beacon.invalidMessage=Der Bakentext ist ungültig: {0} +beacon.intervalTooShort=Einen ganzzahligen Bakenabstand von mindestens {0} Minute eingeben. +notification.invalidPort=„{0}" ist kein gültiger TCP-Port. Einen Wert von 1 bis 65535 eingeben. +notification.invalidSpotter=„{0}" ist kein gültiges Spotter-Rufzeichen. +notification.invalidCallsign=Bitte ein gültiges Rufzeichen eingeben. +notification.alreadyMonitored=Dieses Grundrufzeichen steht bereits in der Überwachungsliste. +settings.windowTitle=Client-Einstellungen ändern +settings.noticeInfo=Info +settings.noticeProblem=Einstellungen +profiles.unknown=unbekannt +profiles.copyOf=Kopie von {0} +profiles.sharedDatabase=gemeinsame Stationsdatenbank +profiles.ownDatabase=eigene Datenbank diff --git a/app-desktop/src/main/i18n/strings_en.properties b/app-desktop/src/main/i18n/strings_en.properties index d70d456b..37b2f09b 100644 --- a/app-desktop/src/main/i18n/strings_en.properties +++ b/app-desktop/src/main/i18n/strings_en.properties @@ -388,3 +388,49 @@ contrast.textOnWindowSurface=Text on the window surface contrast.textInsideField=Text inside a field contrast.accentOnWindowSurface=Accent on the window surface contrast.textOnMenuStrip=Text on the menu strip +gui.startInEveningDesign=Start in the evening design +gui.filterPublic=Public messages +gui.filterPmToMe=Private messages to me +gui.filterPmToOthers=Private messages to others +gui.filterNothing=No filter +gui.hintFreshCall=Mark a callsign seen for the first time +gui.hintLargeGridWorked=Mark a worked large grid field +form.choose=Choose... +form.notSelected=not selected +dialog.apply=Apply +chat.clear=clear +selected.showOnMap=🗺 Show on map +selected.notQrvAll=all +column.category=Category +column.sender=Sender +column.receiver=Receiver +column.lastQrg=Last QRG +column.airplane=AP [minutes / pot%] +column.score=Score +monitor.title=Cluster & QSO of the other +profiles.createFailed=The profile could not be created. The profile registry could not be written. +profiles.createdWithoutCredentials=The profile "{0}" was created without callsign and password. Enter them on the Station tab after switching to it. +profiles.duplicateFailed=The profile could not be duplicated. +profiles.rootNotDeletable=The default profile uses the files of the installation itself and cannot be deleted. +profiles.activeNotDeletable=The profile currently in use cannot be deleted. Switch to another profile first. +profiles.deleteFailed=The profile could not be deleted. +profiles.deletionFolder=The following folder is removed permanently:\n{0} +profiles.deletionKeepsShared=The common station worked stations are not touched. +profiles.deletionRemovesOwn=The worked stations of this profile are deleted as well. +profiles.rootAlwaysShared=The default profile always uses the common station worked stations, because that database is the one of the installation itself. +profiles.changeAfterSwitch=The change takes effect after switching to this profile again. +profiles.alreadyActive=This profile is already active. +profiles.selectFirst=Select a profile in the table first. +beacon.invalidMessage=The beacon message is invalid: {0} +beacon.intervalTooShort=Enter a whole beacon interval of at least {0} minute. +notification.invalidPort="{0}" is not a valid TCP port. Enter a value from 1 to 65535. +notification.invalidSpotter="{0}" is not a valid spotter callsign. +notification.invalidCallsign=Please enter a valid callsign. +notification.alreadyMonitored=This base callsign is already in the monitoring list. +settings.windowTitle=Change Client Settings +settings.noticeInfo=Info +settings.noticeProblem=Settings +profiles.unknown=unknown +profiles.copyOf=Copy of {0} +profiles.sharedDatabase=common station database +profiles.ownDatabase=own database diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputRow.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputRow.kt index 0dbea70f..981988c6 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputRow.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputRow.kt @@ -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() diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/ComposeAlert.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/ComposeAlert.kt index 6a75325b..f62d7ef3 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/ComposeAlert.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/ComposeAlert.kt @@ -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, ) diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/DirectedMessageColumns.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/DirectedMessageColumns.kt index 33e9cb8f..0821bc49 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/DirectedMessageColumns.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/DirectedMessageColumns.kt @@ -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) } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/FormControls.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/FormControls.kt index f68acbc9..3cdc6828 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/FormControls.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/FormControls.kt @@ -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, ) } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorColumns.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorColumns.kt index db53e363..66214e78 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorColumns.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorColumns.kt @@ -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) } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt index f3400b40..7c62b8dd 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/MonitorWindow.kt @@ -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), ) diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/PublicMessageColumns.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/PublicMessageColumns.kt index 67c375b8..a8e00905 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/PublicMessageColumns.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/PublicMessageColumns.kt @@ -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) } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationMessageColumns.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationMessageColumns.kt index 6b80f4f4..1ef2f491 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationMessageColumns.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationMessageColumns.kt @@ -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, ), diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationPanel.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationPanel.kt index aa7e91ca..aaa8644e 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationPanel.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/SelectedStationPanel.kt @@ -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, diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsTabs.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsTabs.kt index 46cc2733..0606b884 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsTabs.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsTabs.kt @@ -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, diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsWindow.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsWindow.kt index c9cf7e1d..d0046f18 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsWindow.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsWindow.kt @@ -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) } }, ) } } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/StationColumns.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/StationColumns.kt index 8268582a..511f7558 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/StationColumns.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/StationColumns.kt @@ -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() }, diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/BeaconTabState.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/BeaconTabState.kt index 905d8ccb..4b02894d 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/BeaconTabState.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/BeaconTabState.kt @@ -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) diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ExternalInterfaceFields.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ExternalInterfaceFields.kt index 263a8660..eb42433f 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ExternalInterfaceFields.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ExternalInterfaceFields.kt @@ -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 } } } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/GuiOptionsTab.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/GuiOptionsTab.kt index 357b3699..9ebafe54 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/GuiOptionsTab.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/GuiOptionsTab.kt @@ -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 } } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/NotificationTabState.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/NotificationTabState.kt index 9deb9fd4..e2876631 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/NotificationTabState.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/NotificationTabState.kt @@ -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) diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTab.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTab.kt index 8298ee5e..f753f00f 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTab.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTab.kt @@ -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) } }, ) } diff --git a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTabState.kt b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTabState.kt index 6b68052d..375a8c5d 100644 --- a/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTabState.kt +++ b/app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/ProfilesTabState.kt @@ -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. */ diff --git a/app-desktop/src/test/kotlin/kst4contest/view/i18n/RemainingTextsFollowTheLanguageTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/i18n/RemainingTextsFollowTheLanguageTest.kt new file mode 100644 index 00000000..0a88ec77 --- /dev/null +++ b/app-desktop/src/test/kotlin/kst4contest/view/i18n/RemainingTextsFollowTheLanguageTest.kt @@ -0,0 +1,121 @@ +package kst4contest.view.i18n + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.runComposeUiTest +import kst4contest.model.ChatPreferences +import kst4contest.view.compose.SettingsContent +import kst4contest.view.compose.SettingsNotices +import kst4contest.view.compose.SettingsTab +import kst4contest.view.compose.StationColumns +import kst4contest.view.compose.tabs.BeaconTabState +import kst4contest.view.compose.tabs.GuiOptionsTab +import kst4contest.view.compose.tabs.GuiOptionsTabState +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import java.util.Locale + +/** + * The texts that stayed English after stage 9 now follow the language as well. + * + * One sample from each of the three ways a text reaches the operator: a label read inside a + * composition, a column title built outside one, and a refusal produced by a tab state. All + * three have to switch, and each goes through a different door. + */ +@OptIn(ExperimentalTestApi::class) +class RemainingTextsFollowTheLanguageTest { + + /* Never the no-argument ChatPreferences: it would touch the operator's real profile. */ + @TempDir + lateinit var directory: Path + + @Test + fun theMovedTextsAppearInGermanAfterTheSwitch() = runComposeUiTest { + var stored = "en" + val store = LanguageStore({ stored }, { stored = it }, Locale.UK) + val prefs = ChatPreferences(directory.resolve("preferences.xml")) + val state = GuiOptionsTabState(prefs, store) + val beacon = BeaconTabState( + prefs, + validateBeaconTemplate = {}, + restartBeaconTimer = {}, + mainCategoryName = "Microwave", + secondCategoryName = "144/432 MHz", + ) + val score = StationColumns.all( + tropoOf = { "" }, + priorityScoreOf = { 0.0 }, + formatBand = { _, _, _ -> "" }, + ).single { it.id == "score" } + + setContent { + CompositionLocalProvider(LocalStrings provides store.strings) { + GuiOptionsTab(state) + } + } + waitForIdle() + + onNodeWithText("Start in the evening design").assertExists() + assertEquals("Score", score.title()) + + try { + runOnIdle { store.use("de") } + waitForIdle() + + onNodeWithText("Im Abenddesign starten").assertExists() + onNodeWithText("Kein Filter").assertExists() + onNodeWithText("Ein gearbeitetes Großfeld markieren").assertExists() + + assertEquals("Wertung", score.title(), "the column title stayed in the old language") + assertEquals( + "Einen ganzzahligen Bakenabstand von mindestens 1 Minute eingeben.", + beacon.commitBeaconInterval("0"), + "the refusal from the tab state stayed in the old language", + ) + } finally { + // use() also sets the JVM-wide CurrentStrings; leave the next test the base language. + runOnIdle { store.use("en") } + } + } + + /** + * The settings window's notice: its title is read in the composition, its message is + * produced outside one by the save action -- the same split as above, one window further. + */ + @Test + fun theSettingsNoticeFollowsTheLanguage() = runComposeUiTest { + var stored = "en" + val store = LanguageStore({ stored }, { stored = it }, Locale.UK) + val notices = SettingsNotices() + + setContent { + CompositionLocalProvider(LocalStrings provides store.strings) { + SettingsContent( + tabs = listOf(SettingsTab("Tab") {}), + notices = notices, + buttons = {}, + close = {}, + ) + } + } + + try { + runOnIdle { + store.use("de") + notices.problem(CurrentStrings.get().settingsWriteFailed) + } + waitForIdle() + + onNodeWithText("Einstellungen").assertExists() + onNodeWithText( + "Die Konfigurationsdatei konnte nicht geschrieben werden. " + + "Die Einstellungen bleiben für diese Sitzung aktiv.", + ).assertExists() + } finally { + runOnIdle { store.use("en") } + } + } +}