From cec9975b4d0d7dbfa8a709e0d502547d9dc828da Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Tue, 3 May 2022 09:16:22 +0200 Subject: [PATCH] Implement fastlane step 2: Translating (#6643) * Implement fastlane step 2: Translating * Placing the Font family Label before the Thread is nicer --- .../jsons/translations/German.properties | 12 +++ .../jsons/translations/template.properties | 11 +++ .../translations/TranslationFileWriter.kt | 47 ++++++++- .../ui/worldscreen/mainmenu/OptionsPopup.kt | 97 +++++++++++-------- docs/Other/Translating.md | 14 ++- 5 files changed, 133 insertions(+), 48 deletions(-) diff --git a/android/assets/jsons/translations/German.properties b/android/assets/jsons/translations/German.properties index 99d4e6cdc6..b2d9a64d21 100644 --- a/android/assets/jsons/translations/German.properties +++ b/android/assets/jsons/translations/German.properties @@ -10,6 +10,18 @@ StartWithCapitalLetter = true +# Fastlane +# These will be automatically copied to the fastlane descriptions used by F-Droid. Their keys are not as usual the english original, please read those directly as linked. + +# Documentation: https://f-droid.org/en/docs/Build_Metadata_Reference/#Summary +# English to translate: https://github.com/yairm210/Unciv/blob/master/fastlane/metadata/android/en-US/short_description.txt +Fastlane_short_description = 4X-Spiel: Zivilisationen bauen + +# Documentation: https://f-droid.org/en/docs/Build_Metadata_Reference/#Description +# English to translate: https://github.com/yairm210/Unciv/blob/master/fastlane/metadata/android/en-US/full_description.txt +Fastlane_full_description = Eine Neuimplementierung vom berühmtesten Zivilisations-Aufbau-Spiel aller Zeiten. Schnell, klein, Spam-los und für immer frei!\n\nBaue Deine Zivilisation auf, erforsche Technologien, entwickle Deine Städte und besiege Deine Kontrahenten!\n\nWünsche? Bugs? Die Liste der zu erledigenden Probleme is die [github issues - Seite](https://github.com/yairm210/UnCiv/issues) - jede noch so kleine Hilfe ist willkommen!\n\nFragen? Kommentare? Einfach nur gelangweilt? Chatte mit uns auf [Discord](https://discord.gg/bjrB4Xw).\n\nWillst Du bei der Übersetzung helfen? Schau auf [dem Wiki](https://yairm210.github.io/Unciv/Other/Translating/), wie man Übersetzungen vorschlägt.\n\nSchnallst Du Kotlin oder Java? Mach bei der Entwicklung im [github repository](https://github.com/yairm210/UnCiv) mit.\n\nDie Welt erwartet Dich! Wirst Du Deine Zivilisations zu einem Imperium entwickeln, das dem Zahn der Zeit standhält?\n\nDie App-Berechtigung 'Auf alle Netzwerke zugreifen' wird benötigt für vom Benutzer initiierte Downloads und das Online Mehrspieler-Feature. Alle anderen aufgeführten Berechtigungen werden vom API automatisch hinzugefügt durch die Implementierung der Zug-Benachrichtigungen für das Mehrspieler-Feature. Netzwerk-Berechtigungen werden verwendet für das Abfragen verfügbarer Mods und deren Download, den Download von Musik und das Hoch-und Herunterladen der Spielstände für Online Mehrspieler. Unciv initiiert keine anderen Verbindungen ins Internet. + + # Starting from here normal translations start, as described in # https://yairm210.github.io/Unciv/Other/Translating/ diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index e112103bb5..092123745d 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -10,6 +10,17 @@ StartWithCapitalLetter = +# Fastlane +# These will be automatically copied to the fastlane descriptions used by F-Droid. Their keys are not as usual the english original, please read those directly as linked. + +# Documentation: https://f-droid.org/en/docs/Build_Metadata_Reference/#Summary +# English to translate: https://github.com/yairm210/Unciv/blob/master/fastlane/metadata/android/en-US/short_description.txt +Fastlane_short_description = + +# Documentation: https://f-droid.org/en/docs/Build_Metadata_Reference/#Description +# English to translate: https://github.com/yairm210/Unciv/blob/master/fastlane/metadata/android/en-US/full_description.txt +Fastlane_full_description = + # Starting from here normal translations start, as described in # https://yairm210.github.io/Unciv/Other/Translating/ diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index dc51cd9712..4b4a0710b8 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Array import com.unciv.JsonParser import com.unciv.models.metadata.BaseRuleset +import com.unciv.models.metadata.LocaleCode import com.unciv.models.ruleset.* import com.unciv.models.ruleset.tech.TechColumn import com.unciv.models.ruleset.tile.Terrain @@ -14,6 +15,7 @@ import com.unciv.models.ruleset.unique.* import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.UnitType +import java.io.File import java.lang.reflect.Field import java.lang.reflect.Modifier @@ -22,7 +24,14 @@ object TranslationFileWriter { private const val specialNewLineCode = "# This is an empty line " const val templateFileLocation = "jsons/translations/template.properties" private const val languageFileLocation = "jsons/translations/%s.properties" + private const val shortDescriptionKey = "Fastlane_short_description" + private const val shortDescriptionFile = "short_description.txt" + private const val fullDescriptionKey = "Fastlane_full_description" + private const val fullDescriptionFile = "full_description.txt" + // Current dir on desktop should be assets, so use two '..' get us to project root + private const val fastlanePath = "../../fastlane/metadata/android/" + //region Update translation files fun writeNewTranslationFiles(): String { try { val translations = Translations() @@ -38,7 +47,8 @@ object TranslationFileWriter { writeLanguagePercentages(modPercentages, modFolder) // unused by the game but maybe helpful for the mod developer } - return "Translation files are generated successfully." + return "Translation files are generated successfully.\n" + + writeTranslatedFastlaneFiles(translations) } catch (ex: Throwable) { ex.printStackTrace() return ex.localizedMessage ?: ex.javaClass.simpleName @@ -464,4 +474,39 @@ object TranslationFileWriter { } } } + + //endregion + //region Fastlane + + /** This writes translated short_description.txt and full_description.txt files into the Fastlane structure. + * @param [translations] A [Translations] instance with all languages loaded. + * @return Success or error message. + */ + private fun writeTranslatedFastlaneFiles(translations: Translations): String { + return try { + writeFastlaneFiles(shortDescriptionFile, translations[shortDescriptionKey], false) + writeFastlaneFiles(fullDescriptionFile, translations[fullDescriptionKey], true) + "Fastlane files are generated successfully." + } catch (ex: Throwable) { + ex.printStackTrace() + ex.localizedMessage ?: ex.javaClass.simpleName + } + } + + private fun writeFastlaneFiles(fileName: String, translationEntry: TranslationEntry?, endWithNewline: Boolean) { + if (translationEntry == null) return + for ((language, translated) in translationEntry) { + val fileContent = when { + endWithNewline && !translated.endsWith('\n') -> translated + '\n' + !endWithNewline && translated.endsWith('\n') -> translated.removeSuffix("\n") + else -> translated + } + val localeCode = LocaleCode.valueOf(language) + val path = fastlanePath + localeCode.language + File(path).mkdirs() + File(path + File.separator + fileName).writeText(fileContent) + } + } + + //endregion } diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index 9cc3b03875..69d178f648 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -4,10 +4,8 @@ import com.badlogic.gdx.Application import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.ui.Label -import com.badlogic.gdx.scenes.scene2d.ui.SelectBox -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.badlogic.gdx.scenes.scene2d.ui.TextField +import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.MainMenuScreen @@ -88,12 +86,8 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { tabs.addPage("Language", getLanguageTab(), ImageGetter.getImage("FlagIcons/${settings.language}"), 24f) tabs.addPage("Sound", getSoundTab(), ImageGetter.getImage("OtherIcons/Speaker"), 24f) tabs.addPage("Multiplayer", getMultiplayerTab(), ImageGetter.getImage("OtherIcons/Multiplayer"), 24f) - crashHandlingThread(name="Add Advanced Options Tab") { - val fontNames = Fonts.getAvailableFontFamilyNames() // This is a heavy operation and causes ANRs - postCrashHandlingRunnable { - tabs.addPage("Advanced", getAdvancedTab(fontNames), ImageGetter.getImage("OtherIcons/Settings"), 24f) - } - } + tabs.addPage("Advanced", getAdvancedTab(), ImageGetter.getImage("OtherIcons/Settings"), 24f) + if (RulesetCache.size > BaseRuleset.values().size) { val content = ModCheckTab(this) { if (modCheckFirstRun) runModChecker() @@ -305,7 +299,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { }).row() } - private fun getAdvancedTab(fontNames: Collection) = Table(BaseScreen.skin).apply { + private fun getAdvancedTab() = Table(BaseScreen.skin).apply { pad(10f) defaults().pad(5f) @@ -326,7 +320,7 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { } } - addFontFamilySelect(fontNames) + addFontFamilySelect() addTranslationGeneration() @@ -863,48 +857,65 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) { add(maxZoomSlider).pad(5f).row() } - private fun Table.addFontFamilySelect(fonts: Collection) { - if (fonts.isEmpty()) return - + private fun Table.addFontFamilySelect() { add("Font family".toLabel()).left().fillX() + val selectCell = add() + row() - val fontSelectBox = SelectBox(skin) - val fontsLocalName = GdxArray().apply { add("Default Font".tr()) } - val fontsEnName = GdxArray().apply { add("") } - for (font in fonts) { - fontsLocalName.add(font.localName) - fontsEnName.add(font.enName) + fun loadFontSelect(fonts: Collection, selectCell: Cell) { + if (fonts.isEmpty()) return + + val fontSelectBox = SelectBox(skin) + val fontsLocalName = GdxArray().apply { add("Default Font".tr()) } + val fontsEnName = GdxArray().apply { add("") } + for (font in fonts) { + fontsLocalName.add(font.localName) + fontsEnName.add(font.enName) + } + + val selectedIndex = fontsEnName.indexOf(settings.fontFamily).let { if (it == -1) 0 else it } + + fontSelectBox.items = fontsLocalName + fontSelectBox.selected = fontsLocalName[selectedIndex] + + selectCell.setActor(fontSelectBox).minWidth(selectBoxMinWidth).pad(10f) + + fontSelectBox.onChange { + settings.fontFamily = fontsEnName[fontSelectBox.selectedIndex] + ToastPopup( + "You need to restart the game for this change to take effect.", previousScreen + ) + } } - val selectedIndex = fontsEnName.indexOf(settings.fontFamily).let { if (it == -1) 0 else it } - - fontSelectBox.items = fontsLocalName - fontSelectBox.selected = fontsLocalName[selectedIndex] - - add(fontSelectBox).minWidth(selectBoxMinWidth).pad(10f).row() - - fontSelectBox.onChange { - settings.fontFamily = fontsEnName[fontSelectBox.selectedIndex] - ToastPopup( - "You need to restart the game for this change to take effect.", previousScreen - ) + crashHandlingThread(name="Add Font Select") { + val fonts = Fonts.getAvailableFontFamilyNames() // This is a heavy operation and causes ANRs + postCrashHandlingRunnable { loadFontSelect(fonts, selectCell) } } } private fun Table.addTranslationGeneration() { - if (Gdx.app.type == Application.ApplicationType.Desktop) { - val generateTranslationsButton = "Generate translation files".toTextButton() - val generateAction = { + if (Gdx.app.type != Application.ApplicationType.Desktop) return + + val generateTranslationsButton = "Generate translation files".toTextButton() + + val generateAction: ()->Unit = { + tabs.selectPage("Advanced") + generateTranslationsButton.setText("Working...".tr()) + crashHandlingThread { val result = TranslationFileWriter.writeNewTranslationFiles() - // notify about completion - generateTranslationsButton.setText(result.tr()) - generateTranslationsButton.disable() + postCrashHandlingRunnable { + // notify about completion + generateTranslationsButton.setText(result.tr()) + generateTranslationsButton.disable() + } } - generateTranslationsButton.onClick(generateAction) - keyPressDispatcher[Input.Keys.F12] = generateAction - generateTranslationsButton.addTooltip("F12",18f) - add(generateTranslationsButton).colspan(2).row() } + + generateTranslationsButton.onClick(generateAction) + keyPressDispatcher[Input.Keys.F12] = generateAction + generateTranslationsButton.addTooltip("F12",18f) + add(generateTranslationsButton).colspan(2).row() } private fun Table.addCheckbox(text: String, initialState: Boolean, updateWorld: Boolean = false, action: ((Boolean) -> Unit)) { diff --git a/docs/Other/Translating.md b/docs/Other/Translating.md index 7b0af5bcb8..516d5410d7 100644 --- a/docs/Other/Translating.md +++ b/docs/Other/Translating.md @@ -12,6 +12,14 @@ You don't need to download anything, all translation work can be done on the Git When you feel that you're ready to add your translation to the game, you'll need to create a merge request, which takes your changes and puts them into the main version of the game - it's pretty straightforward once you do it +## App store text + +There are two special entries that won't show in the game but are automatically used to provide short and long descriptions for F-Droid (and possibly other stores soon). They're near the beginning of each language file and marked "Fastlane". See the comments just above each for help, and where to find the actual english original to translate. Do not overlook the note on line breaks in [Other notes](#Other_notes) for the full description! + +## App store images + +The stores can show screenshots. To show translated versions of these images a different approach is necessary: they must be merged into appropriate subfolders of [fastlane/metadata/android] in the Unciv repository. If in doubt on how to do this, look at the existing ones for the proper dimensions and offer your version using an issue. Hints: relative paths and names must match the 'en-US' subfolder with 'en-US' replaced with the _two-letter_ ISO code of your language. You can use Add file - Upload files if the folder you need already exits. Using the github site to create a PR with new folders is possible, but outside the scope of this document. + ## Pitfalls - If a translation template (the stuff to the left of "` = `") contains square brackets, you will have to include each of them _verbatim_ in your translation, but you can move them. Upper/lower case is relevant! e.g. `All [personFilter] are cool` can be translated as `Tous les [personFilter] sont cool`, but ***not*** as `Tous les [personnages] sont cool`, and neither as `Nous sommes vraiment cool`. Failing this is the main cause of your PR's showing up with red "x"es and "checks failed". @@ -44,12 +52,10 @@ Each untranslated phrase will have a "requires translation" line before it, so y Order of lines does not matter, they will be rearranged automatically each release. -When Unciv no longer needs a translation entry, it will be moved to the end and prefixed with `#~~`. Those removed lines will survive automatic processes, so please check them and remove those where you are sure they will indeed no longer be needed. They exist for cases where e.g. a detail in the english source version needed changing - that will invalidate existing translations, but now you can recover previous versions and adapt them to the new template. - -You can leave very limited comments to yourself or other translators - prefix comments with `#~~# = ` (assign sequential numbers to that `` placeholder) and place them at the end of the file. The comment style you see in the rest of the file is reserved for internal use, and placing such comments in earlier parts will lead to them being moved to the end by the next release. - Do as much as you're comfortable with - it's a big game with a lot of named objects, so don't feel pressured into doing everything =) +Some entries have line breaks expressed as `\n`: Your translation can and in most cases should use them as well, but you do not need to distribute them exactly as in the original. Try to find a translation that reads nicely, then place the line break codes at roughly the same intervals as the original uses (less if your language's glyphs are wider than latin ones). Important: You cannot use normal line breaks, you must use the `\n` codes, normal line breaks are not part of a translation. + Note that Right-to-Left languages such as Arabic and Hebrew are not supported by the framework :/ # Translation generation - for developers