diff --git a/sitebricks/src/main/java/com/google/sitebricks/Localizer.java b/sitebricks/src/main/java/com/google/sitebricks/Localizer.java index d74ea145..f623db63 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/Localizer.java +++ b/sitebricks/src/main/java/com/google/sitebricks/Localizer.java @@ -1,5 +1,6 @@ package com.google.sitebricks; +import com.google.sitebricks.locale.LocaleProvider; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -40,7 +41,7 @@ public class Localizer { /** * A value object that represents the localization of an i18n interface to a locale - * and corresponding set of messages. + * and corresponding set of messages. */ public static class Localization { // TODO(dhanji): Convert class reference to weak? @@ -53,7 +54,7 @@ public Localization(Class clazz, Locale locale, Map messageBu this.locale = locale; this.messageBundle = messageBundle; } - + public Class getClazz() { return this.clazz; } @@ -168,7 +169,7 @@ private void bindMessageProvider(final Class iface, // Wonderful Guice hack to get around not using assisted inject. @Inject - private final Provider requestProvider = null; + final LocaleProvider localeProvider = null; // This is our delegate field that proxies the interface. private final Object instance = Proxy.newProxyInstance( @@ -179,7 +180,7 @@ private void bindMessageProvider(final Class iface, * Returns the localized message bundle value, keyed by the method name invoked. */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Locale locale = requestProvider.get().getLocale(); + Locale locale = localeProvider.getLocale(); Map messages = getMessagesWithFallback(locale); // Use default if we don't support the given locale. @@ -189,7 +190,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl MessageDescriptor descriptor = messages.get(method.getName()); if (descriptor == null) { - throw new IllegalStateException("Could not find message '" + throw new IllegalStateException("Could not find message '" + method.getName() + "' in " + messages); } return descriptor.render(args); @@ -199,7 +200,7 @@ private Map getMessagesWithFallback(Locale locale) { String localeInterfaceKey = createLocaleInterfaceKey(iface, locale); Map result = localizedValues.get(localeInterfaceKey); if (result == null) { - result = localizedValues.get(new Locale(locale.getLanguage())); + result = localizedValues.get(createLocaleInterfaceKey(iface, new Locale(locale.getLanguage()))); } return result; } @@ -215,7 +216,7 @@ public Object get() { } - private String createLocaleInterfaceKey(final Class iface, Locale locale) { + private String createLocaleInterfaceKey(final Class iface, Locale locale) { return locale.toString() + ":" + iface.getName(); } diff --git a/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java b/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java index af6050d0..6877913d 100644 --- a/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java +++ b/sitebricks/src/main/java/com/google/sitebricks/SitebricksModule.java @@ -1,5 +1,6 @@ package com.google.sitebricks; +import com.google.sitebricks.locale.LocaleProviderModule; import java.lang.annotation.Annotation; import java.util.Enumeration; import java.util.List; @@ -120,11 +121,20 @@ protected final void configure() { configureTemplateSystem(); + /* Now bind the locale provider.*/ + bindLocaleProvider(); + } + + /** + * Used to bind the Locale provider. Can be overwritten if custom Locale behavior is desider. + */ + protected void bindLocaleProvider() { + install(new LocaleProviderModule()); } protected void configureTemplateSystem() { // - // Map of all the implementations keyed by type they can handle + // Map of all the implementations keyed by type they can handle // ImmutableMap.Builder> builder = ImmutableMap.builder(); @@ -232,7 +242,7 @@ public void using(Locale locale, ResourceBundle bundle) { public void usingDefault() { add(Localizer.defaultLocalizationFor(iface)); } - + }; } @@ -246,7 +256,7 @@ private void add(Localizer.Localization localization) { } localeLocalizer.put(localization.getLocale(), localization); } - + protected final void scan(Package pack) { Preconditions.checkArgument(null != pack, "Package parameter to scan() cannot be null"); packages.add(pack); diff --git a/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProvider.java b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProvider.java new file mode 100644 index 00000000..0ef1ecc7 --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProvider.java @@ -0,0 +1,17 @@ +package com.google.sitebricks.locale; + +import java.util.Locale; + +/** + * Provides the {@link java.util.Locale} for the internationalization. + */ +public interface LocaleProvider { + + /** + * Retrieves the locale that is to be used for the i18n of the translatable messages. + * + * @return the requested {@link java.util.Locale}. + */ + Locale getLocale(); + +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderImpl.java b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderImpl.java new file mode 100644 index 00000000..e36b46fc --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderImpl.java @@ -0,0 +1,38 @@ +package com.google.sitebricks.locale; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** + * Provides a default implementation of the {@link LocaleProvider}. It retrieves the Locale as stored in the + * {@link javax.servlet.http.HttpServletRequest}. + */ +class LocaleProviderImpl implements LocaleProvider { + + private final Provider requestProvider; + + @Inject + LocaleProviderImpl(final Provider requestProvider) { + this.requestProvider = requestProvider; + } + + /** + * @return the Locale as stored in the {@link javax.servlet.http.HttpServletRequest}. + */ + public Locale getLocale() { + if (requestProvider == null) { + throw new IllegalStateException("The HttpServletRequest provider must be bound."); + } + + final HttpServletRequest request = requestProvider.get(); + if (request == null) { + throw new IllegalStateException("No HttpServletRequest could be retrieved. Cannot determine user locale."); + } + + return request.getLocale(); + } +} diff --git a/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderModule.java b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderModule.java new file mode 100644 index 00000000..e5998a5f --- /dev/null +++ b/sitebricks/src/main/java/com/google/sitebricks/locale/LocaleProviderModule.java @@ -0,0 +1,11 @@ +package com.google.sitebricks.locale; + +import com.google.inject.AbstractModule; +import com.google.inject.Singleton; + +public class LocaleProviderModule extends AbstractModule { + @Override + protected final void configure() { + bind(LocaleProvider.class).to(LocaleProviderImpl.class).in(Singleton.class); + } +} diff --git a/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java b/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java index 5bfbb2ba..751da34e 100644 --- a/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java +++ b/sitebricks/src/test/java/com/google/sitebricks/LocalizationTest.java @@ -9,6 +9,9 @@ import com.google.inject.Provider; import com.google.inject.name.Named; import com.google.sitebricks.i18n.Message; +import com.google.sitebricks.locale.LocaleProvider; +import org.easymock.EasyMock; +import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -29,15 +32,15 @@ */ public class LocalizationTest { private static final String HELLO = "hello"; - private HttpServletRequest requestMock; + private LocaleProvider localeProviderMock; @BeforeMethod public final void setup() { - requestMock = createNiceMock(HttpServletRequest.class); + localeProviderMock = createNiceMock(LocaleProvider.class); - expect(requestMock.getLocale()).andReturn(Locale.ENGLISH); + expect(localeProviderMock.getLocale()).andReturn(Locale.ENGLISH); - replay(requestMock); + replay(localeProviderMock); } @Test @@ -54,7 +57,7 @@ protected void configure() { Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(Localized.class) .hello(); @@ -74,7 +77,7 @@ protected void configure() { locs.add(new Localizer.Localization(Localized.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }); } @@ -92,7 +95,7 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedMissingAnnotation.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedMissingAnnotation.class); } @@ -111,7 +114,7 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedWrongReturnType.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedWrongReturnType.class); } @@ -130,7 +133,7 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedWrongArgAnnotation.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedWrongArgAnnotation.class); } @@ -150,7 +153,7 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedBrokenTemplate.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedBrokenTemplate.class); } @@ -169,7 +172,7 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedTemplate.class, Locale.ENGLISH, resourceBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedTemplate.class) .hello("Dude"); @@ -186,15 +189,11 @@ public final void parameterizedLocalizeTemplateMultipleLocales() { resourceBundle.put(LocalizationTest.HELLO, "hello ${name}"); final HashMap japaneseBundle = Maps.newHashMap(); - japaneseBundle.put(LocalizationTest.HELLO, "konichiwa ${name}"); + japaneseBundle.put(LocalizationTest.HELLO, "konichiwa ${name} sama"); - // Simulate an Accept-Language of Japanese - HttpServletRequest japaneseRequest = createNiceMock(HttpServletRequest.class); - expect(japaneseRequest.getLocale()).andReturn(Locale.JAPANESE); - replay(japaneseRequest); - - final AtomicReference mockToUse - = new AtomicReference(japaneseRequest); + final LocaleProvider customLocaleProviderMock = createNiceMock(LocaleProvider.class); + expect(customLocaleProviderMock.getLocale()).andReturn(Locale.JAPANESE); + replay(customLocaleProviderMock); Injector injector = Guice.createInjector(new AbstractModule() { @Override @@ -204,38 +203,31 @@ protected void configure() { locs.add(new Localizer.Localization(LocalizedTemplate.class, Locale.JAPANESE, japaneseBundle)); Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toProvider(new Provider() { - public HttpServletRequest get() { - return mockToUse.get(); - } - }); + bind(LocaleProvider.class).toInstance(customLocaleProviderMock); } }); String msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); - assert "konichiwa Dude".equals(msg) : msg; - - verify(japaneseRequest); + assert "konichiwa Dude sama".equals(msg) : msg; // Now let's simulate english. - mockToUse.set(requestMock); + EasyMock.reset(customLocaleProviderMock); + expect(customLocaleProviderMock.getLocale()).andReturn(Locale.ENGLISH); + replay(customLocaleProviderMock); + msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); assert "hello Dude".equals(msg); // Now let's simulate a totally different locale (should default to english). // Simulate an Accept-Language of French - HttpServletRequest frenchRequest = createNiceMock(HttpServletRequest.class); - expect(frenchRequest.getLocale()).andReturn(Locale.FRENCH); - replay(frenchRequest); - - mockToUse.set(frenchRequest); + EasyMock.reset(customLocaleProviderMock); + expect(customLocaleProviderMock.getLocale()).andReturn(Locale.FRENCH); + replay(customLocaleProviderMock); // Assert that it uses the english locale (set as default above) msg = injector.getInstance(LocalizedTemplate.class).hello("Dude"); assert "hello Dude".equals(msg); - - verify(frenchRequest, requestMock); } @@ -249,7 +241,7 @@ protected void configure() { Localizer.localizeAll(binder(), locs); - bind(HttpServletRequest.class).toInstance(requestMock); + bind(LocaleProvider.class).toInstance(localeProviderMock); } }).getInstance(LocalizedTemplate.class) .hello("Dudette"); @@ -257,6 +249,45 @@ protected void configure() { assert "hello Dudette!".equals(msg); } + /** + * Given: there are multiple localization properties for the same language, once with a country and once without.
+ * When: the localization is requested for an unknown locale with a known language.
+ * Then: the fallback localization for the language should be retrieved.
+ */ + @Test + public final void localizationLanguageFallback() { + //Create 2 resource bundles, one with a country and one without. Both are for the same language. + final Map resourceBundleEnglish = Maps.newHashMap(); + resourceBundleEnglish.put(LocalizationTest.HELLO, "Hello from the English bundle!"); + + final Map resourceBundleUs = Maps.newHashMap(); + resourceBundleUs.put(LocalizationTest.HELLO, "Hello from the Us bundle!"); + + //Make sure that the locale provider returns an unexpected locale (English in Germany). + final LocaleProvider customLocaleProviderMock = createNiceMock(LocaleProvider.class); + expect(customLocaleProviderMock.getLocale()).andReturn(new Locale("en", "DE")); + replay(customLocaleProviderMock); + + //Create the injector and bind add the two localization bindings. + final Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + final Set localizations = Sets.newHashSet(); + localizations.add(new Localizer.Localization(Localized.class, Locale.US, resourceBundleUs)); + localizations.add(new Localizer.Localization(Localized.class, Locale.ENGLISH, resourceBundleEnglish)); + + Localizer.localizeAll(binder(), localizations); + bind(LocaleProvider.class).toInstance(customLocaleProviderMock); + } + }); + + //Request the localized message and assert it equals the expected one. + final Localized localized = injector.getInstance(Localized.class); + Assert.assertNotNull(localized); + + final String greeting = localized.hello(); + Assert.assertEquals("Hello from the English bundle!", greeting); + } public static interface Localized { @Message(message = "hello world!")