Skip to content

Commit bf4d163

Browse files
authored
Refactor Locale class and add LocaleProvider test (#1283)
* Refactor Locale class and add LocaleProvider test * Make setLocale parameter nullable in Locale class Changed the setLocale parameter in the Locale class to be nullable and updated its usage to safely invoke it. This allows for more flexible instantiation when a setLocale function is not required. * Add compose ui test to the deps * Update locale change method in test Replaces the call to locale.setLocale with locale.set in LocaleKtTest to match the updated API for changing the locale.
1 parent d3681f3 commit bf4d163

File tree

3 files changed

+162
-22
lines changed

3 files changed

+162
-22
lines changed

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import org.gradle.internal.jvm.Jvm
22
import org.gradle.internal.os.OperatingSystem
33
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
4+
import org.jetbrains.compose.ExperimentalComposeLibrary
45
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
56
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
67
import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download
@@ -119,6 +120,8 @@ dependencies {
119120
implementation(libs.markdown)
120121
implementation(libs.markdownJVM)
121122

123+
@OptIn(ExperimentalComposeLibrary::class)
124+
testImplementation(compose.uiTest)
122125
testImplementation(kotlin("test"))
123126
testImplementation(libs.mockitoKotlin)
124127
testImplementation(libs.junitJupiter)
Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
package processing.app.ui.theme
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.CompositionLocalProvider
5-
import androidx.compose.runtime.compositionLocalOf
6-
import processing.app.LocalPreferences
7-
import processing.app.Messages
8-
import processing.app.Platform
9-
import processing.app.PlatformStart
10-
import processing.app.watchFile
3+
import androidx.compose.runtime.*
4+
import androidx.compose.ui.platform.LocalLayoutDirection
5+
import androidx.compose.ui.unit.LayoutDirection
6+
import processing.app.*
117
import java.io.File
128
import java.io.InputStream
139
import java.util.*
1410

15-
class Locale(language: String = "") : Properties() {
11+
/**
12+
* The Locale class extends the standard Java Properties class
13+
* to provide localization capabilities.
14+
* It loads localization resources from property files based on the specified language code.
15+
* The class also provides a method to change the current locale and update the application accordingly.
16+
* Usage:
17+
* ```
18+
* val locale = Locale("es") { newLocale ->
19+
* // Handle locale change, e.g., update UI or restart application
20+
* }
21+
* val localizedString = locale["someKey"]
22+
* ```
23+
*/
24+
class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() {
25+
var locale: java.util.Locale = java.util.Locale.getDefault()
26+
1627
init {
17-
val locale = java.util.Locale.getDefault()
18-
load(ClassLoader.getSystemResourceAsStream("PDE.properties"))
19-
load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream())
20-
load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream())
21-
load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream())
28+
loadResourceUTF8("PDE.properties")
29+
loadResourceUTF8("PDE_${locale.language}.properties")
30+
loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties")
31+
loadResourceUTF8("PDE_${language}.properties")
32+
}
33+
34+
fun loadResourceUTF8(path: String) {
35+
val stream = ClassLoader.getSystemResourceAsStream(path)
36+
stream?.reader(charset = Charsets.UTF_8)?.use { reader ->
37+
load(reader)
38+
}
2239
}
2340

2441
@Deprecated("Use get instead", ReplaceWith("get(key)"))
@@ -28,18 +45,86 @@ class Locale(language: String = "") : Properties() {
2845
return value
2946
}
3047
operator fun get(key: String): String = getProperty(key, key)
48+
fun set(locale: java.util.Locale) {
49+
setLocale?.invoke(locale)
50+
}
3151
}
32-
val LocalLocale = compositionLocalOf { Locale() }
52+
/**
53+
* A CompositionLocal to provide access to the Locale instance
54+
* throughout the composable hierarchy. see [LocaleProvider]
55+
* Usage:
56+
* ```
57+
* val locale = LocalLocale.current
58+
* val localizedString = locale["someKey"]
59+
* ```
60+
*/
61+
val LocalLocale = compositionLocalOf<Locale> { error("No Locale Set") }
62+
63+
/**
64+
* This composable function sets up a locale provider that manages application localization.
65+
* It initializes the locale from a language file, watches for changes to that file, and updates
66+
* the locale accordingly. It uses a [Locale] class to handle loading of localized resources.
67+
*
68+
* Usage:
69+
* ```
70+
* LocaleProvider {
71+
* // Your app content here
72+
* }
73+
* ```
74+
*
75+
* To access the locale:
76+
* ```
77+
* val locale = LocalLocale.current
78+
* val localizedString = locale["someKey"]
79+
* ```
80+
*
81+
* To change the locale:
82+
* ```
83+
* locale.set(java.util.Locale("es"))
84+
* ```
85+
* This will update the `language.txt` file and reload the locale.
86+
*/
3387
@Composable
3488
fun LocaleProvider(content: @Composable () -> Unit) {
35-
PlatformStart()
89+
val preferencesFolderOverride: File? = System.getProperty("processing.app.preferences.folder")?.let { File(it) }
90+
91+
val settingsFolder = preferencesFolderOverride ?: remember{
92+
Platform.init()
93+
Platform.getSettingsFolder()
94+
}
95+
val languageFile = settingsFolder.resolve("language.txt")
96+
remember(languageFile){
97+
if(languageFile.exists()) return@remember
3698

37-
val settingsFolder = Platform.getSettingsFolder()
38-
val languageFile = File(settingsFolder, "language.txt")
39-
watchFile(languageFile)
99+
Messages.log("Creating language file at ${languageFile.absolutePath}")
100+
settingsFolder.mkdirs()
101+
languageFile.writeText(java.util.Locale.getDefault().language)
102+
}
103+
104+
val update = watchFile(languageFile)
105+
var code by remember(languageFile, update){ mutableStateOf(languageFile.readText().substring(0, 2)) }
106+
remember(code) {
107+
val locale = java.util.Locale(code)
108+
java.util.Locale.setDefault(locale)
109+
}
110+
111+
fun setLocale(locale: java.util.Locale) {
112+
Messages.log("Setting locale to ${locale.language}")
113+
languageFile.writeText(locale.language)
114+
code = locale.language
115+
}
116+
117+
118+
val locale = Locale(code, ::setLocale)
119+
remember(code) { Messages.log("Loaded Locale: $code") }
120+
val dir = when(locale["locale.direction"]) {
121+
"rtl" -> LayoutDirection.Rtl
122+
else -> LayoutDirection.Ltr
123+
}
40124

41-
val locale = Locale(languageFile.readText().substring(0, 2))
42-
CompositionLocalProvider(LocalLocale provides locale) {
43-
content()
125+
CompositionLocalProvider(LocalLayoutDirection provides dir) {
126+
CompositionLocalProvider(LocalLocale provides locale) {
127+
content()
128+
}
44129
}
45130
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package processing.app
2+
3+
import androidx.compose.material.Button
4+
import androidx.compose.material.Text
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.platform.testTag
7+
import androidx.compose.ui.test.ExperimentalTestApi
8+
import androidx.compose.ui.test.assertTextEquals
9+
import androidx.compose.ui.test.onNodeWithTag
10+
import androidx.compose.ui.test.performClick
11+
import androidx.compose.ui.test.runComposeUiTest
12+
import processing.app.ui.theme.LocalLocale
13+
import processing.app.ui.theme.LocaleProvider
14+
import kotlin.io.path.createTempDirectory
15+
import kotlin.test.Test
16+
17+
class LocaleKtTest {
18+
@OptIn(ExperimentalTestApi::class)
19+
@Test
20+
fun testLocale() = runComposeUiTest {
21+
val tempPreferencesDir = createTempDirectory("preferences")
22+
23+
System.setProperty("processing.app.preferences.folder", tempPreferencesDir.toFile().absolutePath)
24+
25+
setContent {
26+
LocaleProvider {
27+
val locale = LocalLocale.current
28+
Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText"))
29+
30+
Button(onClick = {
31+
locale.set(java.util.Locale("es"))
32+
}, modifier = Modifier.testTag("button")) {
33+
Text("Change")
34+
}
35+
}
36+
}
37+
38+
// Check if usage generates the language file if it doesn't exist
39+
val languageFile = tempPreferencesDir.resolve("language.txt").toFile()
40+
assert(languageFile.exists())
41+
42+
// Check if the text is localised
43+
onNodeWithTag("localisedText").assertTextEquals("New")
44+
45+
// Change the locale to Spanish
46+
onNodeWithTag("button").performClick()
47+
onNodeWithTag("localisedText").assertTextEquals("Nuevo")
48+
49+
// Check if the preference was saved to file
50+
assert(languageFile.readText().substring(0, 2) == "es")
51+
}
52+
}

0 commit comments

Comments
 (0)