001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.util.ArrayList;
005import java.util.List;
006import java.util.Locale;
007
008/**
009 * This is a utility class that provides information about locales and allows to convert locale codes.
010 */
011public final class LanguageInfo {
012
013    private LanguageInfo() {
014        // Hide default constructor for utils classes
015    }
016
017    /**
018     * Type of the locale to use
019     * @since 5915
020     */
021    public enum LocaleType {
022        /** The current default language */
023        DEFAULT,
024        /** The current default language, but not english */
025        DEFAULTNOTENGLISH,
026        /** The base language (i.e. pt for pt_BR) */
027        BASELANGUAGE,
028        /** The standard english texts */
029        ENGLISH,
030        /** The locale prefix on the OSM wiki */
031        OSM_WIKI,
032    }
033
034    /**
035     * Replies the wiki language prefix for the given locale. The wiki language
036     * prefix has the form 'Xy:' where 'Xy' is a ISO 639 language code in title
037     * case (or Xy_AB: for sub languages).
038     *
039     * @param type the type
040     * @return the wiki language prefix or {@code null} for {@link LocaleType#BASELANGUAGE}, when
041     * base language is identical to default or english
042     * @since 5915
043     */
044    public static String getWikiLanguagePrefix(LocaleType type) {
045        return getWikiLanguagePrefix(Locale.getDefault(), type);
046    }
047
048    static String getWikiLanguagePrefix(Locale locale, LocaleType type) {
049        if (type == LocaleType.ENGLISH) {
050            return "";
051        } else if (type == LocaleType.OSM_WIKI && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
052            return "";
053        } else if (type == LocaleType.OSM_WIKI && Locale.SIMPLIFIED_CHINESE.equals(locale)) {
054            return "Zh-hans:";
055        } else if (type == LocaleType.OSM_WIKI && Locale.TRADITIONAL_CHINESE.equals(locale)) {
056            return "Zh-hant:";
057        }
058
059        String code = getJOSMLocaleCode(locale);
060
061        if (type == LocaleType.OSM_WIKI) {
062            if (code.matches("[^_@]+[_@][^_]+")) {
063                code = code.substring(0, 2);
064                if ("en".equals(code)) {
065                    return "";
066                }
067            }
068            switch (code) {
069                case "nb":          /* OSM-Wiki has "no", but no "nb" */
070                    return "No:";
071                case "sr@latin":    /* OSM-Wiki has "Sr-latn" and not Sr-latin */
072                    return "Sr-latn:";
073                case "de":
074                case "es":
075                case "fr":
076                case "it":
077                case "nl":
078                case "ru":
079                case "ja":
080                    return code.toUpperCase(Locale.ENGLISH) + ":";
081                default:
082                    return code.substring(0, 1).toUpperCase(Locale.ENGLISH) + code.substring(1) + ":";
083            }
084        }
085
086        if (type == LocaleType.BASELANGUAGE) {
087            if (code.matches("[^_]+_[^_]+")) {
088                code = code.substring(0, 2);
089                if ("en".equals(code))
090                    return null;
091            } else {
092                return null;
093            }
094        } else if (type == LocaleType.DEFAULTNOTENGLISH && "en".equals(code)) {
095            return null;
096        } else if (code.matches(".+@.+")) {
097            return code.substring(0, 1).toUpperCase(Locale.ENGLISH)
098                    + code.substring(1, 2)
099                    + '-'
100                    + code.substring(3, 4).toUpperCase(Locale.ENGLISH)
101                    + code.substring(4)
102                    + ':';
103        }
104        return code.substring(0, 1).toUpperCase(Locale.ENGLISH) + code.substring(1) + ':';
105    }
106
107    /**
108     * Replies the wiki language prefix for the current locale.
109     *
110     * @return the wiki language prefix
111     * @see Locale#getDefault()
112     * @see #getWikiLanguagePrefix(LocaleType)
113     */
114    public static String getWikiLanguagePrefix() {
115        return getWikiLanguagePrefix(LocaleType.DEFAULT);
116    }
117
118    /**
119     * Replies the JOSM locale code for the default locale.
120     *
121     * @return the JOSM locale code for the default locale
122     * @see #getJOSMLocaleCode(Locale)
123     */
124    public static String getJOSMLocaleCode() {
125        return getJOSMLocaleCode(Locale.getDefault());
126    }
127
128    /**
129     * Replies the locale code used by JOSM for a given locale.
130     *
131     * In most cases JOSM uses the 2-character ISO 639 language code ({@link Locale#getLanguage()}
132     * to identify the locale of a localized resource, but in some cases it may use the
133     * programmatic name for locales, as replied by {@link Locale#toString()}.
134     *
135     * For unknown country codes and variants this function already does fallback to
136     * internally known translations.
137     *
138     * @param locale the locale. Replies "en" if null.
139     * @return the JOSM code for the given locale
140     */
141    public static String getJOSMLocaleCode(Locale locale) {
142        if (locale == null) return "en";
143        for (String full : getLanguageCodes(locale)) {
144            if ("iw_IL".equals(full))
145                return "he";
146            else if ("in".equals(full))
147                return "id";
148            else if (I18n.hasCode(full)) // catch all non-single codes
149                return full;
150        }
151
152        // return single code as fallback
153        return locale.getLanguage();
154    }
155
156    /**
157     * Replies the locale code used by Java for a given locale.
158     *
159     * In most cases JOSM and Java uses the same codes, but for some exceptions this is needed.
160     *
161     * @param localeName the locale. Replies "en" if null.
162     * @return the Java code for the given locale
163     * @since 8232
164     */
165    public static String getJavaLocaleCode(String localeName) {
166        if (localeName == null) return "en";
167        switch (localeName) {
168            case "ca@valencia":
169                return "ca__valencia";
170            case "sr@latin":
171                return "sr__latin";
172            case "he":
173                return "iw_IL";
174            case "id":
175                return "in";
176        }
177        return localeName;
178    }
179
180    /**
181     * Replies the display string used by JOSM for a given locale.
182     *
183     * In most cases returns text replied by {@link Locale#getDisplayName()}, for some
184     * locales an override is used (i.e. when unsupported by Java).
185     *
186     * @param locale the locale. Replies "en" if null.
187     * @return the display string for the given locale
188     * @since 8232
189     */
190    public static String getDisplayName(Locale locale) {
191        String currentCountry = Locale.getDefault().getCountry();
192        String localeCountry = locale.getCountry();
193        // Don't display locale country if country has been forced to current country at JOSM startup
194        if (currentCountry.equals(localeCountry) && !I18n.hasCode(getLanguageCodes(locale).get(0))) {
195            return new Locale(locale.getLanguage(), "", locale.getVariant()).getDisplayName();
196        }
197        return locale.getDisplayName();
198    }
199
200    /**
201     * Replies the locale used by Java for a given language code.
202     *
203     * Accepts JOSM and Java codes as input.
204     *
205     * @param localeName the locale code.
206     * @return the resulting locale
207     */
208    public static Locale getLocale(String localeName) {
209        return getLocale(localeName, false);
210    }
211
212    /**
213     * Replies the locale used by Java for a given language code.
214     *
215     * Accepts JOSM, Java and POSIX codes as input.
216     *
217     * @param localeName the locale code.
218     * @param useDefaultCountry if {@code true}, the current locale country will be used if no country is specified
219     * @return the resulting locale
220     * @since 15547
221     */
222    public static Locale getLocale(String localeName, boolean useDefaultCountry) {
223        final int encoding = localeName.indexOf('.');
224        if (encoding > 0) {
225            localeName = localeName.substring(0, encoding);
226        }
227        int country = localeName.indexOf('_');
228        int variant = localeName.indexOf('@');
229        if (variant < 0 && country >= 0)
230            variant = localeName.indexOf('_', country+1);
231        Locale l;
232        if (variant > 0 && country > 0) {
233            l = new Locale(localeName.substring(0, country), localeName.substring(country+1, variant), localeName.substring(variant + 1));
234        } else if (variant > 0) {
235            l = new Locale(localeName.substring(0, variant), "", localeName.substring(variant + 1));
236        } else if (country > 0) {
237            l = new Locale(localeName.substring(0, country), localeName.substring(country + 1));
238        } else {
239            l = new Locale(localeName, useDefaultCountry ? Locale.getDefault().getCountry() : "");
240        }
241        return l;
242    }
243
244    /**
245     * Check if a new language is better than a previous existing. Can be used in classes where
246     * multiple user supplied language marked strings appear and the best one is searched. Following
247     * priorities: current language, english, any other
248     *
249     * @param oldLanguage the language code of the existing string
250     * @param newLanguage the language code of the new string
251     * @return true if new one is better
252     * @since 8091
253     */
254    public static boolean isBetterLanguage(String oldLanguage, String newLanguage) {
255        if (oldLanguage == null)
256            return true;
257        String want = getJOSMLocaleCode();
258        return want.equals(newLanguage) || (!want.equals(oldLanguage) && newLanguage.startsWith("en"));
259    }
260
261    /**
262     * Replies the language prefix for use in XML elements (with a dot appended).
263     *
264     * @return the XML language prefix
265     * @see #getJOSMLocaleCode()
266     */
267    public static String getLanguageCodeXML() {
268        String code = getJOSMLocaleCode();
269        code = code.replace('@', '-');
270        return code+'.';
271    }
272
273    /**
274     * Replies the language prefix for use in manifests (with an underscore appended).
275     *
276     * @return the manifest language prefix
277     * @see #getJOSMLocaleCode()
278     */
279    public static String getLanguageCodeManifest() {
280        String code = getJOSMLocaleCode();
281        code = code.replace('@', '-');
282        return code+'_';
283    }
284
285    /**
286     * Replies a list of language codes for local names. Prefixes range from very specific
287     * to more generic.
288     * <ul>
289     *   <li>lang_COUNTRY@variant  of the current locale</li>
290     *   <li>lang@variant  of the current locale</li>
291     *   <li>lang_COUNTRY of the current locale</li>
292     *   <li>lang of the current locale</li>
293     * </ul>
294     *
295     * @param l the locale to use, <code>null</code> for default locale
296     * @return list of codes
297     * @since 8283
298     */
299    public static List<String> getLanguageCodes(Locale l) {
300        List<String> list = new ArrayList<>(4);
301        if (l == null)
302            l = Locale.getDefault();
303        String lang = l.getLanguage();
304        String c = l.getCountry();
305        String v = l.getVariant();
306        if (c.isEmpty())
307            c = null;
308        if (!Utils.isEmpty(v)) {
309            if (c != null)
310                list.add(lang+'_'+c+'@'+v);
311            list.add(lang+'@'+v);
312        }
313        if (c != null)
314            list.add(lang+'_'+c);
315        list.add(lang);
316        return list;
317    }
318}