Skip to content

Instantly share code, notes, and snippets.

@eungju
Last active March 28, 2024 14:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eungju/4c7ce6282d36fde68027b56c0f5bf515 to your computer and use it in GitHub Desktop.
Save eungju/4c7ce6282d36fde68027b56c0f5bf515 to your computer and use it in GitHub Desktop.
Simple i18n
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())
}
}
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'.")
}
}
}
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