Follow-up: reproduced — RequestLocaleProvider returns the request locale (de) instead of the matched available locale (de_DE), so a non-available locale ends up in ClientDetails
Jmix 2.8.2 · Vaadin 24.10.6 · Spring Boot 3.5.14 · OIDC/Keycloak login (jmix-oidc)
Thanks, Ivan — you were right that with available-locales = de_DE an English browser falls back to de_DE (I confirmed it in an isolated unit test). The reason you couldn’t reproduce is that our trigger was never en — it is de vs de_DE. I can now reproduce the “non-available locale applied” case you said is not intended.
Root cause
io.jmix.security.util.RequestLocaleProvider.getLocale() (unchanged 2.7↔2.8) decides like this (decompiled):
Locale req = request.getLocale(); // e.g. de (language only, no region)
List<Locale> available = coreProperties.getAvailableLocales(); // [de_DE]
if (available.contains(req) // de_DE.equals(de) -> false
|| available.stream().anyMatch(l -> l.getLanguage().equals(req.getLanguage()))) // "de".equals("de") -> TRUE
result = req; // <-- returns req (= "de"), NOT the matched de_DE
else { log.warn(... "not supported ... ignored"); result = null; }
In the language-match branch it returns the request locale (de), not the matched available locale (de_DE). So ClientDetails.locale = de, which is not in available-locales = [de_DE]. Since only messages_de_DE.properties exists (no messages_de), message resolution does not find the bundle → raw message keys.
This is the same “a locale not in available-locales is applied” you confirmed is unintended — it just arrives via the language-match returns request-locale path, not via en.
Why only one browser, and why only after 2.8
- The browser’s
Accept-Language ordering differs:
- Chrome:
de-DE,de;q=0.9,... → request.getLocale() = de_DE → exact match → de_DE → fine.
- Edge:
de,de-DE;q=0.9,... (user had “Deutsch” listed above “Deutsch (Deutschland)”) → request.getLocale() = de → language-match → returns de → raw keys.
- 2.7 → 2.8 behavior change: In 2.7 the OIDC UI session did not derive its locale from the request (no
ClientDetails/UiClientDetailsSource wiring), so the UI locale was the application default de_DE regardless of Accept-Language. In 2.8 (the WebAuthenticationDetails → ClientDetails change, #4842) the request locale flows into ClientDetails, so Edge’s de now takes effect. The browser always sent de first — 2.7 simply ignored it.
- Visible symptom: the page shows German briefly (initial render at default
de_DE), then flips to raw keys the moment the authenticated UI applies CurrentAuthentication.getLocale() = ClientDetails.locale = de.
Reproduction
- Jmix 2.8.x app, OIDC/Keycloak login,
jmix.core.available-locales = de_DE (German only), full messages_de_DE.properties, no messages_de.properties.
- Browser whose
Accept-Language starts with de (language only), e.g. Edge with “Deutsch” above “Deutsch (Deutschland)”, header de,de-DE;q=0.9,.... (Your English test takes the null → fallback path, which is why it looked fine.)
- Log in via OIDC.
- Expected: German (de_DE is the only available locale). Actual: UI renders German for a moment, then flips to raw keys —
ClientDetails.locale = de, which is not an available locale.
Isolated unit test (pure Mockito against stock RequestLocaleProvider) confirms: available=[de_DE], request.getLocale()=Locale.GERMAN → getLocale() returns de (≠ de_DE, not contained in available-locales).
Suggested fix
In the language-match branch, return the matched available locale, not the request locale:
return available.stream()
.filter(l -> l.getLanguage().equals(req.getLanguage()))
.findFirst()
.orElse(null); // -> de_DE for a "de" request when available = [de_DE]
That way a language-only request (de) resolves to the available region locale (de_DE) and the region-specific message bundle is found — and a locale outside available-locales is never applied.
Workaround in use
@Primary RequestLocaleProvider returning Locale.GERMANY (de_DE) for our German-only product — forces the region and prevents the flip.
Regards,
Maik