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