/**
* Copyright (c) 2012-2015 Edgar Espina
*
* This file is part of Handlebars.java.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.jknack.handlebars.helper;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notEmpty;
import static org.apache.commons.lang3.Validate.notNull;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
import com.github.jknack.handlebars.internal.Locales;
/**
* Implementation of i18n helper for Java and JavaScript.
* <p>
* The Java implementation use {@link ResourceBundle}.
* </p>
* <p>
* The JavaScript version use the <a href="https://github.com/fnando/i18n-js">I18n</a> library.
* {@link ResourceBundle} are converted to JavaScript code.
* </p>
*
* @author edgar.espina
* @since 0.9.0
* @see ResourceBundle
*/
public enum I18nHelper implements Helper<String> {
/**
* <p>
* A helper built on top of {@link ResourceBundle}. A {@link ResourceBundle} is the most well
* known mechanism for internationalization (i18n).
* </p>
*
* <h3>messages.properties:</h3>
*
* <pre>
* hello=Hola
* </pre>
*
* <h3>Basic Usage:</h3>
*
* <pre>
* {{i18n "hello"}}
* </pre>
*
* <p>
* Will result in: <code>Hola</code>
* </p>
* <h3>Using a locale:</h3>
*
* <pre>
* {{i18n "hello" locale="es_AR"}}
* </pre>
*
* <h3>Using a different bundle:</h3>
*
* <pre>
* {{i18n "hello" bundle="myMessages"}}
* </pre>
*
* <h3>Using a message format:</h3>
*
* <pre>
* hello=Hola {0}!
* </pre>
*
* <pre>
* {{i18n "hello" "Handlebars.java"}}
* </pre>
*
* @author edgar.espina
* @since 0.9.0
* @see ResourceBundle
*/
i18n {
/**
* <p>
* A helper built on top of {@link ResourceBundle}. A {@link ResourceBundle} is the most well
* known mechanism for internationalization (i18n).
* </p>
* <p>
* <h3>messages.properties:</h3>
* </p>
*
* <pre>
* hello=Hola
* </pre>
*
* <h3>Basic Usage:</h3>
*
* <pre>
* {{i18n "hello"}}
* </pre>
*
* <p>
* Will result in: <code>Hola</code>
* </p>
* <h3>Using a locale:</h3>
*
* <pre>
* {{i18n "hello" locale="es_AR"}}
* </pre>
*
* <h3>Using a different bundle:</h3>
*
* <pre>
* {{i18n "hello" bundle="myMessages"}}
* </pre>
*
* <h3>Using a message format</h3>
*
* <pre>
* hello=Hola {0}!
* </pre>
*
* <pre>
* {{i18n "hello" "Handlebars.java"}}
* </pre>
*
* @param key The bundle's key. Required.
* @param options The helper's options. Not null.
* @return An i18n message.
* @throws IOException If the bundle wasn't resolve.
*/
@Override
public Object apply(final String key, final Options options) throws IOException {
notEmpty(key, "found: '%s', expected 'bundle's key'", key);
Locale locale = Locales
.fromString((String) options.hash("locale", defaultLocale.toString()));
String baseName = options.hash("bundle", defaultBundle);
ClassLoader classLoader = options.hash("classLoader", getClass().getClassLoader());
I18nSource localSource = source == null
? new DefI18nSource(baseName, locale, classLoader) : source;
return localSource.message(key, locale, options.params);
}
},
/**
* <p>
* Translate a {@link ResourceBundle} into JavaScript code. The generated code assume you added
* the <a href="https://github.com/fnando/i18n-js">I18n</a>
* </p>
* <p>
* It converts message patterns like: <code>Hi {0}</code> into <code>Hi {{arg0}}</code>. This make
* possible to the I18n JS library to interpolate variables.
* </p>
* <p>
* Note: make sure you include <a href="https://github.com/fnando/i18n-js">I18n</a> in your
* application. Otherwise, the generated code will fail.
* </p>
* <p>
* Usage:
* </p>
*
* <pre>
* {{i18nJs locale?}}
* </pre>
*
* If locale argument is present it will translate that locale to JavaScript. Otherwise, the
* default locale.
*/
i18nJs {
/**
* The message format pattern.
*/
private final Pattern pattern = Pattern.compile("\\{(\\d+)\\}");
/**
* <p>
* Translate a {@link ResourceBundle} into JavaScript code. The generated code assume you added
* the <a href="https://github.com/fnando/i18n-js">I18n</a>
* </p>
* <p>
* It converts message patterns like: <code>Hi {0}</code> into <code>Hi {{arg0}}</code>. This
* make possible to the I18n JS library to interpolate variables.
* </p>
* <p>
* Note: make sure you include <a href="https://github.com/fnando/i18n-js">I18n</a> in your
* application. Otherwise, the generated code will fail.
* </p>
* <p>
* Usage:
* </p>
*
* <pre>
* {{i18nJs [locale] [bundle=messages] [wrap=true]}}
* </pre>
*
* If locale argument is present it will translate that locale to JavaScript. Otherwise, the
* default locale.
*
* Use wrap=true for wrapping the code with a script tag.
*
* @param localeName The locale's name. Optional.
* @param options The helper's options. Not null.
* @return JavaScript code from {@link ResourceBundle}.
* @throws IOException If bundle wasn't resolve.
*/
@Override
public Object apply(final String localeName, final Options options) throws IOException {
Locale locale = Locales.fromString(defaultIfEmpty(localeName, defaultLocale.toString()));
String baseName = options.hash("bundle", defaultBundle);
ClassLoader classLoader = options.hash("classLoader", getClass().getClassLoader());
I18nSource localSource = source == null
? new DefI18nSource(baseName, locale, classLoader) : source;
StringBuilder buffer = new StringBuilder();
Boolean wrap = options.hash("wrap", true);
if (wrap) {
buffer.append("<script type='text/javascript'>\n");
}
buffer.append(" /* ").append(locale.getDisplayName()).append(" */\n");
buffer.append(" I18n.translations = I18n.translations || {};\n");
buffer.append(" I18n.translations['").append(locale.toString()).append("'] = {\n");
StringBuilder body = new StringBuilder();
String separator = ",\n";
String[] keys = localSource.keys(baseName, locale);
for (String key : keys) {
String message = message(localSource.message(key, locale));
body.append(" \"").append(key).append("\": ");
body.append("\"").append(message).append("\"").append(separator);
}
if (body.length() > 0) {
body.setLength(body.length() - separator.length());
buffer.append(body);
}
buffer.append("\n };\n");
if (wrap) {
buffer.append("</script>\n");
}
return new Handlebars.SafeString(buffer);
}
/**
* Convert expression <code>{0}</code> into <code>{{arg0}}</code> and escape EcmaScript
* characters.
*
* @param message The candidate message.
* @return A valid I18n message.
*/
private String message(final String message) {
CharSequence escapedMessage = Handlebars.Utils.escapeExpression(message);
Matcher matcher = pattern.matcher(escapedMessage);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(result, "{{arg" + matcher.group(1) + "}}");
}
matcher.appendTail(result);
return result.toString();
}
};
/**
* The default locale. Required.
*/
protected Locale defaultLocale = Locale.getDefault();
/**
* The default's bundle. Required.
*/
protected String defaultBundle = "messages";
/** The message source to use. */
protected I18nSource source;
/**
* Set the message source.
*
* NotThreadSafe Make sure to call this method ONCE at start time.
*
* @param source The message source. Required.
*/
public void setSource(final I18nSource source) {
this.source = notNull(source, "The i18n source is required.");
}
/**
* Set the default bundle's name. Default is: messages and this method let you override the
* default bundle's name to something else.
*
* NotThreadSafe Make sure to call this method ONCE at start time.
*
* @param bundle The default's bundle name. Required.
*/
public void setDefaultBundle(final String bundle) {
this.defaultBundle = notEmpty(bundle, "A bundle's name is required.");
}
/**
* Set the default locale. Default is system dependent and this method let you override the
* default bundle's name to something else.
*
* NotThreadSafe Make sure to call this method ONCE at start time.
*
* @param locale The default locale name. Required.
*/
public void setDefaultLocale(final Locale locale) {
this.defaultLocale = notNull(locale, "A locale is required.");
}
}
/** Default implementation of I18nSource. */
class DefI18nSource implements I18nSource {
/** The resource bundle. */
private ResourceBundle bundle;
/**
* Creates a new {@link DefI18nSource}.
*
* @param baseName The base name.
* @param locale The locale.
* @param classLoader The classloader.
*/
public DefI18nSource(final String baseName, final Locale locale, final ClassLoader classLoader) {
bundle = ResourceBundle.getBundle(baseName, locale, classLoader);
}
@Override
public String[] keys(final String basename, final Locale locale) {
Enumeration<String> keys = bundle.getKeys();
List<String> result = new ArrayList<String>();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
result.add(key);
}
return result.toArray(new String[result.size()]);
}
@Override
public String message(final String key, final Locale locale, final Object... args) {
isTrue(bundle.containsKey(key), "no message found: '%s' for locale '%s'.", key, locale);
String message = bundle.getString(key);
if (args.length == 0) {
return message;
}
MessageFormat format = new MessageFormat(message, locale);
return format.format(args);
}
};