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