package net.i2p.util; import java.text.MessageFormat; import java.util.Arrays; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import gnu.gettext.GettextResource; import net.i2p.I2PAppContext; import net.i2p.util.ConcurrentHashSet; /** * Translate strings efficiently. * We don't include an English or default ResourceBundle, we simply check * for "en" and return the original string. * Support real-time language changing with the routerconsole.lang * and routerconsole.country properties. * * To change language in router context, set the context properties PROP_LANG and PROP_COUNTRY. * To change language in app context, set the System properties PROP_LANG and PROP_COUNTRY. * * @author zzz, from a base generated by eclipse. * @since 0.7.9 */ public abstract class Translate { public static final String PROP_LANG = "routerconsole.lang"; /** @since 0.9.10 */ public static final String PROP_COUNTRY = "routerconsole.country"; /** non-null, two- or three-letter lower case, may be "" */ private static final String _localeLang = Locale.getDefault().getLanguage(); /** non-null, two-letter upper case, may be "" */ private static final String _localeCountry = Locale.getDefault().getCountry(); private static final Map<String, ResourceBundle> _bundles = new ConcurrentHashMap<String, ResourceBundle>(16); private static final Set<String> _missing = new ConcurrentHashSet<String>(16); /** use to look for untagged strings */ private static final String TEST_LANG = "xx"; private static final String TEST_STRING = "XXXX"; /** lang in routerconsole.lang property, else current locale */ public static String getString(String key, I2PAppContext ctx, String bun) { String lang = getLanguage(ctx); if (lang.equals("en")) return key; else if (lang.equals(TEST_LANG)) return TEST_STRING; // shouldnt happen but dont dump the po headers if it does if (key.equals("")) return key; ResourceBundle bundle = findBundle(bun, lang, getCountry(ctx)); if (bundle == null) return key; try { return bundle.getString(key); } catch (MissingResourceException e) { return key; } } /** * translate a string with a parameter * This is a lot more expensive than getString(s, ctx), so use sparingly. * * @param s string to be translated containing {0} * The {0} will be replaced by the parameter. * Single quotes must be doubled, i.e. ' -> '' in the string. * @param o parameter, not translated. * To translate parameter also, use _t("foo {0} bar", _t("baz")) * Do not double the single quotes in the parameter. * Use autoboxing to call with ints, longs, floats, etc. */ public static String getString(String s, Object o, I2PAppContext ctx, String bun) { return getString(s, ctx, bun, o); } /** for {0} and {1} */ public static String getString(String s, Object o, Object o2, I2PAppContext ctx, String bun) { return getString(s, ctx, bun, o, o2); } /** * Varargs * @param oArray parameters * @since 0.9.8 */ public static String getString(String s, I2PAppContext ctx, String bun, Object... oArray) { String lang = getLanguage(ctx); if (lang.equals(TEST_LANG)) return TEST_STRING + Arrays.toString(oArray) + TEST_STRING; String x = getString(s, ctx, bun); try { MessageFormat fmt = new MessageFormat(x, new Locale(lang)); return fmt.format(oArray, new StringBuffer(), null).toString(); } catch (IllegalArgumentException iae) { System.err.println("Bad format: orig: \"" + s + "\" trans: \"" + x + "\" params: " + Arrays.toString(oArray) + " lang: " + lang); return "FIXME: " + x + ' ' + Arrays.toString(oArray); } } /** * Use GNU ngettext * For .po file format see http://www.gnu.org/software/gettext/manual/gettext.html.gz#Translating-plural-forms * * @param n how many * @param s singluar string, optionally with {0} e.g. "one tunnel" * @param p plural string optionally with {0} e.g. "{0} tunnels" * @since 0.7.14 */ public static String getString(int n, String s, String p, I2PAppContext ctx, String bun) { String lang = getLanguage(ctx); if (lang.equals(TEST_LANG)) return TEST_STRING + '(' + n + ')' + TEST_STRING; ResourceBundle bundle = null; if (!lang.equals("en")) bundle = findBundle(bun, lang, getCountry(ctx)); String x; if (bundle == null) x = n == 1 ? s : p; else x = GettextResource.ngettext(bundle, s, p, n); Object[] oArray = new Object[1]; oArray[0] = Integer.valueOf(n); try { MessageFormat fmt = new MessageFormat(x, new Locale(lang)); return fmt.format(oArray, new StringBuffer(), null).toString(); } catch (IllegalArgumentException iae) { System.err.println("Bad format: sing: \"" + s + "\" plural: \"" + p + "\" lang: " + lang); return "FIXME: " + s + ' ' + p + ',' + n; } } /** * Two- or three-letter lower case * @return lang in routerconsole.lang property, else current locale */ public static String getLanguage(I2PAppContext ctx) { String lang = ctx.getProperty(PROP_LANG); if (lang == null || lang.length() <= 0) lang = _localeLang; return lang; } /** * Two-letter upper case or "" * @return country in routerconsole.country property, else current locale * @since 0.9.10 */ public static String getCountry(I2PAppContext ctx) { // property may be empty so we don't have a non-default // language and a default country return ctx.getProperty(PROP_COUNTRY, _localeCountry); } /** * Only for use by standalone apps in App Context. * NOT for use in Router Context. * Does not persist, apps must implement their own persistence. * Does NOT override context properties. * * @param lang Two- or three-letter lower case, or null for default * @param country Two-letter upper case, or null for default, or "" for none * @since 0.9.27 */ public static void setLanguage(String lang, String country) { if (lang != null) System.setProperty(PROP_LANG, lang); else System.clearProperty(PROP_LANG); if (country != null) System.setProperty(PROP_COUNTRY, country); else System.clearProperty(PROP_COUNTRY); } /** * cache both found and not found for speed * @param lang non-null, if "" returns null * @param country non-null, may be "" * @return null if not found */ private static ResourceBundle findBundle(String bun, String lang, String country) { String key = bun + '-' + lang + '-' + country; ResourceBundle rv = _bundles.get(key); if (rv == null && !_missing.contains(key)) { if ("".equals(lang)) { _missing.add(key); return null; } try { Locale loc; if ("".equals(country)) loc = new Locale(lang); else loc = new Locale(lang, country); // We must specify the class loader so that a webapp can find the bundle in the .war rv = ResourceBundle.getBundle(bun, loc, Thread.currentThread().getContextClassLoader()); if (rv != null) _bundles.put(key, rv); } catch (MissingResourceException e) { _missing.add(key); } } return rv; } /** * Return the "display language", e.g. "English" for the language specified * by langCode, using the current language. * Uses translation if available, then JVM Locale.getDisplayLanguage() if available, else default param. * * @param langCode two- or three-letter lower-case * @param dflt e.g. "English" * @since 0.9.5 */ public static String getDisplayLanguage(String langCode, String dflt, I2PAppContext ctx, String bun) { String curLang = getLanguage(ctx); if (!"en".equals(curLang)) { String rv = getString(dflt, ctx, bun); if (!rv.equals(dflt)) return rv; Locale curLocale = new Locale(curLang); rv = (new Locale(langCode)).getDisplayLanguage(curLocale); if (rv.length() > 0 && !rv.equals(langCode)) return rv; } return dflt; } /** * Clear the cache. * Call this after adding new bundles to the classpath. * @since 0.7.12 */ public static void clearCache() { _missing.clear(); _bundles.clear(); ResourceBundle.clearCache(); } }