-
-
Couldn't load subscription status.
- Fork 102
Refactor Locale class and add LocaleProvider test #1283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,41 @@ | ||
| package processing.app.ui.theme | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.CompositionLocalProvider | ||
| import androidx.compose.runtime.compositionLocalOf | ||
| import processing.app.LocalPreferences | ||
| import processing.app.Messages | ||
| import processing.app.Platform | ||
| import processing.app.PlatformStart | ||
| import processing.app.watchFile | ||
| import androidx.compose.runtime.* | ||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||
| import androidx.compose.ui.unit.LayoutDirection | ||
| import processing.app.* | ||
| import java.io.File | ||
| import java.io.InputStream | ||
| import java.util.* | ||
|
|
||
| class Locale(language: String = "") : Properties() { | ||
| /** | ||
| * The Locale class extends the standard Java Properties class | ||
| * to provide localization capabilities. | ||
| * It loads localization resources from property files based on the specified language code. | ||
| * The class also provides a method to change the current locale and update the application accordingly. | ||
| * Usage: | ||
| * ``` | ||
| * val locale = Locale("es") { newLocale -> | ||
| * // Handle locale change, e.g., update UI or restart application | ||
| * } | ||
| * val localizedString = locale["someKey"] | ||
| * ``` | ||
| */ | ||
| class Locale(language: String = "", val setLocale: ((java.util.Locale) -> Unit)? = null) : Properties() { | ||
| var locale: java.util.Locale = java.util.Locale.getDefault() | ||
|
|
||
| init { | ||
| val locale = java.util.Locale.getDefault() | ||
| load(ClassLoader.getSystemResourceAsStream("PDE.properties")) | ||
| load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream()) | ||
| load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream()) | ||
| load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream()) | ||
| loadResourceUTF8("PDE.properties") | ||
| loadResourceUTF8("PDE_${locale.language}.properties") | ||
| loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties") | ||
| loadResourceUTF8("PDE_${language}.properties") | ||
| } | ||
|
|
||
| fun loadResourceUTF8(path: String) { | ||
| val stream = ClassLoader.getSystemResourceAsStream(path) | ||
| stream?.reader(charset = Charsets.UTF_8)?.use { reader -> | ||
| load(reader) | ||
| } | ||
| } | ||
|
|
||
| @Deprecated("Use get instead", ReplaceWith("get(key)")) | ||
|
|
@@ -28,18 +45,86 @@ class Locale(language: String = "") : Properties() { | |
| return value | ||
| } | ||
| operator fun get(key: String): String = getProperty(key, key) | ||
| fun set(locale: java.util.Locale) { | ||
| setLocale?.invoke(locale) | ||
| } | ||
| } | ||
| val LocalLocale = compositionLocalOf { Locale() } | ||
| /** | ||
| * A CompositionLocal to provide access to the Locale instance | ||
| * throughout the composable hierarchy. see [LocaleProvider] | ||
| * Usage: | ||
| * ``` | ||
| * val locale = LocalLocale.current | ||
| * val localizedString = locale["someKey"] | ||
| * ``` | ||
| */ | ||
| val LocalLocale = compositionLocalOf<Locale> { error("No Locale Set") } | ||
|
|
||
| /** | ||
| * This composable function sets up a locale provider that manages application localization. | ||
| * It initializes the locale from a language file, watches for changes to that file, and updates | ||
| * the locale accordingly. It uses a [Locale] class to handle loading of localized resources. | ||
| * | ||
| * Usage: | ||
| * ``` | ||
| * LocaleProvider { | ||
| * // Your app content here | ||
| * } | ||
| * ``` | ||
| * | ||
| * To access the locale: | ||
| * ``` | ||
| * val locale = LocalLocale.current | ||
| * val localizedString = locale["someKey"] | ||
| * ``` | ||
| * | ||
| * To change the locale: | ||
| * ``` | ||
| * locale.set(java.util.Locale("es")) | ||
| * ``` | ||
| * This will update the `language.txt` file and reload the locale. | ||
| */ | ||
| @Composable | ||
| fun LocaleProvider(content: @Composable () -> Unit) { | ||
| PlatformStart() | ||
| val preferencesFolderOverride: File? = System.getProperty("processing.app.preferences.folder")?.let { File(it) } | ||
|
|
||
| val settingsFolder = preferencesFolderOverride ?: remember{ | ||
| Platform.init() | ||
| Platform.getSettingsFolder() | ||
| } | ||
| val languageFile = settingsFolder.resolve("language.txt") | ||
| remember(languageFile){ | ||
| if(languageFile.exists()) return@remember | ||
|
|
||
| val settingsFolder = Platform.getSettingsFolder() | ||
| val languageFile = File(settingsFolder, "language.txt") | ||
| watchFile(languageFile) | ||
| Messages.log("Creating language file at ${languageFile.absolutePath}") | ||
| settingsFolder.mkdirs() | ||
| languageFile.writeText(java.util.Locale.getDefault().language) | ||
| } | ||
|
|
||
| val update = watchFile(languageFile) | ||
| var code by remember(languageFile, update){ mutableStateOf(languageFile.readText().substring(0, 2)) } | ||
| remember(code) { | ||
| val locale = java.util.Locale(code) | ||
| java.util.Locale.setDefault(locale) | ||
| } | ||
|
|
||
| fun setLocale(locale: java.util.Locale) { | ||
| Messages.log("Setting locale to ${locale.language}") | ||
| languageFile.writeText(locale.language) | ||
| code = locale.language | ||
| } | ||
|
|
||
|
|
||
| val locale = Locale(code, ::setLocale) | ||
| remember(code) { Messages.log("Loaded Locale: $code") } | ||
| val dir = when(locale["locale.direction"]) { | ||
| "rtl" -> LayoutDirection.Rtl | ||
| else -> LayoutDirection.Ltr | ||
| } | ||
|
|
||
| val locale = Locale(languageFile.readText().substring(0, 2)) | ||
| CompositionLocalProvider(LocalLocale provides locale) { | ||
| content() | ||
| CompositionLocalProvider(LocalLayoutDirection provides dir) { | ||
| CompositionLocalProvider(LocalLocale provides locale) { | ||
| content() | ||
| } | ||
|
Comment on lines
+125
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah cool! So this is like how things are composable? People can pass in a block that will be wrapped by this Locale work There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly, if you want access to localised strings you do And the beatiful things about these contexts is that if you want to display say something in Spanish within the English interface, you can override the |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package processing.app | ||
|
|
||
| import androidx.compose.material.Button | ||
| import androidx.compose.material.Text | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.platform.testTag | ||
| import androidx.compose.ui.test.ExperimentalTestApi | ||
| import androidx.compose.ui.test.assertTextEquals | ||
| import androidx.compose.ui.test.onNodeWithTag | ||
| import androidx.compose.ui.test.performClick | ||
| import androidx.compose.ui.test.runComposeUiTest | ||
| import processing.app.ui.theme.LocalLocale | ||
| import processing.app.ui.theme.LocaleProvider | ||
| import kotlin.io.path.createTempDirectory | ||
| import kotlin.test.Test | ||
|
|
||
| class LocaleKtTest { | ||
| @OptIn(ExperimentalTestApi::class) | ||
| @Test | ||
| fun testLocale() = runComposeUiTest { | ||
| val tempPreferencesDir = createTempDirectory("preferences") | ||
|
|
||
| System.setProperty("processing.app.preferences.folder", tempPreferencesDir.toFile().absolutePath) | ||
|
|
||
| setContent { | ||
| LocaleProvider { | ||
| val locale = LocalLocale.current | ||
| Text(locale["menu.file.new"], modifier = Modifier.testTag("localisedText")) | ||
|
|
||
| Button(onClick = { | ||
| locale.set(java.util.Locale("es")) | ||
| }, modifier = Modifier.testTag("button")) { | ||
| Text("Change") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Check if usage generates the language file if it doesn't exist | ||
| val languageFile = tempPreferencesDir.resolve("language.txt").toFile() | ||
| assert(languageFile.exists()) | ||
|
|
||
| // Check if the text is localised | ||
| onNodeWithTag("localisedText").assertTextEquals("New") | ||
|
|
||
| // Change the locale to Spanish | ||
| onNodeWithTag("button").performClick() | ||
| onNodeWithTag("localisedText").assertTextEquals("Nuevo") | ||
|
|
||
| // Check if the preference was saved to file | ||
| assert(languageFile.readText().substring(0, 2) == "es") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is so nice