001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.awt.GraphicsEnvironment; 005import java.io.BufferedInputStream; 006import java.io.File; 007import java.io.FileInputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.lang.annotation.Retention; 011import java.lang.annotation.RetentionPolicy; 012import java.net.URL; 013import java.nio.charset.StandardCharsets; 014import java.text.MessageFormat; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.Collection; 018import java.util.Comparator; 019import java.util.HashMap; 020import java.util.Locale; 021import java.util.Map; 022import java.util.jar.JarInputStream; 023import java.util.zip.ZipEntry; 024 025import javax.swing.JColorChooser; 026import javax.swing.JFileChooser; 027import javax.swing.UIManager; 028 029import org.openstreetmap.gui.jmapviewer.FeatureAdapter.TranslationAdapter; 030import org.openstreetmap.josm.Main; 031import org.openstreetmap.josm.gui.util.GuiHelper; 032import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 033 034/** 035 * Internationalisation support. 036 * 037 * @author Immanuel.Scholz 038 */ 039public final class I18n { 040 041 /** 042 * This annotates strings which do not permit a clean i18n. This is mostly due to strings 043 * containing two nouns which can occur in singular or plural form. 044 * <br> 045 * No behaviour is associated with this annotation. 046 */ 047 @Retention(RetentionPolicy.SOURCE) 048 public @interface QuirkyPluralString { 049 } 050 051 private I18n() { 052 // Hide default constructor for utils classes 053 } 054 055 /** 056 * Enumeration of possible plural modes. It allows us to identify and implement logical conditions of 057 * plural forms defined on <a href="https://help.launchpad.net/Translations/PluralForms">Launchpad</a>. 058 * See <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html">CLDR</a> 059 * for another complete list. 060 * @see #pluralEval 061 */ 062 private enum PluralMode { 063 /** Plural = Not 1. This is the default for many languages, including English: 1 day, but 0 days or 2 days. */ 064 MODE_NOTONE, 065 /** No plural. Mainly for Asian languages (Indonesian, Chinese, Japanese, ...) */ 066 MODE_NONE, 067 /** Plural = Greater than 1. For some latin languages (French, Brazilian Portuguese) */ 068 MODE_GREATERONE, 069 /* Special mode for 070 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar">Arabic</a>.* 071 MODE_AR,*/ 072 /** Special mode for 073 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#cs">Czech</a>. */ 074 MODE_CS, 075 /** Special mode for 076 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#pl">Polish</a>. */ 077 MODE_PL, 078 /* Special mode for 079 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ro">Romanian</a>.* 080 MODE_RO,*/ 081 /** Special mode for 082 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#lt">Lithuanian</a>. */ 083 MODE_LT, 084 /** Special mode for 085 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru">Russian</a>. */ 086 MODE_RU, 087 /** Special mode for 088 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#sk">Slovak</a>. */ 089 MODE_SK, 090 /* Special mode for 091 * <a href="http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#sl">Slovenian</a>.* 092 MODE_SL,*/ 093 } 094 095 private static volatile PluralMode pluralMode = PluralMode.MODE_NOTONE; /* english default */ 096 private static volatile String loadedCode = "en"; 097 098 /* Localization keys for file chooser (and color chooser). */ 099 private static final String[] javaInternalMessageKeys = new String[] { 100 /* JFileChooser windows laf */ 101 "FileChooser.detailsViewActionLabelText", 102 "FileChooser.detailsViewButtonAccessibleName", 103 "FileChooser.detailsViewButtonToolTipText", 104 "FileChooser.fileAttrHeaderText", 105 "FileChooser.fileDateHeaderText", 106 "FileChooser.fileNameHeaderText", 107 "FileChooser.fileNameLabelText", 108 "FileChooser.fileSizeHeaderText", 109 "FileChooser.fileTypeHeaderText", 110 "FileChooser.filesOfTypeLabelText", 111 "FileChooser.homeFolderAccessibleName", 112 "FileChooser.homeFolderToolTipText", 113 "FileChooser.listViewActionLabelText", 114 "FileChooser.listViewButtonAccessibleName", 115 "FileChooser.listViewButtonToolTipText", 116 "FileChooser.lookInLabelText", 117 "FileChooser.newFolderAccessibleName", 118 "FileChooser.newFolderActionLabelText", 119 "FileChooser.newFolderToolTipText", 120 "FileChooser.refreshActionLabelText", 121 "FileChooser.saveInLabelText", 122 "FileChooser.upFolderAccessibleName", 123 "FileChooser.upFolderToolTipText", 124 "FileChooser.viewMenuLabelText", 125 126 /* JFileChooser gtk laf */ 127 "FileChooser.acceptAllFileFilterText", 128 "FileChooser.cancelButtonText", 129 "FileChooser.cancelButtonToolTipText", 130 "FileChooser.deleteFileButtonText", 131 "FileChooser.filesLabelText", 132 "FileChooser.filterLabelText", 133 "FileChooser.foldersLabelText", 134 "FileChooser.newFolderButtonText", 135 "FileChooser.newFolderDialogText", 136 "FileChooser.openButtonText", 137 "FileChooser.openButtonToolTipText", 138 "FileChooser.openDialogTitleText", 139 "FileChooser.pathLabelText", 140 "FileChooser.renameFileButtonText", 141 "FileChooser.renameFileDialogText", 142 "FileChooser.renameFileErrorText", 143 "FileChooser.renameFileErrorTitle", 144 "FileChooser.saveButtonText", 145 "FileChooser.saveButtonToolTipText", 146 "FileChooser.saveDialogTitleText", 147 148 /* JFileChooser motif laf */ 149 //"FileChooser.cancelButtonText", 150 //"FileChooser.cancelButtonToolTipText", 151 "FileChooser.enterFileNameLabelText", 152 //"FileChooser.filesLabelText", 153 //"FileChooser.filterLabelText", 154 //"FileChooser.foldersLabelText", 155 "FileChooser.helpButtonText", 156 "FileChooser.helpButtonToolTipText", 157 //"FileChooser.openButtonText", 158 //"FileChooser.openButtonToolTipText", 159 //"FileChooser.openDialogTitleText", 160 //"FileChooser.pathLabelText", 161 //"FileChooser.saveButtonText", 162 //"FileChooser.saveButtonToolTipText", 163 //"FileChooser.saveDialogTitleText", 164 "FileChooser.updateButtonText", 165 "FileChooser.updateButtonToolTipText", 166 167 /* gtk color chooser */ 168 "GTKColorChooserPanel.blueText", 169 "GTKColorChooserPanel.colorNameText", 170 "GTKColorChooserPanel.greenText", 171 "GTKColorChooserPanel.hueText", 172 "GTKColorChooserPanel.nameText", 173 "GTKColorChooserPanel.redText", 174 "GTKColorChooserPanel.saturationText", 175 "GTKColorChooserPanel.valueText", 176 177 /* JOptionPane */ 178 "OptionPane.okButtonText", 179 "OptionPane.yesButtonText", 180 "OptionPane.noButtonText", 181 "OptionPane.cancelButtonText" 182 }; 183 private static volatile Map<String, String> strings; 184 private static volatile Map<String, String[]> pstrings; 185 private static Map<String, PluralMode> languages = new HashMap<>(); 186 187 /** 188 * Translates some text for the current locale. 189 * These strings are collected by a script that runs on the source code files. 190 * After translation, the localizations are distributed with the main program. 191 * <br> 192 * For example, <code>tr("JOSM''s default value is ''{0}''.", val)</code>. 193 * <br> 194 * Use {@link #trn} for distinguishing singular from plural text, i.e., 195 * do not use {@code tr(size == 1 ? "singular" : "plural")} nor 196 * {@code size == 1 ? tr("singular") : tr("plural")} 197 * 198 * @param text the text to translate. 199 * Must be a string literal. (No constants or local vars.) 200 * Can be broken over multiple lines. 201 * An apostrophe ' must be quoted by another apostrophe. 202 * @param objects the parameters for the string. 203 * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ... 204 * @return the translated string. 205 * @see #trn 206 * @see #trc 207 * @see #trnc 208 */ 209 public static String tr(String text, Object... objects) { 210 if (text == null) return null; 211 return MessageFormat.format(gettext(text, null), objects); 212 } 213 214 /** 215 * Translates some text in a context for the current locale. 216 * There can be different translations for the same text within different contexts. 217 * 218 * @param context string that helps translators to find an appropriate 219 * translation for {@code text}. 220 * @param text the text to translate. 221 * @return the translated string. 222 * @see #tr 223 * @see #trn 224 * @see #trnc 225 */ 226 public static String trc(String context, String text) { 227 if (context == null) 228 return tr(text); 229 if (text == null) 230 return null; 231 return MessageFormat.format(gettext(text, context), (Object) null); 232 } 233 234 public static String trc_lazy(String context, String text) { 235 if (context == null) 236 return tr(text); 237 if (text == null) 238 return null; 239 return MessageFormat.format(gettext_lazy(text, context), (Object) null); 240 } 241 242 /** 243 * Marks a string for translation (such that a script can harvest 244 * the translatable strings from the source files). 245 * 246 * For example, <code> 247 * String[] options = new String[] {marktr("up"), marktr("down")}; 248 * lbl.setText(tr(options[0]));</code> 249 * @param text the string to be marked for translation. 250 * @return {@code text} unmodified. 251 */ 252 public static String marktr(String text) { 253 return text; 254 } 255 256 public static String marktrc(String context, String text) { 257 return text; 258 } 259 260 /** 261 * Translates some text for the current locale and distinguishes between 262 * {@code singularText} and {@code pluralText} depending on {@code n}. 263 * <br> 264 * For instance, {@code trn("There was an error!", "There were errors!", i)} or 265 * <code>trn("Found {0} error in {1}!", "Found {0} errors in {1}!", i, Integer.toString(i), url)</code>. 266 * 267 * @param singularText the singular text to translate. 268 * Must be a string literal. (No constants or local vars.) 269 * Can be broken over multiple lines. 270 * An apostrophe ' must be quoted by another apostrophe. 271 * @param pluralText the plural text to translate. 272 * Must be a string literal. (No constants or local vars.) 273 * Can be broken over multiple lines. 274 * An apostrophe ' must be quoted by another apostrophe. 275 * @param n a number to determine whether {@code singularText} or {@code pluralText} is used. 276 * @param objects the parameters for the string. 277 * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ... 278 * @return the translated string. 279 * @see #tr 280 * @see #trc 281 * @see #trnc 282 */ 283 public static String trn(String singularText, String pluralText, long n, Object... objects) { 284 return MessageFormat.format(gettextn(singularText, pluralText, null, n), objects); 285 } 286 287 /** 288 * Translates some text in a context for the current locale and distinguishes between 289 * {@code singularText} and {@code pluralText} depending on {@code n}. 290 * There can be different translations for the same text within different contexts. 291 * 292 * @param context string that helps translators to find an appropriate 293 * translation for {@code text}. 294 * @param singularText the singular text to translate. 295 * Must be a string literal. (No constants or local vars.) 296 * Can be broken over multiple lines. 297 * An apostrophe ' must be quoted by another apostrophe. 298 * @param pluralText the plural text to translate. 299 * Must be a string literal. (No constants or local vars.) 300 * Can be broken over multiple lines. 301 * An apostrophe ' must be quoted by another apostrophe. 302 * @param n a number to determine whether {@code singularText} or {@code pluralText} is used. 303 * @param objects the parameters for the string. 304 * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ... 305 * @return the translated string. 306 * @see #tr 307 * @see #trc 308 * @see #trn 309 */ 310 public static String trnc(String context, String singularText, String pluralText, long n, Object... objects) { 311 return MessageFormat.format(gettextn(singularText, pluralText, context, n), objects); 312 } 313 314 private static String gettext(String text, String ctx, boolean lazy) { 315 int i; 316 if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) { 317 ctx = text.substring(2, i-1); 318 text = text.substring(i+1); 319 } 320 if (strings != null) { 321 String trans = strings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 322 if (trans != null) 323 return trans; 324 } 325 if (pstrings != null) { 326 i = pluralEval(1); 327 String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 328 if (trans != null && trans.length > i) 329 return trans[i]; 330 } 331 return lazy ? gettext(text, null) : text; 332 } 333 334 private static String gettext(String text, String ctx) { 335 return gettext(text, ctx, false); 336 } 337 338 /* try without context, when context try fails */ 339 private static String gettext_lazy(String text, String ctx) { 340 return gettext(text, ctx, true); 341 } 342 343 private static String gettextn(String text, String plural, String ctx, long num) { 344 int i; 345 if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) { 346 ctx = text.substring(2, i-1); 347 text = text.substring(i+1); 348 } 349 if (pstrings != null) { 350 i = pluralEval(num); 351 String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text); 352 if (trans != null && trans.length > i) 353 return trans[i]; 354 } 355 356 return num == 1 ? text : plural; 357 } 358 359 public static String escape(String msg) { 360 if (msg == null) return null; 361 return msg.replace("\'", "\'\'").replace("{", "\'{\'").replace("}", "\'}\'"); 362 } 363 364 private static URL getTranslationFile(String lang) { 365 return Main.class.getResource("/data/"+lang.replace('@', '-')+".lang"); 366 } 367 368 /** 369 * Get a list of all available JOSM Translations. 370 * @return an array of locale objects. 371 */ 372 public static Locale[] getAvailableTranslations() { 373 Collection<Locale> v = new ArrayList<>(languages.size()); 374 if (getTranslationFile("en") != null) { 375 for (String loc : languages.keySet()) { 376 if (getTranslationFile(loc) != null) { 377 v.add(LanguageInfo.getLocale(loc)); 378 } 379 } 380 } 381 v.add(Locale.ENGLISH); 382 Locale[] l = new Locale[v.size()]; 383 l = v.toArray(l); 384 Arrays.sort(l, new Comparator<Locale>() { 385 @Override 386 public int compare(Locale o1, Locale o2) { 387 return o1.toString().compareTo(o2.toString()); 388 } 389 }); 390 return l; 391 } 392 393 /** 394 * Determines if a language exists for the given code. 395 * @param code The language code 396 * @return {@code true} if a language exists, {@code false} otherwise 397 */ 398 public static boolean hasCode(String code) { 399 return languages.containsKey(code); 400 } 401 402 /** 403 * I18n initialization. 404 */ 405 public static void init() { 406 // Enable CLDR locale provider on Java 8 to get additional languages, such as Khmer. 407 // http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html#cldr 408 // FIXME: This can be removed after we switch to a minimal version of Java that enables CLDR by default 409 // or includes all languages we need in the JRE. See http://openjdk.java.net/jeps/252 for Java 9 410 Utils.updateSystemProperty("java.locale.providers", "JRE,CLDR"); 411 412 //languages.put("ar", PluralMode.MODE_AR); 413 languages.put("ast", PluralMode.MODE_NOTONE); 414 languages.put("bg", PluralMode.MODE_NOTONE); 415 languages.put("be", PluralMode.MODE_RU); 416 languages.put("ca", PluralMode.MODE_NOTONE); 417 languages.put("ca@valencia", PluralMode.MODE_NOTONE); 418 languages.put("cs", PluralMode.MODE_CS); 419 languages.put("da", PluralMode.MODE_NOTONE); 420 languages.put("de", PluralMode.MODE_NOTONE); 421 languages.put("el", PluralMode.MODE_NOTONE); 422 languages.put("en_AU", PluralMode.MODE_NOTONE); 423 languages.put("en_GB", PluralMode.MODE_NOTONE); 424 languages.put("es", PluralMode.MODE_NOTONE); 425 languages.put("et", PluralMode.MODE_NOTONE); 426 //languages.put("eu", PluralMode.MODE_NOTONE); 427 languages.put("fi", PluralMode.MODE_NOTONE); 428 languages.put("fr", PluralMode.MODE_GREATERONE); 429 languages.put("gl", PluralMode.MODE_NOTONE); 430 //languages.put("he", PluralMode.MODE_NOTONE); 431 languages.put("hu", PluralMode.MODE_NOTONE); 432 languages.put("id", PluralMode.MODE_NONE); 433 //languages.put("is", PluralMode.MODE_NOTONE); 434 languages.put("it", PluralMode.MODE_NOTONE); 435 languages.put("ja", PluralMode.MODE_NONE); 436 // fully supported only with Java 8 and later (needs CLDR) 437 languages.put("km", PluralMode.MODE_NONE); 438 languages.put("lt", PluralMode.MODE_LT); 439 languages.put("nb", PluralMode.MODE_NOTONE); 440 languages.put("nl", PluralMode.MODE_NOTONE); 441 languages.put("pl", PluralMode.MODE_PL); 442 languages.put("pt", PluralMode.MODE_NOTONE); 443 languages.put("pt_BR", PluralMode.MODE_GREATERONE); 444 //languages.put("ro", PluralMode.MODE_RO); 445 languages.put("ru", PluralMode.MODE_RU); 446 languages.put("sk", PluralMode.MODE_SK); 447 //languages.put("sl", PluralMode.MODE_SL); 448 languages.put("sv", PluralMode.MODE_NOTONE); 449 //languages.put("tr", PluralMode.MODE_NONE); 450 languages.put("uk", PluralMode.MODE_RU); 451 languages.put("vi", PluralMode.MODE_NONE); 452 languages.put("zh_CN", PluralMode.MODE_NONE); 453 languages.put("zh_TW", PluralMode.MODE_NONE); 454 455 /* try initial language settings, may be changed later again */ 456 if (!load(LanguageInfo.getJOSMLocaleCode())) { 457 Locale.setDefault(Locale.ENGLISH); 458 } 459 } 460 461 public static void addTexts(File source) { 462 if ("en".equals(loadedCode)) 463 return; 464 final String enfile = "data/en.lang"; 465 final String langfile = "data/"+loadedCode+".lang"; 466 try ( 467 FileInputStream fis = new FileInputStream(source); 468 JarInputStream jar = new JarInputStream(fis) 469 ) { 470 ZipEntry e; 471 boolean found = false; 472 while (!found && (e = jar.getNextEntry()) != null) { 473 String name = e.getName(); 474 if (enfile.equals(name)) 475 found = true; 476 } 477 if (found) { 478 try ( 479 FileInputStream fisTrans = new FileInputStream(source); 480 JarInputStream jarTrans = new JarInputStream(fisTrans) 481 ) { 482 found = false; 483 while (!found && (e = jarTrans.getNextEntry()) != null) { 484 String name = e.getName(); 485 if (name.equals(langfile)) 486 found = true; 487 } 488 if (found) 489 load(jar, jarTrans, true); 490 } 491 } 492 } catch (IOException e) { 493 // Ignore 494 if (Main.isTraceEnabled()) { 495 Main.trace(e.getMessage()); 496 } 497 } 498 } 499 500 private static boolean load(String l) { 501 if ("en".equals(l) || "en_US".equals(l)) { 502 strings = null; 503 pstrings = null; 504 loadedCode = "en"; 505 pluralMode = PluralMode.MODE_NOTONE; 506 return true; 507 } 508 URL en = getTranslationFile("en"); 509 if (en == null) 510 return false; 511 URL tr = getTranslationFile(l); 512 if (tr == null || !languages.containsKey(l)) { 513 return false; 514 } 515 try ( 516 InputStream enStream = en.openStream(); 517 InputStream trStream = tr.openStream() 518 ) { 519 if (load(enStream, trStream, false)) { 520 pluralMode = languages.get(l); 521 loadedCode = l; 522 return true; 523 } 524 } catch (IOException e) { 525 // Ignore exception 526 if (Main.isTraceEnabled()) { 527 Main.trace(e.getMessage()); 528 } 529 } 530 return false; 531 } 532 533 private static boolean load(InputStream en, InputStream tr, boolean add) { 534 Map<String, String> s; 535 Map<String, String[]> p; 536 if (add) { 537 s = strings; 538 p = pstrings; 539 } else { 540 s = new HashMap<>(); 541 p = new HashMap<>(); 542 } 543 /* file format: 544 Files are always a group. English file and translated file must provide identical datasets. 545 546 for all single strings: 547 { 548 unsigned short (2 byte) stringlength 549 - length 0 indicates missing translation 550 - length 0xFFFE indicates translation equal to original, but otherwise is equal to length 0 551 string 552 } 553 unsigned short (2 byte) 0xFFFF (marks end of single strings) 554 for all multi strings: 555 { 556 unsigned char (1 byte) stringcount 557 - count 0 indicates missing translations 558 - count 0xFE indicates translations equal to original, but otherwise is equal to length 0 559 for stringcount 560 unsigned short (2 byte) stringlength 561 string 562 } 563 */ 564 try { 565 InputStream ens = new BufferedInputStream(en); 566 InputStream trs = new BufferedInputStream(tr); 567 byte[] enlen = new byte[2]; 568 byte[] trlen = new byte[2]; 569 boolean multimode = false; 570 byte[] str = new byte[4096]; 571 for (;;) { 572 if (multimode) { 573 int ennum = ens.read(); 574 int trnum = trs.read(); 575 if (trnum == 0xFE) /* marks identical string, handle equally to non-translated */ 576 trnum = 0; 577 if ((ennum == -1 && trnum != -1) || (ennum != -1 && trnum == -1)) /* files do not match */ 578 return false; 579 if (ennum == -1) { 580 break; 581 } 582 String[] enstrings = new String[ennum]; 583 for (int i = 0; i < ennum; ++i) { 584 int val = ens.read(enlen); 585 if (val != 2) /* file corrupt */ 586 return false; 587 val = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]); 588 if (val > str.length) { 589 str = new byte[val]; 590 } 591 int rval = ens.read(str, 0, val); 592 if (rval != val) /* file corrupt */ 593 return false; 594 enstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8); 595 } 596 String[] trstrings = new String[trnum]; 597 for (int i = 0; i < trnum; ++i) { 598 int val = trs.read(trlen); 599 if (val != 2) /* file corrupt */ 600 return false; 601 val = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]); 602 if (val > str.length) { 603 str = new byte[val]; 604 } 605 int rval = trs.read(str, 0, val); 606 if (rval != val) /* file corrupt */ 607 return false; 608 trstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8); 609 } 610 if (trnum > 0 && !p.containsKey(enstrings[0])) { 611 p.put(enstrings[0], trstrings); 612 } 613 } else { 614 int enval = ens.read(enlen); 615 int trval = trs.read(trlen); 616 if (enval != trval) /* files do not match */ 617 return false; 618 if (enval == -1) { 619 break; 620 } 621 if (enval != 2) /* files corrupt */ 622 return false; 623 enval = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]); 624 trval = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]); 625 if (trval == 0xFFFE) /* marks identical string, handle equally to non-translated */ 626 trval = 0; 627 if (enval == 0xFFFF) { 628 multimode = true; 629 if (trval != 0xFFFF) /* files do not match */ 630 return false; 631 } else { 632 if (enval > str.length) { 633 str = new byte[enval]; 634 } 635 if (trval > str.length) { 636 str = new byte[trval]; 637 } 638 int val = ens.read(str, 0, enval); 639 if (val != enval) /* file corrupt */ 640 return false; 641 String enstr = new String(str, 0, enval, StandardCharsets.UTF_8); 642 if (trval != 0) { 643 val = trs.read(str, 0, trval); 644 if (val != trval) /* file corrupt */ 645 return false; 646 String trstr = new String(str, 0, trval, StandardCharsets.UTF_8); 647 if (!s.containsKey(enstr)) 648 s.put(enstr, trstr); 649 } 650 } 651 } 652 } 653 } catch (IOException e) { 654 return false; 655 } 656 if (!s.isEmpty()) { 657 strings = s; 658 pstrings = p; 659 return true; 660 } 661 return false; 662 } 663 664 /** 665 * Sets the default locale (see {@link Locale#setDefault(Locale)} to the local 666 * given by <code>localName</code>. 667 * 668 * Ignored if localeName is null. If the locale with name <code>localName</code> 669 * isn't found the default local is set to <tt>en</tt> (english). 670 * 671 * @param localeName the locale name. Ignored if null. 672 */ 673 public static void set(String localeName) { 674 if (localeName != null) { 675 Locale l = LanguageInfo.getLocale(localeName); 676 if (load(LanguageInfo.getJOSMLocaleCode(l))) { 677 Locale.setDefault(l); 678 } else { 679 if (!"en".equals(l.getLanguage())) { 680 Main.info(tr("Unable to find translation for the locale {0}. Reverting to {1}.", 681 LanguageInfo.getDisplayName(l), LanguageInfo.getDisplayName(Locale.getDefault()))); 682 } else { 683 strings = null; 684 pstrings = null; 685 } 686 } 687 } 688 } 689 690 /** 691 * Localizations for file chooser dialog. 692 * For some locales (e.g. de, fr) translations are provided 693 * by Java, but not for others (e.g. ru, uk). 694 */ 695 public static void translateJavaInternalMessages() { 696 Locale l = Locale.getDefault(); 697 698 AbstractFileChooser.setDefaultLocale(l); 699 JFileChooser.setDefaultLocale(l); 700 JColorChooser.setDefaultLocale(l); 701 for (String key : javaInternalMessageKeys) { 702 String us = UIManager.getString(key, Locale.US); 703 String loc = UIManager.getString(key, l); 704 // only provide custom translation if it is not already localized by Java 705 if (us != null && us.equals(loc)) { 706 UIManager.put(key, tr(us)); 707 } 708 } 709 } 710 711 private static int pluralEval(long n) { 712 switch(pluralMode) { 713 case MODE_NOTONE: /* bg, da, de, el, en, en_GB, es, et, eu, fi, gl, is, it, iw_IL, nb, nl, sv */ 714 return (n != 1) ? 1 : 0; 715 case MODE_NONE: /* id, vi, ja, km, tr, zh_CN, zh_TW */ 716 return 0; 717 case MODE_GREATERONE: /* fr, pt_BR */ 718 return (n > 1) ? 1 : 0; 719 case MODE_CS: 720 return (n == 1) ? 0 : (((n >= 2) && (n <= 4)) ? 1 : 2); 721 //case MODE_AR: 722 // return ((n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((((n % 100) >= 3) 723 // && ((n % 100) <= 10)) ? 3 : ((((n % 100) >= 11) && ((n % 100) <= 99)) ? 4 : 5))))); 724 case MODE_PL: 725 return (n == 1) ? 0 : (((((n % 10) >= 2) && ((n % 10) <= 4)) 726 && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2); 727 //case MODE_RO: 728 // return ((n == 1) ? 0 : ((((n % 100) > 19) || (((n % 100) == 0) && (n != 0))) ? 2 : 1)); 729 case MODE_LT: 730 return ((n % 10) == 1) && ((n % 100) != 11) ? 0 : (((n % 10) >= 2) 731 && (((n % 100) < 10) || ((n % 100) >= 20)) ? 1 : 2); 732 case MODE_RU: 733 return (((n % 10) == 1) && ((n % 100) != 11)) ? 0 : (((((n % 10) >= 2) 734 && ((n % 10) <= 4)) && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2); 735 case MODE_SK: 736 return (n == 1) ? 1 : (((n >= 2) && (n <= 4)) ? 2 : 0); 737 //case MODE_SL: 738 // return (((n % 100) == 1) ? 1 : (((n % 100) == 2) ? 2 : ((((n % 100) == 3) 739 // || ((n % 100) == 4)) ? 3 : 0))); 740 } 741 return 0; 742 } 743 744 public static TranslationAdapter getTranslationAdapter() { 745 return new TranslationAdapter() { 746 @Override 747 public String tr(String text, Object... objects) { 748 return I18n.tr(text, objects); 749 } 750 }; 751 } 752 753 /** 754 * Setup special font for Khmer script, as the default Java fonts do not display these characters. 755 * 756 * @since 8282 757 */ 758 public static void setupLanguageFonts() { 759 // Use special font for Khmer script, as the default Java font do not display these characters 760 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 761 Collection<String> fonts = Arrays.asList( 762 GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); 763 for (String f : new String[]{"Khmer UI", "DaunPenh", "MoolBoran"}) { 764 if (fonts.contains(f)) { 765 GuiHelper.setUIFont(f); 766 break; 767 } 768 } 769 } 770 } 771}