Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add internationalization support #133

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open

Add internationalization support #133

wants to merge 32 commits into from

Conversation

jpraet
Copy link
Contributor

@jpraet jpraet commented Nov 4, 2024

Fixes #124.

@jpraet
Copy link
Contributor Author

jpraet commented Nov 4, 2024

@pvdbosch I have some questions related to this PR:

  • Current implementation only considers the first value in Accept-Language header, and falls back to English if it's not supported. Is that OK, or should we consider the full list of prioritized languages?
  • Should we return HTTP 406 Not Acceptable if requested language is not supported? I would say no: it's not because the language is not supported by the problem library, that it couldn't be supported by the API itself.
  • Should we set Content-Language response header? Again there could be some differences between the languages supported by the problem library, and those supported by the API. But maybe we can set Content-Language response header for problem responses only.
  • Should we set Vary: Accept-Language response header? Are error responses typically cached?

@pvdbosch
Copy link
Contributor

pvdbosch commented Nov 4, 2024

Hi @jpraet , I'd abstract away resolving the locale, including setting response Content-Language or Vary headers.
Internationalization is a separate API-wide concern and shouldn't be the responsibility of a problem library.

Some framework support may be used:

If an API supports an explicit way of specifying the language (e.g. ?lang=...), this usually has precedence.
From MDN :

This header serves as a hint when the server cannot determine the target content language otherwise (for example, use a specific URL that depends on an explicit user decision).

All languages in the list should be considered (with q values), but framework support should best be used to resolve them.

I agree to continue with a default language when no match found:

The server may send back a 406 Not Acceptable error code when unable to serve content in a matching language, but this is rarely implemented. Servers often ignore the Accept-Language header in such cases and send a successful response with the most appropriate resource instead for a better user experience.

@jpraet
Copy link
Contributor Author

jpraet commented Nov 4, 2024

For Spring Boot I use RequestContextUtils.getLocale() which should delegate to the configured LocaleResolver.

Maybe the Belgif REST Guide should be adapted, it currently says "In case the server could not honor any of the requested languages, it SHOULD return a 406 Not Acceptable error."
-> change SHOULD to MAY?

@pvdbosch
Copy link
Contributor

pvdbosch commented Nov 4, 2024

For Spring, could LocaleContextHolder be used instead?

From RequestContextUtils javadoc:

Consider using LocaleContextHolder.getLocale() which will normally be populated with the same Locale.

It should also work outside a servlet environment, and wouldn't require registering a filter or syncing with a threadlocal variable.

I believe resolving and setting the locale shouldn't be part of the rest-problem-java library so APIs could choose their own logic e.g. based on the authenticated user's language preference. Perhaps in a separate library if the framework doesn't provide an abstraction.
For Jakarta EE, there is Jakarta MVC support but that's probably more for UIs rather than REST APIs and it isn't part of Jakarta EE 10.

@pvdbosch
Copy link
Contributor

pvdbosch commented Nov 4, 2024

Maybe the Belgif REST Guide should be adapted, it currently says "In case the server could not honor any of the requested languages, it SHOULD return a 406 Not Acceptable error." -> change SHOULD to MAY?

Agreed, or recommend instead to continue with a supported language. Created belgif/rest-guide#205

@jpraet
Copy link
Contributor Author

jpraet commented Nov 4, 2024

For Spring, could LocaleContextHolder be used instead?

I will look into it. Something like a pluggable LocaleResolver then, configurable via /META-INF/services/io.github.belgif.rest.problem.i18n.LocaleResolver? Or did you have something else in mind?

@pvdbosch
Copy link
Contributor

pvdbosch commented Nov 5, 2024

Something like a pluggable LocaleResolver then, configurable via /META-INF/services/io.github.belgif.rest.problem.i18n.LocaleResolver? Or did you have something else in mind?

Yes, rest-problem-java should only depend on "consulting the current locale", but not on any logic that deduces it from request or user context.

These aspects should probably be best in a separate I18N module for JEE; because it's not really logical to search for such functionality in a rest-problem module. Maybe we should consider having a belgif-rest-java-sdk as root project that bundles modules...

@jpraet jpraet marked this pull request as ready for review November 5, 2024 14:04
@jpraet
Copy link
Contributor Author

jpraet commented Nov 5, 2024

These aspects should probably be best in a separate I18N module for JEE; because it's not really logical to search for such functionality in a rest-problem module. Maybe we should consider having a belgif-rest-java-sdk as root project that bundles modules...

Current implementation has pluggable LocaleResolver, which on Spring Boot delegates to LocaleContextHolder.
On JEE it does currently provide a default implementation that derives it from Accept-Language HTTP header, but can be overridden with a custom LocaleResolver implementation by the implementation. Note that Bean Validation (at least on JBoss / WildFly) also seems to be using the Locale from Accept-Language HTTP header by default.

belgif-rest-java-sdk would be in a separate repository?

@jpraet jpraet requested a review from a team November 5, 2024 14:14
@pvdbosch
Copy link
Contributor

pvdbosch commented Nov 5, 2024

On JEE it does currently provide a default implementation that derives it from Accept-Language HTTP header, but can be overridden with a custom LocaleResolver implementation by the implementation. Note that Bean Validation (at least on JBoss / WildFly) also seems to be using the Locale from Accept-Language HTTP header by default.

Ok, seems like hibernate wrote their own "LocaleResolver" SPI, which integrates with RestEasy.

Too bad that this isn't standardized in JEE, so we could reuse it. If the locale resolver is overridable by JEE applications, it's probably sufficient then not to warrant another module.

belgif-rest-java-sdk would be in a separate repository?

I was thinking about a repo to group all possible belgif rest java modules ( incl rest-problem-java, i18n, possible future validation lib, ...). But if internationalization can be done without an additional module, probably not needed.

Copy link
Collaborator

@jflabatBCSS jflabatBCSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review the NL and DE translations :-)

Peter talks about a separate module of -problem , are you planning to more everything afterwards

@jpraet
Copy link
Contributor Author

jpraet commented Nov 28, 2024

Peter talks about a separate module of -problem, are you planning to move everything afterwards

Not planned at this point.

@jpraet jpraet requested a review from jflabatBCSS December 9, 2024 15:14
@jpraet jpraet requested a review from clone1612 December 11, 2024 10:20
@jpraet
Copy link
Contributor Author

jpraet commented Dec 11, 2024

There's a failure on quarkus:

 Error:  Failed to execute goal io.quarkus.platform:quarkus-maven-plugin:3.17.3:build (default) on project belgif-rest-problem-quarkus-it: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
Error: [ERROR] 	[error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: jakarta.enterprise.inject.UnsatisfiedResolutionException: Unsatisfied dependency for type jakarta.servlet.ServletContext and qualifiers [@Default]
Error:  	- injection target: io.github.belgif.rest.problem.jaxrs.i18n.I18NAcceptLanguageFilter#servletContext
Error:  	- declared on CLASS bean [types=[jakarta.ws.rs.container.ContainerRequestFilter, io.github.belgif.rest.problem.jaxrs.i18n.I18NAcceptLanguageFilter, jakarta.ws.rs.container.ContainerResponseFilter, java.lang.Object], qualifiers=[@Default, @Any], target=io.github.belgif.rest.problem.jaxrs.i18n.I18NAcceptLanguageFilter]

[[i18n]]
== Internationalization (I18N)

The detail messages of Belgif problem and issue types can be localized to the language requested in the https://www.belgif.be/specification/rest/api-guide/#i18n-negotiation[Accept-Language HTTP request header].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a LocaleResolver interface that determines the language to use, clients could in theory provide a custom implementation that has nothing to do with the header (unlikely though). Shouldn't we describe or at least mention the locale resolver logic as we provide default implementations for Spring and Java EE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some documentation on LocaleResolver.

@Test
void i18n() {
getSpec().when()
.header("Accept-Language", "nl-BE")
Copy link
Collaborator

@clone1612 clone1612 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be interesting to add an additional test that verifies behavior with multiple values that use weighting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added. Note that we just take the one with the highest weight though, and if that one is not supported, it will fallback to English. So with Accept-Language = "fr-BE;q=0.5, es;q=0.7" the resulting language will be English, not French.

@@ -338,4 +339,39 @@ void constraintViolationBodyInheritance() {
.body("issues[1].detail", equalTo("must not be blank"));
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never test localization can be disabled, would it be easy to add a test for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could expose an endpoint that calls I18N.setEnabled(false).

* Checks whether I18N should be enabled, based on system property / environment variable.
*/
static void init() {
if (System.getProperty(I18N_FLAG) != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this code Java EE specific? For Spring we always let Spring look for the property in all its locations through @ConfigurationProperties and set its state here with I18NConfigurator.

The only scenario where this method is relevant is for a Java EE application where it wasn't provided as a servlet context parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to java ee I18NConfigurator.

@@ -67,7 +69,7 @@ public static InputValidationIssue schemaViolation(InEnum in, String name, Objec

public static InputValidationIssue unknownInput(InEnum in, String name, Object value) {
return new InputValidationIssue(ISSUE_TYPE_UNKNOWN_INPUT, "Unknown input")
.detail(String.format("Input %s is unknown", name))
.detail(I18N.getLocalizedString("unknownInput.detail", name))
Copy link
Collaborator

@clone1612 clone1612 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth it to add a localizedDetail method, similar to I18N.getLocalizedDetail(...)? Would make the code less verbose + remove duplication of ".detail" part of the bundle key.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a risk we ever forget to keep the files in sync? Shouldn't happen with a good review though, but was thinking about adding a test that verifies all messages are defined in all languages.

If we ever forget an entry it would result in a MissingResourceException and cause the request to fail, but risk seems limited.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check for missing keys.

@jpraet jpraet requested a review from clone1612 December 16, 2024 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support localized problem detail messages
4 participants