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