diff --git a/source/funkin/backend/FunkinText.hx b/source/funkin/backend/FunkinText.hx index 6da272f9..84cfe407 100644 --- a/source/funkin/backend/FunkinText.hx +++ b/source/funkin/backend/FunkinText.hx @@ -5,7 +5,7 @@ import flixel.text.FlxText; class FunkinText extends FlxText { public function new(X:Float = 0, Y:Float = 0, FieldWidth:Float = 0, ?Text:String, Size:Int = 16, Border:Bool = true, ?TranslationID:String, ?TranslParameters:Array) { - super(X, Y, FieldWidth, TranslationsUtil.getTransl(Text, TranslationID, TranslParameters), Size); + super(X, Y, FieldWidth, TranslationsUtil.get(Text, TranslationID, TranslParameters), Size); setFormat(Paths.font("vcr.ttf"), Size, FlxColor.WHITE); if (Border) { borderStyle = OUTLINE; diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index 461a7e8e..f9aef3a0 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -55,7 +55,7 @@ class MainState extends FlxState { Main.refreshAssets(); ModsFolder.onModSwitch.dispatch(ModsFolder.currentModFolder); - TranslationsUtil.setTransl(); + TranslationsUtil.setLanguage(); DiscordUtil.init(); EventsData.reloadEvents(); TitleState.initialized = false; @@ -69,7 +69,10 @@ class MainState extends FlxState { #if sys sys.FileSystem.createDirectory('./.temp/'); - #if windows new funkin.backend.utils.native.HiddenProcess("attrib +h .temp"); #end + #if windows + try new funkin.backend.utils.native.HiddenProcess("attrib +h .temp") + catch(e) Logs.trace('Failed to set hidden attribute on .temp folder', WARNING); + #end #end } } \ No newline at end of file diff --git a/source/funkin/backend/utils/TranslationsUtil.hx b/source/funkin/backend/utils/TranslationsUtil.hx index d9305f24..fc040acc 100644 --- a/source/funkin/backend/utils/TranslationsUtil.hx +++ b/source/funkin/backend/utils/TranslationsUtil.hx @@ -17,7 +17,12 @@ class TranslationsUtil * * Using this class function it'll never be `null`. */ - public static var transMap(default, null):Map = []; + public static var transMap(default, set):Map = []; + + @:noCompletion private static function set_transMap(value:Map):Map { + if (value == null) value = []; + return value; + } /** * The default language used inside of the source code. @@ -38,21 +43,25 @@ class TranslationsUtil /** * Returns if the current language is the default one (`DEFAULT_LANGUAGE`). */ - inline public static function is_defaultLanguage():Bool + public static var isDefaultLanguage(get, never):Bool; + @:noCompletion private static function get_isDefaultLanguage():Bool return Options.language == DEFAULT_LANGUAGE; /** - * Returns if any translation is loaded. + * Returns if any translation are loaded. */ - inline public static function is_anyTransLoaded():Bool - return transMap != []; + public static var isAnyTransLoaded(get, never):Bool; + @:noCompletion private static function get_isAnyTransLoaded():Bool + return Lambda.count(transMap) > 0; /** - * Changes the translations map. + * Updates the language. + * Also changes the translations map. * - * If `name` is `null`, it's gonna use the current language. + * If `name` is `null`, its gonne reload current language. + * If `name` is not `null`, it will load the translations for the given language. */ - public static function setTransl(?name:String) + public static function setLanguage(?name:String) transMap = loadLanguage(name == null ? curLanguage : name); /** @@ -60,38 +69,38 @@ class TranslationsUtil * * If `id` is `null` then it's gonna search using `defString`. */ - public static function getTransl(defString:String, ?id:String, ?parms:Array):String + public static function get(defString:String, ?id:String, ?parms:Array):String { - if(parms == null) parms = []; + if (parms == null) parms = []; #if TRANSLATIONS_SUPPORT - if(id == null) id = defString; - if(transMap.exists(id)) return transMap.get(id).format(parms); + if (id == null) id = defString; + if (transMap.exists(id)) return transMap.get(id).format(parms); #end - return new FormatInfo(defString).format(parms); + return FormatUtil.get(defString).format(parms); } /** - * Returns an array that specifies which translations were found. + * Returns an array that specifies which languages were found. */ - public static function translList():Array + public static function getLanguages():Array { - var translations:Array = []; + var languages:Array = []; #if TRANSLATIONS_SUPPORT var main:String = Paths.translationsMain(''); - for(l in Paths.assetsTree.getFolders(main)) for(f in Paths.assetsTree.getFiles(main + l)) - if(Path.extension('$l/$f') == "xml") translations.push(Path.withoutExtension('$l/$f')); + for (l in Paths.assetsTree.getFolders(main)) for (f in Paths.assetsTree.getFiles(main + l)) + if (Path.extension('$l/$f') == "xml") languages.push(Path.withoutExtension('$l/$f')); #end - return translations; + return languages; } /** * Returns a map of translations based on its XML. */ - public static function loadLanguage(name:String):Map + public static function loadLanguage(name:String):Map { #if TRANSLATIONS_SUPPORT var path:String = Paths.translationsMain(name); - if(!path.endsWith(".xml")) path += ".xml"; + if (!path.endsWith(".xml")) path += ".xml"; if (!Assets.exists(path, TEXT)) return []; var xml:Access = null; @@ -107,7 +116,8 @@ class TranslationsUtil return []; } - var leMap:Map = []; + FormatUtil.clear(); // Clean up the format cache + var leMap:Map = []; for(node in xml.node.translations.elements) { switch(node.name) { case "trans": @@ -115,11 +125,10 @@ class TranslationsUtil FlxG.log.warn("A translation node requires an ID attribute."); continue; } - if (!node.has.string) { - FlxG.log.warn("A translation node requires a string attribute."); - continue; - } - leMap.set(node.att.id, new FormatInfo(node.att.string)); + + var string:String = node.has.string ? node.att.string : node.x.nodeValue; + string = string.replace("\\n", "\n").replace("\r", ""); // remove stupid windows line breaks and convert newline literals to newlines + leMap.set(node.att.id, FormatUtil.get(string)); } } return leMap; @@ -138,37 +147,82 @@ indexes: [1, 0] strings[0] + values[indexes[0]] + strings[1] + values[indexes[1]] + strings[2] */ -class FormatInfo { +class FormatUtil { + private static var cache:Map = new Map(); + + public static function get(id:String):IFormatInfo { + if (cache.exists(id)) + return cache.get(id); + + var fi:IFormatInfo = ParamFormatInfo.returnOnlyIfValid(id); + if(fi == null) fi = new StrFormatInfo(id); + cache.set(id, fi); + return fi; + } + + public inline static function clear() { + cache.clear(); + } +} + +class StrFormatInfo implements IFormatInfo { + public var string:String; + + public function new(str:String) { + this.string = str; + } + + public function format(values:Array):String { + return string; + } +} + +// TODO: add support for @:({0}==1?(Hello):(World)) +class ParamFormatInfo implements IFormatInfo { public var strings:Array = []; public var indexes:Array = []; public function new(str:String) { var i = 0; - while(i < str.length) { + while (i < str.length) { var fi = str.indexOf("{", i); // search from the start of i - if(fi == -1) { - strings.push(str.substring(i)); + if (fi == -1) { + // if there are no more parameters, just add the rest of the string + this.strings.push(str.substring(i)); break; } var fe = str.indexOf("}", fi); - strings.push(str.substring(i, fi)); - indexes.push(Std.parseInt(str.substring(fi+1, fe))); + this.strings.push(str.substring(i, fi)); + this.indexes.push(Std.parseInt(str.substring(fi+1, fe))); i = fe + 1; } } + public static function isValid(str:String):Bool { + var fi = new ParamFormatInfo(str); + return fi.indexes.length > 0; + } + public static function returnOnlyIfValid(str:String):Bool { + var fi = new ParamFormatInfo(str); + return fi.indexes.length > 0 ? fi : null; + } + public function format(values:Array):String { var str:String = ""; - for(i=>s in strings) { + for (i=>s in strings) { str += s; - if(i < indexes.length) + if (i < indexes.length) str += values[indexes[i]]; } return str; } +} + +interface IFormatInfo { + public function format(values:Array):String; } \ No newline at end of file diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index a1a1114e..78f54f25 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -347,10 +347,10 @@ class FreeplayState extends MusicBeatState * Array containing all labels for Co-Op / Opponent modes. */ public var coopLabels:Array = [ - TranslationsUtil.getTransl("[TAB] Solo"), - TranslationsUtil.getTransl("[TAB] Opponent Mode"), - TranslationsUtil.getTransl("[TAB] Co-Op Mode"), - TranslationsUtil.getTransl("[TAB] Co-Op Mode (Switched)") + TranslationsUtil.get("[TAB] Solo"), + TranslationsUtil.get("[TAB] Opponent Mode"), + TranslationsUtil.get("[TAB] Co-Op Mode"), + TranslationsUtil.get("[TAB] Co-Op Mode (Switched)") ]; /** diff --git a/source/funkin/menus/ui/Alphabet.hx b/source/funkin/menus/ui/Alphabet.hx index b9e274ea..becebce1 100644 --- a/source/funkin/menus/ui/Alphabet.hx +++ b/source/funkin/menus/ui/Alphabet.hx @@ -81,7 +81,7 @@ class Alphabet extends FlxSpriteGroup { super(x, y); - _finalText = this.text = TranslationsUtil.getTransl(text, transID, transParams); + _finalText = this.text = TranslationsUtil.get(text, transID, transParams); isBold = bold; var alphabetPath = Paths.xml("alphabet"); diff --git a/source/funkin/options/categories/MiscOptions.hx b/source/funkin/options/categories/MiscOptions.hx index 3b4fa5e3..858524b6 100644 --- a/source/funkin/options/categories/MiscOptions.hx +++ b/source/funkin/options/categories/MiscOptions.hx @@ -4,18 +4,32 @@ package funkin.options.categories; class MiscOptions extends OptionsScreen { public override function new() { super("Miscellaneous", "Use this menu to reset save data or engine settings."); - var lanArray:Array = TranslationsUtil.translList(); - if(!lanArray.contains(TranslationsUtil.DEFAULT_LANGUAGE)) lanArray.push(TranslationsUtil.DEFAULT_LANGUAGE); - add(new ArrayOption( - "Language", - 'The language that the engine currently uses (the default one is ${TranslationsUtil.DEFAULT_LANGUAGE}).', - lanArray, [for(lan in lanArray) lan.split("/").last()], "language", - function(path:String) { - TranslationsUtil.setTransl(path); - parent.remove(this); - this.clear(); - parent.add(new MiscOptions()); + + { // Language Option + var lanArray:Array = TranslationsUtil.getLanguages(); + if (lanArray.contains(TranslationsUtil.DEFAULT_LANGUAGE)) + lanArray.remove(TranslationsUtil.DEFAULT_LANGUAGE); + lanArray.insert(0, TranslationsUtil.DEFAULT_LANGUAGE); + + add(new ArrayOption( + "Language", + 'The language that the engine currently uses (the default one is ${TranslationsUtil.DEFAULT_LANGUAGE}).', + lanArray, [for(lan in lanArray) lan.split("/").last()], "language", + function(path:String) { + TranslationsUtil.setLanguage(path); + parent.remove(this); + // Reload the current screen + // todo add parent.reload(); + this.clear(); + parent.add(new MiscOptions()); })); + } + + add(new TextOption( + "Force Crash", + "Select this option to force a crash.", + () -> { throw new haxe.Exception("Forced crash."); } + )); #if UPDATE_CHECKING add(new Checkbox( "Enable Nightly Updates",