Files
CodenameEngine/source/funkin/backend/utils/TranslationUtil.hx
T
2024-04-10 16:06:03 +02:00

243 lines
7.0 KiB
Haxe

package funkin.backend.utils;
import funkin.backend.assets.TranslatedAssetLibrary;
import funkin.backend.assets.ModsFolderLibrary;
import funkin.backend.assets.ModsFolder;
import funkin.backend.assets.IModsAssetLibrary;
import funkin.backend.assets.ZipFolderLibrary;
import openfl.utils.Assets;
import haxe.io.Path;
import haxe.xml.Access;
import haxe.Exception;
/**
* The class used for translations based on the XMLs inside the translations folders.
*
* Made by @NexIsDumb originally for the Poldhub mod.
*/
class TranslationUtil
{
/**
* The current language selected translation map; If the current language it's english, this map will be empty.
*
* It'll never be `null`.
*/
public static var transMap(default, set):Map<String, IFormatInfo> = [];
@:noCompletion private static function set_transMap(value:Map<String, IFormatInfo>):Map<String, IFormatInfo> {
if (value == null) value = [];
return transMap = value;
}
/**
* The default language used inside of the source code.
*/
public static var DEFAULT_LANGUAGE:String = 'en/English'; // no inline so psycopathic mods can edit it - Nex
/**
* Returns the current language.
*/
public static var curLanguage(get, set):String;
@:noCompletion private static function get_curLanguage():String {
return Options.language;
}
@:noCompletion private static function set_curLanguage(value:String):String {
return Options.language = value;
}
/**
* Returns if the current language is the default one (`DEFAULT_LANGUAGE`).
*/
public static var isDefaultLanguage(get, never):Bool;
@:noCompletion private static function get_isDefaultLanguage():Bool
return Options.language == DEFAULT_LANGUAGE;
/**
* Returns if any translation are loaded.
*/
public static var isAnyTransLoaded(get, never):Bool;
@:noCompletion private static function get_isAnyTransLoaded():Bool
return Lambda.count(transMap) > 0;
/**
* Updates the language.
* Also changes the translations map.
*
* If `name` is `null`, its gonna use the current language.
* If `name` is not `null`, it will load the translations for the given language.
*/
public static function setLanguage(?name:String) {
#if TRANSLATIONS_SUPPORT
if(name == null) name = curLanguage;
name = name.split("/")[0];
transMap = loadLanguage(name);
for(mod in ModsFolder.getLoadedModsLibs()) if(mod is TranslatedAssetLibrary)
cast(mod, TranslatedAssetLibrary).langFolder = name.split("/")[0];
#end
}
/**
* This is for checking and getting a translation, `defString` it's just the string that gets returned just in case it won't find the translation OR the current language selected is ``DEFAULT_LANGUAGE``.
*
* If `id` is `null` then it's gonna search using `defString`.
*/
public static function get(defString:String, ?id:String, ?params:Array<Dynamic>):String
{
#if TRANSLATIONS_SUPPORT
if (id == null) id = defString;
if (transMap.exists(id)) return transMap.get(id).format(params);
#end
return FormatUtil.get(defString).format(params);
}
/**
* Returns an array that specifies which languages were found.
*/
public static function getLanguages():Array<String>
{
var languages:Array<String> = [];
#if TRANSLATIONS_SUPPORT
var main:String = Paths.translationsMain('');
for (l in Paths.assetsTree.getFolders(main)) {
var path:String = main + l + "/config.ini";
if(Assets.exists(path)) languages.push('$l/' + IniUtil.parseAsset(path, ["name" => l])["name"]);
else for(f in Paths.assetsTree.getFiles(main + l)) if(Path.extension(path = '$l/$f') == "xml") {
languages.push(Path.withoutExtension(path));
break; // only gets the first xml's name if theres no config.ini! - Nex
}
}
#end
return languages;
}
/**
* Returns a map of translations based on its XML.
*/
public static function loadLanguage(folderName:String):Map<String, IFormatInfo>
{
#if TRANSLATIONS_SUPPORT
var mainPath:String = Paths.translationsMain(folderName);
var leMap:Map<String, IFormatInfo> = [];
for(file in Paths.getFolderContent(mainPath)) if(Path.extension(file) == "xml") {
var xml:Access = null;
try xml = new Access(Xml.parse(Assets.getText('$mainPath/$file')))
catch(e) {
var msg:String = 'Error while parsing $file: ${Std.string(e)}';
FlxG.log.error(msg);
throw new Exception(msg);
}
if (xml == null) return [];
if (!xml.hasNode.translations) {
Logs.trace("A translation xml file requires a translations root element.", WARNING, YELLOW);
return [];
}
FormatUtil.clear(); // Clean up the format cache
for(node in xml.node.translations.elements) {
switch(node.name) {
case "trans":
if (!node.has.id) {
FlxG.log.warn("A translation node requires an ID attribute.");
continue;
}
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;
#else
return [];
#end
}
}
/**
* The class used to format strings based on parameters.
*
* For example if the parameter list is just an `Int` which is `9`, `You have been blue balled {0} times` becomes `You have been blue balled 9 times`.
*/
class FormatUtil {
private static var cache:Map<String, IFormatInfo> = 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(params:Array<Dynamic>):String {
return string;
}
}
// TODO: add support for @:({0}==1?(Hello):(World))
class ParamFormatInfo implements IFormatInfo {
public var strings:Array<String> = [];
public var indexes:Array<Int> = [];
public function new(str:String) {
var i = 0;
while (i < str.length) {
var fi = str.indexOf("{", i); // search from the start of 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);
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):IFormatInfo {
var fi = new ParamFormatInfo(str);
return fi.indexes.length > 0 ? fi : null;
}
public function format(params:Array<Dynamic>):String {
if (params == null) params = [];
var str:String = "";
for (i=>s in strings) {
str += s;
if (i < indexes.length)
str += params[indexes[i]];
}
return str;
}
}
interface IFormatInfo {
public function format(params:Array<Dynamic>):String;
}