Skip to content

Commit 407fc2e

Browse files
committed
[UI] Add full localization support with English and Russian translations
Added localization system: - Compose Multiplatform resource-based localization with English and Russian - Language enum and selector in Settings dialog - Platform-specific locale switching (Desktop, Android, iOS, WasmJS) - LocalizedModeConfig component for localized enum dropdowns - All UI dialogs converted to use stringResource() instead of hardcoded strings Technical implementation: - Desktop/Android: java.util.Locale.setDefault() - iOS: NSUserDefaults AppleLanguages override - WasmJS: navigator.language override with page reload - App.kt uses key() for recomposition on language change - Dialog states managed outside key() scope to prevent closing on language change - UiSettings extended with language preference field New resource files: - composeResources/values/strings.xml (English) - composeResources/values-ru/strings.xml (Russian)
1 parent 7756b94 commit 407fc2e

File tree

17 files changed

+467
-49
lines changed

17 files changed

+467
-49
lines changed

composeApp/src/androidMain/kotlin/org/dots/game/PlatformUtils.android.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ import androidx.compose.ui.Modifier
66
import com.russhwolf.settings.Settings
77
import java.io.File
88
import java.net.URI
9+
import java.util.Locale
910

1011
actual var appSettings: Settings? = null
1112

13+
actual fun applyLocale(language: Language) {
14+
@Suppress("DEPRECATION")
15+
val locale = when (language) {
16+
Language.English -> Locale.ENGLISH
17+
Language.Russian -> Locale("ru")
18+
}
19+
Locale.setDefault(locale)
20+
}
21+
1222
@Composable
1323
actual fun VerticalScrollbar(
1424
scrollState: ScrollState,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<resources>
2+
<!-- Main menu items -->
3+
<string name="menu_new">Новая</string>
4+
<string name="menu_reset">Сбросить</string>
5+
<string name="menu_load">Загрузить</string>
6+
<string name="menu_save">Сохранить</string>
7+
<string name="menu_settings">Настройки</string>
8+
9+
<!-- New Game Dialog -->
10+
<string name="new_game_width">Ширина</string>
11+
<string name="new_game_height">Высота</string>
12+
<string name="new_game_init_pos_type">Стартовая позиция</string>
13+
<string name="new_game_init_pos_empty">Нет</string>
14+
<string name="new_game_init_pos_single">Точка</string>
15+
<string name="new_game_init_pos_cross">Скрест</string>
16+
<string name="new_game_init_pos_double_cross">Двойной скрест</string>
17+
<string name="new_game_init_pos_quadruple_cross">4X</string>
18+
<string name="new_game_base_mode">Режим баз</string>
19+
<string name="new_game_base_mode_at_least_one">Захват при одной точке</string>
20+
<string name="new_game_base_mode_any_surrounding">Захват пустой области</string>
21+
<string name="new_game_base_mode_all_opponent">Захват при полном заполнении</string>
22+
<string name="new_game_capture_by_border">Захват через край</string>
23+
<string name="new_game_suicide_allowed">Самоубийство разрешено</string>
24+
<string name="new_game_round_draw">Возможность ничьи</string>
25+
<string name="new_game_komi">Коми</string>
26+
<string name="new_game_random_start_position">Случайная начальная позиция</string>
27+
<string name="new_game_create">Создать новую игру</string>
28+
29+
<!-- Load Dialog -->
30+
<string name="load_path_or_content">Путь или содержимое</string>
31+
<string name="load_path_or_content_placeholder">Введите путь к файлу .sgf(s) или его содержимое</string>
32+
<string name="load_browse">Обзор</string>
33+
<string name="load_rewind_to_end">Просмотр с конца</string>
34+
<string name="load_add_finishing_move">Добавить завершающий ход</string>
35+
<string name="load_open">Открыть</string>
36+
<string name="load_open_sgf_file">Открыть файл SGF</string>
37+
38+
<!-- Save Dialog -->
39+
<string name="save_sgf">SGF</string>
40+
<string name="save_field_representation">Отображение поля</string>
41+
<string name="save_print_numbers">Печатать номера</string>
42+
<string name="save_padding">Отступ</string>
43+
<string name="save_print_coordinates">Печатать координаты</string>
44+
<string name="save_debug_info">Отладочная информация</string>
45+
46+
<!-- Settings Dialog -->
47+
<string name="settings_connection_draw_mode">Отрисовка соединений</string>
48+
<string name="settings_connection_draw_none">Нет</string>
49+
<string name="settings_connection_draw_lines">Линии</string>
50+
<string name="settings_connection_draw_polygon_outline">Контуры полигонов</string>
51+
<string name="settings_connection_draw_polygon_fill">Заливка полигонов</string>
52+
<string name="settings_connection_draw_polygon_outline_and_fill">Контуры и заливка полигонов</string>
53+
54+
<string name="settings_polygon_draw_mode">Отрисовка окружений</string>
55+
<string name="settings_polygon_draw_outline">Контур</string>
56+
<string name="settings_polygon_draw_fill">Заливка</string>
57+
<string name="settings_polygon_draw_outline_and_fill">Контур и заливка</string>
58+
59+
<string name="settings_diagonal_connections">Диагональные соединения</string>
60+
<string name="settings_threats">Угрозы окружения</string>
61+
<string name="settings_surroundings">Области под угрозой</string>
62+
<string name="settings_developer_mode">Режим разработчика</string>
63+
64+
<!-- Language Settings -->
65+
<string name="settings_language">Язык</string>
66+
<string name="language_system">Системный</string>
67+
<string name="language_english">Английский</string>
68+
<string name="language_russian">Русский</string>
69+
</resources>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<resources>
2+
<!-- Main menu items -->
3+
<string name="menu_new">New</string>
4+
<string name="menu_reset">Reset</string>
5+
<string name="menu_load">Load</string>
6+
<string name="menu_save">Save</string>
7+
<string name="menu_settings">Settings</string>
8+
9+
<!-- New Game Dialog -->
10+
<string name="new_game_width">Width</string>
11+
<string name="new_game_height">Height</string>
12+
<string name="new_game_init_pos_type">Init Pos Type</string>
13+
<string name="new_game_init_pos_empty">Empty</string>
14+
<string name="new_game_init_pos_single">Single</string>
15+
<string name="new_game_init_pos_cross">Cross</string>
16+
<string name="new_game_init_pos_double_cross">Double Cross</string>
17+
<string name="new_game_init_pos_quadruple_cross">Quadruple Cross</string>
18+
<string name="new_game_base_mode">Base Mode</string>
19+
<string name="new_game_base_mode_at_least_one">At Least One Opponent Dot</string>
20+
<string name="new_game_base_mode_any_surrounding">Any Surrounding</string>
21+
<string name="new_game_base_mode_all_opponent">All Opponent</string>
22+
<string name="new_game_capture_by_border">Capture by border</string>
23+
<string name="new_game_suicide_allowed">Suicide allowed</string>
24+
<string name="new_game_round_draw">Round Draw</string>
25+
<string name="new_game_komi">Komi</string>
26+
<string name="new_game_random_start_position">Random start position</string>
27+
<string name="new_game_create">Create new game</string>
28+
29+
<!-- Load Dialog -->
30+
<string name="load_path_or_content">Path or Content</string>
31+
<string name="load_path_or_content_placeholder">Enter path to .sgf(s) file or its content</string>
32+
<string name="load_browse">Browse</string>
33+
<string name="load_rewind_to_end">Rewind to End</string>
34+
<string name="load_add_finishing_move">Add Finishing Move</string>
35+
<string name="load_open">Open</string>
36+
<string name="load_open_sgf_file">Open SGF File</string>
37+
38+
<!-- Save Dialog -->
39+
<string name="save_sgf">SGF</string>
40+
<string name="save_field_representation">Field Representation</string>
41+
<string name="save_print_numbers">Print numbers</string>
42+
<string name="save_padding">Padding</string>
43+
<string name="save_print_coordinates">Print coordinates</string>
44+
<string name="save_debug_info">Debug info</string>
45+
46+
<!-- Settings Dialog -->
47+
<string name="settings_connection_draw_mode">Connection Draw Mode</string>
48+
<string name="settings_connection_draw_none">None</string>
49+
<string name="settings_connection_draw_lines">Lines</string>
50+
<string name="settings_connection_draw_polygon_outline">Polygon Outline</string>
51+
<string name="settings_connection_draw_polygon_fill">Polygon Fill</string>
52+
<string name="settings_connection_draw_polygon_outline_and_fill">Polygon Outline And Fill</string>
53+
54+
<string name="settings_polygon_draw_mode">Polygon Draw Mode</string>
55+
<string name="settings_polygon_draw_outline">Outline</string>
56+
<string name="settings_polygon_draw_fill">Fill</string>
57+
<string name="settings_polygon_draw_outline_and_fill">Outline And Fill</string>
58+
59+
<string name="settings_diagonal_connections">Diagonal Connections</string>
60+
<string name="settings_threats">Threats</string>
61+
<string name="settings_surroundings">Surroundings</string>
62+
<string name="settings_developer_mode">Developer Mode</string>
63+
64+
<!-- Language Settings -->
65+
<string name="settings_language">Language</string>
66+
<string name="language_system">System</string>
67+
<string name="language_english">English</string>
68+
<string name="language_russian">Russian</string>
69+
</resources>

composeApp/src/commonMain/kotlin/org/dots/game/App.kt

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import androidx.compose.material.ButtonDefaults
99
import androidx.compose.material.MaterialTheme
1010
import androidx.compose.material.Text
1111
import androidx.compose.runtime.*
12+
import androidx.compose.runtime.LaunchedEffect
13+
import androidx.compose.runtime.key
1214
import androidx.compose.ui.Alignment
1315
import androidx.compose.ui.Modifier
1416
import androidx.compose.ui.draw.clip
@@ -24,13 +26,63 @@ import androidx.compose.ui.unit.dp
2426
import kotlinx.coroutines.launch
2527
import org.dots.game.core.*
2628
import org.dots.game.views.*
29+
import org.jetbrains.compose.resources.stringResource
30+
import dotsgame.composeapp.generated.resources.Res
31+
import dotsgame.composeapp.generated.resources.menu_new
32+
import dotsgame.composeapp.generated.resources.menu_reset
33+
import dotsgame.composeapp.generated.resources.menu_load
34+
import dotsgame.composeapp.generated.resources.menu_save
35+
import dotsgame.composeapp.generated.resources.menu_settings
2736
import org.jetbrains.compose.ui.tooling.preview.Preview
2837

2938
@Composable
3039
@Preview
3140
fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), onGamesChange: (games: Games?) -> Unit = { }) {
3241
MaterialTheme {
3342
var uiSettings by remember { mutableStateOf(loadUiSettings()) }
43+
var showNewGameDialog by remember { mutableStateOf(false) }
44+
var openGameDialog by remember { mutableStateOf(false) }
45+
var showSaveGameDialog by remember { mutableStateOf(false) }
46+
var showUiSettingsForm by remember { mutableStateOf(false) }
47+
48+
key(uiSettings.language) {
49+
AppContent(
50+
uiSettings,
51+
{ uiSettings = it },
52+
currentGameSettings,
53+
onGamesChange,
54+
showNewGameDialog,
55+
{ showNewGameDialog = it },
56+
openGameDialog,
57+
{ openGameDialog = it },
58+
showSaveGameDialog,
59+
{ showSaveGameDialog = it },
60+
showUiSettingsForm,
61+
{ showUiSettingsForm = it }
62+
)
63+
}
64+
}
65+
}
66+
67+
@Composable
68+
private fun AppContent(
69+
initialUiSettings: UiSettings,
70+
onUiSettingsChange: (UiSettings) -> Unit,
71+
currentGameSettings: CurrentGameSettings,
72+
onGamesChange: (games: Games?) -> Unit,
73+
showNewGameDialog: Boolean,
74+
onShowNewGameDialog: (Boolean) -> Unit,
75+
openGameDialog: Boolean,
76+
onOpenGameDialog: (Boolean) -> Unit,
77+
showSaveGameDialog: Boolean,
78+
onShowSaveGameDialog: (Boolean) -> Unit,
79+
showUiSettingsForm: Boolean,
80+
onShowUiSettingsForm: (Boolean) -> Unit
81+
) {
82+
var uiSettings by remember { mutableStateOf(initialUiSettings) }
83+
LaunchedEffect(initialUiSettings) {
84+
uiSettings = initialUiSettings
85+
}
3486
var newGameDialogRules by remember { mutableStateOf(loadRules()) }
3587
var openGameSettings by remember { mutableStateOf(loadOpenGameSettings()) }
3688

@@ -49,11 +101,7 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
49101
var player1Score by remember { mutableStateOf(0) }
50102
var player2Score by remember { mutableStateOf(0) }
51103
var moveNumber by remember { mutableStateOf(0) }
52-
var showNewGameDialog by remember { mutableStateOf(false) }
53-
var openGameDialog by remember { mutableStateOf(false) }
54104
var dumpParameters by remember { mutableStateOf(loadDumpParameters())}
55-
var showSaveGameDialog by remember { mutableStateOf(false) }
56-
var showUiSettingsForm by remember { mutableStateOf(false) }
57105
var moveMode by remember { mutableStateOf(MoveMode.Next) }
58106

59107
val focusRequester = remember { FocusRequester() }
@@ -104,11 +152,11 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
104152
newGameDialogRules,
105153
uiSettings,
106154
onDismiss = {
107-
showNewGameDialog = false
155+
onShowNewGameDialog(false)
108156
focusRequester.requestFocus()
109157
},
110158
) {
111-
showNewGameDialog = false
159+
onShowNewGameDialog(false)
112160
newGameDialogRules = it
113161
saveRules(newGameDialogRules)
114162
reset(newGame = true)
@@ -146,11 +194,11 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
146194
newGameDialogRules,
147195
openGameSettings,
148196
onDismiss = {
149-
openGameDialog = false
197+
onOpenGameDialog(false)
150198
focusRequester.requestFocus()
151199
},
152200
onConfirmation = { newGames, newOpenGameSettings, path, content ->
153-
openGameDialog = false
201+
onOpenGameDialog(false)
154202
openGameSettings = newOpenGameSettings
155203
saveOpenGameSettings(openGameSettings)
156204
currentGameSettings.path = path
@@ -170,7 +218,7 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
170218
getField(),
171219
dumpParameters,
172220
onDismiss = {
173-
showSaveGameDialog = false
221+
onShowSaveGameDialog(false)
174222
focusRequester.requestFocus()
175223
dumpParameters = it
176224
saveDumpParameters(it)
@@ -181,8 +229,9 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
181229
UiSettingsForm(uiSettings, onUiSettingsChange = {
182230
uiSettings = it
183231
saveUiSettings(it)
232+
onUiSettingsChange(it)
184233
}, onDismiss = {
185-
showUiSettingsForm = false
234+
onShowUiSettingsForm(false)
186235
focusRequester.requestFocus()
187236
})
188237
}
@@ -258,20 +307,20 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
258307
val selectedModeButtonColor = Color.Magenta
259308

260309
Row(rowModifier) {
261-
Button(onClick = { showNewGameDialog = true }, controlButtonModifier) {
262-
Text("New")
310+
Button(onClick = { onShowNewGameDialog(true) }, controlButtonModifier) {
311+
Text(stringResource(Res.string.menu_new))
263312
}
264313
Button(onClick = { reset(newGame = false) }, controlButtonModifier) {
265-
Text("Reset")
314+
Text(stringResource(Res.string.menu_reset))
266315
}
267-
Button(onClick = { openGameDialog = true }, controlButtonModifier) {
268-
Text("Load")
316+
Button(onClick = { onOpenGameDialog(true) }, controlButtonModifier) {
317+
Text(stringResource(Res.string.menu_load))
269318
}
270-
Button(onClick = { showSaveGameDialog = true }, controlButtonModifier) {
271-
Text("Save")
319+
Button(onClick = { onShowSaveGameDialog(true) }, controlButtonModifier) {
320+
Text(stringResource(Res.string.menu_save))
272321
}
273-
Button(onClick = { showUiSettingsForm = true }, controlButtonModifier) {
274-
Text("Settings")
322+
Button(onClick = { onShowUiSettingsForm(true) }, controlButtonModifier) {
323+
Text(stringResource(Res.string.menu_settings))
275324
}
276325
}
277326

@@ -371,4 +420,3 @@ fun App(currentGameSettings: CurrentGameSettings = loadCurrentGameSettings(), on
371420
}
372421
}
373422
}
374-
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.dots.game
2+
3+
enum class Language {
4+
English,
5+
Russian
6+
}

composeApp/src/commonMain/kotlin/org/dots/game/Settings.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import org.dots.game.sgf.SgfWriter
77
import org.dots.game.views.OpenGameSettings
88
import kotlin.random.Random
99

10+
// Platform-specific function to apply locale
11+
expect fun applyLocale(language: Language)
12+
1013
fun loadRules(): Rules {
1114
val settings = appSettings ?: return Rules.Standard
1215
val ruleClass = Rules::class // TODO: inline after KT-80853
@@ -78,6 +81,7 @@ fun loadUiSettings(): UiSettings {
7881
showThreats = getSetting(UiSettings::showThreats),
7982
showSurroundings = getSetting(UiSettings::showSurroundings),
8083
developerMode = getSetting(UiSettings::developerMode),
84+
language = getEnumSetting(UiSettings::language),
8185
)
8286
}
8387
}
@@ -94,6 +98,7 @@ fun saveUiSettings(uiSettings: UiSettings) {
9498
setSetting(UiSettings::showThreats)
9599
setSetting(UiSettings::showSurroundings)
96100
setSetting(UiSettings::developerMode)
101+
setSetting(UiSettings::language)
97102
}
98103
}
99104

composeApp/src/commonMain/kotlin/org/dots/game/UiSettings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ data class UiSettings(
1717
val showThreats: Boolean = false,
1818
val showSurroundings: Boolean = false,
1919
val developerMode: Boolean = false,
20+
val language: Language = Language.English,
2021
) {
2122
companion object {
2223
val Standard = UiSettings()

0 commit comments

Comments
 (0)