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