KEYCLOAK-1152 Initial commit for i18n support
This commit is contained in:
parent
76f3842dad
commit
4898d74c6d
15 changed files with 3205 additions and 11 deletions
|
@ -197,7 +197,11 @@ public class FreeMarkerAccountProvider implements AccountProvider {
|
||||||
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
|
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
|
||||||
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
|
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
|
||||||
BrowserSecurityHeaderSetup.headers(builder, realm);
|
BrowserSecurityHeaderSetup.headers(builder, realm);
|
||||||
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri,realm.getName()));
|
|
||||||
|
String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName());
|
||||||
|
String ngTranslateCookiePath = Urls.ngTranslateLocaleCookiePath(baseUri, realm.getName());
|
||||||
|
|
||||||
|
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath, ngTranslateCookiePath);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
} catch (FreeMarkerException e) {
|
} catch (FreeMarkerException e) {
|
||||||
logger.error("Failed to process template", e);
|
logger.error("Failed to process template", e);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import java.util.*;
|
||||||
*/
|
*/
|
||||||
public class LocaleHelper {
|
public class LocaleHelper {
|
||||||
public final static String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
|
public final static String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
|
||||||
|
public final static String NG_LOCALE_COOKIE = "NG_TRANSLATE_LANG_KEY";
|
||||||
public static final String UI_LOCALES_PARAM = "ui_locales";
|
public static final String UI_LOCALES_PARAM = "ui_locales";
|
||||||
public static final String KC_LOCALE_PARAM = "kc_locale";
|
public static final String KC_LOCALE_PARAM = "kc_locale";
|
||||||
|
|
||||||
|
@ -89,12 +90,18 @@ public class LocaleHelper {
|
||||||
return Locale.ENGLISH;
|
return Locale.ENGLISH;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateLocaleCookie(Response.ResponseBuilder builder, Locale locale, RealmModel realm, UriInfo uriInfo, String path) {
|
public static void updateLocaleCookie(Response.ResponseBuilder builder,
|
||||||
|
Locale locale,
|
||||||
|
RealmModel realm,
|
||||||
|
UriInfo uriInfo,
|
||||||
|
String keycloakLocaleCookiePath,
|
||||||
|
String ngTranslateLocaleCookiePath) {
|
||||||
if (locale == null) {
|
if (locale == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
|
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
|
||||||
builder.cookie(new NewCookie(LocaleHelper.LOCALE_COOKIE, locale.toLanguageTag(), path, null, null, 31536000, secure));
|
builder.cookie(new NewCookie(LocaleHelper.LOCALE_COOKIE, locale.toLanguageTag(), keycloakLocaleCookiePath, null, null, 31536000, secure),
|
||||||
|
new NewCookie(LocaleHelper.NG_LOCALE_COOKIE, "%22" + locale.toLanguageTag() + "%22", ngTranslateLocaleCookiePath, null, null, 31536000, secure));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Locale findLocale(Set<String> supportedLocales, String ... localeStrings) {
|
public static Locale findLocale(Set<String> supportedLocales, String ... localeStrings) {
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
<script src="${resourceUrl}/lib/angular/angular.js"></script>
|
<script src="${resourceUrl}/lib/angular/angular.js"></script>
|
||||||
<script src="${resourceUrl}/lib/angular/angular-resource.js"></script>
|
<script src="${resourceUrl}/lib/angular/angular-resource.js"></script>
|
||||||
<script src="${resourceUrl}/lib/angular/angular-route.js"></script>
|
<script src="${resourceUrl}/lib/angular/angular-route.js"></script>
|
||||||
|
<script src="${resourceUrl}/lib/angular/angular-cookies.js"></script>
|
||||||
|
<script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script>
|
||||||
|
<script src="${resourceUrl}/lib/angular/angular-translate.js"></script>
|
||||||
|
<script src="${resourceUrl}/lib/angular/angular-translate-storage-cookie.js"></script>
|
||||||
|
<script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script>
|
||||||
<script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script>
|
<script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script>
|
||||||
|
|
||||||
<script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script>
|
<script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script>
|
||||||
|
|
|
@ -7,7 +7,7 @@ var configUrl = consoleBaseUrl + "/config";
|
||||||
|
|
||||||
var auth = {};
|
var auth = {};
|
||||||
|
|
||||||
var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload' ]);
|
var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'pascalprecht.translate', 'ngCookies', 'ngSanitize']);
|
||||||
var resourceRequests = 0;
|
var resourceRequests = 0;
|
||||||
var loadingTimer = -1;
|
var loadingTimer = -1;
|
||||||
|
|
||||||
|
@ -52,8 +52,12 @@ module.factory('authInterceptor', function($q, Auth) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.config(function($translateProvider) {
|
||||||
|
$translateProvider.useSanitizeValueStrategy('sanitize');
|
||||||
|
$translateProvider.preferredLanguage('en');
|
||||||
|
$translateProvider.useCookieStorage();
|
||||||
|
$translateProvider.useUrlLoader('messages.json');
|
||||||
|
});
|
||||||
|
|
||||||
module.config([ '$routeProvider', function($routeProvider) {
|
module.config([ '$routeProvider', function($routeProvider) {
|
||||||
$routeProvider
|
$routeProvider
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*!
|
||||||
|
* angular-translate - v2.7.2 - 2015-06-01
|
||||||
|
* http://github.com/angular-translate/angular-translate
|
||||||
|
* Copyright (c) 2015 ; Licensed MIT
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
// AMD. Register as an anonymous module unless amdModuleId is set
|
||||||
|
define([], function () {
|
||||||
|
return (factory());
|
||||||
|
});
|
||||||
|
} else if (typeof exports === 'object') {
|
||||||
|
// Node. Does not work with strict CommonJS, but
|
||||||
|
// only CommonJS-like environments that support module.exports,
|
||||||
|
// like Node.
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
factory();
|
||||||
|
}
|
||||||
|
}(this, function () {
|
||||||
|
|
||||||
|
angular.module('pascalprecht.translate')
|
||||||
|
/**
|
||||||
|
* @ngdoc object
|
||||||
|
* @name pascalprecht.translate.$translateUrlLoader
|
||||||
|
* @requires $q
|
||||||
|
* @requires $http
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Creates a loading function for a typical dynamic url pattern:
|
||||||
|
* "locale.php?lang=en_US", "locale.php?lang=de_DE", "locale.php?language=nl_NL" etc.
|
||||||
|
* Prefixing the specified url, the current requested, language id will be applied
|
||||||
|
* with "?{queryParameter}={key}".
|
||||||
|
* Using this service, the response of these urls must be an object of
|
||||||
|
* key-value pairs.
|
||||||
|
*
|
||||||
|
* @param {object} options Options object, which gets the url, key and
|
||||||
|
* optional queryParameter ('lang' is used by default).
|
||||||
|
*/
|
||||||
|
.factory('$translateUrlLoader', $translateUrlLoader);
|
||||||
|
|
||||||
|
function $translateUrlLoader($q, $http) {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
return function (options) {
|
||||||
|
|
||||||
|
if (!options || !options.url) {
|
||||||
|
throw new Error('Couldn\'t use urlLoader since no url is given!');
|
||||||
|
}
|
||||||
|
|
||||||
|
var deferred = $q.defer(),
|
||||||
|
requestParams = {};
|
||||||
|
|
||||||
|
requestParams[options.queryParameter || 'lang'] = options.key;
|
||||||
|
|
||||||
|
$http(angular.extend({
|
||||||
|
url: options.url,
|
||||||
|
params: requestParams,
|
||||||
|
method: 'GET'
|
||||||
|
}, options.$http)).success(function (data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}).error(function () {
|
||||||
|
deferred.reject(options.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$translateUrlLoader.$inject = ['$q', '$http'];
|
||||||
|
|
||||||
|
$translateUrlLoader.displayName = '$translateUrlLoader';
|
||||||
|
return 'pascalprecht.translate';
|
||||||
|
|
||||||
|
}));
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*!
|
||||||
|
* angular-translate - v2.6.1 - 2015-03-01
|
||||||
|
* http://github.com/angular-translate/angular-translate
|
||||||
|
* Copyright (c) 2015 ; Licensed MIT
|
||||||
|
*/
|
||||||
|
angular.module('pascalprecht.translate')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc object
|
||||||
|
* @name pascalprecht.translate.$translateCookieStorage
|
||||||
|
* @requires $cookieStore
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Abstraction layer for cookieStore. This service is used when telling angular-translate
|
||||||
|
* to use cookieStore as storage.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
.factory('$translateCookieStorage', ['$cookieStore', function ($cookieStore) {
|
||||||
|
|
||||||
|
var $translateCookieStorage = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name pascalprecht.translate.$translateCookieStorage#get
|
||||||
|
* @methodOf pascalprecht.translate.$translateCookieStorage
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Returns an item from cookieStorage by given name.
|
||||||
|
*
|
||||||
|
* @param {string} name Item name
|
||||||
|
* @return {string} Value of item name
|
||||||
|
*/
|
||||||
|
get: function (name) {
|
||||||
|
return $cookieStore.get(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name pascalprecht.translate.$translateCookieStorage#set
|
||||||
|
* @methodOf pascalprecht.translate.$translateCookieStorage
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Sets an item in cookieStorage by given name.
|
||||||
|
*
|
||||||
|
* @deprecated use #put
|
||||||
|
*
|
||||||
|
* @param {string} name Item name
|
||||||
|
* @param {string} value Item value
|
||||||
|
*/
|
||||||
|
set: function (name, value) {
|
||||||
|
$cookieStore.put(name, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name pascalprecht.translate.$translateCookieStorage#put
|
||||||
|
* @methodOf pascalprecht.translate.$translateCookieStorage
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Sets an item in cookieStorage by given name.
|
||||||
|
*
|
||||||
|
* @param {string} name Item name
|
||||||
|
* @param {string} value Item value
|
||||||
|
*/
|
||||||
|
put: function (name, value) {
|
||||||
|
$cookieStore.put(name, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return $translateCookieStorage;
|
||||||
|
}]);
|
2904
forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js
vendored
Normal file
2904
forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -276,7 +276,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
||||||
builder.header(entry.getKey(), entry.getValue());
|
builder.header(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName()));
|
|
||||||
|
String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName());
|
||||||
|
String ngTranslateCookiePath = Urls.ngTranslateLocaleCookiePath(baseUri, realm.getName());
|
||||||
|
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath, ngTranslateCookiePath);
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
} catch (FreeMarkerException e) {
|
} catch (FreeMarkerException e) {
|
||||||
logger.error("Failed to process template", e);
|
logger.error("Failed to process template", e);
|
||||||
|
@ -374,7 +378,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
|
||||||
builder.header(entry.getKey(), entry.getValue());
|
builder.header(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName()));
|
|
||||||
|
String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName());
|
||||||
|
String ngTranslateCookiePath = Urls.ngTranslateLocaleCookiePath(baseUri, realm.getName());
|
||||||
|
|
||||||
|
LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath, ngTranslateCookiePath);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
} catch (FreeMarkerException e) {
|
} catch (FreeMarkerException e) {
|
||||||
logger.error("Failed to process template", e);
|
logger.error("Failed to process template", e);
|
||||||
|
|
|
@ -208,6 +208,15 @@ public class Urls {
|
||||||
return realmBase(baseUri).path(realmName).build().getRawPath();
|
return realmBase(baseUri).path(realmName).build().getRawPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String ngTranslateLocaleCookiePath(URI baseUri, String realmName) {
|
||||||
|
// I'm only using using localeCookiePath to get the /auth part of the path.
|
||||||
|
// I can't assume the URL starts with "/auth". Keycloak could be installed
|
||||||
|
// as root context. Typically, the angular-translate cookie path needs to be
|
||||||
|
// /auth/admin/{realmName}/console
|
||||||
|
String basePath = localeCookiePath(baseUri, realmName);
|
||||||
|
return basePath.substring(0, basePath.indexOf("realms")) + "admin/" + realmName + "/console";
|
||||||
|
}
|
||||||
|
|
||||||
public static URI themeRoot(URI baseUri) {
|
public static URI themeRoot(URI baseUri) {
|
||||||
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
|
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,9 @@ import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -302,4 +304,27 @@ public class AdminConsole {
|
||||||
return Response.status(302).location(uriInfo.getRequestUriBuilder().path("../").build()).build();
|
return Response.status(302).location(uriInfo.getRequestUriBuilder().path("../").build()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("messages.json")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Properties getMessages(@QueryParam("lang") String lang) {
|
||||||
|
if (lang == null) {
|
||||||
|
logger.warn("Locale not specified for messages.json");
|
||||||
|
lang = "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Properties msgs = AdminMessagesLoader.getMessages(lang);
|
||||||
|
if (msgs.isEmpty()) {
|
||||||
|
logger.warn("Message bundle not found for language code '" + lang + "'");
|
||||||
|
msgs = AdminMessagesLoader.getMessages("en"); // fall back to en
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgs.isEmpty()) logger.fatal("Message bundle not found for language code 'en'");
|
||||||
|
|
||||||
|
return msgs;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
|
||||||
|
* as indicated by the @author tags. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple loader for message bundles consumed by angular-translate.
|
||||||
|
*
|
||||||
|
* Note that these bundles are converted to JSON before being shipped to the UI.
|
||||||
|
* Also, the content should be formatted such that it can be interpolated by
|
||||||
|
* angular-translate. This is somewhat different from an ordinary Java bundle.
|
||||||
|
*
|
||||||
|
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
|
||||||
|
*/
|
||||||
|
public class AdminMessagesLoader {
|
||||||
|
|
||||||
|
private static final Map<String, Properties> allMessages = new HashMap<String, Properties>();
|
||||||
|
|
||||||
|
static Properties getMessages(String locale) throws IOException {
|
||||||
|
Properties messages = allMessages.get(locale);
|
||||||
|
if (messages != null) return messages;
|
||||||
|
|
||||||
|
return loadMessages(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties loadMessages(String locale) throws IOException {
|
||||||
|
Properties msgs = new Properties();
|
||||||
|
|
||||||
|
try (InputStream msgStream = getBundleStream(locale)){
|
||||||
|
if (msgStream == null) return msgs;
|
||||||
|
msgs.load(msgStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
allMessages.put(locale, msgs);
|
||||||
|
return msgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InputStream getBundleStream(String locale) {
|
||||||
|
String filename = "admin-messages_" + locale + ".properties";
|
||||||
|
return AdminMessagesLoader.class.getResourceAsStream(filename);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
enabled=Enabled
|
||||||
|
name=Name
|
||||||
|
save=Save
|
||||||
|
cancel=Cancel
|
||||||
|
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
|
|
@ -0,0 +1,5 @@
|
||||||
|
enabled=Enabled
|
||||||
|
name=Name
|
||||||
|
save=Save
|
||||||
|
cancel=Cancel
|
||||||
|
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
|
|
@ -0,0 +1,5 @@
|
||||||
|
enabled=Enabled
|
||||||
|
name=Name
|
||||||
|
save=Save
|
||||||
|
cancel=Cancel
|
||||||
|
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
|
|
@ -0,0 +1,5 @@
|
||||||
|
enabled=Enabled
|
||||||
|
name=Name
|
||||||
|
save=Save
|
||||||
|
cancel=Cancel
|
||||||
|
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
|
Loading…
Reference in a new issue