KEYCLOAK-1152 Initial commit for i18n support

This commit is contained in:
Stan Silvert 2015-08-19 10:36:03 -04:00
parent 76f3842dad
commit 4898d74c6d
15 changed files with 3205 additions and 11 deletions

View file

@ -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);
@ -209,7 +213,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
this.passwordSet = passwordSet;
return this;
}
protected void setMessage(MessageType type, String message, Object... parameters) {
messageType = type;
messages = new ArrayList<>();
@ -225,7 +229,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
return message.getMessage();
}
}
@Override
public AccountProvider setErrors(List<FormMessage> messages) {
this.messageType = MessageType.ERROR;

View file

@ -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";
@ -38,7 +39,7 @@ public class LocaleHelper {
return locale;
}
}
//1. Locale cookie
if(httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)){
String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
@ -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) {

View file

@ -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>

View file

@ -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

View file

@ -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';
}));

View file

@ -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;
}]);

View file

@ -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);

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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