scoped programmatic i18n-keys (#190)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/190 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package net.hostsharing.hsadminng.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -10,6 +12,7 @@ import java.util.Locale;
|
||||
|
||||
@Service
|
||||
@RequestScope
|
||||
@Slf4j
|
||||
public class MessageTranslator {
|
||||
|
||||
@Autowired
|
||||
@@ -21,19 +24,26 @@ public class MessageTranslator {
|
||||
public String translateTo(final Locale locale, final String messageKey, final Object... args) {
|
||||
try {
|
||||
// we don't use the method which also takes a default message right away ...
|
||||
final var translatedMessage = messageSource.getMessage(messageKey, args, locale);
|
||||
val translatedMessage = messageSource.getMessage(messageKey, args, locale);
|
||||
return translatedMessage;
|
||||
} catch (final Exception e) {
|
||||
final var defaultMessage = messageKey.replace("'", "''");
|
||||
final var translatedMessage = messageSource.getMessage(messageKey, args, defaultMessage, locale);
|
||||
if (locale != Locale.ENGLISH) {
|
||||
// ... because we want to add a hint that the translation is missing, even if placeholders got replaced
|
||||
return translatedMessage + " [" + locale + " translation missing]";
|
||||
}
|
||||
return translatedMessage;
|
||||
// ... but log the missing translation ...
|
||||
log.error("Missing translation for message key '{}' in locale '{}'", messageKey, locale, e);
|
||||
|
||||
// and decorate the default message to mark it as not really translated:
|
||||
val defaultMessage = messageKey.substring(messageKey.indexOf('.') + 1)
|
||||
.replaceAll("--+", " - ")
|
||||
.replaceAll("(?<! )-(?! )", " ")
|
||||
.replace("'", "''");
|
||||
val fallbackMessage = messageSource.getMessage(messageKey, args, defaultMessage, Locale.ENGLISH);
|
||||
return decorateMissingTranslation(fallbackMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static String decorateMissingTranslation(final String translatedMessage) {
|
||||
return "【⍰" + translatedMessage + "⍰】";
|
||||
}
|
||||
|
||||
public String translate(final String messageKey, final Object... args) {
|
||||
return translateTo(httpRequest.getLocale(), messageKey, args);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public class MessagesResourceConfig {
|
||||
final var source = new ResourceBundleMessageSource();
|
||||
source.setBasenames("i18n/messages");
|
||||
source.setDefaultEncoding("UTF-8");
|
||||
source.setFallbackToSystemLocale(false);
|
||||
source.setUseCodeAsDefaultMessage(false);
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ public class RealCasAuthenticator implements CasAuthenticator {
|
||||
|
||||
private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException {
|
||||
if ( !serviceTicket.startsWith("ST-") ) {
|
||||
throwBadCredentialsException("unknown authorization ticket");
|
||||
throwBadCredentialsException("auth.unknown-authorization-ticket");
|
||||
}
|
||||
|
||||
final var url = casServerUrl + "/cas/p3/serviceValidate" +
|
||||
@@ -74,7 +74,7 @@ public class RealCasAuthenticator implements CasAuthenticator {
|
||||
private String extractUserName(final Document verification) {
|
||||
|
||||
if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
|
||||
throwBadCredentialsException("CAS service-ticket could not be verified");
|
||||
throwBadCredentialsException("auth.cas-service-ticket-could-not-be-verified");
|
||||
}
|
||||
return verification.getElementsByTagName("cas:user").item(0).getTextContent();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import static java.util.Locale.ENGLISH;
|
||||
|
||||
public class ReferenceNotFoundException extends RuntimeException {
|
||||
|
||||
private final String TRANSLATABLE_MESSAGE = "{0} \"{1}\" not found";
|
||||
private final String TRANSLATABLE_MESSAGE = "general.{0}-{1}-not-found";
|
||||
|
||||
private final MessageTranslator translator;
|
||||
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ public class RestResponseEntityExceptionHandler
|
||||
}
|
||||
|
||||
private Function<FieldError, String> toEnrichedFieldErrorMessage() {
|
||||
final var translatedButIsLiteral = messageTranslator.translate("but is");
|
||||
final var translatedButIsLiteral = messageTranslator.translate("general.but-is");
|
||||
// TODO.i18n: the following does not work in all languages, e.g. not in right-to-left languages
|
||||
return fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() +
|
||||
" " + translatedButIsLiteral + " " + optionallyQuoted(fieldError.getRejectedValue());
|
||||
|
||||
+4
-2
@@ -52,13 +52,15 @@ public class CredentialContextResourceToEntityMapper {
|
||||
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
||||
if (existingContextEntity == null) {
|
||||
throw new EntityNotFoundException(
|
||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
||||
messageTranslator.translate(
|
||||
"general.{0}-{1}-not-found-or-not-accessible",
|
||||
"credentials uuid", resource.getUuid()));
|
||||
}
|
||||
if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) ||
|
||||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
||||
throw new EntityNotFoundException(
|
||||
messageTranslator.translate("existing {0} does not match given resource {1}",
|
||||
messageTranslator.translate(
|
||||
"credentials.existing-{0}-does-not-match-given-resource-{1}",
|
||||
existingContextEntity, resource));
|
||||
}
|
||||
entities.add(existingContextEntity);
|
||||
|
||||
@@ -203,12 +203,12 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
private void validate(final HsCredentialsEntity newCredentialsEntity) {
|
||||
// the referenced person must be represented by currently logged in person
|
||||
final var personUuid = newCredentialsEntity.getPerson().getUuid();
|
||||
final var representedPersonUuids = rbacPersonRepo.findPersonsrepresentedByPersonWithUuid(personUuid)
|
||||
final var representedPersonUuids = rbacPersonRepo.findPersonsRepresentedByPersonWithUuid(personUuid)
|
||||
.stream().map(HsOfficePerson::getUuid).toList();
|
||||
if ( !representedPersonUuids.contains(personUuid)) {
|
||||
throw new ValidationException(
|
||||
messageTranslator.translate(
|
||||
"access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person",
|
||||
"credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person",
|
||||
personUuid));
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
||||
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
|
||||
() -> new EntityNotFoundException(
|
||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
||||
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", personUuid)
|
||||
)
|
||||
|
||||
);
|
||||
@@ -269,7 +269,7 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
||||
() -> new EntityNotFoundException(
|
||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid())
|
||||
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", resource.getPersonUuid())
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ public class HsCoopAssetTranslations implements RetroactiveTranslator {
|
||||
|
||||
@Override
|
||||
public boolean canTranslate(final String message) {
|
||||
return message.equals("ERROR: [400] coop assets transaction would result in a negative balance of assets");
|
||||
return message.equals("ERROR: [400] office.coop-assets.transaction-would-result-in-a-negative-balance-of-assets");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -26,7 +26,7 @@ public class HsCoopAssetTranslations implements RetroactiveTranslator {
|
||||
// and in this case it's just one
|
||||
return ERROR_400_PREFIX + messageTranslator.translate(message.substring(ERROR_400_PREFIX.length()));
|
||||
|
||||
// HOWTO extract variable parts from a messages which got created without i18n support:
|
||||
// HOWTO extract variable parts from messages which got created without i18n support:
|
||||
// final var regex = "(?<propertyName>[^ ]+) (?<propertyValue>.+) not found";
|
||||
// final var pattern = Pattern.compile(regex);
|
||||
// final var matcher = pattern.matcher(message);
|
||||
|
||||
+15
-13
@@ -141,7 +141,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
final ArrayList<String> violations) {
|
||||
if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() < 0) {
|
||||
violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be positive but is {1,number,#0.00}",
|
||||
violations.add(messageTranslator.translate(
|
||||
"office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-positive-but-is-{1,number,#0.00}",
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
}
|
||||
}
|
||||
@@ -152,7 +153,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS)
|
||||
.contains(requestBody.getTransactionType())
|
||||
&& requestBody.getAssetValue().signum() > 0) {
|
||||
violations.add(messageTranslator.translate("for transactionType={0}, assetValue must be negative but is {1,number,#0.00}",
|
||||
violations.add(messageTranslator.translate(
|
||||
"office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-negative-but-is-{1,number,#0.00}",
|
||||
requestBody.getTransactionType(), requestBody.getAssetValue()));
|
||||
}
|
||||
}
|
||||
@@ -161,7 +163,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (requestBody.getAssetValue().signum() == 0) {
|
||||
violations.add(messageTranslator.translate("assetValue must not be 0"));
|
||||
violations.add(messageTranslator.translate("office.coop-assets.assetvalue-must-not-be-0"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,18 +229,18 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
resource.getMembershipUuid()))
|
||||
.orElseThrow(() -> new EntityNotFoundException(
|
||||
messageTranslator.translate(
|
||||
"{0} \"{1}\" not found", "membership.uuid", resource.getMembershipUuid())));
|
||||
"general.{0}-{1}-not-found", "membership.uuid", resource.getMembershipUuid())));
|
||||
entity.setMembership(membership);
|
||||
}
|
||||
|
||||
if (entity.getTransactionType() == REVERSAL) {
|
||||
if (resource.getRevertedAssetTxUuid() == null) {
|
||||
throw new ValidationException(messageTranslator.translate(
|
||||
"a REVERSAL asset transaction requires specifying a revertedAssetTx.uuid"));
|
||||
"office.coop-assets.a-reversal-asset-transaction-requires-specifying-a-revertedassettx-uuid"));
|
||||
}
|
||||
final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
|
||||
.orElseThrow(() -> new EntityNotFoundException(messageTranslator.translate(
|
||||
"{0} \"{1}\" not found",
|
||||
"general.{0}-{1}-not-found",
|
||||
"revertedAssetTx.uuid",
|
||||
resource.getRevertedAssetTxUuid())));
|
||||
revertedAssetTx.setReversalAssetTx(entity);
|
||||
@@ -246,7 +248,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
|
||||
throw new ValidationException(
|
||||
messageTranslator.translate(
|
||||
"given assetValue {0,number,#0.00} must be the negative value of the reverted asset transaction: {1,number,#0.00}",
|
||||
"office.coop-assets.given-assetvalue-{0,number,#0.00}-must-be-the-negative-value-of-the-reverted-asset-transaction-{1,number,#0.00}",
|
||||
resource.getAssetValue(), revertedAssetTx.getAssetValue()));
|
||||
}
|
||||
|
||||
@@ -270,7 +272,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
final var adoptingMembership = determineAdoptingMembership(resource);
|
||||
if ( entity.getMembership() == adoptingMembership) {
|
||||
throw new ValidationException(messageTranslator.translate(
|
||||
"transferring and adopting membership must be different, but both are {0}",
|
||||
"office.coop-assets.transferring-and-adopting-membership-must-be-different-but-both-are-{0}",
|
||||
adoptingMembership.getTaggedMemberNumber()));
|
||||
}
|
||||
final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership);
|
||||
@@ -285,8 +287,8 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
// @formatter:off
|
||||
final var message = messageTranslator.translate(
|
||||
resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
|
||||
? "either {0} or {1} must be given, not both"
|
||||
: "neither {0} nor {1} must be given for transactionType={2}",
|
||||
? "office.coop-assets.either-{0}-or-{1}-must-be-given-not-both"
|
||||
: "office.coop-assets.neither-{0}-nor-{1}-must-be-given-for-transactiontype-{2}",
|
||||
"adoptingMembership.uuid",
|
||||
"adoptingMembership.memberNumber",
|
||||
resource.getTransactionType());
|
||||
@@ -298,7 +300,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid);
|
||||
return adoptingMembership.orElseThrow(() ->
|
||||
new ValidationException(messageTranslator.translate(
|
||||
"{0} \"{1}\" not found or not accessible",
|
||||
"general.{0}-{1}-not-found-or-not-accessible",
|
||||
"adoptingMembership.uuid",
|
||||
adoptingMembershipUuid)));
|
||||
}
|
||||
@@ -309,14 +311,14 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
return adoptingMembership.orElseThrow( () ->
|
||||
new ValidationException(
|
||||
messageTranslator.translate(
|
||||
"{0} \"{1}\" not found or not accessible",
|
||||
"general.{0}-{1}-not-found-or-not-accessible",
|
||||
"adoptingMembership.memberNumber",
|
||||
adoptingMembershipMemberNumber)));
|
||||
}
|
||||
|
||||
throw new ValidationException(
|
||||
messageTranslator.translate(
|
||||
"either {0} or {1} must be given for transactionType={2}",
|
||||
"office.coop-assets.either-{0}-or-{1}-must-be-given-for-transactiontype-{2}",
|
||||
"adoptingMembership.uuid",
|
||||
"adoptingMembership.memberNumber",
|
||||
resource.getTransactionType()
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ public class HsCoopShareTranslations implements RetroactiveTranslator {
|
||||
|
||||
@Override
|
||||
public boolean canTranslate(final String message) {
|
||||
return message.equals("ERROR: [400] coop shares transaction would result in a negative number of shares");
|
||||
return message.equals("ERROR: [400] office.coop-shares.transaction-would-result-in-a-negative-number-of-shares");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-3
@@ -129,7 +129,8 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
final ArrayList<String> violations) {
|
||||
if (requestBody.getTransactionType() == SUBSCRIPTION
|
||||
&& requestBody.getShareCount() < 0) {
|
||||
violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be positive but is {1}",
|
||||
violations.add(messageTranslator.translate(
|
||||
"office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}",
|
||||
requestBody.getTransactionType(), requestBody.getShareCount()));
|
||||
}
|
||||
}
|
||||
@@ -139,7 +140,8 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
final ArrayList<String> violations) {
|
||||
if (requestBody.getTransactionType() == CANCELLATION
|
||||
&& requestBody.getShareCount() > 0) {
|
||||
violations.add(messageTranslator.translate("for transactionType={0}, shareCount must be negative but is {1}",
|
||||
violations.add(messageTranslator.translate(
|
||||
"office.coop-shares.for-transactiontype-{0}-sharecount-must-be-negative-but-is-{1}",
|
||||
requestBody.getTransactionType(), requestBody.getShareCount()));
|
||||
}
|
||||
}
|
||||
@@ -148,7 +150,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
final HsOfficeCoopSharesTransactionInsertResource requestBody,
|
||||
final ArrayList<String> violations) {
|
||||
if (requestBody.getShareCount() == 0) {
|
||||
violations.add(messageTranslator.translate("shareCount must not be 0"));
|
||||
violations.add(messageTranslator.translate("office.coop-shares.sharecount-must-not-be-0"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var entities = representedByPersonUuid != null
|
||||
? personRepo.findPersonsrepresentedByPersonWithUuid(representedByPersonUuid)
|
||||
? personRepo.findPersonsRepresentedByPersonWithUuid(representedByPersonUuid)
|
||||
: personRepo.findPersonByOptionalNameLike(name);
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ public interface HsOfficePersonRbacRepository extends Repository<HsOfficePersonR
|
||||
OR person.uuid = :personUuid
|
||||
""", nativeQuery = true)
|
||||
@Timed("app.office.persons.repo.findRepresentedPersons.rbac")
|
||||
List<HsOfficePersonRbacEntity> findPersonsrepresentedByPersonWithUuid(UUID personUuid);
|
||||
List<HsOfficePersonRbacEntity> findPersonsRepresentedByPersonWithUuid(UUID personUuid);
|
||||
|
||||
@Timed("app.office.persons.repo.save.rbac")
|
||||
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
|
||||
|
||||
@@ -21,9 +21,8 @@ public class PingController implements TestApi {
|
||||
@PreAuthorize("permitAll()")
|
||||
@Timed("app.api.ping")
|
||||
public ResponseEntity<String> ping() {
|
||||
final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
|
||||
final var translatedMessage = messageTranslator.translate("pong {0} - in English", userName);
|
||||
final var translatedMessage = messageTranslator.translate("test.pinged--in-your-language");
|
||||
return ResponseEntity.ok(translatedMessage + "\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user