Skip to content
This repository was archived by the owner on Jun 20, 2025. It is now read-only.

Commit 1e2aba3

Browse files
authored
Accept i18n errors (#684)
* WIP Provide i18n form errors * Fix test * Change to messages file to add more cases * Test error formatter * Support hash args * Fix test * Remove unused error key * Change hashArgs to a more neutral name
1 parent 98e3f00 commit 1e2aba3

File tree

24 files changed

+271
-54
lines changed

24 files changed

+271
-54
lines changed

common/app/com/commercetools/sunrise/common/controllers/SunriseFrameworkController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ protected final void saveFormError(final Form<?> form, final String message) {
252252
}
253253

254254
protected final void saveUnexpectedFormError(final Form<?> form, final Throwable throwable, final Logger logger) {
255-
form.reject("Something went wrong, please try again"); // TODO i18n
255+
form.reject("messages:defaultError");
256256
logger.error("The CTP request raised an unexpected exception", throwable);
257257
}
258258
}

common/app/com/commercetools/sunrise/common/template/engine/handlebars/PlayJavaFormResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private boolean isFalsy(@Nullable final String value) {
5353
return value != null && value.equals("false");
5454
}
5555

56-
ErrorsBean extractErrors(@Nullable final Form<?> form) {
56+
ErrorsBean extractErrors(@Nullable final Form<?> form) {
5757
final ErrorsBean errorsBean = new ErrorsBean();
5858
final List<ErrorBean> errorList = new ArrayList<>();
5959
if (form != null) {

common/app/com/commercetools/sunrise/common/template/i18n/I18nResolver.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ public interface I18nResolver {
1717
* Resolves i18n message identified by a bundle and a key for the first found given locale.
1818
* @param locales the list of locales used to translate the message
1919
* @param i18nIdentifier identifier of the i18n message
20-
* @param hashArgs list of hash arguments
20+
* @param args list of named arguments
2121
* @return the resolved message in the first found given language, or absent if it could not be found
2222
*/
23-
Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs);
23+
Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args);
2424

2525
/**
2626
* Resolves i18n message identified by a bundle and a key for the first found given locale.
@@ -36,11 +36,11 @@ default Optional<String> get(final List<Locale> locales, final I18nIdentifier i1
3636
* Resolves i18n message identified by a bundle and a key for the first found given locale.
3737
* @param locales the list of locales used to translate the message
3838
* @param i18nIdentifier identifier of the i18n message
39-
* @param hashArgs list of hash arguments
39+
* @param args list of named arguments
4040
* @return the resolved message in the first found given language, or empty string if it could not be found
4141
*/
42-
default String getOrEmpty(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs) {
43-
return get(locales, i18nIdentifier, hashArgs).orElse("");
42+
default String getOrEmpty(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args) {
43+
return get(locales, i18nIdentifier, args).orElse("");
4444
}
4545

4646
/**
@@ -57,11 +57,11 @@ default String getOrEmpty(final List<Locale> locales, final I18nIdentifier i18nI
5757
* Resolves i18n message identified by a bundle and a key for the first found given locale.
5858
* @param locales the list of locales used to translate the message
5959
* @param i18nIdentifier identifier of the i18n message
60-
* @param hashArgs list of hash arguments
60+
* @param args list of named arguments
6161
* @return the resolved message in the first found given language, or the message key if it could not be found
6262
*/
63-
default String getOrKey(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs) {
64-
return get(locales, i18nIdentifier, hashArgs).orElse(i18nIdentifier.messageKey());
63+
default String getOrKey(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args) {
64+
return get(locales, i18nIdentifier, args).orElse(i18nIdentifier.messageKey());
6565
}
6666

6767
/**

common/app/com/commercetools/sunrise/common/template/i18n/composite/CompositeI18nResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ private CompositeI18nResolver(final List<I18nResolver> i18nResolvers) {
2222
}
2323

2424
@Override
25-
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs) {
25+
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args) {
2626
for (I18nResolver i18nResolver : i18nResolvers) {
27-
final Optional<String> message = i18nResolver.get(locales, i18nIdentifier, hashArgs);
27+
final Optional<String> message = i18nResolver.get(locales, i18nIdentifier, args);
2828
if (message.isPresent()) {
2929
return message;
3030
}

common/app/com/commercetools/sunrise/common/template/i18n/yaml/YamlI18nResolver.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ private YamlI18nResolver(final String filepath, final List<Locale> locales, fina
4040
}
4141

4242
@Override
43-
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs) {
44-
final String message = findPluralizedTranslation(locales, i18nIdentifier, hashArgs)
43+
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args) {
44+
final String message = findPluralizedTranslation(locales, i18nIdentifier, args)
4545
.orElseGet(() -> findFirstTranslation(locales, i18nIdentifier.bundle(), i18nIdentifier.messageKey())
4646
.orElse(null));
47-
return Optional.ofNullable(message).map(resolvedValue -> replaceParameters(resolvedValue, hashArgs));
47+
return Optional.ofNullable(message).map(resolvedValue -> replaceParameters(resolvedValue, args));
4848
}
4949

5050
@Override
@@ -59,8 +59,8 @@ public static YamlI18nResolver of(final String filepath, final List<Locale> loca
5959
}
6060

6161
private Optional<String> findPluralizedTranslation(final List<Locale> locales, final I18nIdentifier i18nIdentifier,
62-
final Map<String, Object> hashArgs) {
63-
if (containsPlural(hashArgs)) {
62+
final Map<String, Object> args) {
63+
if (containsPlural(args)) {
6464
final String pluralizedKey = i18nIdentifier.messageKey() + "_plural";
6565
return findFirstTranslation(locales, i18nIdentifier.bundle(), pluralizedKey);
6666
} else {
@@ -78,9 +78,9 @@ private Optional<String> findFirstTranslation(final List<Locale> locales, final
7878
return Optional.empty();
7979
}
8080

81-
private String replaceParameters(final String resolvedValue, final Map<String, Object> hashArgs) {
81+
private String replaceParameters(final String resolvedValue, final Map<String, Object> args) {
8282
String message = StringUtils.defaultString(resolvedValue);
83-
for (final Map.Entry<String, Object> entry : hashArgs.entrySet()) {
83+
for (final Map.Entry<String, Object> entry : args.entrySet()) {
8484
if (entry.getValue() != null) {
8585
final String parameter = "__" + entry.getKey() + "__";
8686
message = message.replace(parameter, entry.getValue().toString());

common/app/com/commercetools/sunrise/common/utils/ErrorFormatter.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import com.google.inject.ImplementedBy;
44
import play.data.validation.ValidationError;
55

6+
import java.util.HashMap;
67
import java.util.List;
78
import java.util.Locale;
9+
import java.util.Map;
10+
import java.util.stream.IntStream;
811

912
/**
1013
* Allows to format errors according to the available locales and error information.
@@ -16,18 +19,25 @@ public interface ErrorFormatter {
1619
* Formats the error message somehow, with the translation to the first available locale.
1720
* @param locales current given locales
1821
* @param message error message
22+
* @param args list of named arguments
1923
* @return the error message localized and formatted
2024
*/
21-
String format(final List<Locale> locales, final String message);
25+
String format(final List<Locale> locales, final String message, final Map<String, Object> args);
2226

2327
/**
2428
* Formats the Play error message somehow, with the translation to the first available locale.
29+
* As hash arguments, it defines the field key as "field" and all other arguments as its index, e.g. "0", "1", "2".
2530
* @param locales current given locales
2631
* @param error Play's validation error
2732
* @return the error message localized and formatted
2833
*/
2934
default String format(final List<Locale> locales, final ValidationError error) {
30-
final String message = format(locales, error.message());
31-
return !error.key().isEmpty() ? message + ": " + error.key() : message;
35+
final Map<String, Object> args = new HashMap<>();
36+
if (error.key() != null && !error.key().isEmpty()) {
37+
args.put("field", error.key());
38+
}
39+
IntStream.range(0, error.arguments().size())
40+
.forEach(index -> args.put(String.valueOf(index), error.arguments().get(index)));
41+
return format(locales, error.message(), args);
3242
}
3343
}

common/app/com/commercetools/sunrise/common/utils/ErrorFormatterImpl.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
import javax.inject.Inject;
88
import java.util.List;
99
import java.util.Locale;
10+
import java.util.Map;
1011

1112
class ErrorFormatterImpl implements ErrorFormatter {
1213

14+
private final I18nResolver i18nResolver;
15+
private final I18nIdentifierFactory i18nIdentifierFactory;
16+
1317
@Inject
14-
private I18nResolver i18nResolver;
15-
@Inject
16-
private I18nIdentifierFactory i18nIdentifierFactory;
18+
ErrorFormatterImpl(final I18nResolver i18nResolver, final I18nIdentifierFactory i18nIdentifierFactory) {
19+
this.i18nResolver = i18nResolver;
20+
this.i18nIdentifierFactory = i18nIdentifierFactory;
21+
}
1722

1823
@Override
19-
public String format(final List<Locale> locales, final String messageKey) {
24+
public String format(final List<Locale> locales, final String messageKey, final Map<String, Object> args) {
2025
final I18nIdentifier i18nIdentifier = i18nIdentifierFactory.create(messageKey);
21-
return i18nResolver.getOrKey(locales, i18nIdentifier);
26+
return i18nResolver.getOrKey(locales, i18nIdentifier, args);
2227
}
2328
}

common/test/com/commercetools/sunrise/common/template/engine/handlebars/PlayJavaFormResolverTest.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,19 @@
22

33
import com.commercetools.sunrise.common.forms.ErrorBean;
44
import com.commercetools.sunrise.common.forms.ErrorsBean;
5-
import com.commercetools.sunrise.common.utils.ErrorFormatter;
6-
import com.google.common.collect.Maps;
75
import org.junit.Test;
86
import org.mockito.Mockito;
97
import play.data.Form;
108
import play.data.validation.ValidationError;
119

1210
import java.util.*;
13-
import java.util.function.Predicate;
1411

1512
import static java.util.Collections.singletonList;
1613
import static org.assertj.core.api.Assertions.assertThat;
1714

1815
public class PlayJavaFormResolverTest {
1916

20-
PlayJavaFormResolver formResolver = new PlayJavaFormResolver(singletonList(Locale.ENGLISH), (locales, message) ->
17+
PlayJavaFormResolver formResolver = new PlayJavaFormResolver(singletonList(Locale.ENGLISH), (locales, message, args) ->
2118
message);
2219

2320
@Test
@@ -28,14 +25,14 @@ public void extractErrors() throws Exception {
2825
ErrorsBean result = formResolver.extractErrors(form);
2926

3027
List<ErrorBean> errors = result.getGlobalErrors();
31-
checkError(errors.get(0), errorField1, "errorkey1", "errorMessage1");
32-
checkError(errors.get(1), errorField1, "errorkey2", "errorMessage2");
33-
checkError(errors.get(2), errorField2, "errorkey21", "errorMessage21");
28+
checkError(errors.get(0), errorField1, "errorMessage1");
29+
checkError(errors.get(1), errorField1, "errorMessage2");
30+
checkError(errors.get(2), errorField2, "errorMessage21");
3431
}
3532

36-
private void checkError(ErrorBean error, String field, String key, String message) {
33+
private void checkError(ErrorBean error, String field, String message) {
3734
assertThat(error.getField()).isEqualTo(field);
38-
assertThat(error.getMessage()).isEqualTo(message + ": " + key);
35+
assertThat(error.getMessage()).isEqualTo(message);
3936

4037
}
4138

@@ -50,4 +47,4 @@ private Form formWithSomeErrorsForFields(String field1, String field2) {
5047
Mockito.when(form.errors()).thenReturn(errorMap);
5148
return form;
5249
}
53-
}
50+
}

common/test/com/commercetools/sunrise/common/template/i18n/TestableI18nResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public TestableI18nResolver(final Map<String, String> i18nMap) {
1414
}
1515

1616
@Override
17-
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> hashArgs) {
17+
public Optional<String> get(final List<Locale> locales, final I18nIdentifier i18nIdentifier, final Map<String, Object> args) {
1818
final String mapKey = String.format("%s/%s:%s", locales.get(0), i18nIdentifier.bundle(), i18nIdentifier.messageKey());
1919
final String message = i18nMap.get(mapKey);
20-
final String parameters = hashArgs.entrySet().stream()
20+
final String parameters = args.entrySet().stream()
2121
.map(hashPair -> hashPair.getKey() + "=" + hashPair.getValue())
2222
.collect(Collectors.joining(","));
2323
if (parameters.isEmpty()) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.commercetools.sunrise.common.utils;
2+
3+
import com.commercetools.sunrise.common.template.i18n.I18nIdentifier;
4+
import com.commercetools.sunrise.common.template.i18n.I18nIdentifierFactory;
5+
import com.commercetools.sunrise.common.template.i18n.I18nResolver;
6+
import org.junit.Before;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.mockito.InjectMocks;
10+
import org.mockito.Mock;
11+
import org.mockito.Spy;
12+
import org.mockito.junit.MockitoJUnitRunner;
13+
14+
import java.util.List;
15+
import java.util.Locale;
16+
import java.util.Map;
17+
import java.util.Optional;
18+
19+
import static java.util.Collections.*;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.mockito.ArgumentMatchers.*;
22+
import static org.mockito.Mockito.when;
23+
24+
@RunWith(MockitoJUnitRunner.class)
25+
public class ErrorFormatterImplTest {
26+
27+
private static final List<Locale> LOCALES = singletonList(Locale.ENGLISH);
28+
private static final String MESSAGE_KEY = "error.required";
29+
private static final I18nIdentifier I18N_IDENTIFIER = I18nIdentifier.of("main", MESSAGE_KEY);
30+
private static final Map<String, Object> ARGS_WITH_FIELD = singletonMap("field", "username");
31+
32+
@Mock
33+
private I18nResolver i18nResolver;
34+
@Spy
35+
private I18nIdentifierFactory i18nIdentifierFactory;
36+
37+
@InjectMocks
38+
private ErrorFormatterImpl errorFormatter;
39+
40+
@Before
41+
public void setUp() throws Exception {
42+
when(i18nResolver.getOrKey(any(), any(), any())).thenCallRealMethod();
43+
}
44+
45+
@Test
46+
public void translatesMessageKey() throws Exception {
47+
mockI18nResolverWithError();
48+
final String errorMessage = errorFormatter.format(LOCALES, MESSAGE_KEY, emptyMap());
49+
assertThat(errorMessage).isEqualTo("Required field");
50+
}
51+
52+
@Test
53+
public void returnsMessageKeyWhenNoMatch() throws Exception {
54+
mockI18nResolverWithoutError();
55+
final String errorMessage = errorFormatter.format(LOCALES, MESSAGE_KEY, emptyMap());
56+
assertThat(errorMessage).isEqualTo(MESSAGE_KEY);
57+
}
58+
59+
@Test
60+
public void returnsMessageKeyWithFieldIfProvided() throws Exception {
61+
mockI18nResolverWithError();
62+
final String errorMessage = errorFormatter.format(LOCALES, MESSAGE_KEY, ARGS_WITH_FIELD);
63+
assertThat(errorMessage).isEqualTo("Required field: username");
64+
}
65+
66+
private void mockI18nResolverWithoutError() {
67+
when(i18nResolver.get(any(), eq(I18N_IDENTIFIER), anyMap())).thenReturn(Optional.empty());
68+
}
69+
70+
private void mockI18nResolverWithError() {
71+
when(i18nResolver.get(any(), eq(I18N_IDENTIFIER), anyMap())).thenReturn(Optional.of("Required field"));
72+
when(i18nResolver.get(any(), eq(I18N_IDENTIFIER), eq(singletonMap("field", "username")))).thenReturn(Optional.of("Required field: username"));
73+
}
74+
}

0 commit comments

Comments
 (0)