i18n: remember chosen language across launches, timeouts, error on bad HTTP
Browse files
app/src/main/java/io/github/abrarul/modularai/i18n/LocaleManager.kt
CHANGED
|
@@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
|
|
| 8 |
import kotlinx.coroutines.withContext
|
| 9 |
import org.json.JSONObject
|
| 10 |
import java.io.File
|
|
|
|
| 11 |
import java.net.URL
|
| 12 |
|
| 13 |
/**
|
|
@@ -16,24 +17,41 @@ import java.net.URL
|
|
| 16 |
*/
|
| 17 |
class LocaleManager(ctx: Context, private val baseUrl: String) {
|
| 18 |
private val dir = File(ctx.filesDir, "locales").apply { mkdirs() }
|
|
|
|
| 19 |
private var strings by mutableStateOf<Map<String, String>>(emptyMap())
|
| 20 |
var current by mutableStateOf("en")
|
| 21 |
private set
|
| 22 |
|
| 23 |
-
/** English
|
| 24 |
fun t(key: String, default: String): String = strings[key] ?: default
|
| 25 |
|
| 26 |
suspend fun apply(code: String, persist: Boolean = true) = withContext(Dispatchers.IO) {
|
|
|
|
| 27 |
val cached = File(dir, "$code.json")
|
| 28 |
-
val text = if (cached.exists()) cached.readText() else
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
val json = JSONObject(text)
|
| 30 |
val map = json.keys().asSequence().associateWith { json.getString(it) }
|
| 31 |
-
if (persist)
|
|
|
|
|
|
|
|
|
|
| 32 |
strings = map
|
| 33 |
current = code
|
| 34 |
}
|
| 35 |
|
| 36 |
-
|
| 37 |
-
fun
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
|
|
|
| 8 |
import kotlinx.coroutines.withContext
|
| 9 |
import org.json.JSONObject
|
| 10 |
import java.io.File
|
| 11 |
+
import java.net.HttpURLConnection
|
| 12 |
import java.net.URL
|
| 13 |
|
| 14 |
/**
|
|
|
|
| 17 |
*/
|
| 18 |
class LocaleManager(ctx: Context, private val baseUrl: String) {
|
| 19 |
private val dir = File(ctx.filesDir, "locales").apply { mkdirs() }
|
| 20 |
+
private val prefs = ctx.getSharedPreferences("locale", Context.MODE_PRIVATE)
|
| 21 |
private var strings by mutableStateOf<Map<String, String>>(emptyMap())
|
| 22 |
var current by mutableStateOf("en")
|
| 23 |
private set
|
| 24 |
|
| 25 |
+
/** English defaults live in code; a missing key returns [default]. */
|
| 26 |
fun t(key: String, default: String): String = strings[key] ?: default
|
| 27 |
|
| 28 |
suspend fun apply(code: String, persist: Boolean = true) = withContext(Dispatchers.IO) {
|
| 29 |
+
require(code.matches(Regex("[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?"))) { "Invalid language code: $code" }
|
| 30 |
val cached = File(dir, "$code.json")
|
| 31 |
+
val text = if (cached.exists()) cached.readText() else {
|
| 32 |
+
val c = URL("$baseUrl/$code.json").openConnection() as HttpURLConnection
|
| 33 |
+
c.connectTimeout = 15_000
|
| 34 |
+
c.readTimeout = 15_000
|
| 35 |
+
if (c.responseCode !in 200..299) error("Language '$code' not available (HTTP ${c.responseCode})")
|
| 36 |
+
c.inputStream.bufferedReader().use { it.readText() }
|
| 37 |
+
}
|
| 38 |
val json = JSONObject(text)
|
| 39 |
val map = json.keys().asSequence().associateWith { json.getString(it) }
|
| 40 |
+
if (persist) {
|
| 41 |
+
cached.writeText(text)
|
| 42 |
+
prefs.edit().putString("code", code).apply()
|
| 43 |
+
}
|
| 44 |
strings = map
|
| 45 |
current = code
|
| 46 |
}
|
| 47 |
|
| 48 |
+
/** Re-applies the saved language from cache at app start; no network needed. */
|
| 49 |
+
suspend fun restore() {
|
| 50 |
+
val code = prefs.getString("code", null) ?: return
|
| 51 |
+
if (File(dir, "$code.json").exists()) apply(code, persist = false)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
fun reset() { strings = emptyMap(); current = "en"; prefs.edit().remove("code").apply() }
|
| 55 |
+
fun purge(code: String) { File(dir, "$code.json").delete(); if (current == code) reset() }
|
| 56 |
+
fun purgeAll() { dir.listFiles()?.forEach { it.delete() }; reset() }
|
| 57 |
}
|