Skip to content

Commit 4546a38

Browse files
committed
Refactor Locale class and add LocaleProvider test
1 parent 845cb0c commit 4546a38

File tree

2 files changed

+159
-22
lines changed

2 files changed

+159
-22
lines changed
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) : 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(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 = 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.setLocale(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)