aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--npm.dependencies1
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/KVManager.kt1
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/core/Widget.kt30
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/select/SelectInput.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/select/SelectOptGroup.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/select/SelectOption.kt4
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/spinner/SpinnerInput.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/form/time/DateTimeInput.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Image.kt2
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/List.kt5
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/html/Tag.kt8
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/i18n/I18n.kt150
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/panel/Root.kt2
15 files changed, 197 insertions, 18 deletions
diff --git a/npm.dependencies b/npm.dependencies
index b557cd0f..84b5723c 100644
--- a/npm.dependencies
+++ b/npm.dependencies
@@ -13,3 +13,4 @@ element-resize-event 2.0.9
bootstrap-fileinput 4.4.7
handlebars 4.0.11
handlebars-loader 1.7.0
+jed 1.1.1
diff --git a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
index 1d1a3dc7..b762bc01 100644
--- a/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/KVManager.kt
@@ -142,6 +142,7 @@ internal object KVManager {
require("handlebars/dist/handlebars.runtime.min.js")
} catch (e: Throwable) {
}
+ private val jed = require("jed")
internal val fecha = require("fecha")
private val sdPatch = Snabbdom.init(
arrayOf(
diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
index 30271cd0..41758d9b 100644
--- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt
@@ -32,6 +32,8 @@ import pl.treksoft.jquery.JQuery
import pl.treksoft.jquery.jQuery
import pl.treksoft.kvision.KVManager
import pl.treksoft.kvision.dropdown.ContextMenu
+import pl.treksoft.kvision.i18n.I18n
+import pl.treksoft.kvision.i18n.I18n.trans
import pl.treksoft.kvision.panel.Root
import pl.treksoft.kvision.utils.SnOn
import pl.treksoft.kvision.utils.hooks
@@ -93,6 +95,8 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() {
private var snOnCache: com.github.snabbdom.On? = null
private var snHooksCache: com.github.snabbdom.Hooks? = null
+ private var lastLanguage: String? = null
+
internal fun <T> singleRender(block: () -> T): T {
getRoot()?.renderDisabled = true
val t = block()
@@ -121,6 +125,22 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() {
}
/**
+ * Translates given text with I18n trans function and sets lastLanguage marker.
+ * @param text a text marked for a dynamic translation
+ * @return translated text
+ */
+ protected fun translate(text: String): String {
+ lastLanguage = I18n.language
+ return trans(text)
+ }
+
+ protected fun translate(text: String?): String? {
+ return text?.let {
+ translate(it)
+ }
+ }
+
+ /**
* Renders current component as a Snabbdom vnode.
* @return Snabbdom vnode
*/
@@ -163,6 +183,7 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() {
}
private fun getSnAttrsInternal(): List<StringPair> {
+ if (lastLanguage != null && lastLanguage != I18n.language) snAttrsCache = null
return snAttrsCache ?: {
val s = getSnAttrs()
snAttrsCache = s
@@ -567,16 +588,17 @@ open class Widget(classes: Set<String> = setOf()) : StyledComponent() {
label: String, icon: String? = null,
image: ResString? = null
): Array<out Any> {
+ val translatedLabel = translate(label)
return if (icon != null) {
if (icon.startsWith("fa-")) {
- arrayOf(KVManager.virtualize("<i class='fa $icon'></i>"), " $label")
+ arrayOf(KVManager.virtualize("<i class='fa $icon'></i>"), " $translatedLabel")
} else {
- arrayOf(KVManager.virtualize("<span class='glyphicon glyphicon-$icon'></span>"), " $label")
+ arrayOf(KVManager.virtualize("<span class='glyphicon glyphicon-$icon'></span>"), " $translatedLabel")
}
} else if (image != null) {
- arrayOf(KVManager.virtualize("<img src='$image' alt='' />"), " $label")
+ arrayOf(KVManager.virtualize("<img src='$image' alt='' />"), " $translatedLabel")
} else {
- arrayOf(label)
+ arrayOf(translatedLabel)
}
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectInput.kt
index 97f3989c..6d6f0fc5 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectInput.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectInput.kt
@@ -255,7 +255,7 @@ open class SelectInput(
sn.add("data-live-search" to "true")
}
placeholder?.let {
- sn.add("title" to it)
+ sn.add("title" to translate(it))
}
autofocus?.let {
if (it) {
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOptGroup.kt b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOptGroup.kt
index 3b523314..e33b3457 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOptGroup.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOptGroup.kt
@@ -79,7 +79,7 @@ open class SelectOptGroup(
override fun getSnAttrs(): List<StringPair> {
val sn = super.getSnAttrs().toMutableList()
- sn.add("label" to label)
+ sn.add("label" to translate(label))
maxOptions?.let {
sn.add("data-max-options" to "" + it)
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOption.kt b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOption.kt
index 5cd23582..d1bb636e 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOption.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/select/SelectOption.kt
@@ -70,7 +70,7 @@ open class SelectOption(
override fun render(): VNode {
return if (!divider) {
- render("option", arrayOf(label ?: value))
+ render("option", arrayOf(translate(label) ?: value))
} else {
render("option")
}
@@ -83,7 +83,7 @@ open class SelectOption(
sn.add("value" to it)
}
subtext?.let {
- sn.add("data-subtext" to it)
+ sn.add("data-subtext" to translate(it))
}
icon?.let {
if (it.startsWith("fa-")) {
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/spinner/SpinnerInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/spinner/SpinnerInput.kt
index d0bde3fe..7d3af684 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/spinner/SpinnerInput.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/spinner/SpinnerInput.kt
@@ -177,7 +177,7 @@ open class SpinnerInput(
sn.add("value" to it.toString())
}
placeholder?.let {
- sn.add("placeholder" to it)
+ sn.add("placeholder" to translate(it))
}
name?.let {
sn.add("name" to it)
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt
index 52cc7792..e41cfb8f 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/text/AbstractTextInput.kt
@@ -99,7 +99,7 @@ abstract class AbstractTextInput(
override fun getSnAttrs(): List<StringPair> {
val sn = super.getSnAttrs().toMutableList()
placeholder?.let {
- sn.add("placeholder" to it)
+ sn.add("placeholder" to translate(it))
}
name?.let {
sn.add("name" to it)
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt
index 43e522d8..88cb3b86 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/text/RichTextInput.kt
@@ -46,7 +46,7 @@ open class RichTextInput(value: String? = null, classes: Set<String> = setOf())
override fun getSnAttrs(): List<StringPair> {
val sn = super.getSnAttrs().toMutableList()
placeholder?.let {
- sn.add("placeholder" to it)
+ sn.add("placeholder" to translate(it))
}
name?.let {
sn.add("name" to it)
diff --git a/src/main/kotlin/pl/treksoft/kvision/form/time/DateTimeInput.kt b/src/main/kotlin/pl/treksoft/kvision/form/time/DateTimeInput.kt
index 3a40d1af..6ce4d0c3 100644
--- a/src/main/kotlin/pl/treksoft/kvision/form/time/DateTimeInput.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/form/time/DateTimeInput.kt
@@ -136,7 +136,7 @@ open class DateTimeInput(
val sn = super.getSnAttrs().toMutableList()
sn.add("type" to "text")
placeholder?.let {
- sn.add("placeholder" to it)
+ sn.add("placeholder" to translate(it))
}
name?.let {
sn.add("name" to it)
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Image.kt b/src/main/kotlin/pl/treksoft/kvision/html/Image.kt
index 61733fb3..4d373270 100644
--- a/src/main/kotlin/pl/treksoft/kvision/html/Image.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Image.kt
@@ -81,7 +81,7 @@ open class Image(
val pr = super.getSnAttrs().toMutableList()
pr.add("src" to src)
alt?.let {
- pr.add("alt" to it)
+ pr.add("alt" to translate(it))
}
return pr
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/List.kt b/src/main/kotlin/pl/treksoft/kvision/html/List.kt
index 377b805f..5fb489da 100644
--- a/src/main/kotlin/pl/treksoft/kvision/html/List.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/html/List.kt
@@ -116,10 +116,11 @@ open class ListTag(
}
private fun element(name: String, value: String, rich: Boolean): VNode {
+ val translatedValue = translate(value)
return if (rich) {
- h(name, arrayOf(KVManager.virtualize("<span>$value</span>")))
+ h(name, arrayOf(KVManager.virtualize("<span>$translatedValue</span>")))
} else {
- h(name, value)
+ h(name, translatedValue)
}
}
diff --git a/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt
index 75536b88..3a15a4d0 100644
--- a/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/html/Tag.kt
@@ -135,10 +135,14 @@ open class Tag(
override fun render(): VNode {
return if (content != null) {
+ val translatedContent = content?.let { translate(it) }
if (rich) {
- render(type.tagName, arrayOf(KVManager.virtualize("<span>$content</span>")) + childrenVNodes())
+ render(
+ type.tagName,
+ arrayOf(KVManager.virtualize("<span>$translatedContent</span>")) + childrenVNodes()
+ )
} else {
- render(type.tagName, childrenVNodes() + arrayOf(content))
+ render(type.tagName, childrenVNodes() + arrayOf(translatedContent))
}
} else {
render(type.tagName, childrenVNodes())
diff --git a/src/main/kotlin/pl/treksoft/kvision/i18n/I18n.kt b/src/main/kotlin/pl/treksoft/kvision/i18n/I18n.kt
new file mode 100644
index 00000000..6d0b0bfa
--- /dev/null
+++ b/src/main/kotlin/pl/treksoft/kvision/i18n/I18n.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2017-present Robert Jaros
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package pl.treksoft.kvision.i18n
+
+import org.w3c.xhr.XMLHttpRequest
+import pl.treksoft.kvision.panel.Root
+import kotlin.browser.window
+import kotlin.js.Promise
+
+external class Jed(json: dynamic) {
+ fun gettext(key: String): String
+ fun ngettext(singularKey: String, pluralKey: String, value: Int): String
+ fun sprintf(format: String, value: Int): String
+}
+
+private const val I18N_SINGLE_DELIMITER = "###KvI18nS###"
+private const val I18N_PLURAL_DELIMITER = "###KvI18nP###"
+
+/**
+ * A singleton object used for translations.
+ */
+object I18n {
+
+ private val defaultLanguage = window.navigator.language.split("-")[0]
+
+ /**
+ * Main language of the application.
+ */
+ var language = defaultLanguage
+ set(value) {
+ field = value
+ Root.roots.forEach { it.reRender() }
+ }
+
+ private val cache = mutableMapOf<String, Jed>()
+
+ /**
+ * I18n initialization function.
+ * Should be called in the main function of the application.
+ * @param languages a list of supported languages.
+ * @param initCallback a code to run after the initialization process is complete.
+ */
+ fun init(vararg languages: String, initCallback: () -> Unit) {
+ val promises = languages.map {
+ I18n.readMessages(it)
+ }.toTypedArray()
+ Promise.all(promises).then { initCallback() }
+ }
+
+ private fun readMessages(language: String): Promise<Jed> {
+ return Promise { resolve, _ ->
+ val xmlHttpRequest = XMLHttpRequest()
+ xmlHttpRequest.overrideMimeType("application/json")
+ xmlHttpRequest.open("GET", "js/messages-$language.json", true)
+ xmlHttpRequest.onreadystatechange = {
+ if (xmlHttpRequest.readyState.toInt() == 4 && (xmlHttpRequest.status.toInt() == 200 ||
+ xmlHttpRequest.status.toInt() == 0)
+ ) {
+ val json = JSON.parse<dynamic>(xmlHttpRequest.responseText)
+ val jed = Jed(json)
+ cache[language] = jed
+ resolve(jed)
+ }
+ }
+ xmlHttpRequest.send()
+ }
+ }
+
+ /**
+ * A static translation function for a singular form.
+ * @param key a translation key.
+ * @return translated text.
+ */
+ fun gettext(key: String): String {
+ return cache[language]?.gettext(key) ?: key
+ }
+
+ /**
+ * A static translation function for a plural form.
+ * @param singularKey a translation key for a singular form.
+ * @param pluralKey a translation key for a plural form.
+ * @param value a count value.
+ * @return translated text.
+ */
+ fun ngettext(singularKey: String, pluralKey: String, value: Int): String {
+ return cache[language]?.run {
+ sprintf(ngettext(singularKey, pluralKey, value), value)
+ } ?: if (value == 1) singularKey else pluralKey
+ }
+
+ /**
+ * A dynamic translation function for a singular form.
+ * @param key a translation key.
+ * @return text marked for a dynamic translation.
+ */
+ fun tr(key: String): String {
+ return I18N_SINGLE_DELIMITER + key
+ }
+
+ /**
+ * A dynamic translation function for a plural form.
+ * @param singularKey a translation key for a singular form.
+ * @param pluralKey a translation key for a plural form.
+ * @param value a count value.
+ * @return text marked for a dynamic translation.
+ */
+ fun ntr(singularKey: String, pluralKey: String, value: Int): String {
+ return I18N_PLURAL_DELIMITER + singularKey + I18N_PLURAL_DELIMITER + pluralKey + I18N_PLURAL_DELIMITER + value
+ }
+
+ internal fun trans(text: String): String {
+ return if (text.startsWith(I18N_SINGLE_DELIMITER)) {
+ gettext(text.substring(I18N_SINGLE_DELIMITER.length))
+ } else if (text.startsWith(I18N_PLURAL_DELIMITER)) {
+ val tab = text.substring(I18N_PLURAL_DELIMITER.length).split(I18N_PLURAL_DELIMITER)
+ if (tab.size == 3) {
+ ngettext(tab[0], tab[1], tab[2].toIntOrNull() ?: 1)
+ } else {
+ text
+ }
+ } else {
+ text
+ }
+ }
+
+ internal fun trans(text: String?): String? {
+ return text?.let {
+ trans(it)
+ }
+ }
+}
diff --git a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt
index 667559b7..fece65e2 100644
--- a/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt
+++ b/src/main/kotlin/pl/treksoft/kvision/panel/Root.kt
@@ -116,7 +116,7 @@ class Root(id: String, private val fixed: Boolean = false, init: (Root.() -> Uni
}
companion object {
- private val roots: MutableList<Root> = mutableListOf()
+ internal val roots: MutableList<Root> = mutableListOf()
internal fun getLastRoot(): Root? {
return if (roots.size > 0)