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 trcLazy(String context, String text) {
235        if (context == null)
236            return tr(text);
237        if (text == null)
238            return null;
239        return MessageFormat.format(gettextLazy(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 gettextLazy(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, Comparator.comparing(Locale::toString));
385        return l;
386    }
387
388    /**
389     * Determines if a language exists for the given code.
390     * @param code The language code
391     * @return {@code true} if a language exists, {@code false} otherwise
392     */
393    public static boolean hasCode(String code) {
394        return languages.containsKey(code);
395    }
396
397    /**
398     * I18n initialization.
399     */
400    public static void init() {
401        // Enable CLDR locale provider on Java 8 to get additional languages, such as Khmer.
402        // http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html#cldr
403        // FIXME: This can be removed after we switch to a minimal version of Java that enables CLDR by default
404        // or includes all languages we need in the JRE. See http://openjdk.java.net/jeps/252 for Java 9
405        System.setProperty("java.locale.providers", "JRE,CLDR"); // Don't call Utils.updateSystemProperty to avoid spurious log at startup
406
407        //languages.put("ar", PluralMode.MODE_AR);
408        languages.put("ast", PluralMode.MODE_NOTONE);
409        languages.put("bg", PluralMode.MODE_NOTONE);
410        languages.put("be", PluralMode.MODE_RU);
411        languages.put("ca", PluralMode.MODE_NOTONE);
412        languages.put("ca@valencia", PluralMode.MODE_NOTONE);
413        languages.put("cs", PluralMode.MODE_CS);
414        languages.put("da", PluralMode.MODE_NOTONE);
415        languages.put("de", PluralMode.MODE_NOTONE);
416        languages.put("el", PluralMode.MODE_NOTONE);
417        languages.put("en_AU", PluralMode.MODE_NOTONE);
418        languages.put("en_GB", PluralMode.MODE_NOTONE);
419        languages.put("es", PluralMode.MODE_NOTONE);
420        languages.put("et", PluralMode.MODE_NOTONE);
421        //languages.put("eu", PluralMode.MODE_NOTONE);
422        languages.put("fi", PluralMode.MODE_NOTONE);
423        languages.put("fr", PluralMode.MODE_GREATERONE);
424        languages.put("gl", PluralMode.MODE_NOTONE);
425        //languages.put("he", PluralMode.MODE_NOTONE);
426        languages.put("hu", PluralMode.MODE_NOTONE);
427        languages.put("id", PluralMode.MODE_NONE);
428        //languages.put("is", PluralMode.MODE_NOTONE);
429        languages.put("it", PluralMode.MODE_NOTONE);
430        languages.put("ja", PluralMode.MODE_NONE);
431        // fully supported only with Java 8 and later (needs CLDR)
432        languages.put("km", PluralMode.MODE_NONE);
433        languages.put("lt", PluralMode.MODE_LT);
434        languages.put("nb", PluralMode.MODE_NOTONE);
435        languages.put("nl", PluralMode.MODE_NOTONE);
436        languages.put("pl", PluralMode.MODE_PL);
437        languages.put("pt", PluralMode.MODE_NOTONE);
438        languages.put("pt_BR", PluralMode.MODE_GREATERONE);
439        //languages.put("ro", PluralMode.MODE_RO);
440        languages.put("ru", PluralMode.MODE_RU);
441        languages.put("sk", PluralMode.MODE_SK);
442        //languages.put("sl", PluralMode.MODE_SL);
443        languages.put("sv", PluralMode.MODE_NOTONE);
444        //languages.put("tr", PluralMode.MODE_NONE);
445        languages.put("uk", PluralMode.MODE_RU);
446        languages.put("vi", PluralMode.MODE_NONE);
447        languages.put("zh_CN", PluralMode.MODE_NONE);
448        languages.put("zh_TW", PluralMode.MODE_NONE);
449
450        /* try initial language settings, may be changed later again */
451        if (!load(LanguageInfo.getJOSMLocaleCode())) {
452            Locale.setDefault(Locale.ENGLISH);
453        }
454    }
455
456    public static void addTexts(File source) {
457        if ("en".equals(loadedCode))
458            return;
459        final String enfile = "data/en.lang";
460        final String langfile = "data/"+loadedCode+".lang";
461        try (
462            FileInputStream fis = new FileInputStream(source);
463            JarInputStream jar = new JarInputStream(fis)
464        ) {
465            ZipEntry e;
466            boolean found = false;
467            while (!found && (e = jar.getNextEntry()) != null) {
468                String name = e.getName();
469                if (enfile.equals(name))
470                    found = true;
471            }
472            if (found) {
473                try (
474                    FileInputStream fisTrans = new FileInputStream(source);
475                    JarInputStream jarTrans = new JarInputStream(fisTrans)
476                ) {
477                    found = false;
478                    while (!found && (e = jarTrans.getNextEntry()) != null) {
479                        String name = e.getName();
480                        if (name.equals(langfile))
481                            found = true;
482                    }
483                    if (found)
484                        load(jar, jarTrans, true);
485                }
486            }
487        } catch (IOException e) {
488            // Ignore
489            Main.trace(e);
490        }
491    }
492
493    private static boolean load(String l) {
494        if ("en".equals(l) || "en_US".equals(l)) {
495            strings = null;
496            pstrings = null;
497            loadedCode = "en";
498            pluralMode = PluralMode.MODE_NOTONE;
499            return true;
500        }
501        URL en = getTranslationFile("en");
502        if (en == null)
503            return false;
504        URL tr = getTranslationFile(l);
505        if (tr == null || !languages.containsKey(l)) {
506            return false;
507        }
508        try (
509            InputStream enStream = en.openStream();
510            InputStream trStream = tr.openStream()
511        ) {
512            if (load(enStream, trStream, false)) {
513                pluralMode = languages.get(l);
514                loadedCode = l;
515                return true;
516            }
517        } catch (IOException e) {
518            // Ignore exception
519            Main.trace(e);
520        }
521        return false;
522    }
523
524    private static boolean load(InputStream en, InputStream tr, boolean add) {
525        Map<String, String> s;
526        Map<String, String[]> p;
527        if (add) {
528            s = strings;
529            p = pstrings;
530        } else {
531            s = new HashMap<>();
532            p = new HashMap<>();
533        }
534        /* file format:
535           Files are always a group. English file and translated file must provide identical datasets.
536
537           for all single strings:
538           {
539             unsigned short (2 byte) stringlength
540               - length 0 indicates missing translation
541               - length 0xFFFE indicates translation equal to original, but otherwise is equal to length 0
542             string
543           }
544           unsigned short (2 byte) 0xFFFF (marks end of single strings)
545           for all multi strings:
546           {
547             unsigned char (1 byte) stringcount
548               - count 0 indicates missing translations
549               - count 0xFE indicates translations equal to original, but otherwise is equal to length 0
550             for stringcount
551               unsigned short (2 byte) stringlength
552               string
553           }
554         */
555        try {
556            InputStream ens = new BufferedInputStream(en);
557            InputStream trs = new BufferedInputStream(tr);
558            byte[] enlen = new byte[2];
559            byte[] trlen = new byte[2];
560            boolean multimode = false;
561            byte[] str = new byte[4096];
562            for (;;) {
563                if (multimode) {
564                    int ennum = ens.read();
565                    int trnum = trs.read();
566                    if (trnum == 0xFE) /* marks identical string, handle equally to non-translated */
567                        trnum = 0;
568                    if ((ennum == -1 && trnum != -1) || (ennum != -1 && trnum == -1)) /* files do not match */
569                        return false;
570                    if (ennum == -1) {
571                        break;
572                    }
573                    String[] enstrings = new String[ennum];
574                    for (int i = 0; i < ennum; ++i) {
575                        int val = ens.read(enlen);
576                        if (val != 2) /* file corrupt */
577                            return false;
578                        val = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]);
579                        if (val > str.length) {
580                            str = new byte[val];
581                        }
582                        int rval = ens.read(str, 0, val);
583                        if (rval != val) /* file corrupt */
584                            return false;
585                        enstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8);
586                    }
587                    String[] trstrings = new String[trnum];
588                    for (int i = 0; i < trnum; ++i) {
589                        int val = trs.read(trlen);
590                        if (val != 2) /* file corrupt */
591                            return false;
592                        val = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]);
593                        if (val > str.length) {
594                            str = new byte[val];
595                        }
596                        int rval = trs.read(str, 0, val);
597                        if (rval != val) /* file corrupt */
598                            return false;
599                        trstrings[i] = new String(str, 0, val, StandardCharsets.UTF_8);
600                    }
601                    if (trnum > 0 && !p.containsKey(enstrings[0])) {
602                        p.put(enstrings[0], trstrings);
603                    }
604                } else {
605                    int enval = ens.read(enlen);
606                    int trval = trs.read(trlen);
607                    if (enval != trval) /* files do not match */
608                        return false;
609                    if (enval == -1) {
610                        break;
611                    }
612                    if (enval != 2) /* files corrupt */
613                        return false;
614                    enval = (enlen[0] < 0 ? 256+enlen[0] : enlen[0])*256+(enlen[1] < 0 ? 256+enlen[1] : enlen[1]);
615                    trval = (trlen[0] < 0 ? 256+trlen[0] : trlen[0])*256+(trlen[1] < 0 ? 256+trlen[1] : trlen[1]);
616                    if (trval == 0xFFFE) /* marks identical string, handle equally to non-translated */
617                        trval = 0;
618                    if (enval == 0xFFFF) {
619                        multimode = true;
620                        if (trval != 0xFFFF) /* files do not match */
621                            return false;
622                    } else {
623                        if (enval > str.length) {
624                            str = new byte[enval];
625                        }
626                        if (trval > str.length) {
627                            str = new byte[trval];
628                        }
629                        int val = ens.read(str, 0, enval);
630                        if (val != enval) /* file corrupt */
631                            return false;
632                        String enstr = new String(str, 0, enval, StandardCharsets.UTF_8);
633                        if (trval != 0) {
634                            val = trs.read(str, 0, trval);
635                            if (val != trval) /* file corrupt */
636                                return false;
637                            String trstr = new String(str, 0, trval, StandardCharsets.UTF_8);
638                            if (!s.containsKey(enstr))
639                                s.put(enstr, trstr);
640                        }
641                    }
642                }
643            }
644        } catch (IOException e) {
645            Main.trace(e);
646            return false;
647        }
648        if (!s.isEmpty()) {
649            strings = s;
650            pstrings = p;
651            return true;
652        }
653        return false;
654    }
655
656    /**
657     * Sets the default locale (see {@link Locale#setDefault(Locale)} to the local
658     * given by <code>localName</code>.
659     *
660     * Ignored if localeName is null. If the locale with name <code>localName</code>
661     * isn't found the default local is set to <tt>en</tt> (english).
662     *
663     * @param localeName the locale name. Ignored if null.
664     */
665    public static void set(String localeName) {
666        if (localeName != null) {
667            Locale l = LanguageInfo.getLocale(localeName);
668            if (load(LanguageInfo.getJOSMLocaleCode(l))) {
669                Locale.setDefault(l);
670            } else {
671                if (!"en".equals(l.getLanguage())) {
672                    Main.info(tr("Unable to find translation for the locale {0}. Reverting to {1}.",
673                            LanguageInfo.getDisplayName(l), LanguageInfo.getDisplayName(Locale.getDefault())));
674                } else {
675                    strings = null;
676                    pstrings = null;
677                }
678            }
679        }
680    }
681
682    /**
683     * Localizations for file chooser dialog.
684     * For some locales (e.g. de, fr) translations are provided
685     * by Java, but not for others (e.g. ru, uk).
686     */
687    public static void translateJavaInternalMessages() {
688        Locale l = Locale.getDefault();
689
690        AbstractFileChooser.setDefaultLocale(l);
691        JFileChooser.setDefaultLocale(l);
692        JColorChooser.setDefaultLocale(l);
693        for (String key : javaInternalMessageKeys) {
694            String us = UIManager.getString(key, Locale.US);
695            String loc = UIManager.getString(key, l);
696            // only provide custom translation if it is not already localized by Java
697            if (us != null && us.equals(loc)) {
698                UIManager.put(key, tr(us));
699            }
700        }
701    }
702
703    private static int pluralEval(long n) {
704        switch(pluralMode) {
705        case MODE_NOTONE: /* bg, da, de, el, en, en_GB, es, et, eu, fi, gl, is, it, iw_IL, nb, nl, sv */
706            return (n != 1) ? 1 : 0;
707        case MODE_NONE: /* id, vi, ja, km, tr, zh_CN, zh_TW */
708            return 0;
709        case MODE_GREATERONE: /* fr, pt_BR */
710            return (n > 1) ? 1 : 0;
711        case MODE_CS:
712            return (n == 1) ? 0 : (((n >= 2) && (n <= 4)) ? 1 : 2);
713        //case MODE_AR:
714        //    return ((n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((((n % 100) >= 3)
715        //            && ((n % 100) <= 10)) ? 3 : ((((n % 100) >= 11) && ((n % 100) <= 99)) ? 4 : 5)))));
716        case MODE_PL:
717            return (n == 1) ? 0 : (((((n % 10) >= 2) && ((n % 10) <= 4))
718                    && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2);
719        //case MODE_RO:
720        //    return ((n == 1) ? 0 : ((((n % 100) > 19) || (((n % 100) == 0) && (n != 0))) ? 2 : 1));
721        case MODE_LT:
722            return ((n % 10) == 1) && ((n % 100) != 11) ? 0 : (((n % 10) >= 2)
723                    && (((n % 100) < 10) || ((n % 100) >= 20)) ? 1 : 2);
724        case MODE_RU:
725            return (((n % 10) == 1) && ((n % 100) != 11)) ? 0 : (((((n % 10) >= 2)
726                    && ((n % 10) <= 4)) && (((n % 100) < 10) || ((n % 100) >= 20))) ? 1 : 2);
727        case MODE_SK:
728            return (n == 1) ? 1 : (((n >= 2) && (n <= 4)) ? 2 : 0);
729        //case MODE_SL:
730        //    return (((n % 100) == 1) ? 1 : (((n % 100) == 2) ? 2 : ((((n % 100) == 3)
731        //            || ((n % 100) == 4)) ? 3 : 0)));
732        }
733        return 0;
734    }
735
736    public static TranslationAdapter getTranslationAdapter() {
737        return I18n::tr;
738    }
739
740    /**
741     * Setup special font for Khmer script, as the default Java fonts do not display these characters.
742     *
743     * @since 8282
744     */
745    public static void setupLanguageFonts() {
746        // Use special font for Khmer script, as the default Java font do not display these characters
747        if ("km".equals(LanguageInfo.getJOSMLocaleCode())) {
748            Collection<String> fonts = Arrays.asList(
749                    GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
750            for (String f : new String[]{"Khmer UI", "DaunPenh", "MoolBoran"}) {
751                if (fonts.contains(f)) {
752                    GuiHelper.setUIFont(f);
753                    break;
754                }
755            }
756        }
757    }
758}