기존 코드를 일부 개선했다.
RestringHelperBase를 BaseRestringHelper로 이름변경하고 abstract 클래스로 변경했다.
public abstract class BaseRestringHelper {
public void init(@NonNull Context context) {
}
public Context wrapContext(@NonNull Context context) {
return null;
}
public String getString(@NonNull Context context, @StringRes int stringId) {
return null;
}
public void downloadTranslation(@NonNull Activity context) {
}
public void changeLanguage(@NonNull Activity context) {
}
}
getString는 R.string.XXX ID로 Restring에 초기화된 언어에서 문자열을 가져온다.
downloadTranslation은 google docs에서 언어 파일을 내려받아서 언어를 업데이트 한다.
changeLanguage는 번역에서 언어를 선택한다.
LanguageDownloader도 개선했다. 크게 바뀐 것은 없고, 파일캐시를 추가했다.
typealias LanguageDownloaderCallback = (result: OptionalResult<String>) -> Unit
class LanguageDownloader {
companion object {
private const val JSON_PREFIX = "google.visualization.Query.setResponse("
private const val JSON_EXPORT_URL =
"https://docs.google.com/spreadsheets/d/1W9HOGd0qWNtLpvgKzsrT0Gf6RlKxrknHUVULRooNYJg/gviz/tq?tqx=out:json"
private const val JSON_FILENAME = "languages.json"
private val lock = Any()
private val httpClient = OkHttpClient()
}
private val languageToColumnMap = mutableMapOf<String, Int>()
private val columnToLanguageMap = mutableMapOf<Int, MutableMap<String, String>>()
private val languageList = mutableListOf<String>()
private val columnToFriendlyNameMap = mutableMapOf<Int, String>() // 4 -> "English"
private val friendlyNameMap = mutableMapOf<String, String>() // "en" -> "English"
fun downloadSheet(callback: LanguageDownloaderCallback) {
if (!BuildConfig.RESTRING) {
return
}
val request = Request.Builder()
.url(JSON_EXPORT_URL)
.build()
httpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(request: Request?, e: IOException?) {
ToastUtils.show(e?.message)
callback(OptionalResult(e))
}
override fun onResponse(response: Response?) {
try {
val bodyString = response?.body()?.string()
?: throw RuntimeException("Download failed: google spreadsheet json file")
parseBodyString(bodyString)
callback(OptionalResult(bodyString))
} catch (ex: Exception) {
ToastUtils.show(ex.message)
callback(OptionalResult(ex))
}
}
})
}
fun downloadSheetSync(): OptionalResult<String> {
if (!BuildConfig.RESTRING) {
return OptionalResult("")
}
val request = Request.Builder()
.url(JSON_EXPORT_URL)
.build()
return try {
val bodyString = httpClient.newCall(request).execute().body()?.string()
?: throw RuntimeException("Download failed: google spreadsheet json file")
parseBodyString(bodyString)
OptionalResult(bodyString)
} catch (ex: Exception) {
OptionalResult(ex)
}
}
fun parseBodyString(bodyString1: String) {
if (!BuildConfig.RESTRING) {
return
}
var bodyString = bodyString1
val prefixIndex = bodyString.indexOf(JSON_PREFIX)
if (prefixIndex > 0) {
val startIndex = prefixIndex + JSON_PREFIX.length
val endIndex = bodyString.lastIndexOf(')')
if (endIndex > 0) {
bodyString = bodyString.substring(startIndex, endIndex)
}
}
parseJson(JSONObject(bodyString))
}
private fun parseJson(jsonObject: JSONObject) {
if (!BuildConfig.RESTRING) {
return
}
val rowsArray = jsonObject.optJSONObject("table")?.optJSONArray("rows")
?: throw RuntimeException("Parse failed: table , rows element not found")
for (row in 0 until rowsArray.length()) {
val c = rowsArray.getJSONObject(row).optJSONArray("c") ?: break
var stringId: String? = null
for (column in 0 until c.length()) {
val text = c.optJSONObject(column)?.optString("v") ?: continue
if (column == 0) {
if (text == "[common]" || text == "[app]" || text == "--end--") {
continue
}
stringId = text
}
if (stringId == null || column < 4) {
continue
}
// check header row
if (stringId == "Language Name") {
if (text != "null") {
columnToFriendlyNameMap[column] = text
}
} else if (stringId == "Language Id") {
if (text != "null") {
val language = text
languageToColumnMap[language] = column
columnToLanguageMap[column] = HashMap()
languageList.add(language)
columnToFriendlyNameMap[column]?.let { friendlyNameMap[language] = it }
}
} else {
columnToLanguageMap[column]?.put(stringId, touchString(text))
}
}
}
}
private fun touchString(text: String): String {
if (!BuildConfig.RESTRING) {
return text
}
return text.replace("\${str}", "%s")
.replace("\${str1}", "%1\$s")
.replace("\${str2}", "%2\$s")
.replace("\${str3}", "%3\$s")
.replace("\${str4}", "%4\$s")
.replace("\${num}", "%d")
.replace("\${num1}", "%1\$d")
.replace("\${num2}", "%2\$d")
.replace("\${num3}", "%3\$d")
.replace("\${num4}", "%4\$d")
}
private fun printStrings(language: String) {
if (!BuildConfig.RESTRING) {
return
}
val column = languageToColumnMap[language] ?: return
val map = columnToLanguageMap[column] ?: return
for ((key, value) in map.entries) {
Log.e("__T", "$language $key=$value")
}
Log.e("__T", "$language count=${map.size}")
}
fun getLanguageList(): MutableList<String> {
return languageList
}
fun getStrings(language: String): MutableMap<String, String>? {
languageToColumnMap[language]?.let { column ->
return columnToLanguageMap[column]
}
return null
}
fun readCache(context: Context): String? {
synchronized(lock) {
val file = File(context.filesDir, JSON_FILENAME)
return FileUtils.readFileContent(file)
}
}
fun writeCache(context: Context, content: String) {
synchronized(lock) {
val file = File(context.filesDir, JSON_FILENAME)
FileUtils.writeFileContent(file, content)
}
}
fun getFriendlyName(language: String): String? = friendlyNameMap[language]
}
OptionResult는 간단한 유틸 클래스다.
public class OptionalResult<T> {
private T result;
private Exception exception;
public OptionalResult(T result) {
this.result = result;
}
public OptionalResult(Exception exception) {
this.exception = exception;
}
public boolean hasResult() {
return result != null;
}
@Nullable
public T getResult() {
return result;
}
public boolean hasException() {
return exception != null;
}
@Nullable
public Exception getException() {
return exception;
}
}
RestringHelper도 개선했다. 언어파일을 내려받고 언어를 변경할 수 있고, 내려 받은 번역 스트링을 Restring에 초기화하는 구현코드다.
class RestringHelper : BaseRestringHelper() {
companion object {
private val LANGUAGE = SettingString("LANGUAGE", Locale.getDefault().language)
}
private val handler = Handler(Looper.getMainLooper())
private var strings: MutableMap<String, String>? = null
override fun init(context: Context) {
Restring.init(context,
RestringConfig.Builder()
.persist(false)
//.stringsLoader(SampleStringsLoader())
.build())
val downloader = LanguageDownloader()
try {
val cacheJson = downloader.readCache(context)
downloader.parseBodyString(cacheJson!!)
setLanguageStrings(downloader.getStrings(LANGUAGE.value))
} catch (ex: Exception) {
// empty
}
}
private fun setLanguageStrings(strings: MutableMap<String, String>?) {
strings?.let {
this.strings = strings
Restring.setStrings(Locale.getDefault().language, strings)
} ?: ToastUtils.show(R.string.error_unknown)
}
override fun wrapContext(context: Context): Context {
return Restring.wrapContext(context)
}
override fun getString(context: Context, resourceId: Int): String {
return try {
val stringId = context.resources.getResourceEntryName(resourceId)
strings?.get(stringId) ?: context.getString(resourceId)
} catch (ex: Exception) {
context.getString(resourceId)
}
}
override fun downloadTranslation(context: Activity) {
val downloader = LanguageDownloader()
AsyncTaskEx.showProgressExecute(context) {
val optionalResult = downloader.downloadSheetSync()
handler.post {
optionalResult.result?.let {
downloader.writeCache(context, it)
onDownloadSucceeded(context, downloader)
} ?: ToastUtils.show(R.string.download_translation_failed)
}
}
}
private fun onDownloadSucceeded(context: Activity, downloader: LanguageDownloader) {
DialogHelper.showConfirmCancel(context, R.string.download_translation_succeeded, { _, _ ->
changeLanguage(context, downloader, false)
}, { _, _ ->
setLanguageStrings(downloader.getStrings(LANGUAGE.value))
restartApp(context)
}).apply {
setCancelable(false)
getButton(AlertDialog.BUTTON_NEGATIVE).text = context.getString(R.string.change_language)
}
}
override fun changeLanguage(context: Activity) {
val downloader = LanguageDownloader()
try {
val cacheJson = downloader.readCache(context)
downloader.parseBodyString(cacheJson!!)
changeLanguage(context, downloader)
} catch (ex: Exception) {
AsyncTaskEx.showProgressExecute(context) {
val optionalResult = downloader.downloadSheetSync()
handler.post {
optionalResult.result?.let {
downloader.writeCache(context, it)
changeLanguage(context, downloader)
} ?: ToastUtils.show(R.string.download_translation_failed)
}
}
}
}
private fun changeLanguage(context: Context, downloader: LanguageDownloader, cancelable: Boolean = true) {
val languageList = downloader.getLanguageList()
val helper = DialogItemsHelper.Builder()
.also {
languageList.forEachIndexed { index, lang ->
val friendlyName = downloader.getFriendlyName(lang)
if (friendlyName != null) {
it.add("$friendlyName - $lang", index)
} else {
it.add(lang, index)
}
}
}.build()
DialogHelper.showItems(context, context.getString(R.string.change_language), helper.menuItems) { d, which ->
val language = languageList[which]
setLanguageStrings(downloader.getStrings(language))
LANGUAGE.value = language
restartApp(context)
}.setCancelable(cancelable)
}
private fun restartApp(context: Context) {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}