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/252 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}