diff --git a/pom.xml b/pom.xml index 1754ae0d21800acfbff80aac77402987f375d5dd..a44e612c315457d35a4587446bdaa02b775eb37e 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ <version>2.12.1.0</version> <packaging>war</packaging> <name>GWT Experiments</name> - <description>Experiments with GWT 2.11.0</description> + <description>Experiments with GWT 2.12.1</description> <inceptionYear>2018</inceptionYear> <organization> <name>INRAE AgroClim</name> @@ -36,9 +36,10 @@ <jackson.version>2.17.2</jackson.version> <jersey.version>2.45</jersey.version> <junit.version>5.11.3</junit.version> - <junit4.version>4.13.2</junit4.version> + <junit4.version>4.13.2</junit4.version> <log4j.version>2.24.1</log4j.version> <lombok.version>1.18.34</lombok.version> + <mockito.version>5.11.0</mockito.version> <swagger.version>2.2.21</swagger.version> <swagger-ui.version>5.17.14</swagger-ui.version> <weld.version>3.1.9.Final</weld.version> @@ -54,7 +55,7 @@ <!-- other properties --> <analytics.tracking.id></analytics.tracking.id> - <javase.api.link>https://docs.oracle.com/en/java/javase/11/docs/api</javase.api.link> + <javase.api.link>https://docs.oracle.com/en/java/javase/17/docs/api</javase.api.link> <!-- Maven profile --> <environment>dev</environment> @@ -377,7 +378,24 @@ <version>${junit.version}</version> <scope>test</scope> </dependency> - + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/Gwtexpe.gwt.xml b/src/main/java/fr/inrae/agroclim/gwtexpe/Gwtexpe.gwt.xml index ff2e892cadb45bb36f8b61e08a1506b0f0b52406..9581aa68f18608f63dcaa8cfeb12ffcc71d38cd2 100644 --- a/src/main/java/fr/inrae/agroclim/gwtexpe/Gwtexpe.gwt.xml +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/Gwtexpe.gwt.xml @@ -55,6 +55,12 @@ <source path='client' /> <source path='shared' /> + <!-- Setting I18N --> + <inherits name="com.google.gwt.i18n.I18N" /> + <extend-property name="locale" values="fr" /> + <extend-property name="locale" values="en" /> + <set-property-fallback name="locale" value="fr" /> + <!-- allow Super Dev Mode --> <add-linker name="xsiframe" /> diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.java b/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.java index 80a965ba16dfe4643480caa114f425c42d02a8bd..ddd4ffa21e8a3934132d494add24a30df1ed2cc9 100644 --- a/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.java +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.java @@ -35,6 +35,8 @@ import com.google.gwt.user.client.ui.HasText; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; +import fr.inrae.agroclim.gwtexpe.client.i18n.AppMessages; + /** * Implementation of HelloView. */ @@ -46,6 +48,11 @@ public class HelloViewImpl extends Composite implements HelloView { interface HelloViewImplUiBinder extends UiBinder<Widget, HelloViewImpl> { } + /** + * I18n messages. + */ + private static final AppMessages MSGS = GWT.create(AppMessages.class); + /** * UI binder. */ @@ -64,6 +71,12 @@ public class HelloViewImpl extends Composite implements HelloView { @UiField protected HasText errorLabel; + /** + * Label to display greetings. + */ + @UiField + protected HasText greetings; + /** * Presenter of Hello. */ @@ -132,6 +145,7 @@ public class HelloViewImpl extends Composite implements HelloView { @Override public final void setName(final String name) { + greetings.setText(MSGS.hello(name)); textBox.setText(name); } diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.ui.xml b/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.ui.xml index e76a00b7caecd0794d484c32ae816ffe5f1f4026..10d11c192567b0cb874980c7a7fb658529ba0073 100644 --- a/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.ui.xml +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/client/hello/HelloViewImpl.ui.xml @@ -24,11 +24,14 @@ <!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"> + <ui:with field="csts" + type="fr.inrae.agroclim.gwtexpe.client.i18n.AppConstants" /> <g:VerticalPanel> - <g:Label text="Web Application Starter Project" /> - <g:Label text="Please enter your name:" /> + <g:Label text="{csts.appName}" /> + <g:Label ui:field="greetings" /> + <g:Label text="{csts.enterName}" /> <g:TextBox ui:field="textBox" /> <g:Label ui:field="errorLabel" /> - <g:Button ui:field="button" text="Send" /> + <g:Button ui:field="button" text="{csts.send}" /> </g:VerticalPanel> </ui:UiBinder> diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants.java b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..05e776864e9c2b0b96a8539240c773d912c439a7 --- /dev/null +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants.java @@ -0,0 +1,28 @@ +package fr.inrae.agroclim.gwtexpe.client.i18n; + +/** + * Interface to represent the constants contained in resource bundle: + * 'fr/inrae/agroclim/gwt-expe/client/i18n/AppConstants.properties'. + * + * @author Olivier Maury + */ +public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLookup { + + /** + * @return translation + */ + @DefaultStringValue("Web Application Starter Project") + String appName(); + + /** + * @return translation + */ + @DefaultStringValue("Please enter your name:") + String enterName(); + + /** + * @return translation + */ + @DefaultStringValue("Send") + String send(); +} diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages.java b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages.java new file mode 100644 index 0000000000000000000000000000000000000000..44258c7a56fe9b25fb73f977588f8b20dde7ad32 --- /dev/null +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages.java @@ -0,0 +1,19 @@ +package fr.inrae.agroclim.gwtexpe.client.i18n; + +import com.google.gwt.i18n.client.Messages; + +/** + * Internationalization messages. + * + * @author Olivier Maury + */ +public interface AppMessages extends Messages { + + /** + * @param name user name + * @return translation + */ + @DefaultMessage("Hello {0}") + String hello(String name); + +} diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/package-info.java b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d32c2bde971a8cb6b61f735b02a517f9a1e3a608 --- /dev/null +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/client/i18n/package-info.java @@ -0,0 +1,4 @@ +/** + * Internationalization classes. + */ +package fr.inrae.agroclim.gwtexpe.client.i18n; diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/server/GreetingServiceImpl.java b/src/main/java/fr/inrae/agroclim/gwtexpe/server/GreetingServiceImpl.java index 06114963792c70e40433bd30545c49302e60cbe9..7ec2a0bda030eca0594162acaad1ebc0cc641ff8 100644 --- a/src/main/java/fr/inrae/agroclim/gwtexpe/server/GreetingServiceImpl.java +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/server/GreetingServiceImpl.java @@ -1,5 +1,7 @@ package fr.inrae.agroclim.gwtexpe.server; +import java.util.Locale; + /*- * #%L * GWT Experiments @@ -27,6 +29,7 @@ import javax.enterprise.event.Event; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; import com.google.gwt.user.server.rpc.RemoteServiceServlet; @@ -49,6 +52,12 @@ implements GreetingService { */ private static final long serialVersionUID = 4079295733539308238L; + /** + * HTTP request. + */ + @Inject + private HttpServletRequest request; + /** * DAO for Person. */ @@ -80,8 +89,8 @@ implements GreetingService { if (html == null) { return null; } - return html.replaceAll("&", "&").replaceAll("<", "<") - .replaceAll(">", ">"); + return html.replace("&", "&").replace("<", "<") + .replace(">", ">"); } @Override @@ -122,10 +131,10 @@ implements GreetingService { final String msg = escapeHtml(input); userAgent = escapeHtml(userAgent); - return String.format( - "Hello %s! I know you as %s.<br/> I am running %s.<br><br>" - + "It looks like you are using:<br>%s", - msg, known, serverInfo, userAgent); + final Locale locale = LocaleUtils.getLocale(request); + final I18n i18n = new I18n("fr.inrae.agroclim.gwtexpe.server.i18n", locale); + + return i18n.format("hello", msg, known, serverInfo, userAgent); } @Override diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/server/I18n.java b/src/main/java/fr/inrae/agroclim/gwtexpe/server/I18n.java new file mode 100644 index 0000000000000000000000000000000000000000..a70a18ddf547532de7753b81f630ce071494be62 --- /dev/null +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/server/I18n.java @@ -0,0 +1,250 @@ +package fr.inrae.agroclim.gwtexpe.server; + +import java.text.MessageFormat; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.NonNull; + +/** + * Localized messages with plural handling à la GWT. + * + * @author Olivier Maury + */ +public class I18n { + /** + * Accepted operators, ordered. + */ + public enum Operator implements BiFunction<Integer, Integer, Boolean> { + /** + * Equals. + */ + AEQ("=", Objects::equals), + /** + * Inferior or equals. + */ + BINFEQ("<=", (a, b) -> a <= b), + /** + * Strictly inferior. + */ + CINF("<", (a, b) -> a < b), + /** + * Superior or equals. + */ + DSUPEQ(">=", (a, b) -> a >= b), + /** + * Strictly superior. + */ + ESUP(">", (a, b) -> a > b); + + /** + * Guess the right operator in the string comparison. + * + * @param string string comparison (eg.: ">=10") + * @return operator matching the string comparison + */ + static Optional<Operator> extract(final String string) { + for (final Operator op : values()) { + if (string.startsWith(op.symbol)) { + return Optional.of(op); + } + } + return Optional.empty(); + } + /** + * Comparison function of the operator. + */ + private final BiPredicate<Integer, Integer> function; + /** + * String representation of the operator. + */ + private final String symbol; + /** + * Constructor. + * + * @param string String representation of the operator. + * @param func Comparison function of the operator. + */ + Operator(final String string, + final BiPredicate<Integer, Integer> func) { + symbol = string; + function = func; + } + + @Override + public Boolean apply(final Integer arg0, final Integer arg1) { + return function.test(arg0, arg1); + } + + /** + * @return length of string representation + */ + public int getLength() { + return symbol.length(); + } + } + + /** + * Bundle name. + */ + public static final String BUNDLE_NAME = "fr.inrae.agroclim.gwtexpe.server.i18n"; + + /** + * Check if the comparison string matches the value. + * + * @param comparison comparison string (eg.: ">10") + * @param plural value + * @return if the comparison string matches the value. + */ + static boolean matches(final String comparison, final int plural) { + final var operator = Operator.extract(comparison); + if (operator.isPresent()) { + final var op = operator.get(); + var val = comparison.substring(op.getLength()); + if (val != null) { + val = val.trim(); + if (!val.isEmpty()) { + final var value = Integer.valueOf(val); + return op.apply(plural, value); + } + } + } + return false; + } + + /** + * Fallback resources from .properties file, in case of missing translation. + */ + private final ResourceBundle fallbackResources; + + /** + * Translation keys. + */ + private final Set<String> keys = new HashSet<>(); + + /** + * Requested locale. + */ + @Getter + private final Locale locale; + + /** + * Resources from .properties file. + */ + private final ResourceBundle resources; + + /** + * Constructor. + * + * @param rb the bundle. + */ + public I18n(final ResourceBundle rb) { + resources = rb; + locale = rb.getLocale(); + final String bundleName = rb.getBaseBundleName(); + if (bundleName != null) { + fallbackResources = ResourceBundle.getBundle(bundleName, Locale.ROOT); + } else { + fallbackResources = null; + } + } + + /** + * Constructor. + * + * @param bundleName Path of .property resource. + * @param requestedLocale The requested locale for the bundle. + */ + public I18n(@NonNull final String bundleName, @NonNull final Locale requestedLocale) { + locale = requestedLocale; + fallbackResources = ResourceBundle.getBundle(bundleName, Locale.ROOT); + final ResourceBundle res = ResourceBundle.getBundle(bundleName, locale); + if (res.getLocale().equals(locale)) { + resources = res; + } else { + resources = fallbackResources; + } + keys.addAll(Collections.list(resources.getKeys())); + keys.addAll(Collections.list(fallbackResources.getKeys())); + } + + /** + * Return message with inlined arguments. + * + * @param plural value for plural form + * @param key message key + * @param messageArguments arguments for the message. + * @return message with arguments or exclamation message + */ + public String format(final int plural, final String key, + final Object... messageArguments) { + String keyWithSuffix; + + // the suffix for the value + keyWithSuffix = key + "[=" + plural + "]"; + if (getKeys().contains(keyWithSuffix)) { + return format(keyWithSuffix, messageArguments); + } + + // with comparators <, <=, >, >= + final var suffixes = getKeys().stream() + .filter(k -> k.startsWith(key + "[") && k.endsWith("]")) + .map(k -> k.substring(key.length() + 1, k.length() - 1)) + .collect(Collectors.toList()); + for (final String suf : suffixes) { + if (matches(suf, plural)) { + keyWithSuffix = key + "[" + suf + "]"; + return format(keyWithSuffix, messageArguments); + } + } + // if not defined, used default + return format(key, messageArguments); + } + + /** + * Return message with inlined arguments. + * + * @param key message key + * @param messageArguments arguments for the message. + * @return message with arguments or exclamation message + */ + public String format(final String key, final Object... messageArguments) { + final String format = this.get(key); + final MessageFormat messageFormat = new MessageFormat(format, locale); + return messageFormat.format(messageArguments); + } + + /** + * Retrieve message from key. + * + * @param key message key + * @return message value or exclamation message + */ + public String get(final String key) { + if (resources.containsKey(key)) { + return resources.getString(key); + } + if (fallbackResources != null && fallbackResources.containsKey(key)) { + return fallbackResources.getString(key); + } + return "!" + key + "!"; + } + + + /** + * @return all keys in the .properties file + */ + public Set<String> getKeys() { + return Collections.unmodifiableSet(keys); + } + +} diff --git a/src/main/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtils.java b/src/main/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..f8ff189708a2b1091ed2264aedb88139d8c9b7b0 --- /dev/null +++ b/src/main/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtils.java @@ -0,0 +1,77 @@ +package fr.inrae.agroclim.gwtexpe.server; + +import java.util.Locale; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +/** + * Locale utils for servlet and server side service. + * + * @author Olivier Maury + */ +public interface LocaleUtils { + /** + * @return The default language for the application: English. + */ + static Locale getDefaultLocale() { + return Locale.ENGLISH; + } + + /** + * Guess the allowed locale according to URL parameter, cookie and request. + * + * @param request an {@link HttpServletRequest} object that contains the request + * the client has made of the servlet + * @return the locale from the user request + */ + static Locale getLocale(final HttpServletRequest request) { + if (request == null) { + return getDefaultLocale(); + } + // 1. URL parameter + final String localeParam = request.getParameter("locale"); + if (localeParam != null) { + final var found = getLocale(localeParam); + if (found != null) { + return found; + } + } + // 2. HTTP header + if (request.getLocale() == null) { + return getDefaultLocale(); + } + if (Locale.FRANCE == request.getLocale()) { + return Locale.FRENCH; + } + if (getLocales().contains(request.getLocale())) { + return request.getLocale(); + } + return getDefaultLocale(); + } + + /** + * Guess the allowed locale according to the language tag. + * + * @param tag language tag + * @return found allowed language or null + */ + static Locale getLocale(final String tag) { + if (tag.startsWith("fr")) { + return Locale.FRENCH; + } + final var found = Locale.forLanguageTag(tag); + if (found == null || !getLocales().contains(found)) { + return Locale.ENGLISH; + } + return found; + } + + /** + * @return supported locales. + */ + static Set<Locale> getLocales() { + return Set.of(getDefaultLocale(), Locale.FRENCH); + } + +} diff --git a/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants_fr.properties b/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants_fr.properties new file mode 100644 index 0000000000000000000000000000000000000000..047d082ec222d1a0959e63fb43d3a12231475719 --- /dev/null +++ b/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppConstants_fr.properties @@ -0,0 +1,4 @@ +# Ce fichier est encodé en UTF-8. +appName = Projet de démarrage d'application web +enterName = Veuillez saisir votre nom\u00a0: +send = Envoyer diff --git a/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages_fr.properties b/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages_fr.properties new file mode 100644 index 0000000000000000000000000000000000000000..fc4ede5b51c5b861807a2282595e3d1a430fc2fb --- /dev/null +++ b/src/main/resources/fr/inrae/agroclim/gwtexpe/client/i18n/AppMessages_fr.properties @@ -0,0 +1,2 @@ +# Ce fichier est encodé en UTF-8. +hello = Bonjour {0} diff --git a/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n.properties b/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n.properties new file mode 100644 index 0000000000000000000000000000000000000000..843db7bd8773d6094fcda7d23bf6f370f610a570 --- /dev/null +++ b/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n.properties @@ -0,0 +1,2 @@ +# Ce fichier est encodé en UTF-8. +hello = Hello {0}! I know you as {1}.<br/> I am running {2}.<br><br>It looks like you are using:<br>{3} diff --git a/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n_fr.properties b/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n_fr.properties new file mode 100644 index 0000000000000000000000000000000000000000..39038e6be915cd0860def746858107d3190d6cce --- /dev/null +++ b/src/main/resources/fr/inrae/agroclim/gwtexpe/server/i18n_fr.properties @@ -0,0 +1,2 @@ +# Ce fichier est encodé en UTF-8. +hello = Bonjour {0}\u00a0! Je vous reconnais comme {1}.<br/>Je tourne sous {2}.<br><br>Il semble que vous utilisez\u00a0:<br>{3} diff --git a/src/test/java/fr/inrae/agroclim/gwtexpe/server/I18nTest.java b/src/test/java/fr/inrae/agroclim/gwtexpe/server/I18nTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2159f83b485c04460a909ecec0538c52066212a3 --- /dev/null +++ b/src/test/java/fr/inrae/agroclim/gwtexpe/server/I18nTest.java @@ -0,0 +1,159 @@ +package fr.inrae.agroclim.gwtexpe.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test plural form handling with I18n. + */ +class I18nTest { + + + /** + * Key for plural messages to test. + */ + private static final String KEY = "cartItems"; + /** + * Name of test bundle. + */ + private static final String NAME = "fr.inrae.agroclim.gwtexpe.server.test"; + + /** + * {@link MethodSource} annotation marks this method as parameters provider. + * + * @return combinaisons of nb, key, expected translation + */ + static Stream<Arguments> formatPluralData() { + return Stream.of(// + Arguments.of(1, KEY, "Il y a un produit dans votre panier."), // + Arguments.of(2, KEY, "Il y a deux produits dans votre panier."), // + Arguments.of(3, KEY, "Il y a 3 produits dans votre panier, ce qui est peu."), // + Arguments.of(42, KEY, "Il y a 42 produits dans votre panier, ce qui est un nombre spécial."), // + Arguments.of(101, KEY, "Il y a 101 produits dans votre panier, ce qui est beaucoup."), // + Arguments.of(1_314, KEY, "Il y a 1 314 produits dans votre panier, ce qui est beaucoup."), // + Arguments.of(1, "cats", "Un chat"), // + // Variation is not present, default value. + Arguments.of(2, "cats", "2 chats"), // + Arguments.of(1, "comparison", "Valeur == 1"), // + Arguments.of(10, "comparison", "Valeur >= 10"), // + Arguments.of(11, "comparison", "Valeur > 10"), // + Arguments.of(-10, "comparison", "Valeur <= -10"), // + Arguments.of(-101, "comparison", "Valeur < -100") // + ); + } + + /** + * French translations. + */ + private final I18n res = new I18n(NAME, Locale.FRENCH); + + @Test + void contructorWithResourceBundle() { + final var bundle = ResourceBundle.getBundle(NAME, Locale.FRENCH); + final var i18n = new I18n(bundle); + String actual; + String expected; + actual = i18n.get("error.title"); + expected = "Erreur"; + assertEquals(expected, actual); + + actual = i18n.get("not.translated"); + expected = "This message is not translated."; + assertEquals(expected, actual); + } + + @Test + void format() { + final var actual = res.format("warning.missing", "climat", 1, "WIND"); + final var expected = "climat : ligne 1 : WIND manque."; + assertEquals(expected, actual); + } + + @ParameterizedTest(name = "formatPluralDefault {index}: {0} {1}") + @MethodSource("formatPluralData") + void formatPluralDefault(final int nb, final String key, final String expected) { + final var actual = res.format(nb, key, nb); + assertEquals(expected, actual); + } + + @Test + void formatPluralSpecialDefaultProperties() { + final int nb = 42; + final var korea = new I18n(NAME, Locale.KOREA); + final var actual = korea.format(nb, KEY, nb); + final var expected = "There are 42 items in your cart, a special number!"; + assertEquals(expected, actual); + } + + @Test + void getStringDefault() { + final var actual = res.get("not.translated"); + final var expected = "This message is not translated."; + assertEquals(expected, actual); + } + + @Test + void getStringExisting() { + final var actual = res.get("error.title"); + final var expected = "Erreur"; + assertEquals(expected, actual); + } + + @Test + void matches() { + final Map<String, Integer> doesMatch = new HashMap<>(); + doesMatch.put("=1", 1); + doesMatch.put(">0", 1); + doesMatch.put(">=0", 1); + doesMatch.put(">=1", 1); + doesMatch.put("<0", -1); + doesMatch.put("<=0", -1); + doesMatch.put("<=1", -1); + doesMatch.put("<-100", -101); + doesMatch.forEach((comparison, plural) -> { + final boolean actual = I18n.matches(comparison, plural); + assertTrue(actual, comparison + " must be true for " + plural); + }); + final Map<String, Integer> dontMatch = new HashMap<>(); + dontMatch.put("=0", 1); + dontMatch.put(">1", 1); + dontMatch.put(">2", 1); + dontMatch.put(">=2", 1); + dontMatch.put("<-1", -1); + dontMatch.put("<-2", -1); + dontMatch.put("<=-2", -1); + dontMatch.forEach((comparison, plural) -> { + final boolean actual = I18n.matches(comparison, plural); + assertFalse(actual, comparison + " must be false for " + plural); + }); + } + + @Test + void operatorExtract() { + final Map<String, I18n.Operator> operators = new HashMap<>(); + operators.put("1", null); + operators.put("=1", I18n.Operator.AEQ); + operators.put(">0", I18n.Operator.ESUP); + operators.put(">=0", I18n.Operator.DSUPEQ); + operators.put(">=1", I18n.Operator.DSUPEQ); + operators.put("<0", I18n.Operator.CINF); + operators.put("<=0", I18n.Operator.BINFEQ); + operators.forEach((comparison, expected) -> { + final I18n.Operator actual = I18n.Operator.extract(comparison) // + .orElse(null); + assertEquals(expected, actual, expected + " must be extracted from " + comparison); + }); + } +} diff --git a/src/test/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtilsTest.java b/src/test/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d4a5eaedd02cc99fdc5480cadbe03d4a651adad1 --- /dev/null +++ b/src/test/java/fr/inrae/agroclim/gwtexpe/server/LocaleUtilsTest.java @@ -0,0 +1,87 @@ +package fr.inrae.agroclim.gwtexpe.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * JUnit test of {@link LocaleUtils}. + * + * @author Olivier Maury + */ +@ExtendWith(MockitoExtension.class) +class LocaleUtilsTest { + + /** + * HTTP servlet request with "locale" parameter. + */ + private final HttpServletRequest request; + + LocaleUtilsTest(@Mock final HttpServletRequest mockedRequest) { + this.request = mockedRequest; + } + + @Test + void getLocaleDefault() { + when(request.getParameter("locale")).thenReturn(null); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = LocaleUtils.getDefaultLocale(); + assertEquals(expected, actual); + } + + @Test + void getLocaleEnParam() { + when(request.getParameter("locale")).thenReturn("en"); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = Locale.ENGLISH; + assertEquals(expected, actual); + } + + @Test + void getLocaleEoParam() { + when(request.getParameter("locale")).thenReturn("eo"); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = LocaleUtils.getDefaultLocale(); + assertEquals(expected, actual); + } + + @Test + void getLocaleFrFR() { + final Locale actual = LocaleUtils.getLocale(Locale.FRANCE.toLanguageTag()); + final Locale expected = Locale.FRENCH; + assertEquals(expected, actual); + } + + @Test + void getLocaleFrFRLocale() { + when(request.getLocale()).thenReturn(Locale.FRANCE); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = Locale.FRENCH; + assertEquals(expected, actual); + } + + @Test + void getLocaleFrFRParam() { + when(request.getParameter("locale")).thenReturn("fr_FR"); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = Locale.FRENCH; + assertEquals(expected, actual); + } + + @Test + void getLocaleFrParam() { + when(request.getParameter("locale")).thenReturn("fr"); + final Locale actual = LocaleUtils.getLocale(request); + final Locale expected = Locale.FRENCH; + assertEquals(expected, actual); + } + +} diff --git a/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test.properties b/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test.properties new file mode 100644 index 0000000000000000000000000000000000000000..15a4e5393a7c16528a914ad4ba2fc18f4e9abc09 --- /dev/null +++ b/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test.properties @@ -0,0 +1,16 @@ +cartItems=There are {0,number} items in your cart. +cartItems=There are {0,number} items in your cart, which are many. +cartItems[\=0]=There are no items in your cart. +cartItems[\=1]=There is one item in your cart. +cartItems[\=2]=There are two items in your cart. +cartItems[\=3]=There are {0,number} items in your cart, which are few. +cartItems[\=42]=There are {0,number} items in your cart, a special number! +cats={0,number} cats +cats[\=1]=One cat +date.iso_local_date_time=2018-08-27T11:46:30 +date.yyyyMMddHHmmss=20180827114630 +not.translated=This message is not translated. +error.title=Error +warning.missing={0}: line {1}: {2} is missing +build.date=2018-08-28 14:59:30 +version=1.2.3 \ No newline at end of file diff --git a/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test_fr.properties b/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test_fr.properties new file mode 100644 index 0000000000000000000000000000000000000000..4a1e24b43fbd13b7ab1d5876f54096f8f36dd19b --- /dev/null +++ b/src/test/resources/fr/inrae/agroclim/gwtexpe/server/test_fr.properties @@ -0,0 +1,17 @@ +cartItems=Il y a {0,number} produits dans votre panier. +cartItems=Il y a {0,number} produits dans votre panier, ce qui est beaucoup. +cartItems[\=0]=Il n'y a aucun produit dans votre panier. +cartItems[\=1]=Il y a un produit dans votre panier. +cartItems[\=2]=Il y a deux produits dans votre panier. +cartItems[\=3]=Il y a {0,number} produits dans votre panier, ce qui est peu. +cartItems[\=42]=Il y a 42 produits dans votre panier, ce qui est un nombre spécial. +cats={0,number} chats +cats[\=1]=Un chat +comparison=Valeur par défaut pour {0,number} +comparison[<-100]=Valeur < -100 +comparison[<\=-10]=Valeur <= -10 +comparison[\=1]=Valeur == 1 +comparison[>\=10]=Valeur >= 10 +comparison[>10]=Valeur > 10 +error.title=Erreur +warning.missing={0}\u00a0: ligne {1}\u00a0: {2} manque.