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}