Reworked Locale handling + deprecations + linting (#13142)

* Linting and deprecations

* Trial and error for Locale factories

* Reworked Locale handling

* Clean up fastlane for Rusyn

* Decide open question in favour of supported methods
This commit is contained in:
SomeTroglodyte 2025-04-06 10:45:29 +02:00 committed by GitHub
parent 8a1f14e683
commit 1c7a50e1c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 117 additions and 124 deletions

View File

@ -143,7 +143,7 @@ open class FileChooser(
add(fileScroll).colspan(2).fill().row()
addSeparator(height = 1f)
fileNameCell = add().colspan(2).growX()
row()
super.row()
addCloseButton(Constants.cancel) {
reportResult(false)

View File

@ -59,7 +59,7 @@ class UncivFiles(
}
fun getModsFolder() = getLocalFile("mods")
fun getModFolder(modName: String) = getModsFolder().child(modName)
fun getModFolder(modName: String): FileHandle = getModsFolder().child(modName)
/** The folder that holds data that the game changes while running - all the mods, maps, save files, etc */
fun getDataFolder() = getLocalFile("")
@ -167,7 +167,7 @@ class UncivFiles(
/**
* Only use this with a [FileHandle] obtained by one of the methods of this class!
*/
fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
private fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try {
debug("Saving GameInfo %s to %s", game.gameId, file.path())
val string = gameInfoToString(game)
@ -322,7 +322,7 @@ class UncivFiles(
//endregion
//region Scenarios
val scenarioFolder = "scenarios"
private val scenarioFolder = "scenarios"
fun getScenarioFiles() = sequence {
for (mod in RulesetCache.values) {
val modFolder = mod.folderLocation ?: continue

View File

@ -19,7 +19,7 @@ import java.io.FileFilter
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.net.URI
import java.nio.ByteBuffer
import java.util.zip.ZipException
@ -48,7 +48,7 @@ object Github {
// Consider merging this with the Dropbox function
/**
* Helper opens an url and accesses its input stream, logging errors to the console
* @param url String representing a [URL] to download.
* @param url String representing a [URI] to download.
* @param preDownloadAction Optional callback that will be executed between opening the connection and
* accessing its data - passes the [connection][HttpURLConnection] and allows e.g. reading the response headers.
* @return The [InputStream] if successful, `null` otherwise.
@ -56,9 +56,8 @@ object Github {
fun download(url: String, preDownloadAction: (HttpURLConnection) -> Unit = {}): InputStream? {
try {
// Problem type 1 - opening the URL connection
@Suppress("DEPRECATION") // We still support Java < 20
with(URL(url).openConnection() as HttpURLConnection)
{
// URL(string) is deprecated, URI.toUrl(string) API level 36, see [Android Doc](https://developer.android.com/reference/java/net/URI#toURL()):
with(URI(url).toURL().openConnection() as HttpURLConnection) {
preDownloadAction(this)
// Problem type 2 - getting the information
try {

View File

@ -9,8 +9,7 @@ import java.io.DataOutputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.Charset
import java.net.URI
import java.util.Date
import java.util.Timer
import kotlin.concurrent.timer
@ -25,7 +24,8 @@ object DropBox: FileStorage {
if (remainingRateLimitSeconds > 0)
throw FileStorageRateLimitReached(remainingRateLimitSeconds)
with(URL(url).openConnection() as HttpURLConnection) {
// URL(string) is deprecated, URI.toUrl(string) API level 36:
with(URI(url).toURL().openConnection() as HttpURLConnection) {
requestMethod = "POST" // default is GET
@Suppress("SpellCheckingInspection")

View File

@ -7,8 +7,6 @@ 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
@ -175,7 +173,7 @@ class GameSettings {
}
fun updateLocaleFromLanguage() {
locale = getLocaleFromLanguage(language)
locale = LocaleCode.getLocale(language)
}
fun getFontSize(): Int {
@ -193,7 +191,7 @@ class GameSettings {
}
fun getCurrentNumberFormat(): NumberFormat {
return getNumberFormatFromLanguage(language)
return LocaleCode.getNumberFormatFromLanguage(language)
}
//endregion
@ -262,67 +260,6 @@ class GameSettings {
enum class NationPickerListMode { Icons, List }
/** Map Unciv language key to Java locale, for the purpose of getting a Collator for sorting.
* - Effect depends on the Java libraries and may not always conform to expectations.
* If in doubt, debug and see what Locale instance you get and compare its properties with `Locale.getDefault()`.
* (`Collator.getInstance(LocaleCode.*.run { Locale(language, country) }) to Collator.getInstance()`, drill to both `rules`, compare hashes - if equal and other properties equal, then Java doesn't know your Language))
* @property name same as translation file name with ' ', '_', '-', '(', ')' removed
* @property language ISO 639-1 code for the language
* @property country ISO 3166 code for the nation this is predominantly spoken in
* @property trueLanguage If set, used instead of language to trick Java into supplying a close-enough collator (a no-match would otherwise give us the default collator, not a collator for a partial match)
*/
enum class LocaleCode(val language: String, val country: String, val trueLanguage: String? = null) {
Afrikaans("af", "ZA"),
Arabic("ar", "IQ"),
Bangla("bn", "BD"),
Belarusian("be", "BY"),
Bosnian("bs", "BA"),
BrazilianPortuguese("pt", "BR"),
Bulgarian("bg", "BG"),
Catalan("ca", "ES"),
Croatian("hr", "HR"),
Czech("cs", "CZ"),
Danish("da", "DK"),
Dutch("nl", "NL"),
English("en", "US"),
Estonian("et", "EE"),
Finnish("fi", "FI"),
French("fr", "FR"),
Galician("gl", "ES"),
German("de", "DE"),
Greek("el", "GR"),
Hindi("hi", "IN"),
Hungarian("hu", "HU"),
Indonesian("in", "ID"),
Italian("it", "IT"),
Japanese("ja", "JP"),
Korean("ko", "KR"),
Latin("la", "IT"),
Latvian("lv", "LV"),
Lithuanian("lt", "LT"),
Malay("ms", "MY"),
Norwegian("no", "NO"),
NorwegianNynorsk("nn", "NO"),
PersianPinglishDIN("fa", "IR"), // These might just fall back to default
PersianPinglishUN("fa", "IR"),
Polish("pl", "PL"),
Portuguese("pt", "PT"),
Romanian("ro", "RO"),
Russian("ru", "RU"),
Rusyn("uk", "UA", "rus"), // No specific locale for rus exists, so use closest for collator
Serbian("sr", "RS"),
SimplifiedChinese("zh", "CN"),
Slovak("sk", "SK"),
Spanish("es", "ES"),
Swedish("sv", "SE"),
Thai("th", "TH"),
TraditionalChinese("zh", "TW"),
Turkish("tr", "TR"),
Ukrainian("uk", "UA"),
Vietnamese("vi", "VN"),
Zulu("zu", "ZA")
}
//endregion
//region Multiplayer-specific

View File

@ -0,0 +1,97 @@
package com.unciv.models.metadata
import java.text.NumberFormat
import java.util.Locale
/** Map Unciv language key to Java locale, for the purpose of getting a Collator for sorting.
* - Effect depends on the Java libraries and may not always conform to expectations.
* If in doubt, debug and see what Locale instance you get and compare its properties with `Locale.getDefault()`.
* (`Collator.getInstance(LocaleCode.*.run { Locale(language, country) }) to Collator.getInstance()`, drill to both `rules`, compare hashes - if equal and other properties equal, then Java doesn't know your Language))
* - For languages without an easy predefined Locale, collation or numeric formats can be forced using [Unicode Extensions for BCP 47](https://www.unicode.org/reports/tr35/#Locale_Extension_Key_and_Type_Data).
*
* @property name **Must** be the same as the translation file name with ' ', '_', '-', '(', ')' removed
* @property languageTag IETF BCP 47 language tag - see [forLanguageTag][Locale.forLanguageTag] or [Android reference][https://developer.android.com/reference/java/util/Locale#forLanguageTag(java.lang.String)]
* Usually the ISO 639-1 code for the language, a dash, and the ISO 3166 code for the nation this is predominantly spoken in
* @property fastlaneFolder If set, it's used instead of the language part of [languageTag] as fastlane folder name
*/
enum class LocaleCode(val languageTag: String, private val fastlaneFolder: String? = null) {
Afrikaans("af-ZA"),
Arabic("ar-IQ"),
Bangla("bn-BD"),
Belarusian("be-BY"),
Bosnian("bs-BA"),
BrazilianPortuguese("pt-BR"),
Bulgarian("bg-BG"),
Catalan("ca-ES"),
Croatian("hr-HR"),
Czech("cs-CZ"),
Danish("da-DK"),
Dutch("nl-NL"),
English("en-US"),
Estonian("et-EE"),
Finnish("fi-FI"),
French("fr-FR"),
Galician("gl-ES"),
German("de-DE"),
Greek("el-GR"),
Hindi("hi-IN"),
Hungarian("hu-HU"),
Indonesian("in-ID"),
Italian("it-IT"),
Japanese("ja-JP"),
Korean("ko-KR"),
Latin("la-IT"),
Latvian("lv-LV"),
Lithuanian("lt-LT"),
Malay("ms-MY"),
Norwegian("no-NO"),
NorwegianNynorsk("nn-NO"),
PersianPinglishDIN("fa-IR"), // These might just fall back to default
PersianPinglishUN("fa-IR"),
Polish("pl-PL"),
Portuguese("pt-PT"),
Romanian("ro-RO"),
Russian("ru-RU"),
Rusyn("rue-SK-u-kr-cyrl-latn-digit", "rue"), // No specific locale exists, so use explicit cyrillic collation. Chose country with most speakers.
Serbian("sr-RS"),
SimplifiedChinese("zh-CN"),
Slovak("sk-SK"),
Spanish("es-ES"),
Swedish("sv-SE"),
Thai("th-TH"),
TraditionalChinese("zh-TW"),
Turkish("tr-TR"),
Ukrainian("uk-UA"),
Vietnamese("vi-VN"),
Zulu("zu-ZA")
;
fun locale(): Locale = Locale.forLanguageTag(languageTag)
fun fastlaneFolder(): String = this.fastlaneFolder ?: locale().language
companion object {
private val bannedCharacters = listOf(' ', '_', '-', '(', ')') // Things not to have in enum names
/** Find a LocaleCode for a [language] as stored in GameSettings */
fun find(language: String): LocaleCode? {
val languageName = language.filterNot { it in bannedCharacters }
return LocaleCode.entries.firstOrNull { it.name == languageName }
}
/** Get a Java Locale for a [language] as stored in GameSettings */
fun getLocale(language: String): Locale =
find(language)?.locale() ?: Locale.getDefault()
/** Get the fastlane folder name for a [language] as stored in GameSettings */
fun fastlaneFolder(language: String) =
find(language)?.fastlaneFolder() ?: "en"
// NumberFormat cache, key: language, value: NumberFormat
private val languageToNumberFormat = mutableMapOf<String, NumberFormat>()
fun getNumberFormatFromLanguage(language: String): NumberFormat =
languageToNumberFormat.getOrPut(language) {
NumberFormat.getInstance(getLocale(language))
}
}
}

View File

@ -8,7 +8,7 @@ import com.unciv.json.json
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.models.SpyAction
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameSettings.LocaleCode
import com.unciv.models.metadata.LocaleCode
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Event
@ -558,8 +558,7 @@ object TranslationFileWriter {
!endWithNewline && translated.endsWith('\n') -> translated.removeSuffix("\n")
else -> translated
}
val localeCode = LocaleCode.valueOf(language.replace("_",""))
val path = fastlanePath + (localeCode.trueLanguage ?: localeCode.language)
val path = fastlanePath + LocaleCode.fastlaneFolder(language)
File(path).mkdirs()
File(path + File.separator + fileName).writeText(fileContent)
}

View File

@ -3,7 +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.metadata.LocaleCode
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.stats.Stat
@ -14,7 +14,6 @@ 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.
@ -209,11 +208,8 @@ class Translations : LinkedHashMap<String, TranslationEntry>() {
return getText(englishConditionalOrderingString, language, null)
}
fun placeConditionalsAfterUnique(language: String): Boolean {
if (get(conditionalUniqueOrderString, language, null)?.get(language) == "before")
return false
return true
}
fun placeConditionalsAfterUnique(language: String) =
get(conditionalUniqueOrderString, language, null)?.get(language) != "before"
/** Returns the equivalent of a space in the given language
* Defaults to a space if no translation is provided
@ -239,26 +235,6 @@ class Translations : LinkedHashMap<String, TranslationEntry>() {
const val conditionalUniqueOrderString = "ConditionalsPlacement"
const val shouldCapitalizeString = "StartWithCapitalLetter"
const val effectBeforeCause = "EffectBeforeCause"
// NumberFormat cache, key: language, value: NumberFormat
private val languageToNumberFormat = mutableMapOf<String, NumberFormat>()
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))
}
}
}
@ -572,5 +548,5 @@ fun Number.tr(): String {
// formats number according to given language
fun Number.tr(language: String): String {
return Translations.getNumberFormatFromLanguage(language).format(this)
return LocaleCode.getNumberFormatFromLanguage(language).format(this)
}

View File

@ -1,14 +0,0 @@
Удтвореня майпопуларної бавкы за створьованя цвілізацій фрішноє, манінькоє, без рекламы, навхтема бесплатноє
Стройте свою цівілізацію, изучайте технолоґії, росширяйте свої території, сказіт тай дотовчіт своїх ворогув!
Понуканя? Багы? Испис задач сьої бавкы увидьте туйкы https://github.com/yairm210/Unciv/issues, ушитка помуч дуже ся чистує!
Звіданкы? Коментарії? Просто лудно? Прикапчуйте ся ид нам у діскордови https://discord.gg/bjrB4Xw.
Хотіли бы-сьте помочи перекласти бавку вашым языком? Напишіт ми мейл на yairm210@hotmail.com.
Знаєте за Grok Java авадь Kotlin? Пойте сюды https://github.com/yairm210/Unciv.
Світ вас чекат! Ци вчините из свойої цівілізації імперію, яка утримат провбу часом?
Позволеня на доступ ид сіті мусай дати про ініціовані бавлячом стирьханя тай про мультіплеєрноє бавліня. Ушиткі другі позволеня самі ся додавут API, якый изхосновали-сьме використали про уповісткы за ходы у совмістнуй бавці. Позволеня на доступ ид интернетови мусай дати, обы пак побрати си моды тай музыку, а ще для коректного ладованя мултіплеєрных бавок. Ушиткі другі міньбы не ініціювут ся ігров Unciv.

View File

@ -1 +0,0 @@
4X-стратеґія за створьованя цівілізацій

View File

@ -5,7 +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.metadata.LocaleCode
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.stats.Stats