Skip to content

Commit

Permalink
feat: add alternative translate methods to I18NProvider (#19461)
Browse files Browse the repository at this point in the history
* feat: add alternative translate methods to I18NProvider

Adds static `I18NProvider#translate` methods for alternative way to get translation. Compares to calling getTranslation via `VaadinService.getCurrent().getInstantiator().getI18NProvider()`. `translate` methods throws IllegalStateException without active VaadinService.

Fixes: #19333

* test: fixed test

* chore: add LocaleUtil#getLocale()

* chore: update error message

* test: clean VaadinService after
  • Loading branch information
tltv authored May 29, 2024
1 parent 2da7e3b commit 04e58be
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 4 deletions.
40 changes: 40 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/i18n/I18NProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.List;
import java.util.Locale;

import com.vaadin.flow.internal.LocaleUtil;

/**
* I18N provider interface for internationalization usage.
*
Expand Down Expand Up @@ -67,4 +69,42 @@ public interface I18NProvider extends Serializable {
default String getTranslation(Object key, Locale locale, Object... params) {
return getTranslation(key.toString(), locale, params);
}

/**
* Get the translation for key via {@link I18NProvider} instance retrieved
* from the current VaadinService. Uses the current UI locale, or if not
* available, then the default locale.
*
* @param key
* translation key
* @param params
* parameters used in translation string
* @return translation for key if found
* @throws IllegalStateException
* thrown if no I18NProvider found from the VaadinService
*/
static String translate(String key, Object... params) {
return translate(LocaleUtil.getLocale(), key, params);
}

/**
* Get the translation for key with given locale via {@link I18NProvider}
* instance retrieved from the current VaadinService.
*
* @param locale
* locale to use
* @param key
* translation key
* @param params
* parameters used in translation string
* @return translation for key if found
* @throws IllegalStateException
* thrown if no I18NProvider found from the VaadinService
*/
static String translate(Locale locale, String key, Object... params) {
return LocaleUtil.getI18NProvider()
.orElseThrow(() -> new IllegalStateException(
"I18NProvider is not available via current VaadinService. VaadinService, Instantiator or I18NProvider is null."))
.getTranslation(key, locale, params);
}
}
22 changes: 19 additions & 3 deletions flow-server/src/main/java/com/vaadin/flow/internal/LocaleUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.function.Supplier;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.i18n.I18NProvider;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
Expand Down Expand Up @@ -96,12 +97,13 @@ public static Optional<Locale> getLocaleMatchByLanguage(
* @return the optional value of I18nProvider
*/
public static Optional<I18NProvider> getI18NProvider() {
return Optional.ofNullable(
VaadinService.getCurrent().getInstantiator().getI18NProvider());
return Optional.ofNullable(VaadinService.getCurrent())
.map(VaadinService::getInstantiator)
.map(Instantiator::getI18NProvider);
}

/**
* Get the locale for the given UI.
* Get the locale from the current UI or from the given I18NProvider.
* <p>
* -> If UI is not null, then it is used to get the locale, -> if UI is
* null, then the I18NProvider providedLocales first match will be returned,
Expand All @@ -119,4 +121,18 @@ public static Locale getLocale(
.flatMap(locales -> locales.stream().findFirst()))
.orElseGet(Locale::getDefault);
}

/**
* Get the locale from the current UI or from the I18NProvider from the
* current VaadinService.
* <p>
* -> If UI is not null, then it is used to get the locale, -> if UI is
* null, then the I18NProvider providedLocales first match will be returned,
* -> if I18NProvider is null, then default locale is returned.
*
* @return the locale for the UI
*/
public static Locale getLocale() {
return getLocale(LocaleUtil::getI18NProvider);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public void init()
public void cleanup() throws NoSuchFieldException, IllegalAccessException {
ResourceBundle.clearCache(urlClassLoader);
I18NProviderTest.clearI18NProviderField();
VaadinService.setCurrent(null);
}

@Test
Expand All @@ -78,6 +79,7 @@ public void translationFileOnClasspath_instantiateDefaultI18N()

VaadinService service = Mockito.mock(VaadinService.class);
mockLookup(service);
VaadinService.setCurrent(service);

DefaultInstantiator defaultInstantiator = new DefaultInstantiator(
service) {
Expand All @@ -86,28 +88,40 @@ protected ClassLoader getClassLoader() {
return urlClassLoader;
}
};
Mockito.when(service.getInstantiator()).thenReturn(defaultInstantiator);
I18NProvider i18NProvider = defaultInstantiator.getI18NProvider();
Assert.assertNotNull(i18NProvider);
Assert.assertTrue(i18NProvider instanceof DefaultI18NProvider);

Assert.assertEquals("Suomi",
i18NProvider.getTranslation("title", new Locale("fi", "FI")));
Assert.assertEquals("Suomi",
I18NProvider.translate(new Locale("fi", "FI"), "title"));

Assert.assertEquals("deutsch",
i18NProvider.getTranslation("title", new Locale("de")));
Assert.assertEquals("deutsch",
I18NProvider.translate(new Locale("de"), "title"));

Assert.assertEquals(
"non existing country should select language bundle", "deutsch",
i18NProvider.getTranslation("title", new Locale("de", "AT")));
Assert.assertEquals(
"non existing country should select language bundle", "deutsch",
I18NProvider.translate(new Locale("de", "AT"), "title"));

Assert.assertEquals("Korean",
i18NProvider.getTranslation("title", new Locale("ko", "KR")));
Assert.assertEquals("Korean",
I18NProvider.translate(new Locale("ko", "KR"), "title"));

// Note!
// default translations.properties will be used if
// the locale AND system default locale is not found
Assert.assertEquals("Default lang",
i18NProvider.getTranslation("title", new Locale("en", "GB")));
Assert.assertEquals("Default lang",
I18NProvider.translate(new Locale("en", "GB"), "title"));
}

@Test
Expand All @@ -120,6 +134,7 @@ public void onlyDefaultTranslation_instantiateDefaultI18N()

VaadinService service = Mockito.mock(VaadinService.class);
mockLookup(service);
VaadinService.setCurrent(service);

DefaultInstantiator defaultInstantiator = new DefaultInstantiator(
service) {
Expand All @@ -128,24 +143,33 @@ protected ClassLoader getClassLoader() {
return urlClassLoader;
}
};
Mockito.when(service.getInstantiator()).thenReturn(defaultInstantiator);
I18NProvider i18NProvider = defaultInstantiator.getI18NProvider();
Assert.assertNotNull(i18NProvider);
Assert.assertTrue(i18NProvider instanceof DefaultI18NProvider);

Assert.assertEquals("Default lang",
i18NProvider.getTranslation("title", new Locale("fi", "FI")));
Assert.assertEquals("Default lang",
I18NProvider.translate(new Locale("fi", "FI"), "title"));

Assert.assertEquals("Default lang",
i18NProvider.getTranslation("title", new Locale("de")));
Assert.assertEquals("Default lang",
I18NProvider.translate(new Locale("de"), "title"));

Assert.assertEquals("Default lang",
i18NProvider.getTranslation("title", new Locale("ko", "KR")));
Assert.assertEquals("Default lang",
I18NProvider.translate(new Locale("ko", "KR"), "title"));

// Note!
// default translations.properties will be used if
// the locale AND system default locale is not found
Assert.assertEquals("Default lang",
i18NProvider.getTranslation("title", new Locale("en", "GB")));
Assert.assertEquals("Default lang",
I18NProvider.translate(new Locale("en", "GB"), "title"));
}

@Test
Expand All @@ -158,6 +182,7 @@ public void onlyLangTransalation_nonExistingLangReturnsKey()

VaadinService service = Mockito.mock(VaadinService.class);
mockLookup(service);
VaadinService.setCurrent(service);

DefaultInstantiator defaultInstantiator = new DefaultInstantiator(
service) {
Expand All @@ -166,15 +191,31 @@ protected ClassLoader getClassLoader() {
return urlClassLoader;
}
};
Mockito.when(service.getInstantiator()).thenReturn(defaultInstantiator);
I18NProvider i18NProvider = defaultInstantiator.getI18NProvider();
Assert.assertNotNull(i18NProvider);
Assert.assertTrue(i18NProvider instanceof DefaultI18NProvider);

Assert.assertEquals("No Default",
i18NProvider.getTranslation("title", new Locale("ja")));
Assert.assertEquals("No Default",
I18NProvider.translate(new Locale("ja"), "title"));

Assert.assertEquals("title",
i18NProvider.getTranslation("title", new Locale("en", "GB")));
Assert.assertEquals("title",
I18NProvider.translate(new Locale("en", "GB"), "title"));
}

@Test
public void translate_withoutInstantiator_throwsIllegalStateException() {
VaadinService service = Mockito.mock(VaadinService.class);
VaadinService.setCurrent(service);

Assert.assertThrows(
"Should throw exception without Instantiator in VaadinService",
IllegalStateException.class,
() -> I18NProvider.translate("foo.bar"));
}

private static void createTranslationFiles(File translationsFolder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,34 @@ public void with_defined_provider_locale_should_be_the_available_one()
VaadinSession.getCurrent().getLocale());
}

@Test
public void translate_calls_provider()
throws ServletException, ServiceException {
config.setApplicationOrSystemProperty(InitParameters.I18N_PROVIDER,
TestProvider.class.getName());

initServletAndService(config);

Assert.assertEquals("translate method should return a value",
"!foo.bar!", I18NProvider.translate("foo.bar"));
}

@Test
public void translate_withoutVaadinService_throwIllegalStateException()
throws ServletException, ServiceException {
config.setApplicationOrSystemProperty(InitParameters.I18N_PROVIDER,
TestProvider.class.getName());

initServletAndService(config);

VaadinService.setCurrent(null);

Assert.assertThrows(
"Should throw exception without active VaadinService",
IllegalStateException.class,
() -> I18NProvider.translate("foo.bar"));
}

@Before
public void initState()
throws NoSuchFieldException, IllegalAccessException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ public TranslationView() {
dynamic = new Span("waiting");
dynamic.setId("dynamic");

Span staticMethod = new Span(
I18NProvider.translate(Locale.ENGLISH, "label"));
staticMethod.setId("static-method");

add(defaultLang, new Div(), german, new Div(), germany, new Div(),
finnish, new Div(), french, new Div(), japanese, new Div(),
dynamic);
dynamic, new Div(), staticMethod);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public void translationFilesExist_defaultI18NInstantiated_languagesWork() {
$(SpanElement.class).id("french").getText());
Assert.assertEquals("日本語",
$(SpanElement.class).id("japanese").getText());

Assert.assertEquals("Default",
$(SpanElement.class).id("static-method").getText());
}

@Test
Expand Down

0 comments on commit 04e58be

Please sign in to comment.