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