001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.io.BufferedInputStream; 005import java.io.File; 006import java.io.IOException; 007import java.io.InputStream; 008import java.lang.annotation.Retention; 009import java.lang.annotation.RetentionPolicy; 010import java.net.URL; 011import java.nio.charset.StandardCharsets; 012import java.nio.file.InvalidPathException; 013import java.text.MessageFormat; 014import java.util.ArrayList; 015import java.util.Arrays; 016import java.util.Collection; 017import java.util.Comparator; 018import java.util.HashMap; 019import java.util.Locale; 020import java.util.Map; 021import java.util.zip.ZipEntry; 022import java.util.zip.ZipFile; 023 024/** 025 * Internationalisation support. 026 * 027 * @author Immanuel.Scholz 028 */ 029public final class I18n { 030 031 /** 032 * This annotates strings which do not permit a clean i18n. This is mostly due to strings 033 * containing two nouns which can occur in singular or plural form. 034 * <br> 035 * No behaviour is associated with this annotation. 036 */ 037 @Retention(RetentionPolicy.SOURCE) 038 public @interface QuirkyPluralString { 039 } 040 041 private I18n() { 042 // Hide default constructor for utils classes 043 } 044 045 /** 046 * Enumeration of possible plural modes. It allows us to identify and implement logical conditions of 047 * plural forms defined on <a href="https://help.launchpad.net/Translations/PluralForms">Launchpad</a>. 048 * See <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html">CLDR</a> 049 * for another complete list. 050 * @see #pluralEval 051 */ 052 private enum PluralMode { 053 /** Plural = Not 1. This is the default for many languages, including English: 1 day, but 0 days or 2 days. */ 054 MODE_NOTONE, 055 /** No plural. Mainly for Asian languages (Indonesian, Chinese, Japanese, ...) */ 056 MODE_NONE, 057 /** Plural = Greater than 1. For some latin languages (French, Brazilian Portuguese) */ 058 MODE_GREATERONE, 059 /* Special mode for 060 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar">Arabic</a>.* 061 MODE_AR,*/ 062 /** Special mode for 063 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#cs">Czech</a>. */ 064 MODE_CS, 065 /** Special mode for 066 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#pl">Polish</a>. */ 067 MODE_PL, 068 /* Special mode for 069 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ro">Romanian</a>.* 070 MODE_RO,*/ 071 /** Special mode for 072 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#lt">Lithuanian</a>. */ 073 MODE_LT, 074 /** Special mode for 075 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru">Russian</a>. */ 076 MODE_RU, 077 /** Special mode for 078 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#sk">Slovak</a>. */ 079 MODE_SK, 080 /* Special mode for 081 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#sl">Slovenian</a>.* 082 MODE_SL,*/ 083 } 084 085 private static volatile PluralMode pluralMode = PluralMode.MODE_NOTONE; /* english default */ 086 private static volatile String loadedCode = "en"; 087 088 /** Map (english/locale) of singular strings **/ 089 private static volatile Map<String, String> strings; 090 /** Map (english/locale) of plural strings **/ 091 private static volatile Map<String, String[]> pstrings; 092 private static Locale originalLocale = Locale.getDefault(); 093 private static Map<String, PluralMode> languages = new HashMap<>(); 094 static { 095 //languages.put("ar", PluralMode.MODE_AR); 096 languages.put("ast", PluralMode.MODE_NOTONE); 097 languages.put("bg", PluralMode.MODE_NOTONE); 098 languages.put("be", PluralMode.MODE_RU); 099 languages.put("ca", PluralMode.MODE_NOTONE); 100 languages.put("ca@valencia", PluralMode.MODE_NOTONE); 101 languages.put("cs", PluralMode.MODE_CS); 102 languages.put("da", PluralMode.MODE_NOTONE); 103 languages.put("de", PluralMode.MODE_NOTONE); 104 languages.put("el", PluralMode.MODE_NOTONE); 105 languages.put("en_AU", PluralMode.MODE_NOTONE); 106 languages.put("en_GB", PluralMode.MODE_NOTONE); 107 languages.put("es", PluralMode.MODE_NOTONE); 108 languages.put("et", PluralMode.MODE_NOTONE); 109 //languages.put("eu", PluralMode.MODE_NOTONE); 110 languages.put("fi", PluralMode.MODE_NOTONE); 111 languages.put("fr", PluralMode.MODE_GREATERONE); 112 languages.put("gl", PluralMode.MODE_NOTONE); 113 //languages.put("he", PluralMode.MODE_NOTONE); 114 languages.put("hu", PluralMode.MODE_NOTONE); 115 languages.put("id", PluralMode.MODE_NONE); 116 //languages.put("is", PluralMode.MODE_NOTONE); 117 languages.put("it", PluralMode.MODE_NOTONE); 118 languages.put("ja", PluralMode.MODE_NONE); 119 languages.put("ko", PluralMode.MODE_NONE); 120 // fully supported only with Java 8 and later (needs CLDR) 121 languages.put("km", PluralMode.MODE_NONE); 122 languages.put("lt", PluralMode.MODE_LT); 123 languages.put("nb", PluralMode.MODE_NOTONE); 124 languages.put("nl", PluralMode.MODE_NOTONE); 125 languages.put("pl", PluralMode.MODE_PL); 126 languages.put("pt", PluralMode.MODE_NOTONE); 127 languages.put("pt_BR", PluralMode.MODE_GREATERONE); 128 //languages.put("ro", PluralMode.MODE_RO); 129 languages.put("ru", PluralMode.MODE_RU); 130 languages.put("sk", PluralMode.MODE_SK); 131 //languages.put("sl", PluralMode.MODE_SL); 132 languages.put("sv", PluralMode.MODE_NOTONE); 133 //languages.put("tr", PluralMode.MODE_NONE); 134 languages.put("uk", PluralMode.MODE_RU); 135 languages.put("vi", PluralMode.MODE_NONE); 136 languages.put("zh_CN", PluralMode.MODE_NONE); 137 languages.put("zh_TW", PluralMode.MODE_NONE); 138 } 139 140 /** 141 * Translates some text for the current locale. 142 * These strings are collected by a script that runs on the source code files. 143 * After translation, the localizations are distributed with the main program. 144 * <br> 145 * For example, <code>tr("JOSM''s default value is ''{0}''.", val)</code>. 146 * <br> 147 * Use {@link #trn} for distinguishing singular from plural text, i.e., 148 * do not use {@code tr(size == 1 ? "singular" : "plural")} nor 149 * {@code size == 1 ? tr("singular") : tr("plural")} 150 * 151 * @param text the text to translate. 152 * Must be a string literal. (No constants or local vars.) 153 * Can be broken over multiple lines. 154 * An apostrophe ' must be quoted by another apostrophe. 155 * @param objects the parameters for the string. 156 * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ... 157 * @return the translated string. 158 * @see #trn 159 * @see #trc 160 * @see #trnc 161 */ 162 public static String tr(String text, Object... objects) { 163 if (text == null) return null; 164 return MessageFormat.format(gettext(text, null), objects); 165 } 166 167 /** 168 * Translates some text in a context for the current locale. 169 * There can be different translations for the same text within different contexts. 170 * 171 * @param context string that helps translators to find an appropriate 172 * translation for {@code text}. 173 * @param text the text to translate. 174 * @return the translated string. 175 * @see #tr 176 * @see #trn 177 * @see #trnc 178 */ 179 public static String trc(String context, String text) { 180 if (context == null) 181 return tr(text); 182 if (text == null) 183 return null; 184 return MessageFormat.format(gettext(text, context), (Object) null); 185 } 186 187 public static String trcLazy(String context, String text) { 188 if (context == null) 189 return tr(text); 190 if (text == null) 191 return null; 192 return MessageFormat.format(gettextLazy(text, context), (Object) null); 193 } 194 195 /** 196 * Marks a string for translation (such that a script can harvest 197 * the translatable strings from the source files). 198 * 199 * For example, <code> 200 * String[] options = new String[] {marktr("up"), marktr("down")}; 201 * lbl.setText(tr(options[0]));</code> 202 * @param text the string to be marked for translation. 203 * @return {@code text} unmodified. 204 */ 205 public static String marktr(String text) { 206 return text; 207 } 208 209 public static String marktrc(String context, String text) { 210 return text; 211 } 212 213 /** 214 * Translates some text for the current locale and distinguishes between 215 * {@code singularText} and {@code pluralText} depending on {@code n}. 216 * <br> 217 * For instance, {@code trn("There was an error!", "There were errors!", i)} or 218 * <code>trn("Found {0} error in {1}!", "Found {0} errors in {1}!", i, Integer.toString(i), url)</code>. 219 * 220 * @param singularText the singular text to translate. 221 * Must be a string literal. (No constants or local vars.) 222 * Can be broken over multiple lines. 223 * An apostrophe ' must be quoted by another apostrophe. 224 * @param pluralText the plural text to translate. 225 * Must be a string literal. (No constants or local vars.) 226 * Can be broken over multiple lines. 227 * An apostrophe ' must be quoted by another apostrophe. 228 * @param n a number to determine whether {@code singularText} or {@code pluralText} is used. 229 * @param objects the parameters for the string. 230 * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ... 231 * @return the translated string. 232 * @see #tr 233 * @see #trc 234 * @see #trnc 235 */ 236 public static String trn(String singularText, String pluralText, long n, Object... objects) { 237 return MessageFormat.format(gettextn(singularText, pluralText, null, n), objects); 238 } 239 240 /** 241 * Translates some text in a context for the current locale and distinguishes between 242 * {@code singularText} and {@code pluralText} depending on {@code n}. 243 * There can be different translations for the same text within different contexts. 244 * 245 * @param context string that helps translators to find an appropriate 246 * translation for {@code text}. 247 * @param singularText the singular text to translate. 248 * Must be a string literal. (No constants or local vars.) 249 * Can be broken over multiple lines. 250 * An apostrophe ' must be quoted by another apostrophe. 251 * @param pluralText the plural text to translate. 252 * Must be a string literal. (No constants or local vars.) 253 * Can be broken over multiple lines. 254 * An apostrophe ' must be quoted by another apostrophe. 255 * @param n a number to determine whether {@code singularText} or {@code pluralText} is used. 256 * @param objects the parameters for the string. 257 * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ... 258 * @return the translated string. 259 * @see #tr 260 * @see #trc 261 * @see #trn 262 */ 263 public static String trnc(String context, String singularText, String pluralText, long n, Object... objects) { 264 return MessageFormat.format(gettextn(singularText, pluralText, context, n), objects); 265 } 266 267 private static String gettext(String text, String ctx, boolean lazy) { 268 int i; 269 if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) { 270 ctx = text.substring(2, i-1); 271 text = text.substring(i+1); 272 } 273 if (strings != null) { 274 String trans = strings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 275 if (trans != null) 276 return trans; 277 } 278 if (pstrings != null) { 279 i = pluralEval(1); 280 String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 281 if (trans != null && trans.length > i) 282 return trans[i]; 283 } 284 return lazy ? gettext(text, null) : text; 285 } 286 287 private static String gettext(String text, String ctx) { 288 return gettext(text, ctx, false); 289 } 290 291 /* try without context, when context try fails */ 292 private static String gettextLazy(String text, String ctx) { 293 return gettext(text, ctx, true); 294 } 295 296 private static String gettextn(String text, String plural, String ctx, long num) { 297 int i; 298 if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) { 299 ctx = text.substring(2, i-1); 300 text = text.substring(i+1); 301 } 302 if (pstrings != null) { 303 i = pluralEval(num); 304 String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 305 if (trans != null && trans.length > i) 306 return trans[i]; 307 } 308 309 return num == 1 ? text : plural; 310 } 311 312 public static String escape(String msg) { 313 if (msg == null) return null; 314 return msg.replace("\'", "\'\'").replace("{", "\'{\'").replace("}", "\'}\'"); 315 } 316 317 private static URL getTranslationFile(String lang) { 318 return I18n.class.getResource("/data/"+lang.replace('@', '-')+".lang"); 319 } 320 321 /** 322 * Get a list of all available JOSM Translations. 323 * @return an array of locale objects. 324 */ 325 public static Locale[] getAvailableTranslations() { 326 Collection<Locale> v = new ArrayList<>(languages.size()); 327 if (getTranslationFile("en") != null) { 328 for (String loc : languages.keySet()) { 329 if (getTranslationFile(loc) != null) { 330 v.add(LanguageInfo.getLocale(loc)); 331 } 332 } 333 } 334 v.add(Locale.ENGLISH); 335 Locale[] l = new Locale[v.size()]; 336 l = v.toArray(l); 337 Arrays.sort(l, Comparator.comparing(Locale::toString)); 338 return l; 339 } 340 341 /** 342 * Determines if a language exists for the given code. 343 * @param code The language code 344 * @return {@code true} if a language exists, {@code false} otherwise 345 */ 346 public static boolean hasCode(String code) { 347 return languages.containsKey(code); 348 } 349 350 static void setupJavaLocaleProviders() { 351 // Look up SPI providers first (for JosmDecimalFormatSymbolsProvider). 352 // Enable CLDR locale provider on Java 8 to get additional languages, such as Khmer. 353 // http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html#cldr 354 // FIXME: This must be updated after we switch to Java 9. 355 // See https://docs.oracle.com/javase/9/docs/api/java/util/spi/LocaleServiceProvider.html 356 try { 357 // Don't call Utils.updateSystemProperty to avoid spurious log at startup 358 System.setProperty("java.locale.providers", "SPI,JRE,CLDR"); 359 } catch (SecurityException e) { 360 // Don't call Logging class, it may not be fully initialized yet 361 System.err.println("Unable to set locale providers: " + e.getMessage()); 362 } 363 } 364 365 /** 366 * I18n initialization. 367 */ 368 public static void init() { 369 setupJavaLocaleProviders(); 370 371 /* try initial language settings, may be changed later again */ 372 if (!load(LanguageInfo.getJOSMLocaleCode())) { 373 Locale.setDefault(Locale.ENGLISH); 374 } 375 } 376 377 /** 378 * I18n initialization for plugins. 379 * @param source file path/name of the JAR or Zip file containing translation strings 380 * @since 4159 381 */ 382 public static void addTexts(File source) { 383 if ("en".equals(loadedCode)) 384 return; 385 final ZipEntry enfile = new ZipEntry("data/en.lang"); 386 final ZipEntry langfile = new ZipEntry("data/"+loadedCode+".lang"); 387 try ( 388 ZipFile zipFile = new ZipFile(source, StandardCharsets.UTF_8); 389 InputStream orig = zipFile.getInputStream(enfile); 390 InputStream trans = zipFile.getInputStream(langfile) 391 ) { 392 if (orig != null && trans != null) 393 load(orig, trans, true); 394 } catch (IOException | InvalidPathException e) { 395 Logging.trace(e); 396 } 397 } 398 399 private static boolean load(String l) { 400 if ("en".equals(l) || "en_US".equals(l)) { 401 strings = null; 402 pstrings = null; 403 loadedCode = "en"; 404 pluralMode = PluralMode.MODE_NOTONE; 405 return true; 406 } 407 URL en = getTranslationFile("en"); 408 if (en == null) 409 return false; 410 URL tr = getTranslationFile(l); 411 if (tr == null || !languages.containsKey(l)) { 412 return false; 413 } 414 try ( 415 InputStream enStream = Utils.openStream(en); 416 InputStream trStream = Utils.openStream(tr) 417 ) { 418 if (load(enStream, trStream, false)) { 419 pluralMode = languages.get(l); 420 loadedCode = l; 421 return true; 422 } 423 } catch (IOException e) { 424 // Ignore exception 425 Logging.trace(e); 426 } 427 return false; 428 } 429 430 private static boolean load(InputStream en, InputStream tr, boolean add) { 431 Map<String, String> s; 432 Map<String, String[]> p; 433 if (add) { 434 s = strings; 435 p = pstrings; 436 } else { 437 s = new HashMap<>(); 438 p = new HashMap<>(); 439 } 440 /* file format: 441 Files are always a group. English file and translated file must provide identical datasets. 442 443 for all single strings: 444 { 445 unsigned short (2 byte) stringlength 446 - length 0 indicates missing translation 447 - length 0xFFFE indicates translation equal to original, but otherwise is equal to length 0 448 string 449 } 450 unsigned short (2 byte) 0xFFFF (marks end of single strings) 451 for all multi strings: 452 { 453 unsigned char (1 byte) stringcount 454 - count 0 indicates missing translations 455 - count 0xFE indicates translations equal to original, but otherwise is equal to length 0 456 for stringcount 457 unsigned short (2 byte) stringlength 458 string 459 } 460 */ 461 try { 462 InputStream ens = new BufferedInputStream(en); 463 InputStream trs = new BufferedInputStream(tr); 464 byte[] enlen = new byte[2]; 465 byte[] trlen = new byte[2]; 466 boolean multimode = false; 467 byte[] str = new byte[4096]; 468 for (;;) { 469 if (multimode) { 470 int ennum = ens.read(); 471 int trnum = trs.read(); 472 if (trnum == 0xFE) /* marks identical string, handle equally to non-translated */ 473 trnum = 0; 474 if ((ennum == -1 && trnum != -1) || (ennum != -1 && trnum == -1)) /* files do not match */ 475 return false; 476 if (ennum == -1) { 477 break; 478 } 479 String[] enstrings = new String[ennum]; 480 for (int i = 0; i < ennum; ++i) { 481 int val = ens.read(enlen); 482 if (val != 2) /* file corrupt */ 483 return false; 484 val = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]); 485 if (val > str.length) { 486 str = new byte[val]; 487 } 488 int rval = ens.read(str, 0, val); 489 if (rval != val) /* file corrupt */ 490 return false; 491 enstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8); 492 } 493 String[] trstrings = new String[trnum]; 494 for (int i = 0; i < trnum; ++i) { 495 int val = trs.read(trlen); 496 if (val != 2) /* file corrupt */ 497 return false; 498 val = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]); 499 if (val > str.length) { 500 str = new byte[val]; 501 } 502 int rval = trs.read(str, 0, val); 503 if (rval != val) /* file corrupt */ 504 return false; 505 trstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8); 506 } 507 if (trnum > 0 && !p.containsKey(enstrings[0])) { 508 p.put(enstrings[0], trstrings); 509 } 510 } else { 511 int enval = ens.read(enlen); 512 int trval = trs.read(trlen); 513 if (enval != trval) /* files do not match */ 514 return false; 515 if (enval == -1) { 516 break; 517 } 518 if (enval != 2) /* files corrupt */ 519 return false; 520 enval = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]); 521 trval = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]); 522 if (trval == 0xFFFE) /* marks identical string, handle equally to non-translated */ 523 trval = 0; 524 if (enval == 0xFFFF) { 525 multimode = true; 526 if (trval != 0xFFFF) /* files do not match */ 527 return false; 528 } else { 529 if (enval > str.length) { 530 str = new byte[enval]; 531 } 532 if (trval > str.length) { 533 str = new byte[trval]; 534 } 535 int val = ens.read(str, 0, enval); 536 if (val != enval) /* file corrupt */ 537 return false; 538 String enstr = new String(str, 0, enval, StandardCharsets.UTF_8); 539 if (trval != 0) { 540 val = trs.read(str, 0, trval); 541 if (val != trval) /* file corrupt */ 542 return false; 543 String trstr = new String(str, 0, trval, StandardCharsets.UTF_8); 544 if (!s.containsKey(enstr)) 545 s.put(enstr, trstr); 546 } 547 } 548 } 549 } 550 } catch (IOException e) { 551 Logging.trace(e); 552 return false; 553 } 554 if (!s.isEmpty()) { 555 strings = s; 556 pstrings = p; 557 return true; 558 } 559 return false; 560 } 561 562 /** 563 * Sets the default locale (see {@link Locale#setDefault(Locale)} to the local 564 * given by <code>localName</code>. 565 * 566 * Ignored if localeName is null. If the locale with name <code>localName</code> 567 * isn't found the default local is set to <code>en</code> (english). 568 * 569 * @param localeName the locale name. Ignored if null. 570 */ 571 public static void set(String localeName) { 572 if (localeName != null) { 573 Locale l = LanguageInfo.getLocale(localeName); 574 if (load(LanguageInfo.getJOSMLocaleCode(l))) { 575 Locale.setDefault(l); 576 } else { 577 if (!"en".equals(l.getLanguage())) { 578 Logging.info(tr("Unable to find translation for the locale {0}. Reverting to {1}.", 579 LanguageInfo.getDisplayName(l), LanguageInfo.getDisplayName(Locale.getDefault()))); 580 } else { 581 strings = null; 582 pstrings = null; 583 } 584 } 585 } 586 } 587 588 private static int pluralEval(long n) { 589 switch(pluralMode) { 590 case MODE_NOTONE: /* bg, da, de, el, en, en_GB, es, et, eu, fi, gl, is, it, iw_IL, nb, nl, sv */ 591 return (n != 1) ? 1 : 0; 592 case MODE_NONE: /* id, vi, ja, km, tr, zh_CN, zh_TW */ 593 return 0; 594 case MODE_GREATERONE: /* fr, pt_BR */ 595 return (n > 1) ? 1 : 0; 596 case MODE_CS: 597 return (n == 1) ? 0 : (((n >= 2) && (n <= 4)) ? 1 : 2); 598 //case MODE_AR: 599 // return ((n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((((n % 100) >= 3) 600 // && ((n % 100) <= 10)) ? 3 : ((((n % 100) >= 11) && ((n % 100) <= 99)) ? 4 : 5))))); 601 case MODE_PL: 602 return (n == 1) ? 0 : (((((n % 10) >= 2) && ((n % 10) <= 4)) 603 && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2); 604 //case MODE_RO: 605 // return ((n == 1) ? 0 : ((((n % 100) > 19) || (((n % 100) == 0) && (n != 0))) ? 2 : 1)); 606 case MODE_LT: 607 return ((n % 10) == 1) && ((n % 100) != 11) ? 0 : (((n % 10) >= 2) 608 && (((n % 100) < 10) || ((n % 100) >= 20)) ? 1 : 2); 609 case MODE_RU: 610 return (((n % 10) == 1) && ((n % 100) != 11)) ? 0 : (((((n % 10) >= 2) 611 && ((n % 10) <= 4)) && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2); 612 case MODE_SK: 613 return (n == 1) ? 1 : (((n >= 2) && (n <= 4)) ? 2 : 0); 614 //case MODE_SL: 615 // return (((n % 100) == 1) ? 1 : (((n % 100) == 2) ? 2 : ((((n % 100) == 3) 616 // || ((n % 100) == 4)) ? 3 : 0))); 617 } 618 return 0; 619 } 620 621 /** 622 * Returns the map of singular translations. 623 * @return the map of singular translations. 624 * @since 13761 625 */ 626 public static Map<String, String> getSingularTranslations() { 627 return new HashMap<>(strings); 628 } 629 630 /** 631 * Returns the map of plural translations. 632 * @return the map of plural translations. 633 * @since 13761 634 */ 635 public static Map<String, String[]> getPluralTranslations() { 636 return new HashMap<>(pstrings); 637 } 638 639 /** 640 * Returns the original default locale found when the JVM started. 641 * Used to guess real language/country of current user disregarding language chosen in JOSM preferences. 642 * @return the original default locale found when the JVM started 643 * @since 14013 644 */ 645 public static Locale getOriginalLocale() { 646 return originalLocale; 647 } 648}