1 /* 2 * Copyright (c) 2017-2018 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: 2017-2018 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/selery/source/selery/lang.d, selery/lang.d) 28 */ 29 module selery.lang; 30 31 import std.algorithm : canFind; 32 import std.array : Appender; 33 import std.conv : to, ConvException; 34 import std.file : exists, read; 35 import std.json : parseJSON; 36 import std.path : dirSeparator; 37 import std.string; 38 39 import selery.config : Files; 40 41 deprecated("Use LanguageManager instead") alias Lang = LanguageManager; 42 43 /** 44 * Stores translatable strings in various languages and provides 45 * methods to translate them with the provided arguments. 46 */ 47 class LanguageManager { 48 49 private const Files files; 50 public immutable string[] acceptedLanguages; 51 public immutable string language; 52 53 private string[string] defaults; 54 55 private TranslationManager[string][string] messages; 56 public string[string][string] raw; // used for web admin 57 58 public this(inout Files files, string language) { 59 this.files = files; 60 string[] accepted; 61 bool languageAccepted = false; 62 foreach(lang, countries; parseJSON(cast(string)files.readAsset("lang/languages.json")).object) { 63 foreach(i, country; countries.array) { 64 immutable code = lang ~ "_" ~ country.str.toUpper; 65 accepted ~= code; 66 if(i == 0) this.defaults[lang] = code; 67 } 68 } 69 this.acceptedLanguages = accepted.idup; 70 this.language = this.best(language); 71 } 72 73 public inout string best(string language) { 74 language = language.toLower; 75 // return full language matching full language (en_GB : en_GB) 76 foreach(lang ; this.acceptedLanguages) { 77 if(language == lang.toLower) return lang; 78 } 79 // return full language matching language only (en : en_GB) 80 if(language.length >= 2) { 81 auto d = language[0..2] in this.defaults; 82 if(d) return *d; 83 } 84 // return server's language 85 return this.language; 86 } 87 88 /** 89 * Loads languages in lang/system and lang/messages. 90 * Throws: RangeError if one of the given languages is not supported by the software. 91 */ 92 public inout void load() { 93 foreach(type ; ["system", "messages"]) { 94 foreach(lang ; acceptedLanguages) { 95 immutable file = "lang/" ~ type ~ "/" ~ lang ~ ".lang"; 96 if(this.files.hasAsset(file)) this.add(lang, this.parseFile(cast(string)this.files.readAsset(file))); 97 } 98 } 99 } 100 101 public inout string[string][string] parseFolder(string folder) { 102 if(!folder.endsWith(dirSeparator)) folder ~= dirSeparator; 103 string[string][string] ret; 104 bool loadImpl(string lang, string file) { 105 if(exists(file)) { 106 ret[lang] = this.parseFile(cast(string)read(file)); 107 return true; 108 } else { 109 return false; 110 } 111 } 112 foreach(lang ; acceptedLanguages) { 113 if(!loadImpl(lang, folder ~ lang ~ ".lang")) loadImpl(lang, folder ~ lang[0..2] ~ ".lang"); 114 } 115 return ret; 116 } 117 118 private inout string[string] parseFile(string data) { 119 string[string] ret; 120 foreach(string line ; split(data, "\n")) { 121 immutable equals = line.indexOf("="); 122 if(equals != -1) { 123 immutable message = line[0..equals].strip; 124 immutable text = line[equals+1..$].strip; 125 if(message.length && message[0] != '#') ret[message] = text; 126 } 127 } 128 return ret; 129 } 130 131 /** 132 * Adds messages using the given associative array of message:text. 133 */ 134 public void add(string language, string[string] messages) { 135 foreach(message, text; messages) { 136 this.raw[language][message] = text; 137 Element[] elements; 138 string next; 139 ptrdiff_t index = -1; 140 foreach(i, c; text) { 141 if(index >= 0) { 142 if(c == '}') { 143 try { 144 auto num = to!size_t(text[index+1..i]); 145 if(next.length) { 146 elements ~= Element(next); 147 next.length = 0; 148 } 149 elements ~= Element(num); 150 } catch(ConvException) { 151 next ~= text[index..i+1]; 152 } 153 index = -1; 154 } 155 } else { 156 if(c == '{') { 157 index = i; 158 } else { 159 next ~= c; 160 } 161 } 162 } 163 if(index >= 0) next ~= text[index..$]; 164 if(next.length) elements ~= Element(next); 165 if(elements.length) this.messages[language][message] = TranslationManager(elements); 166 } 167 } 168 169 /// ditto 170 public const void add(string language, string[string] messages) { 171 (cast()this).add(language, messages); 172 } 173 174 /** 175 * Translates a message in the given language with the given parameters. 176 * If the language is omitted the message is translated using the default 177 * language. 178 * Returns: the translated message if the language and the message exist or the message if not 179 */ 180 public inout string translate(inout string message, inout(string)[] params, string language) { 181 auto lang = language in this.messages; 182 if(lang) { 183 auto translatable = message in *lang; 184 if(translatable) { 185 return (*translatable).build(params); 186 } 187 } 188 return message; 189 } 190 191 /// ditto 192 public inout string translate(string message, string lang) { 193 return this.translate(message, [], language); 194 } 195 196 /// ditto 197 public inout string translate(string message, string[] params=[]) { 198 return this.translate(message, params, this.language); 199 } 200 201 /// ditto 202 public inout string translate(inout Translation translation, string language) { 203 return this.translate(translation.translatable.default_, translation.parameters, language); 204 } 205 206 /// ditto 207 public inout string translate(inout Translation translation) { 208 return this.translate(translation, this.language); 209 } 210 211 private void loadImpl(string language, void[] data) { 212 foreach(string line ; split(cast(string)data, "\n")) { 213 immutable equals = line.indexOf("="); 214 if(equals != -1) { 215 immutable message = line[0..equals].strip; 216 immutable text = line[equals+1..$].strip; 217 if(message.length) { 218 this.raw[language][message] = text; 219 immutable comment = text.indexOf("##"); 220 Element[] elements; 221 string next; 222 ptrdiff_t index = -1; 223 foreach(i, c; text[0..comment==-1?$:comment]) { 224 if(index >= 0) { 225 if(c == '}') { 226 try { 227 auto num = to!size_t(text[index+1..i]); 228 if(next.length) { 229 elements ~= Element(next); 230 next.length = 0; 231 } 232 elements ~= Element(num); 233 } catch(ConvException) { 234 next ~= text[index..i+1]; 235 } 236 index = -1; 237 } 238 } else { 239 if(c == '{') { 240 index = i; 241 } else { 242 next ~= c; 243 } 244 } 245 } 246 if(index >= 0) next ~= text[index..$]; 247 if(next.length) elements ~= Element(next); 248 if(elements.length) this.messages[language][message] = TranslationManager(elements); 249 } 250 } 251 } 252 } 253 254 private static struct TranslationManager { 255 256 Element[] elements; 257 258 public inout string build(inout(string)[] args) { 259 Appender!string ret; 260 foreach(element ; this.elements) { 261 if(element.isString) { 262 ret.put(element.data); 263 } else if(element.index < args.length) { 264 ret.put(args[element.index]); 265 } else { 266 ret.put("{"); 267 ret.put(to!string(element.index)); 268 ret.put("}"); 269 } 270 } 271 return ret.data; 272 } 273 274 } 275 276 private static struct Element { 277 278 union { 279 string data; 280 size_t index; 281 } 282 283 public bool isString; 284 285 public this(string data) { 286 this.data = data; 287 this.isString = true; 288 } 289 290 public this(size_t index) { 291 this.index = index; 292 this.isString = false; 293 } 294 295 } 296 297 } 298 299 struct Translation { 300 301 public Translatable translatable; 302 public string[] parameters; 303 304 public this(E...)(Translatable translatable, E parameters) { 305 this.translatable = translatable; 306 foreach(param ; parameters) { 307 static if(is(typeof(param) : string) || is(typeof(param) == string[])) this.parameters ~= param; 308 else this.parameters ~= param.to!string; 309 } 310 } 311 312 public this(E...)(string default_, E parameters) { 313 this(Translatable.all(default_), parameters); 314 } 315 316 public static Translation server(E...)(string default_, E parameters) { 317 return Translation(Translatable(default_), parameters); 318 } 319 320 } 321 322 /** 323 * Translation container for a multi-platform translation. 324 * The `default_` translation should never be empty and it should be a string that can be 325 * loaded from a language file. 326 * The `minecraft` and `bedrock` strings can either be a client-side translated message 327 * or empty. In that case the `default_` string is translated server-side and sent 328 * to the client. 329 * Example: 330 * --- 331 * // server-side string 332 * Translatable("example.test"); 333 * 334 * // server-side for minecraft and client-side for bedrock 335 * Translatable("description.help", "", "commands.help.description"); 336 * --- 337 */ 338 struct Translatable { 339 340 //TODO move somewhere else 341 enum MULTIPLAYER_JOINED = all("multiplayer.player.joined"); 342 enum MULTIPLAYER_LEFT = all("multiplayer.player.left"); 343 344 public static nothrow @safe @nogc Translatable all(inout string translation) { 345 return Translatable(translation, translation, translation); 346 } 347 348 public static nothrow @safe @nogc Translatable fromJava(inout string translation) { 349 return Translatable(translation, translation, ""); 350 } 351 352 public static nothrow @safe @nogc Translatable fromBedrock(inout string translation) { 353 return Translatable(translation, "", translation); 354 } 355 356 /// Values. 357 public string default_, java, bedrock; 358 359 }