Skip to content

Commit 3da75f5

Browse files
authored
Merge pull request jooby-project#2019 from imeszaros/locales
Ability to match request locale and use it for templates
2 parents 8bdd246 + b9be054 commit 3da75f5

21 files changed

Lines changed: 482 additions & 8 deletions

File tree

‎docs/asciidoc/context.adoc‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,48 @@ explicitly request the registration of it as a service with the following invoca
4242
Important to note that the context is a *request scoped* object, it's only available through the service
4343
registry while the request it belongs to is being processed.
4444

45+
javadoc:Context[Context] also provides derived information about the current request such as a
46+
matching locale (or locales) based on the `Accept-Language` header (if presents). You may use
47+
the result of javadoc:Context[locale] or javadoc:Context[locales] to present content matching to
48+
the user's language preference.
49+
50+
The above methods use `Locale.lookup(...)` and `Locale.filter(...)` respectively to perform the
51+
language tag matching. See their overloads if you need to plug in your own matching strategy.
52+
53+
To leverage language matching however, you need to tell Jooby which languages your application
54+
supports. This can be done by either setting the `application.lang` configuration property
55+
to a value compatible with the
56+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language[Accept-Language]
57+
header:
58+
59+
.application.conf
60+
[source, properties]
61+
----
62+
application.lang = en, en-GB, de
63+
----
64+
65+
or calling the javadoc:Jooby[setLocales, java.util.List]
66+
or javadoc:Jooby[setLocales, java.util.Locale...] method at runtime:
67+
68+
.Java
69+
[source, java, role="primary"]
70+
----
71+
{
72+
setLocales(Locale.GERMAN, new Locale("hu", "HU"));
73+
}
74+
----
75+
76+
.Kotlin
77+
[source, kotlin, role="secondary"]
78+
----
79+
{
80+
setLocales(Locale.GERMAN, Locale("hu", "HU"))
81+
}
82+
----
83+
84+
If you don't set the supported locales explicitly, Jooby uses a single locale provided by
85+
`Locale.getDefault()`.
86+
4587
=== Parameters
4688

4789
There are several parameter types: `header`, `cookie`, `path`, `query`, `form`, `multipart`,

‎docs/asciidoc/templates.adoc‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,47 @@ Templates are available via javadoc:ModelAndView[] and requires a javadoc:Templa
3232
<2> Build the view model
3333
<3> Returns a ModelAndView instance
3434

35+
javadoc:ModelAndView[] allows you to specify the desired locale used for template rendering:
36+
37+
.Java
38+
[source, java, role = "primary"]
39+
----
40+
{
41+
install(new MyTemplateEngineModule());
42+
43+
get("/", ctx -> {
44+
Map<String, Object> model = ...;
45+
return new ModelAndView("index.html", model)
46+
.setLocale(Locale.GERMAN); <1>
47+
});
48+
}
49+
----
50+
51+
.Kotlin
52+
[source, kt, role = "secondary"]
53+
----
54+
{
55+
install(MyTemplateEngineModule())
56+
57+
get("/") { ctx ->
58+
val model = mapOf(...)
59+
ModelAndView("index.html", model)
60+
.setLocale(Locale.GERMAN) <1>
61+
}
62+
}
63+
----
64+
65+
<1> Set the preferred locale
66+
67+
If no locale is specified explicitly, a locale matched by the `Accept-Language` header of the current
68+
request is used.
69+
70+
[NOTE]
71+
====
72+
Not all template engines support setting and using a specific locale. If you use such a template
73+
engine, the above configuration does not have any effect.
74+
====
75+
3576
=== Template Engine
3677

3778
Template engine does the view rendering/encoding. Template engine extends a javadoc:MessageEncoder[]

‎jooby/src/main/java/io/jooby/Context.java‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby;
77

8+
import io.jooby.internal.LocaleUtils;
89
import io.jooby.internal.ParamLookupImpl;
910
import io.jooby.internal.ReadOnlyContext;
1011
import io.jooby.internal.WebSocketSender;
@@ -24,11 +25,14 @@
2425
import java.time.ZoneId;
2526
import java.time.format.DateTimeFormatter;
2627
import java.util.Arrays;
28+
import java.util.Collections;
2729
import java.util.Date;
2830
import java.util.List;
2931
import java.util.Locale;
3032
import java.util.Map;
33+
import java.util.Optional;
3134
import java.util.concurrent.Executor;
35+
import java.util.function.BiFunction;
3236

3337
/**
3438
* HTTP context allows you to interact with the HTTP Request and manipulate the HTTP Response.
@@ -434,6 +438,78 @@ public interface Context extends Registry {
434438
*/
435439
long getRequestLength();
436440

441+
/**
442+
* Returns a list of locales that best matches the current request.
443+
*
444+
* The first filter argument is the value of <code>Accept-Language</code> as a list of
445+
* {@link Locale.LanguageRange} instances while the second argument is a list of supported
446+
* locales specified by {@link Jooby#setLocales(List)} or by the
447+
* <code>application.lang</code> configuration property.
448+
*
449+
* The next example returns a list of matching {@code Locale} instances using the filtering
450+
* mechanism defined in RFC 4647:
451+
*
452+
* <pre>{@code
453+
* ctx.locales(Locale::filter)
454+
* }</pre>
455+
*
456+
* @param filter A locale filter.
457+
* @return A list of matching locales.
458+
*/
459+
@Nonnull default List<Locale> locales(BiFunction<List<Locale.LanguageRange>, List<Locale>, List<Locale>> filter) {
460+
return filter.apply(header("Accept-Language")
461+
.toOptional()
462+
.flatMap(LocaleUtils::parseRanges)
463+
.orElseGet(Collections::emptyList), getRouter().getLocales());
464+
}
465+
466+
/**
467+
* Returns a list of locales that best matches the current request as per {@link Locale#filter}.
468+
*
469+
* @return A list of matching locales or empty list.
470+
* @see #locales(BiFunction)
471+
*/
472+
@Nonnull default List<Locale> locales() {
473+
return locales(Locale::filter);
474+
}
475+
476+
/**
477+
* Returns a locale that best matches the current request.
478+
*
479+
* The first filter argument is the value of <code>Accept-Language</code> as a list of
480+
* {@link Locale.LanguageRange} instances while the second argument is a list of supported
481+
* locales specified by {@link Jooby#setLocales(List)} or by the
482+
* <code>application.lang</code> configuration property.
483+
*
484+
* The next example returns a {@code Locale} instance for the best-matching language
485+
* tag using the lookup mechanism defined in RFC 4647.
486+
*
487+
* <pre>{@code
488+
* ctx.locale(Locale::lookup)
489+
* }</pre>
490+
*
491+
* @param filter A locale filter.
492+
* @return A matching locale.
493+
*/
494+
@Nonnull default Locale locale(BiFunction<List<Locale.LanguageRange>, List<Locale>, Locale> filter) {
495+
return filter.apply(header("Accept-Language")
496+
.toOptional()
497+
.flatMap(LocaleUtils::parseRanges)
498+
.orElseGet(Collections::emptyList), getRouter().getLocales());
499+
}
500+
501+
/**
502+
* Returns a locale that best matches the current request or the default locale specified
503+
* by {@link Jooby#setLocales(List)} or by the <code>application.lang</code>
504+
* configuration property.
505+
*
506+
* @return A matching locale.
507+
*/
508+
@Nonnull default Locale locale() {
509+
return locale((priorityList, locales) -> Optional.ofNullable(Locale.lookup(priorityList, locales))
510+
.orElse(locales.get(0)));
511+
}
512+
437513
/**
438514
* Current user or <code>null</code> if none was set.
439515
*

‎jooby/src/main/java/io/jooby/Jooby.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import com.typesafe.config.Config;
99
import io.jooby.exception.RegistryException;
10+
import io.jooby.internal.LocaleUtils;
1011
import io.jooby.internal.RouterImpl;
1112
import org.slf4j.Logger;
1213
import org.slf4j.LoggerFactory;
@@ -20,11 +21,13 @@
2021
import java.nio.file.Path;
2122
import java.nio.file.Paths;
2223
import java.util.ArrayList;
24+
import java.util.Arrays;
2325
import java.util.Collections;
2426
import java.util.Iterator;
2527
import java.util.LinkedHashMap;
2628
import java.util.LinkedList;
2729
import java.util.List;
30+
import java.util.Locale;
2831
import java.util.Map;
2932
import java.util.Objects;
3033
import java.util.Optional;
@@ -43,6 +46,8 @@
4346
import java.util.stream.Collectors;
4447
import java.util.stream.Stream;
4548

49+
import static java.util.Collections.singletonList;
50+
import static java.util.Objects.requireNonNull;
4651
import static java.util.Spliterators.spliteratorUnknownSize;
4752
import static java.util.stream.StreamSupport.stream;
4853

@@ -100,6 +105,8 @@ public class Jooby implements Router, Registry {
100105

101106
private EnvironmentOptions environmentOptions;
102107

108+
private List<Locale> locales;
109+
103110
private boolean lateInit;
104111

105112
private String name;
@@ -157,6 +164,37 @@ public Jooby() {
157164
return env;
158165
}
159166

167+
/**
168+
* Returns the list of supported locales, or
169+
* {@code null} if none set.
170+
*
171+
* @return The supported locales.
172+
*/
173+
@Nullable @Override public List<Locale> getLocales() {
174+
return locales;
175+
}
176+
177+
/**
178+
* Sets the supported locales.
179+
*
180+
* @param locales The supported locales.
181+
* @return This router.
182+
*/
183+
public Router setLocales(@Nonnull List<Locale> locales) {
184+
this.locales = requireNonNull(locales);
185+
return this;
186+
}
187+
188+
/**
189+
* Sets the supported locales.
190+
*
191+
* @param locales The supported locales.
192+
* @return This router.
193+
*/
194+
public Router setLocales(Locale... locales) {
195+
return setLocales(Arrays.asList(locales));
196+
}
197+
160198
/**
161199
* Application class loader.
162200
*
@@ -704,6 +742,17 @@ public Jooby errorCode(@Nonnull Class<? extends Throwable> type,
704742
mode = ExecutionMode.DEFAULT;
705743
}
706744

745+
if (locales == null) {
746+
String path = "application.lang";
747+
locales = Optional.of(getConfig())
748+
.filter(c -> c.hasPath(path))
749+
.map(c -> c.getString(path))
750+
.map(v -> LocaleUtils.parseLocales(v).orElseThrow(() -> new RuntimeException(String.format(
751+
"Invalid value for configuration property '%s'; check the documentation of %s#parse(): %s",
752+
path, Locale.LanguageRange.class.getName(), v))))
753+
.orElseGet(() -> singletonList(Locale.getDefault()));
754+
}
755+
707756
ServiceRegistry services = getServices();
708757
services.put(Environment.class, getEnvironment());
709758
services.put(Config.class, getConfig());

‎jooby/src/main/java/io/jooby/ModelAndView.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
package io.jooby;
77

88
import javax.annotation.Nonnull;
9+
import javax.annotation.Nullable;
910
import java.util.HashMap;
11+
import java.util.Locale;
1012
import java.util.Map;
1113

1214
/**
@@ -23,6 +25,9 @@ public class ModelAndView {
2325
/** View data. */
2426
private final Map<String, Object> model;
2527

28+
/** Locale used when rendering the view. */
29+
private Locale locale;
30+
2631
/**
2732
* Creates a new model and view.
2833
*
@@ -66,6 +71,19 @@ public ModelAndView put(@Nonnull Map<String, Object> attributes) {
6671
return this;
6772
}
6873

74+
/**
75+
* Sets the locale used when rendering the view, if the template
76+
* engine supports setting it. Specifying {@code null} triggers a
77+
* fallback to a locale determined by the current request.
78+
*
79+
* @param locale The locale used when rendering the view.
80+
* @return This instance.
81+
*/
82+
public ModelAndView setLocale(@Nullable Locale locale) {
83+
this.locale = locale;
84+
return this;
85+
}
86+
6987
/**
7088
* View data (a.k.a as model).
7189
*
@@ -84,6 +102,17 @@ public String getView() {
84102
return view;
85103
}
86104

105+
/**
106+
* Returns the locale used when rendering the view. Defaults to
107+
* {@code null}, which triggers a fallback to a locale
108+
* determined by the current request.
109+
*
110+
* @return The locale used when rendering the view.
111+
*/
112+
@Nullable public Locale getLocale() {
113+
return locale;
114+
}
115+
87116
@Override public String toString() {
88117
return view;
89118
}

‎jooby/src/main/java/io/jooby/Router.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.HashMap;
2121
import java.util.List;
22+
import java.util.Locale;
2223
import java.util.Map;
2324
import java.util.Optional;
2425
import java.util.Set;
@@ -112,6 +113,13 @@ interface Match {
112113
*/
113114
@Nonnull Environment getEnvironment();
114115

116+
/**
117+
* Returns the supported locales.
118+
*
119+
* @return The supported locales.
120+
*/
121+
@Nonnull List<Locale> getLocales();
122+
115123
/**
116124
* Mutable map of application attributes.
117125
*

0 commit comments

Comments
 (0)