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);
|
||||
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
|
||||
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();
|
||||
} catch (FreeMarkerException e) {
|
||||
logger.error("Failed to process template", e);
|
||||
|
|
|
@ -15,6 +15,7 @@ import java.util.*;
|
|||
*/
|
||||
public class LocaleHelper {
|
||||
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 KC_LOCALE_PARAM = "kc_locale";
|
||||
|
||||
|
@ -89,12 +90,18 @@ public class LocaleHelper {
|
|||
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) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
<script src="${resourceUrl}/lib/angular/angular.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-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/select2.js" type="text/javascript"></script>
|
||||
|
|
|
@ -7,7 +7,7 @@ var configUrl = consoleBaseUrl + "/config";
|
|||
|
||||
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 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) {
|
||||
$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()) {
|
||||
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();
|
||||
} catch (FreeMarkerException 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()) {
|
||||
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();
|
||||
} catch (FreeMarkerException e) {
|
||||
logger.error("Failed to process template", e);
|
||||
|
|
|
@ -208,6 +208,15 @@ public class Urls {
|
|||
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) {
|
||||
return themeBase(baseUri).path(Version.RESOURCES_VERSION).build();
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import javax.ws.rs.QueryParam;
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
@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