Last active
March 28, 2024 14:19
-
-
Save eungju/4c7ce6282d36fde68027b56c0f5bf515 to your computer and use it in GitHub Desktop.
Simple i18n
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface I18n { | |
fun localized(languageTag: String): Localized | |
interface Localized { | |
fun text(key: String, args: Map<String, Any>): String | |
fun text(key: String): String = text(key, emptyMap()) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed interface LocalText { | |
fun apply(args: Map<String, Any>): String | |
companion object { | |
fun of(input: String, placeholderBegin: String, placeholderEnd: String): LocalText { | |
val tokens = mutableListOf<Token>() | |
var pos = 0 | |
var placeholderState = false | |
while (pos < input.length) { | |
val lookup = if (placeholderState) placeholderEnd else placeholderBegin | |
val found = input.indexOf(lookup, pos) | |
if (found == -1) { | |
if (placeholderState) { | |
throw IllegalArgumentException("Missing '$placeholderEnd' in '$input'.") | |
} else { | |
val value = input.substring(pos) | |
if (value.isNotEmpty()) { | |
tokens.add(Token.Text(value)) | |
} | |
break | |
} | |
} else { | |
if (placeholderState) { | |
val name = input.substring(pos, found) | |
if (name.isBlank()) { | |
throw IllegalArgumentException("Parameter name is required.") | |
} | |
tokens.add(Token.Placeholder(name)) | |
pos = found + lookup.length | |
placeholderState = false | |
} else { | |
val value = input.substring(pos, found) | |
if (value.isNotEmpty()) { | |
tokens.add(Token.Text(value)) | |
} | |
pos = found + lookup.length | |
placeholderState = true | |
} | |
} | |
} | |
return if (tokens.size == 1 && tokens[0] is Token.Text) { | |
SolidLocalText(tokens[0].apply(emptyMap())) | |
} else { | |
TemplateLocalText(tokens) | |
} | |
} | |
} | |
private class SolidLocalText(private val text: String) : LocalText { | |
override fun apply(args: Map<String, Any>): String = text | |
} | |
private class TemplateLocalText(private val tokens: List<Token>) : LocalText { | |
override fun apply(args: Map<String, Any>): String { | |
val buffer = StringBuilder() | |
tokens.forEach { buffer.append(it.apply(args)) } | |
return buffer.toString() | |
} | |
} | |
private sealed interface Token { | |
fun apply(args: Map<String, Any>): String | |
class Text(private val value: String) : Token { | |
override fun apply(args: Map<String, Any>): String = | |
value | |
} | |
class Placeholder(private val name: String) : Token { | |
override fun apply(args: Map<String, Any>): String = | |
// value의 포맷팅이 필요하면 여기서 한다. | |
args.get(name)?.toString() ?: throw IllegalArgumentException("Missing argument '$name'.") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class YamlI18n( | |
private val bundles: Map<String, Map<String, LocalText>>, | |
private val defaultLanguageTag: String | |
) : I18n { | |
companion object { | |
fun loadFromResources(resourcesPath: String, defaultLanguageTag: String): I18n { | |
val yamlMapper = YAMLMapper() | |
val bundles = languageTags.associate { langTag -> | |
javaClass.getResourceAsStream("$resourcesPath/$langTag.yml")?.use { | |
val bundle = mutableMapOf<String, LocalText>() | |
load(bundle, "", yamlMapper.readTree(it) as ObjectNode) | |
langTag to bundle | |
} ?: error("Cannot find bundle $langTag.yml.") | |
} | |
return YamlI18n(bundles, defaultLanguageTag) | |
} | |
private fun load(bundle: MutableMap<String, LocalText>, namespace: String, node: ObjectNode) { | |
node.fields().forEach { (key: String, node: JsonNode) -> | |
if (node.isObject) { | |
load(bundle, "$namespace$key.", node as ObjectNode) | |
} else { | |
bundle.set("$namespace$key", localText(node.asText())) | |
} | |
} | |
} | |
internal fun localText(text: String) = | |
LocalText.of(text, "%{", "}") | |
private val languageTags = setOf( | |
// KOREAN | |
"ko", | |
// JAPANESE | |
"ja", | |
// ENGLISH | |
"en", | |
) | |
} | |
private val logger = LoggerFactory.getLogger(javaClass) | |
override fun localized(languageTag: String) = | |
object : I18n.Localized { | |
private val bundle = bundles[languageTag] ?: bundles[defaultLanguageTag] | |
override fun text(key: String, args: Map<String, Any>): String { | |
return bundle?.get(key)?.apply(args) | |
?: "Cannot find local text $key in $languageTag.".also { | |
logger.error(it) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment