From 461a7ac99ddcbd6c3f6b247876f459b382780ebb Mon Sep 17 00:00:00 2001 From: "Md. Touhidur Rahman" <46617994+touhidurrr@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:09:19 +0600 Subject: [PATCH] Translate numbers to selected language (#11898) * implement number translation * fix previous error * maybe this will fix things? * try to make the regex more concise and fix the issues * update implementation * Transient cache * implement Number.tr() * Update core/src/com/unciv/models/metadata/GameSettings.kt Co-authored-by: Yair Morgenstern * fix formatting * move getNumberFormatFromLanguage() to Translations.kt and cache NumberFormat * remove null cases and resolve suggestions * why was I doing this? I forgot. * move getLocaleFromLanguage() to Translations.kt * add tests * Update TranslationTests.kt fix issues * also remove also * fix all given cases * fix missing language param --------- Co-authored-by: Yair Morgenstern --- .../com/unciv/models/metadata/GameSettings.kt | 16 +++---- .../unciv/models/translations/Translations.kt | 39 ++++++++++++++- tests/src/com/unciv/logic/TranslationTests.kt | 47 +++++++++++++++++++ 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 2e80e25eef..7f08e840a9 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -7,6 +7,8 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.multiplayer.FriendList import com.unciv.models.UncivSound +import com.unciv.models.translations.Translations.Companion.getLocaleFromLanguage +import com.unciv.models.translations.Translations.Companion.getNumberFormatFromLanguage import com.unciv.ui.components.fonts.FontFamilyData import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.input.KeyboardBindings @@ -15,6 +17,7 @@ import com.unciv.ui.screens.worldscreen.NotificationsScroll import com.unciv.utils.Display import com.unciv.utils.ScreenOrientation import java.text.Collator +import java.text.NumberFormat import java.time.Duration import java.util.Locale import kotlin.reflect.KClass @@ -169,14 +172,7 @@ class GameSettings { } fun updateLocaleFromLanguage() { - val bannedCharacters = listOf(' ', '_', '-', '(', ')') // Things not to have in enum names - val languageName = language.filterNot { it in bannedCharacters } - locale = try { - val code = LocaleCode.valueOf(languageName) - Locale(code.language, code.country) - } catch (_: Exception) { - Locale.getDefault() - } + locale = getLocaleFromLanguage(language) } fun getFontSize(): Int { @@ -193,6 +189,10 @@ class GameSettings { return Collator.getInstance(getCurrentLocale()) } + fun getCurrentNumberFormat(): NumberFormat { + return getNumberFormatFromLanguage(language) + } + //endregion //region diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index 331d1d8c13..d367fb0e4b 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -3,6 +3,7 @@ package com.unciv.models.translations import com.badlogic.gdx.Gdx import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.models.metadata.GameSettings.LocaleCode import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.unique.Unique import com.unciv.models.stats.Stat @@ -13,6 +14,7 @@ import com.unciv.utils.Log import com.unciv.utils.debug import java.util.Locale import org.jetbrains.annotations.VisibleForTesting +import java.text.NumberFormat /** * This collection holds all translations for the game. @@ -231,6 +233,26 @@ class Translations : LinkedHashMap() { const val conditionalUniqueOrderString = "ConditionalsPlacement" const val shouldCapitalizeString = "StartWithCapitalLetter" const val effectBeforeCause = "EffectBeforeCause" + + // NumberFormat cache, key: language, value: NumberFormat + private val languageToNumberFormat = mutableMapOf() + + fun getLocaleFromLanguage(language: String): Locale { + val bannedCharacters = + listOf(' ', '_', '-', '(', ')') // Things not to have in enum names + val languageName = language.filterNot { it in bannedCharacters } + return try { + val code = LocaleCode.valueOf(languageName) + Locale(code.language, code.country) + } catch (_: Exception) { + Locale.getDefault() + } + } + + fun getNumberFormatFromLanguage(language: String): NumberFormat = + languageToNumberFormat.getOrPut(language) { + NumberFormat.getInstance(getLocaleFromLanguage(language)) + } } } @@ -252,6 +274,9 @@ val curlyBraceRegex = Regex("""\{([^}]*)\}""") @Suppress("RegExpRedundantEscape") // Some Android versions need ]}) escaped val pointyBraceRegex = Regex("""\<([^>]*)\>""") +// Used to match continous digits 0, 12, 1232 etc +@Suppress("RegExpRedundantEscape") // Some Android versions need ]}) escaped +val digitsRegex = Regex("""\d+""") object TranslationActiveModsCache { private var cachedHash = Int.MIN_VALUE @@ -443,7 +468,9 @@ private fun String.translatePlaceholders(language: String, hideIcons: Boolean): private fun String.translateIndividualWord(language: String, hideIcons: Boolean): String { if (Stats.isStats(this)) return Stats.parse(this).toString() - val translation = UncivGame.Current.translations.getText(this, language, TranslationActiveModsCache.activeMods) + val translation = UncivGame.Current.translations.getText( + this, language, TranslationActiveModsCache.activeMods + ).replace(digitsRegex) { it.value.toLong().tr(language) } val stat = Stat.safeValueOf(this) if (stat != null) return stat.character + translation @@ -528,3 +555,13 @@ fun String.removeConditionals(): String { .replace(" ", " ") .trim() } + +// formats number according to current language +fun Number.tr(): String { + return UncivGame.Current.settings.getCurrentNumberFormat().format(this) +} + +// formats number according to given language +fun Number.tr(language: String): String { + return Translations.getNumberFormatFromLanguage(language).format(this) +} diff --git a/tests/src/com/unciv/logic/TranslationTests.kt b/tests/src/com/unciv/logic/TranslationTests.kt index e5bb3ad43f..b5be2232f4 100644 --- a/tests/src/com/unciv/logic/TranslationTests.kt +++ b/tests/src/com/unciv/logic/TranslationTests.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.Gdx import com.unciv.Constants import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings +import com.unciv.models.metadata.GameSettings.LocaleCode import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.stats.Stats @@ -392,6 +393,52 @@ class TranslationTests { additionalTest?.invoke(translated) } + @Test + fun testNumberTr() { + UncivGame.Current = UncivGame() + UncivGame.Current.settings = GameSettings() + + val testCases = arrayOf(1, -1, 0.123, -0.123) + + val expectedEnglishOutputs = arrayOf("1", "-1", "0.123", "-0.123") + Assert.assertArrayEquals( + "Number.tr()", expectedEnglishOutputs, testCases.map { it.tr() }.toTypedArray() + ) + Assert.assertArrayEquals( + "Number.tr(${LocaleCode.English.name})", + expectedEnglishOutputs, + testCases.map { it.tr(LocaleCode.English.name) }.toTypedArray() + ) + + val expectedBanglaOutputs = arrayOf("১", "-১", "০.১২৩", "-০.১২৩") + Assert.assertArrayEquals( + "Number.tr(${LocaleCode.Bangla.name})", + expectedBanglaOutputs, + testCases.map { it.tr(LocaleCode.Bangla.name) }.toTypedArray() + ) + } + + @Test + fun testStringsWithNumbers() { + UncivGame.Current = UncivGame() + UncivGame.Current.settings = GameSettings() + + val tests = arrayOf("1", "+1", "-1", "1.0", "+1.0", "-1.0", "0%", "1/2", "(3/4)") + + UncivGame.Current.settings.language = LocaleCode.English.name + Assert.assertArrayEquals( + "English", tests, // assume unchanged + tests.map { it.tr() }.toTypedArray() + ) + + UncivGame.Current.settings.language = LocaleCode.Bangla.name + Assert.assertArrayEquals( + "Bangla", + arrayOf("১", "+১", "-১", "১.০", "+১.০", "-১.০", "০%", "১/২", "(৩/৪)"), + tests.map { it.tr() }.toTypedArray() + ) + } + // @Test // fun allConditionalsAreContainedInConditionalOrderTranslation() { // val orderedConditionals = Translations.englishConditionalOrderingString