keycloak-aplcache
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 25(+22 -3)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties 116(+116 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 116(+116 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html 68(+34 -34)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html 16(+8 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html 16(+8 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html 16(+8 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html 50(+25 -25)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html 36(+18 -18)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html 40(+20 -20)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html 76(+38 -38)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html 12(+6 -6)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html 16(+8 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html 18(+9 -9)
forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js 2904(+2904 -0)
forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js 75(+75 -0)
forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js 71(+71 -0)
Details
diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml
index cd59f38..10b2417 100755
--- a/docbook/reference/en/en-US/modules/themes.xml
+++ b/docbook/reference/en/en-US/modules/themes.xml
@@ -181,6 +181,12 @@ import=common/keycloak
<literal>messages/messages.properties</literal> inside your theme folder and add the following content:
</para>
<programlisting>username=Your Username</programlisting>
+ <para>
+ For the admin console, there is a second resource bundle named <literal>admin-messages.properties</literal>.
+ This resource bundle is converted to JSON and shipped to the console to be processed by
+ angular-translate. It is found in the same directory as messages.properties and can be overridden
+ in the same way as described above.
+ </para>
</section>
<section>
<title>Modifying HTML</title>
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index 35081ce..a4b3771 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -1,3 +1,19 @@
+/*
+ * 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.account.freemarker;
import java.io.IOException;
@@ -197,7 +213,10 @@ 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());
+
+ LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath);
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
@@ -209,7 +228,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 +244,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
return message.getMessage();
}
}
-
+
@Override
public AccountProvider setErrors(List<FormMessage> messages) {
this.messageType = MessageType.ERROR;
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
index 0e98f11..020d349 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
@@ -224,10 +224,15 @@ public class ExtendingThemeManager implements ThemeProvider {
@Override
public Properties getMessages(Locale locale) throws IOException {
+ return getMessages("messages", locale);
+ }
+
+ @Override
+ public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
Properties messages = new Properties();
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
- Properties m = itr.previous().getMessages(locale);
+ Properties m = itr.previous().getMessages(baseBundlename, locale);
if (m != null) {
messages.putAll(m);
}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
index 9f1aafe..14f7f6c 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
@@ -1,6 +1,21 @@
+/*
+ * 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.freemarker;
-import org.jboss.logging.Logger;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -38,7 +53,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,7 +104,11 @@ 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 path) {
if (locale == null) {
return;
}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
index 6a12a49..43107ce 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
@@ -29,8 +29,27 @@ public interface Theme {
public InputStream getResourceAsStream(String path) throws IOException;
+ /**
+ * Same as getMessages(baseBundlename, locale), but uses a default baseBundlename
+ * such as "messages".
+ *
+ * @param locale The locale of the desired message bundle.
+ * @return The localized messages from the bundle.
+ * @throws IOException If bundle can not be read.
+ */
public Properties getMessages(Locale locale) throws IOException;
+ /**
+ * Retrieve localized messages from a message bundle.
+ *
+ * @param baseBundlename The base name of the bundle, such as "messages" in
+ * messages_en.properties.
+ * @param locale The locale of the desired message bundle.
+ * @return The localized messages from the bundle.
+ * @throws IOException If bundle can not be read.
+ */
+ public Properties getMessages(String baseBundlename, Locale locale) throws IOException;
+
public Properties getProperties() throws IOException;
}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java
index 3e92be8..68ac7ca 100755
--- a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java
@@ -100,12 +100,17 @@ public class ClassLoaderTheme implements Theme {
@Override
public Properties getMessages(Locale locale) throws IOException {
+ return getMessages("messages", locale);
+ }
+
+ @Override
+ public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
if(locale == null){
return null;
}
Properties m = new Properties();
- URL url = classLoader.getResource(this.messageRoot + "messages_" + locale.toString() + ".properties");
+ URL url = classLoader.getResource(this.messageRoot + baseBundlename + "_" + locale.toString() + ".properties");
if (url != null) {
m.load(url.openStream());
}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java
index d1593fe..77a65a8 100644
--- a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java
@@ -93,13 +93,18 @@ public class FolderTheme implements Theme {
@Override
public Properties getMessages(Locale locale) throws IOException {
+ return getMessages("messages", locale);
+ }
+
+ @Override
+ public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
if(locale == null){
return null;
}
Properties m = new Properties();
- File file = new File(themeDir, "messages" + File.separator + "messages_" + locale.toString() + ".properties");
+ File file = new File(themeDir, "messages" + File.separator + baseBundlename + "_" + locale.toString() + ".properties");
if (file.isFile()) {
m.load(new FileInputStream(file));
}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
index 1a87bce..1584d0d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
@@ -21,6 +21,10 @@
<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-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>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
new file mode 100644
index 0000000..6442e6e
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
@@ -0,0 +1,116 @@
+# Common messages
+enabled=de Enabled
+name=de Name
+save=de Save
+cancel=de Cancel
+onText=AN
+offText=AUS
+client=de Client
+clear=de Clear
+
+# Realm settings
+realm-detail.enabled.tooltip=de Users and clients can only access a realm if it's enabled
+registrationAllowed=de User registration
+registrationAllowed.tooltip=de Enable/disable the registration page. A link for registration will show on login page too.
+registrationEmailAsUsername=de Email as username
+registrationEmailAsUsername.tooltip=de If enabled then username field is hidden from registration form and email is used as username for new user.
+editUsernameAllowed=de Edit username
+editUsernameAllowed.tooltip=de If enabled, the username field is editable, readonly otherwise.
+resetPasswordAllowed=de Forget password
+resetPasswordAllowed.tooltip=de Show a link on login page for user to click on when they have forgotten their credentials.
+rememberMe=de Remember Me
+rememberMe.tooltip=de Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.
+verifyEmail=de Verify email
+verifyEmail.tooltip=de Require the user to verify their email address the first time they login.
+sslRequired=de Require SSL
+sslRequired.option.all=de all requests
+sslRequired.option.external=de external requests
+sslRequired.option.none=de none
+sslRequired.tooltip=de Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.
+publicKey=de Public key
+gen-new-keys=de Generate new keys
+certificate=de Certificate
+host=de Host
+smtp-host=de SMTP Host
+port=de Port
+smtp-port=de SMTP Port (defaults to 25)
+from=de From
+sender-email-addr=de Sender Email Address
+enable-ssl=de Enable SSL
+enable-start-tls=de Enable StartTLS
+enable-auth=de Enable Authentication
+username=de Username
+login-username=de Login Username
+password=de Password
+login-password=de Login Password
+login-theme=de Login Theme
+select-one=de Select one...
+login-theme.tooltip=de Select theme for login, TOTP, grant, registration, and forgot password pages.
+account-theme=de Account Theme
+account-theme.tooltip=de Select theme for user account management pages.
+admin-console-theme=de Admin Console Theme
+select-theme-admin-console=de Select theme for admin console.
+email-theme=de Email Theme
+select-theme-email=de Select theme for emails that are sent by the server.
+i18n-enabled=de Internationalization Enabled
+supported-locales=de Supported Locales
+supported-locales.placeholder=de Type a locale and enter
+default-locale=de Default Locale
+realm-cache-enabled=de Realm Cache Enabled
+realm-cache-enabled.tooltip=de Enable/disable cache for realm, client and role data.
+user-cache-enabled=de User Cache Enabled
+user-cache-enabled.tooltip=de Enable/disable user and user role mapping cache.
+sso-session-idle=de SSO Session Idle
+seconds=de Seconds
+minutes=de Minutes
+hours=de Hours
+days=de Days
+sso-session-max=de SSO Session Max
+sso-session-idle.tooltip=de Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired.
+sso-session-max.tooltip=de Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
+access-token-lifespan=de Access Token Lifespan
+access-token-lifespan.tooltip=de Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
+client-login-timeout=de Client login timeout
+client-login-timeout.tooltip=de Max time an client has to finish the access token protocol. This should normally be 1 minute.
+login-timeout=de Login timeout
+login-timeout.tooltip=de Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more.
+login-action-timeout=de Login action timeout
+login-action-timeout.tooltip=de Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more.
+headers=de Headers
+brute-force-detection=de Brute Force Detection
+x-frame-options=de X-Frame-Options
+click-label-for-info=de Click on label link for more information. The default value prevents pages from being included via non-origin iframes.
+content-sec-policy=de Content-Security-Policy
+max-login-failures=de Max Login Failures
+max-login-failures.tooltip=de How many failures before wait is triggered.
+wait-increment=de Wait Increment
+wait-increment.tooltip=de When failure threshold has been met, how much time should the user be locked out?
+quick-login-check-millis=de Quick Login Check Milli Seconds
+quick-login-check-millis.tooltip=de If a failure happens concurrently too quickly, lock out the user.
+min-quick-login-wait=de Minimum Quick Login Wait
+min-quick-login-wait.tooltip=de How long to wait after a quick login failure.
+max-wait=de Max Wait
+max-wait.tooltip=de Max time a user will be locked out.
+failure-reset-time=de Failure Reset Time
+failure-reset-time.tooltip=de When will failure count be reset?
+realm-tab-login=de Login
+realm-tab-keys=de Keys
+realm-tab-email=de Email
+realm-tab-themes=de Themes
+realm-tab-cache=de Cache
+realm-tab-tokens=de Tokens
+realm-tab-security-defenses=de Security Defenses
+realm-tab-general=de General
+add-realm=de Add Realm
+
+#Session settings
+realm-sessions=de Realm Sessions
+revocation=de Revocation
+logout-all=de Logout All
+active-sessions=de Active Sessions
+sessions=de Sessions
+not-before=de Not Before
+not-before.tooltip=de Revoke any tokens issued before this date.
+set-to-now=de Set To Now
+push=de Push
+push.tooltip=de For every client that has an admin URL, notify them of the new revocation policy.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
new file mode 100644
index 0000000..be3ef2d
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -0,0 +1,116 @@
+# Common messages
+enabled=Enabled
+name=Name
+save=Save
+cancel=Cancel
+onText=ON
+offText=OFF
+client=Client
+clear=Clear
+
+# Realm settings
+realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
+registrationAllowed=User registration
+registrationAllowed.tooltip=Enable/disable the registration page. A link for registration will show on login page too.
+registrationEmailAsUsername=Email as username
+registrationEmailAsUsername.tooltip=If enabled then username field is hidden from registration form and email is used as username for new user.
+editUsernameAllowed=Edit username
+editUsernameAllowed.tooltip=If enabled, the username field is editable, readonly otherwise.
+resetPasswordAllowed=Forget password
+resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials.
+rememberMe=Remember Me
+rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.
+verifyEmail=Verify email
+verifyEmail.tooltip=Require the user to verify their email address the first time they login.
+sslRequired=Require SSL
+sslRequired.option.all=all requests
+sslRequired.option.external=external requests
+sslRequired.option.none=none
+sslRequired.tooltip=Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.
+publicKey=Public key
+gen-new-keys=Generate new keys
+certificate=Certificate
+host=Host
+smtp-host=SMTP Host
+port=Port
+smtp-port=SMTP Port (defaults to 25)
+from=From
+sender-email-addr=Sender Email Address
+enable-ssl=Enable SSL
+enable-start-tls=Enable StartTLS
+enable-auth=Enable Authentication
+username=Username
+login-username=Login Username
+password=Password
+login-password=Login Password
+login-theme=Login Theme
+select-one=Select one...
+login-theme.tooltip=Select theme for login, TOTP, grant, registration, and forgot password pages.
+account-theme=Account Theme
+account-theme.tooltip=Select theme for user account management pages.
+admin-console-theme=Admin Console Theme
+select-theme-admin-console=Select theme for admin console.
+email-theme=Email Theme
+select-theme-email=Select theme for emails that are sent by the server.
+i18n-enabled=Internationalization Enabled
+supported-locales=Supported Locales
+supported-locales.placeholder=Type a locale and enter
+default-locale=Default Locale
+realm-cache-enabled=Realm Cache Enabled
+realm-cache-enabled.tooltip=Enable/disable cache for realm, client and role data.
+user-cache-enabled=User Cache Enabled
+user-cache-enabled.tooltip=Enable/disable user and user role mapping cache.
+sso-session-idle=SSO Session Idle
+seconds=Seconds
+minutes=Minutes
+hours=Hours
+days=Days
+sso-session-max=SSO Session Max
+sso-session-idle.tooltip=Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired.
+sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
+access-token-lifespan=Access Token Lifespan
+access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
+client-login-timeout=Client login timeout
+client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute.
+login-timeout=Login timeout
+login-timeout.tooltip=Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more.
+login-action-timeout=Login action timeout
+login-action-timeout.tooltip=Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more.
+headers=Headers
+brute-force-detection=Brute Force Detection
+x-frame-options=X-Frame-Options
+click-label-for-info=Click on label link for more information. The default value prevents pages from being included via non-origin iframes.
+content-sec-policy=Content-Security-Policy
+max-login-failures=Max Login Failures
+max-login-failures.tooltip=How many failures before wait is triggered.
+wait-increment=Wait Increment
+wait-increment.tooltip=When failure threshold has been met, how much time should the user be locked out?
+quick-login-check-millis=Quick Login Check Milli Seconds
+quick-login-check-millis.tooltip=If a failure happens concurrently too quickly, lock out the user.
+min-quick-login-wait=Minimum Quick Login Wait
+min-quick-login-wait.tooltip=How long to wait after a quick login failure.
+max-wait=Max Wait
+max-wait.tooltip=Max time a user will be locked out.
+failure-reset-time=Failure Reset Time
+failure-reset-time.tooltip=When will failure count be reset?
+realm-tab-login=Login
+realm-tab-keys=Keys
+realm-tab-email=Email
+realm-tab-themes=Themes
+realm-tab-cache=Cache
+realm-tab-tokens=Tokens
+realm-tab-security-defenses=Security Defenses
+realm-tab-general=General
+add-realm=Add Realm
+
+#Session settings
+realm-sessions=Realm Sessions
+revocation=Revocation
+logout-all=Logout All
+active-sessions=Active Sessions
+sessions=Sessions
+not-before=Not Before
+not-before.tooltip=Revoke any tokens issued before this date.
+set-to-now=Set To Now
+push=Push
+push.tooltip=For every client that has an admin URL, notify them of the new revocation policy.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 756c89e..350f0d0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -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,18 @@ module.factory('authInterceptor', function($q, Auth) {
};
});
-
-
+module.config(['$translateProvider', function($translateProvider) {
+ $translateProvider.useSanitizeValueStrategy('sanitizeParameters');
+
+ var locale = auth.authz.idTokenParsed.locale;
+ if (locale !== undefined) {
+ $translateProvider.preferredLanguage(locale);
+ } else {
+ $translateProvider.preferredLanguage('en');
+ }
+
+ $translateProvider.useUrlLoader('messages.json');
+}]);
module.config([ '$routeProvider', function($routeProvider) {
$routeProvider
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
index e26e126..33845c0 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html
@@ -2,102 +2,102 @@
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
- <li><a href="#/realms/{{realm.realm}}/defense/headers">Headers</a></li>
- <li class="active"><a href="#/realms/{{realm.realm}}/defense/brute-force">Brute Force Detection</a></li>
+ <li><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'headers' | translate}}</a></li>
+ <li class="active"><a href="#/realms/{{realm.realm}}/defense/brute-force">{{:: 'brute-force-detection' | translate}}</a></li>
</ul>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label class="col-md-2 control-label" for="bruteForceProtected">Enabled</label>
+ <label class="col-md-2 control-label" for="bruteForceProtected">{{:: 'enabled' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.bruteForceProtected" name="bruteForceProtected" id="bruteForceProtected" onoffswitch />
+ <input ng-model="realm.bruteForceProtected" name="bruteForceProtected" id="bruteForceProtected" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="failureFactor">Max Login Failures</label>
+ <label class="col-md-2 control-label" for="failureFactor">{{:: 'max-login-failures' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="number" min="1" max="31536000" id="failureFactor" name="failureFactor" data-ng-model="realm.failureFactor" autofocus
required>
</div>
- <kc-tooltip>How many failures before wait is triggered.</kc-tooltip>
+ <kc-tooltip>{{:: 'max-login-failures.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="waitIncrement">Wait Increment</label>
+ <label class="col-md-2 control-label" for="waitIncrement">{{:: 'wait-increment' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.waitIncrement"
id="waitIncrement" name="waitIncrement"/>
<select class="form-control" name="waitIncrementUnit" data-ng-model="realm.waitIncrementUnit" >
- <option data-ng-selected="!realm.waitIncrementUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.waitIncrementUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>When failure threshold has been met, how much time should the user be locked out?</kc-tooltip>
+ <kc-tooltip>{{:: 'wait-increment.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="quickLoginCheckMilliSeconds">Quick Login Check Milli Seconds</label>
+ <label class="col-md-2 control-label" for="quickLoginCheckMilliSeconds">{{:: 'quick-login-check-millis' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="number" min="1" max="31536000" id="quickLoginCheckMilliSeconds" name="quickLoginCheckMilliSeconds" data-ng-model="realm.quickLoginCheckMilliSeconds" autofocus
required>
</div>
- <kc-tooltip>If a failure happens concurrently too quickly, lock out the user.</kc-tooltip>
+ <kc-tooltip>{{:: 'quick-login-check-millis.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="minimumQuickLoginWait">Minimum Quick Login Wait</label>
+ <label class="col-md-2 control-label" for="minimumQuickLoginWait">{{:: 'min-quick-login-wait' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.minimumQuickLoginWait"
id="minimumQuickLoginWait" name="minimumQuickLoginWait"/>
<select class="form-control" name="minimumQuickLoginWaitUnit" data-ng-model="realm.minimumQuickLoginWaitUnit" >
- <option data-ng-selected="!realm.minimumQuickLoginWaitUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.minimumQuickLoginWaitUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>How long to wait after a quick login failure.</kc-tooltip>
+ <kc-tooltip>{{:: 'min-quick-login-wait.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="maxFailureWait">Max Wait</label>
+ <label class="col-md-2 control-label" for="maxFailureWait">{{:: 'max-wait' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.maxFailureWait"
id="maxFailureWait" name="maxFailureWait"/>
<select class="form-control" name="maxFailureWaitUnit" data-ng-model="realm.maxFailureWaitUnit" >
- <option data-ng-selected="!realm.maxFailureWaitUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.maxFailureWaitUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Max time a user will be locked out.</kc-tooltip>
+ <kc-tooltip>{{:: 'max-wait.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.bruteForceProtected">
- <label class="col-md-2 control-label" for="maxDeltaTime">Failure Reset Time</label>
+ <label class="col-md-2 control-label" for="maxDeltaTime">{{:: 'failure-reset-time' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.maxDeltaTime"
id="maxDeltaTime" name="maxDeltaTime"/>
<select class="form-control" name="maxDeltaTimeUnit" data-ng-model="realm.maxDeltaTimeUnit" >
- <option data-ng-selected="!realm.maxDeltaTimeUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.maxDeltaTimeUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>When will failure count be reset?</kc-tooltip>
+ <kc-tooltip>{{:: 'failure-reset-time.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
index c6bd5b7..aa1dc4e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html
@@ -2,31 +2,31 @@
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
- <li class="active"><a href="#/realms/{{realm.realm}}/defense/headers">Headers</a></li>
- <li><a href="#/realms/{{realm.realm}}/defense/brute-force">Brute Force Detection</a></li>
+ <li class="active"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'headers' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/defense/brute-force">{{:: 'brute-force-detection' | translate}}</a></li>
</ul>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label class="col-md-2 control-label" for="xFrameOptions"><a href="http://tools.ietf.org/html/rfc7034">X-Frame-Options</a></label>
+ <label class="col-md-2 control-label" for="xFrameOptions"><a href="http://tools.ietf.org/html/rfc7034">{{:: 'x-frame-options' | translate}}</a></label>
<div class="col-sm-6">
<input class="form-control" id="xFrameOptions" type="text" ng-model="realm.browserSecurityHeaders.xFrameOptions">
</div>
- <kc-tooltip>Click on label link for more information. The default value prevents pages from being included via non-origin iframes.</kc-tooltip>
+ <kc-tooltip>{{:: 'click-label-for-info' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="contentSecurityPolicy"><a href="http://www.w3.org/TR/CSP/">Content-Security-Policy</a></label>
+ <label class="col-md-2 control-label" for="contentSecurityPolicy"><a href="http://www.w3.org/TR/CSP/">{{:: 'content-sec-policy' | translate}}</a></label>
<div class="col-sm-6">
<input class="form-control" id="contentSecurityPolicy" type="text" ng-model="realm.browserSecurityHeaders.contentSecurityPolicy">
</div>
- <kc-tooltip>Click on label link for more information. The default value prevents pages from being included via non-origin iframes.</kc-tooltip>
+ <kc-tooltip>{{:: 'click-label-for-info' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
index c7f9b26..db26796 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html
@@ -3,24 +3,24 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group">
- <label class="col-md-2 control-label" for="realmCacheEnabled">Realm Cache Enabled</label>
+ <label class="col-md-2 control-label" for="realmCacheEnabled">{{:: 'realm-cache-enabled' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.realmCacheEnabled" name="realmCacheEnabled" id="realmCacheEnabled" onoffswitch />
+ <input ng-model="realm.realmCacheEnabled" name="realmCacheEnabled" id="realmCacheEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
- <kc-tooltip>Enable/disable cache for realm, client and role data.</kc-tooltip>
+ <kc-tooltip>{{:: 'realm-cache-enabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="userCacheEnabled">User Cache Enabled</label>
+ <label class="col-md-2 control-label" for="userCacheEnabled">{{:: 'user-cache-enabled' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.userCacheEnabled" name="userCacheEnabled" id="userCacheEnabled" onoffswitch />
+ <input ng-model="realm.userCacheEnabled" name="userCacheEnabled" id="userCacheEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
- <kc-tooltip>Enable/disable user and user role mapping cache.</kc-tooltip>
+ <kc-tooltip>{{:: 'user-cache-enabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
index 469e8c6..d39ba14 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
@@ -3,29 +3,29 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group">
- <label class="col-md-2 control-label" for="name"><span class="required">*</span> Name</label>
+ <label class="col-md-2 control-label" for="name"><span class="required">*</span> {{:: 'name' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="text" id="name" name="name" data-ng-model="realm.realm" autofocus required>
</div>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="enabled">Enabled</label>
+ <label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.enabled" name="enabled" id="enabled" onoffswitch />
+ <input ng-model="realm.enabled" name="enabled" id="enabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>Users and clients can only access a realm if it's enabled</kc-tooltip>
+ <kc-tooltip>{{:: 'realm-detail.enabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="createRealm && access.manageRealm">
- <button kc-save data-ng-show="changed">Save</button>
- <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ <button kc-save data-ng-show="changed">{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
</div>
<div class="col-md-10 col-md-offset-2" data-ng-show="!createRealm && access.manageRealm">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
index ddda540..93301af 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html
@@ -4,7 +4,7 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label class="col-md-2 control-label" for="publicKey">Public key</label>
+ <label class="col-md-2 control-label" for="publicKey">{{:: 'publicKey' | translate}}</label>
<div class="col-md-10">
<textarea type="text" id="publicKey" name="publicKey" class="form-control" rows="4"
@@ -12,7 +12,7 @@
</div>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="certificate">Certificate</label>
+ <label class="col-md-2 control-label" for="certificate">{{:: 'certificate' | translate}}</label>
<div class="col-md-10">
<textarea type="text" id="certificate" name="certificate" class="form-control" rows="8" kc-select-action="click" readonly>{{realm.certificate}}</textarea>
@@ -22,7 +22,7 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button class="btn btn-danger" type="submit" data-ng-click="generate()">Generate new keys</button>
+ <button class="btn btn-danger" type="submit" data-ng-click="generate()">{{:: 'gen-new-keys' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
index adccf3b..9766c7a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
@@ -4,66 +4,66 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label for="registrationAllowed" class="col-md-2 control-label">User registration</label>
+ <label for="registrationAllowed" class="col-md-2 control-label">{{:: 'registrationAllowed' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.registrationAllowed" name="registrationAllowed" id="registrationAllowed" onoffswitch />
+ <input ng-model="realm.registrationAllowed" name="registrationAllowed" id="registrationAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>Enable/disable the registration page. A link for registration will show on login page too.</kc-tooltip>
+ <kc-tooltip>{{:: 'registrationAllowed.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" ng-show="realm.registrationAllowed">
- <label for="registrationEmailAsUsername" class="col-md-2 control-label">Email as username</label>
+ <label for="registrationEmailAsUsername" class="col-md-2 control-label">{{:: 'registrationEmailAsUsername' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.registrationEmailAsUsername" name="registrationEmailAsUsername" id="registrationEmailAsUsername" onoffswitch />
+ <input ng-model="realm.registrationEmailAsUsername" name="registrationEmailAsUsername" id="registrationEmailAsUsername" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>If enabled then username field is hidden from registration form and email is used as username for new user.</kc-tooltip>
+ <kc-tooltip>{{:: 'registrationEmailAsUsername.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label for="editUsernameAllowed" class="col-md-2 control-label">Edit username</label>
+ <label for="editUsernameAllowed" class="col-md-2 control-label">{{:: 'editUsernameAllowed' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.editUsernameAllowed" name="editUsernameAllowed" id="editUsernameAllowed" onoffswitch />
+ <input ng-model="realm.editUsernameAllowed" name="editUsernameAllowed" id="editUsernameAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>If enabled, the username field is editable, readonly otherwise.</kc-tooltip>
+ <kc-tooltip>{{:: 'editUsernameAllowed.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label for="resetPasswordAllowed" class="col-md-2 control-label">Forget password</label>
+ <label for="resetPasswordAllowed" class="col-md-2 control-label">{{:: 'resetPasswordAllowed' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.resetPasswordAllowed" name="resetPasswordAllowed" id="resetPasswordAllowed" onoffswitch />
+ <input ng-model="realm.resetPasswordAllowed" name="resetPasswordAllowed" id="resetPasswordAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>Show a link on login page for user to click on when they have forgotten their credentials.</kc-tooltip>
+ <kc-tooltip>{{:: 'resetPasswordAllowed.tooltip' |translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="rememberMe">Remember Me</label>
+ <label class="col-md-2 control-label" for="rememberMe">{{:: 'rememberMe' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.rememberMe" name="rememberMe" id="rememberMe" onoffswitch />
+ <input ng-model="realm.rememberMe" name="rememberMe" id="rememberMe" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.</kc-tooltip>
+ <kc-tooltip>{{:: 'rememberMe.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label for="verifyEmail" class="col-md-2 control-label">Verify email</label>
+ <label for="verifyEmail" class="col-md-2 control-label">{{:: 'verifyEmail' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.verifyEmail" name="verifyEmail" id="verifyEmail" onoffswitch />
+ <input ng-model="realm.verifyEmail" name="verifyEmail" id="verifyEmail" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
- <kc-tooltip>Require the user to verify their email address the first time they login.</kc-tooltip>
+ <kc-tooltip>{{:: 'verifyEmail.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label for="sslRequired" class="col-md-2 control-label">Require SSL</label>
+ <label for="sslRequired" class="col-md-2 control-label">{{:: 'sslRequired' | translate}}</label>
<div class="col-md-2">
<div>
<select id="sslRequired" ng-model="realm.sslRequired" class="form-control">
- <option value="all">all requests</option>
- <option value="external">external requests</option>
- <option value="none">none</option>
+ <option value="all">{{:: 'sslRequired.option.all' | translate}}</option>
+ <option value="external">{{:: 'sslRequired.option.external' | translate}}</option>
+ <option value="none">{{:: 'sslRequired.option.none' | translate}}</option>
</select>
</div>
</div>
- <kc-tooltip>Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.</kc-tooltip>
+ <kc-tooltip>{{:: 'sslRequired.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
index be703d4..f9b5f6b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html
@@ -3,59 +3,59 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpHost"><span class="required">*</span> Host</label>
+ <label class="col-md-2 control-label" for="smtpHost"><span class="required">*</span> {{:: 'host' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="SMTP Host" required>
+ <input class="form-control" id="smtpHost" type="text" ng-model="realm.smtpServer.host" placeholder="{{:: 'smtp-host' | translate}}" required>
</div>
</div>
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpPort">Port</label>
+ <label class="col-md-2 control-label" for="smtpPort">{{:: 'port' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="smtpPort" type="number" ng-model="realm.smtpServer.port" placeholder="SMTP Port (defaults to 25)">
+ <input class="form-control" id="smtpPort" type="number" ng-model="realm.smtpServer.port" placeholder="{{:: 'smtp-port' | translate}}">
</div>
</div>
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpFrom"><span class="required">*</span> From</label>
+ <label class="col-md-2 control-label" for="smtpFrom"><span class="required">*</span> {{:: 'from' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="smtpFrom" type="email" ng-model="realm.smtpServer.from" placeholder="Sender Email Address" required>
+ <input class="form-control" id="smtpFrom" type="email" ng-model="realm.smtpServer.from" placeholder="{{:: 'sender-email-addr' | translate}}" required>
</div>
</div>
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpSSL">Enable SSL</label>
+ <label class="col-md-2 control-label" for="smtpSSL">{{:: 'enable-ssl' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.smtpServer.ssl" name="smtpSSL" id="smtpSSL" onoffswitch />
+ <input ng-model="realm.smtpServer.ssl" name="smtpSSL" id="smtpSSL" onoffswitch onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpStartTLS">Enable StartTLS</label>
+ <label class="col-md-2 control-label" for="smtpStartTLS">{{:: 'enable-start-tls' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.smtpServer.starttls" name="smtpStartTLS" id="smtpStartTLS" onoffswitch />
+ <input ng-model="realm.smtpServer.starttls" name="smtpStartTLS" id="smtpStartTLS" onoffswitch onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group clearfix">
- <label class="col-md-2 control-label" for="smtpAuth">Enable Authentication</label>
+ <label class="col-md-2 control-label" for="smtpAuth">{{:: 'enable-auth' | translate}}</label>
<div class="col-md-6">
- <input ng-model="realm.smtpServer.auth" name="smtpAuth" id="smtpAuth" onoffswitch />
+ <input ng-model="realm.smtpServer.auth" name="smtpAuth" id="smtpAuth" onoffswitch onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group clearfix" data-ng-show="realm.smtpServer.auth">
- <label class="col-md-2 control-label" for="smtpUsername"><span class="required">*</span> Username</span></label>
+ <label class="col-md-2 control-label" for="smtpUsername"><span class="required">*</span> {{:: 'username' | translate}}</span></label>
<div class="col-md-6">
- <input class="form-control" id="smtpUsername" type="text" ng-model="realm.smtpServer.user" placeholder="Login Username" ng-disabled="!realm.smtpServer.auth" ng-required="realm.smtpServer.auth">
+ <input class="form-control" id="smtpUsername" type="text" ng-model="realm.smtpServer.user" placeholder="{{:: 'login-username' | translate}}" ng-disabled="!realm.smtpServer.auth" ng-required="realm.smtpServer.auth">
</div>
</div>
<div class="form-group clearfix" data-ng-show="realm.smtpServer.auth">
- <label class="col-md-2 control-label" for="smtpPassword"><span class="required">*</span> Password</label>
+ <label class="col-md-2 control-label" for="smtpPassword"><span class="required">*</span> {{:: 'password' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="smtpPassword" type="password" ng-model="realm.smtpServer.password" placeholder="Login Password" ng-disabled="!realm.smtpServer.auth" ng-required="realm.smtpServer.auth">
+ <input class="form-control" id="smtpPassword" type="password" ng-model="realm.smtpServer.password" placeholder="{{:: 'login-password' | translate}}" ng-disabled="!realm.smtpServer.auth" ng-required="realm.smtpServer.auth">
</div>
</div>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button data-kc-save data-ng-disabled="!changed">Save</button>
- <button data-kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button data-kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button data-kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
index caecc6d..4d77b89 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html
@@ -4,72 +4,72 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label class="col-md-2 control-label" for="loginTheme">Login Theme</label>
+ <label class="col-md-2 control-label" for="loginTheme">{{:: 'login-theme' | translate}}</label>
<div class="col-md-3">
<div>
<select class="form-control" id="loginTheme"
ng-model="realm.loginTheme"
ng-options="o as o for o in serverInfo.themes.login">
- <option value="" disabled selected>Select one...</option>
+ <option value="" disabled selected>{{:: 'select-one' | translate}}</option>
</select>
</div>
</div>
- <kc-tooltip>Select theme for login, TOTP, grant, registration, and forgot password pages.</kc-tooltip>
+ <kc-tooltip>{{:: 'login-theme.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="accountTheme">Account Theme</label>
+ <label class="col-md-2 control-label" for="accountTheme">{{:: 'account-theme' | translate}}</label>
<div class="col-md-3">
<div>
<select class="form-control" id="accountTheme"
ng-model="realm.accountTheme"
ng-options="o as o for o in serverInfo.themes.account">
- <option value="" disabled selected>Select one...</option>
+ <option value="" disabled selected>{{:: 'select-one' | translate}}</option>
</select>
</div>
</div>
- <kc-tooltip>Select theme for user account management pages.</kc-tooltip>
+ <kc-tooltip>{{ 'account-theme.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="adminTheme">Admin Console Theme</label>
+ <label class="col-md-2 control-label" for="adminTheme">{{:: 'admin-console-theme' | translate}}</label>
<div class="col-md-3">
<div>
<select class="form-control" id="adminTheme"
ng-model="realm.adminTheme"
ng-options="o as o for o in serverInfo.themes.admin">
- <option value="" disabled selected>Select one...</option>
+ <option value="" disabled selected>{{:: 'select-one' | translate}}</option>
</select>
</div>
</div>
- <kc-tooltip>Select theme for admin console.</kc-tooltip>
+ <kc-tooltip>{{:: 'select-theme-admin-console' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="emailTheme">Email Theme</label>
+ <label class="col-md-2 control-label" for="emailTheme">{{:: 'email-theme' | translate}}</label>
<div class="col-md-3">
<div>
<select class="form-control" id="emailTheme"
ng-model="realm.emailTheme"
ng-options="o as o for o in serverInfo.themes.email">
- <option value="" disabled selected>Select one...</option>
+ <option value="" disabled selected>{{:: 'select-one' | translate}}</option>
</select>
</div>
</div>
- <kc-tooltip>Select theme for emails that are sent by the server.</kc-tooltip>
+ <kc-tooltip>{{:: 'select-theme-email' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="internationalizationEnabled">Internationalization Enabled</label>
+ <label class="col-md-2 control-label" for="internationalizationEnabled">{{:: 'i18n-enabled' | translate}}</label>
<div class="col-md-3">
- <input ng-model="realm.internationalizationEnabled" name="internationalizationEnabled" id="internationalizationEnabled" onoffswitch />
+ <input ng-model="realm.internationalizationEnabled" name="internationalizationEnabled" id="internationalizationEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="realm.internationalizationEnabled">
- <label class="col-md-2 control-label" for="supportedLocales" class="control-label two-lines">Supported Locales</label>
+ <label class="col-md-2 control-label" for="supportedLocales" class="control-label two-lines">{{:: 'supported-locales' | translate}}</label>
<div class="col-md-6">
- <input id="supportedLocales" type="text" ui-select2="supportedLocalesOptions" ng-model="realm.supportedLocales" placeholder="Type a locale and enter" ng-required="realm.internationalizationEnabled" ng-disabled="!realm.internationalizationEnabled">
+ <input id="supportedLocales" type="text" ui-select2="supportedLocalesOptions" ng-model="realm.supportedLocales" placeholder="{{:: 'supported-locales.placeholder' | translate}}" ng-required="realm.internationalizationEnabled" ng-disabled="!realm.internationalizationEnabled">
</div>
</div>
<div class="form-group" data-ng-show="realm.internationalizationEnabled">
- <label class="col-md-2 control-label" for="defaultLocale">Default Locale</label>
+ <label class="col-md-2 control-label" for="defaultLocale">{{:: 'default-locale' | translate}}</label>
<div class="col-md-3">
<div>
<select class="form-control" id="defaultLocale"
@@ -77,7 +77,7 @@
ng-options="o as o for o in realm.supportedLocales"
ng-required="realm.internationalizationEnabled"
ng-disabled="!realm.internationalizationEnabled">
- <option value="" disabled selected>Select one...</option>
+ <option value="" disabled selected>{{:: 'select-one' | translate}}</option>
</select>
</div>
</div>
@@ -86,8 +86,8 @@
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index 56ed0d7..a44b939 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -4,110 +4,110 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group">
- <label class="col-md-2 control-label" for="ssoSessionIdleTimeout">SSO Session Idle</label>
+ <label class="col-md-2 control-label" for="ssoSessionIdleTimeout">{{:: 'sso-session-idle' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.ssoSessionIdleTimeout" id="ssoSessionIdleTimeout"
name="ssoSessionIdleTimeout"/>
<select class="form-control" name="ssoSessionIdleTimeoutUnit" data-ng-model="realm.ssoSessionIdleTimeoutUnit">
- <option data-ng-selected="!realm.ssoSessionIdleTimeoutUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.ssoSessionIdleTimeoutUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired.
+ <kc-tooltip>{{:: 'sso-session-idle.tooltip' | translate}}
</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="ssoSessionMaxLifespan">SSO Session Max</label>
+ <label class="col-md-2 control-label" for="ssoSessionMaxLifespan">{{:: 'sso-session-max' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.ssoSessionMaxLifespan"
id="ssoSessionMaxLifespan" name="ssoSessionMaxLifespan"/>
<select class="form-control" name="ssoSessionMaxLifespanUnit" data-ng-model="realm.ssoSessionMaxLifespanUnit">
- <option data-ng-selected="!realm.ssoSessionMaxLifespanUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.ssoSessionMaxLifespanUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.</kc-tooltip>
+ <kc-tooltip>{{:: 'sso-session-max.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="accessTokenLifespan">Access Token Lifespan</label>
+ <label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.accessTokenLifespan"
id="accessTokenLifespan" name="accessTokenLifespan"/>
<select class="form-control" name="accessTokenLifespanUnit" data-ng-model="realm.accessTokenLifespanUnit">
- <option data-ng-selected="!realm.accessTokenLifespanUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.accessTokenLifespanUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.</kc-tooltip>
+ <kc-tooltip>{{:: 'access-token-lifespan.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="accessCodeLifespan">Client login timeout</label>
+ <label class="col-md-2 control-label" for="accessCodeLifespan">{{:: 'client-login-timeout' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.accessCodeLifespan" id="accessCodeLifespan"
name="accessCodeLifespan">
<select class="form-control" name="accessCodeLifespanUnit" data-ng-model="realm.accessCodeLifespanUnit">
- <option data-ng-selected="!realm.accessCodeLifespanUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.accessCodeLifespanUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Max time an client has to finish the access token protocol. This should normally be 1 minute.</kc-tooltip>
+ <kc-tooltip>{{:: 'client-login-timeout.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="accessCodeLifespanLogin" class="two-lines">Login timeout</label>
+ <label class="col-md-2 control-label" for="accessCodeLifespanLogin" class="two-lines">{{:: 'login-timeout' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.accessCodeLifespanLogin"
id="accessCodeLifespanLogin" name="accessCodeLifespanLogin">
<select class="form-control" name="accessCodeLifespanLoginUnit" data-ng-model="realm.accessCodeLifespanLoginUnit">
- <option data-ng-selected="!realm.accessCodeLifespanLoginUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.accessCodeLifespanLoginUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
- <kc-tooltip>Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more.</kc-tooltip>
+ <kc-tooltip>{{:: 'login-timeout' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="accessCodeLifespanUserAction" class="two-lines">Login action timeout</label>
+ <label class="col-md-2 control-label" for="accessCodeLifespanUserAction" class="two-lines">{{:: 'login-action-timeout' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.accessCodeLifespanUserAction"
id="accessCodeLifespanUserAction" name="accessCodeLifespanUserAction">
<select class="form-control" name="accessCodeLifespanUserActionUnit" data-ng-model="realm.accessCodeLifespanUserActionUnit">
- <option data-ng-selected="!realm.accessCodeLifespanUserActionUnit">Seconds</option>
- <option>Minutes</option>
- <option>Hours</option>
- <option>Days</option>
+ <option data-ng-selected="!realm.accessCodeLifespanUserActionUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>
- Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more.
+ {{:: 'login-action-timeout.tooltip' | translate}}
</kc-tooltip>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
- <button kc-save data-ng-disabled="!changed">Save</button>
- <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
index 5410f09..e724d95 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
@@ -1,9 +1,9 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
- <h1>Sessions</h1>
+ <h1>{{:: 'sessions' | translate}}</h1>
<ul class="nav nav-tabs">
- <li class="active"><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
- <li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
+ <li class="active"><a href="#/realms/{{realm.realm}}/sessions/realm">{{:: 'realm-sessions' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/sessions/revocation">{{:: 'revocation' | translate}}</a></li>
</ul>
<table class="table table-striped table-bordered">
@@ -11,13 +11,13 @@
<tr>
<th class="kc-table-actions" colspan="3">
<div class="pull-right">
- <a id="logoutAllSessions" class="btn btn-default" ng-click="logoutAll()">Logout All</a>
+ <a id="logoutAllSessions" class="btn btn-default" ng-click="logoutAll()">{{:: 'logout-all' | translate}}</a>
</div>
</th>
</tr>
<tr>
- <th>Client</th>
- <th>Active Sessions</th>
+ <th>{{:: 'client' | translate}}</th>
+ <th>{{:: 'active-sessions' | translate}}</th>
</tr>
</thead>
<tbody>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html
index 52296e4..03c6864 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html
@@ -1,27 +1,27 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
- <h1>Sessions</h1>
+ <h1>{{:: 'sessions' | translate}}</h1>
<ul class="nav nav-tabs">
- <li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
- <li class="active"><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
+ <li><a href="#/realms/{{realm.realm}}/sessions/realm">{{:: 'realm-sessions' | translate}}</a></li>
+ <li class="active"><a href="#/realms/{{realm.realm}}/sessions/revocation">{{:: 'revocation' | translate}}</a></li>
</ul>
<form class="form-horizontal" name="credentialForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top">
<div class="form-group">
- <label class="col-md-2 control-label" for="notBefore">Not Before</label>
+ <label class="col-md-2 control-label" for="notBefore">{{:: 'not-before' | translate}}</label>
<div class="col-md-6">
<input ng-disabled="true" class="form-control" type="text" id="notBefore" name="notBefore" data-ng-model="notBefore" autofocus>
</div>
- <kc-tooltip>Revoke any tokens issued before this date.</kc-tooltip>
+ <kc-tooltip>{{:: 'not-before.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
- <button type="submit" data-ng-click="setNotBeforeNow()" class="btn btn-default">Set To Now</button>
- <button type="submit" data-ng-click="clear()" class="btn btn-default">Clear</button>
- <button type="submit" data-ng-click="pushRevocation()" class="btn btn-primary" tooltip-trigger="mouseover mouseout" tooltip="For every client that has an admin URL, notify them of the new revocation policy." tooltip-placement="bottom">Push</button>
+ <button type="submit" data-ng-click="setNotBeforeNow()" class="btn btn-default">{{:: 'set-to-now' | translate}}</button>
+ <button type="submit" data-ng-click="clear()" class="btn btn-default">{{:: 'clear' | translate}}</button>
+ <button type="submit" data-ng-click="pushRevocation()" class="btn btn-primary" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'push.tooltip' | translate}}" tooltip-placement="bottom">{{:: 'push' | translate}}</button>
</div>
</div>
</form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
index 892570d..76f01ea 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
@@ -3,16 +3,16 @@
{{realm.realm|capitalize}}
<i id="removeRealm" class="pficon pficon-delete clickable" data-ng-show="access.manageRealm" data-ng-click="removeRealm()"></i>
</h1>
- <h1 data-ng-show="createRealm">Add Realm</h1>
+ <h1 data-ng-show="createRealm">{{:: 'add-realm' | translate}}</h1>
<ul class="nav nav-tabs">
- <li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">General</a></li>
- <li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">Login</a></li>
- <li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
- <li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
- <li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">Themes</a></li>
- <li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">Cache</a></li>
- <li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">Tokens</a></li>
- <li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">Security Defenses</a></li>
+ <li ng-class="{active: !path[2]}"><a href="#/realms/{{realm.realm}}">{{:: 'realm-tab-general' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'login-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/login-settings">{{:: 'realm-tab-login' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">{{:: 'realm-tab-keys' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">{{:: 'realm-tab-email' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
</ul>
</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js
new file mode 100644
index 0000000..e7183a0
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js
@@ -0,0 +1,2904 @@
+/*!
+ * 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 () {
+
+/**
+ * @ngdoc overview
+ * @name pascalprecht.translate
+ *
+ * @description
+ * The main module which holds everything together.
+ */
+angular.module('pascalprecht.translate', ['ng'])
+ .run(runTranslate);
+
+function runTranslate($translate) {
+
+ 'use strict';
+
+ var key = $translate.storageKey(),
+ storage = $translate.storage();
+
+ var fallbackFromIncorrectStorageValue = function () {
+ var preferred = $translate.preferredLanguage();
+ if (angular.isString(preferred)) {
+ $translate.use(preferred);
+ // $translate.use() will also remember the language.
+ // So, we don't need to call storage.put() here.
+ } else {
+ storage.put(key, $translate.use());
+ }
+ };
+
+ fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue';
+
+ if (storage) {
+ if (!storage.get(key)) {
+ fallbackFromIncorrectStorageValue();
+ } else {
+ $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue);
+ }
+ } else if (angular.isString($translate.preferredLanguage())) {
+ $translate.use($translate.preferredLanguage());
+ }
+}
+runTranslate.$inject = ['$translate'];
+
+runTranslate.displayName = 'runTranslate';
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ *
+ * Configurations for $translateSanitization
+ */
+angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider);
+
+function $translateSanitizationProvider () {
+
+ 'use strict';
+
+ var $sanitize,
+ currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0.
+ hasConfiguredStrategy = false,
+ hasShownNoStrategyConfiguredWarning = false,
+ strategies;
+
+ /**
+ * Definition of a sanitization strategy function
+ * @callback StrategyFunction
+ * @param {string|object} value - value to be sanitized (either a string or an interpolated value map)
+ * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params
+ * @return {string|object}
+ */
+
+ /**
+ * @ngdoc property
+ * @name strategies
+ * @propertyOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Following strategies are built-in:
+ * <dl>
+ * <dt>sanitize</dt>
+ * <dd>Sanitizes HTML in the translation text using $sanitize</dd>
+ * <dt>escape</dt>
+ * <dd>Escapes HTML in the translation</dd>
+ * <dt>sanitizeParameters</dt>
+ * <dd>Sanitizes HTML in the values of the interpolation parameters using $sanitize</dd>
+ * <dt>escapeParameters</dt>
+ * <dd>Escapes HTML in the values of the interpolation parameters</dd>
+ * <dt>escaped</dt>
+ * <dd>Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)</dd>
+ * </dl>
+ *
+ */
+
+ strategies = {
+ sanitize: function (value, mode) {
+ if (mode === 'text') {
+ value = htmlSanitizeValue(value);
+ }
+ return value;
+ },
+ escape: function (value, mode) {
+ if (mode === 'text') {
+ value = htmlEscapeValue(value);
+ }
+ return value;
+ },
+ sanitizeParameters: function (value, mode) {
+ if (mode === 'params') {
+ value = mapInterpolationParameters(value, htmlSanitizeValue);
+ }
+ return value;
+ },
+ escapeParameters: function (value, mode) {
+ if (mode === 'params') {
+ value = mapInterpolationParameters(value, htmlEscapeValue);
+ }
+ return value;
+ }
+ };
+ // Support legacy strategy name 'escaped' for backwards compatibility.
+ // TODO should be removed in 3.0
+ strategies.escaped = strategies.escapeParameters;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Adds a sanitization strategy to the list of known strategies.
+ *
+ * @param {string} strategyName - unique key for a strategy
+ * @param {StrategyFunction} strategyFunction - strategy function
+ * @returns {object} this
+ */
+ this.addStrategy = function (strategyName, strategyFunction) {
+ strategies[strategyName] = strategyFunction;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Removes a sanitization strategy from the list of known strategies.
+ *
+ * @param {string} strategyName - unique key for a strategy
+ * @returns {object} this
+ */
+ this.removeStrategy = function (strategyName) {
+ delete strategies[strategyName];
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy
+ * @methodOf pascalprecht.translate.$translateSanitizationProvider
+ *
+ * @description
+ * Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
+ *
+ * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
+ * @returns {object} this
+ */
+ this.useStrategy = function (strategy) {
+ hasConfiguredStrategy = true;
+ currentStrategy = strategy;
+ return this;
+ };
+
+ /**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateSanitization
+ * @requires $injector
+ * @requires $log
+ *
+ * @description
+ * Sanitizes interpolation parameters and translated texts.
+ *
+ */
+ this.$get = ['$injector', '$log', function ($injector, $log) {
+
+ var applyStrategies = function (value, mode, selectedStrategies) {
+ angular.forEach(selectedStrategies, function (selectedStrategy) {
+ if (angular.isFunction(selectedStrategy)) {
+ value = selectedStrategy(value, mode);
+ } else if (angular.isFunction(strategies[selectedStrategy])) {
+ value = strategies[selectedStrategy](value, mode);
+ } else {
+ throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\'');
+ }
+ });
+ return value;
+ };
+
+ // TODO: should be removed in 3.0
+ var showNoStrategyConfiguredWarning = function () {
+ if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) {
+ $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.');
+ hasShownNoStrategyConfiguredWarning = true;
+ }
+ };
+
+ if ($injector.has('$sanitize')) {
+ $sanitize = $injector.get('$sanitize');
+ }
+
+ return {
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitization#useStrategy
+ * @methodOf pascalprecht.translate.$translateSanitization
+ *
+ * @description
+ * Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
+ *
+ * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
+ */
+ useStrategy: (function (self) {
+ return function (strategy) {
+ self.useStrategy(strategy);
+ };
+ })(this),
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateSanitization#sanitize
+ * @methodOf pascalprecht.translate.$translateSanitization
+ *
+ * @description
+ * Sanitizes a value.
+ *
+ * @param {string|object} value The value which should be sanitized.
+ * @param {string} mode The current sanitization mode, either 'params' or 'text'.
+ * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy.
+ * @returns {string|object} sanitized value
+ */
+ sanitize: function (value, mode, strategy) {
+ if (!currentStrategy) {
+ showNoStrategyConfiguredWarning();
+ }
+
+ if (arguments.length < 3) {
+ strategy = currentStrategy;
+ }
+
+ if (!strategy) {
+ return value;
+ }
+
+ var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy];
+ return applyStrategies(value, mode, selectedStrategies);
+ }
+ };
+ }];
+
+ var htmlEscapeValue = function (value) {
+ var element = angular.element('<div></div>');
+ element.text(value); // not chainable, see #1044
+ return element.html();
+ };
+
+ var htmlSanitizeValue = function (value) {
+ if (!$sanitize) {
+ throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.');
+ }
+ return $sanitize(value);
+ };
+
+ var mapInterpolationParameters = function (value, iteratee) {
+ if (angular.isObject(value)) {
+ var result = angular.isArray(value) ? [] : {};
+
+ angular.forEach(value, function (propertyValue, propertyKey) {
+ result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee);
+ });
+
+ return result;
+ } else if (angular.isNumber(value)) {
+ return value;
+ } else {
+ return iteratee(value);
+ }
+ };
+}
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateProvider
+ * @description
+ *
+ * $translateProvider allows developers to register translation-tables, asynchronous loaders
+ * and similar to configure translation behavior directly inside of a module.
+ *
+ */
+angular.module('pascalprecht.translate')
+.constant('pascalprechtTranslateOverrider', {})
+.provider('$translate', $translate);
+
+function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) {
+
+ 'use strict';
+
+ var $translationTable = {},
+ $preferredLanguage,
+ $availableLanguageKeys = [],
+ $languageKeyAliases,
+ $fallbackLanguage,
+ $fallbackWasString,
+ $uses,
+ $nextLang,
+ $storageFactory,
+ $storageKey = $STORAGE_KEY,
+ $storagePrefix,
+ $missingTranslationHandlerFactory,
+ $interpolationFactory,
+ $interpolatorFactories = [],
+ $loaderFactory,
+ $cloakClassName = 'translate-cloak',
+ $loaderOptions,
+ $notFoundIndicatorLeft,
+ $notFoundIndicatorRight,
+ $postCompilingEnabled = false,
+ $forceAsyncReloadEnabled = false,
+ NESTED_OBJECT_DELIMITER = '.',
+ loaderCache,
+ directivePriority = 0,
+ statefulFilter = true,
+ uniformLanguageTagResolver = 'default',
+ languageTagResolver = {
+ 'default': function (tag) {
+ return (tag || '').split('-').join('_');
+ },
+ java: function (tag) {
+ var temp = (tag || '').split('-').join('_');
+ var parts = temp.split('_');
+ return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp;
+ },
+ bcp47: function (tag) {
+ var temp = (tag || '').split('_').join('-');
+ var parts = temp.split('-');
+ return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp;
+ }
+ };
+
+ var version = '2.7.2';
+
+ // tries to determine the browsers language
+ var getFirstBrowserLanguage = function () {
+
+ // internal purpose only
+ if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) {
+ return pascalprechtTranslateOverrider.getLocale();
+ }
+
+ var nav = $windowProvider.$get().navigator,
+ browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'],
+ i,
+ language;
+
+ // support for HTML 5.1 "navigator.languages"
+ if (angular.isArray(nav.languages)) {
+ for (i = 0; i < nav.languages.length; i++) {
+ language = nav.languages[i];
+ if (language && language.length) {
+ return language;
+ }
+ }
+ }
+
+ // support for other well known properties in browsers
+ for (i = 0; i < browserLanguagePropertyKeys.length; i++) {
+ language = nav[browserLanguagePropertyKeys[i]];
+ if (language && language.length) {
+ return language;
+ }
+ }
+
+ return null;
+ };
+ getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage';
+
+ // tries to determine the browsers locale
+ var getLocale = function () {
+ var locale = getFirstBrowserLanguage() || '';
+ if (languageTagResolver[uniformLanguageTagResolver]) {
+ locale = languageTagResolver[uniformLanguageTagResolver](locale);
+ }
+ return locale;
+ };
+ getLocale.displayName = 'angular-translate/service: getLocale';
+
+ /**
+ * @name indexOf
+ * @private
+ *
+ * @description
+ * indexOf polyfill. Kinda sorta.
+ *
+ * @param {array} array Array to search in.
+ * @param {string} searchElement Element to search for.
+ *
+ * @returns {int} Index of search element.
+ */
+ var indexOf = function(array, searchElement) {
+ for (var i = 0, len = array.length; i < len; i++) {
+ if (array[i] === searchElement) {
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ /**
+ * @name trim
+ * @private
+ *
+ * @description
+ * trim polyfill
+ *
+ * @returns {string} The string stripped of whitespace from both ends
+ */
+ var trim = function() {
+ return this.toString().replace(/^\s+|\s+$/g, '');
+ };
+
+ var negotiateLocale = function (preferred) {
+
+ var avail = [],
+ locale = angular.lowercase(preferred),
+ i = 0,
+ n = $availableLanguageKeys.length;
+
+ for (; i < n; i++) {
+ avail.push(angular.lowercase($availableLanguageKeys[i]));
+ }
+
+ if (indexOf(avail, locale) > -1) {
+ return preferred;
+ }
+
+ if ($languageKeyAliases) {
+ var alias;
+ for (var langKeyAlias in $languageKeyAliases) {
+ var hasWildcardKey = false;
+ var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) &&
+ angular.lowercase(langKeyAlias) === angular.lowercase(preferred);
+
+ if (langKeyAlias.slice(-1) === '*') {
+ hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length-1);
+ }
+ if (hasExactKey || hasWildcardKey) {
+ alias = $languageKeyAliases[langKeyAlias];
+ if (indexOf(avail, angular.lowercase(alias)) > -1) {
+ return alias;
+ }
+ }
+ }
+ }
+
+ if (preferred) {
+ var parts = preferred.split('_');
+
+ if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) {
+ return parts[0];
+ }
+ }
+
+ // If everything fails, just return the preferred, unchanged.
+ return preferred;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translations
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a new translation table for specific language key.
+ *
+ * To register a translation table for specific language, pass a defined language
+ * key as first parameter.
+ *
+ * <pre>
+ * // register translation table for language: 'de_DE'
+ * $translateProvider.translations('de_DE', {
+ * 'GREETING': 'Hallo Welt!'
+ * });
+ *
+ * // register another one
+ * $translateProvider.translations('en_US', {
+ * 'GREETING': 'Hello world!'
+ * });
+ * </pre>
+ *
+ * When registering multiple translation tables for for the same language key,
+ * the actual translation table gets extended. This allows you to define module
+ * specific translation which only get added, once a specific module is loaded in
+ * your app.
+ *
+ * Invoking this method with no arguments returns the translation table which was
+ * registered with no language key. Invoking it with a language key returns the
+ * related translation table.
+ *
+ * @param {string} key A language key.
+ * @param {object} translationTable A plain old JavaScript object that represents a translation table.
+ *
+ */
+ var translations = function (langKey, translationTable) {
+
+ if (!langKey && !translationTable) {
+ return $translationTable;
+ }
+
+ if (langKey && !translationTable) {
+ if (angular.isString(langKey)) {
+ return $translationTable[langKey];
+ }
+ } else {
+ if (!angular.isObject($translationTable[langKey])) {
+ $translationTable[langKey] = {};
+ }
+ angular.extend($translationTable[langKey], flatObject(translationTable));
+ }
+ return this;
+ };
+
+ this.translations = translations;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#cloakClassName
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ *
+ * Let's you change the class name for `translate-cloak` directive.
+ * Default class name is `translate-cloak`.
+ *
+ * @param {string} name translate-cloak class name
+ */
+ this.cloakClassName = function (name) {
+ if (!name) {
+ return $cloakClassName;
+ }
+ $cloakClassName = name;
+ return this;
+ };
+
+ /**
+ * @name flatObject
+ * @private
+ *
+ * @description
+ * Flats an object. This function is used to flatten given translation data with
+ * namespaces, so they are later accessible via dot notation.
+ */
+ var flatObject = function (data, path, result, prevKey) {
+ var key, keyWithPath, keyWithShortPath, val;
+
+ if (!path) {
+ path = [];
+ }
+ if (!result) {
+ result = {};
+ }
+ for (key in data) {
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
+ continue;
+ }
+ val = data[key];
+ if (angular.isObject(val)) {
+ flatObject(val, path.concat(key), result, key);
+ } else {
+ keyWithPath = path.length ? ('' + path.join(NESTED_OBJECT_DELIMITER) + NESTED_OBJECT_DELIMITER + key) : key;
+ if(path.length && key === prevKey){
+ // Create shortcut path (foo.bar == foo.bar.bar)
+ keyWithShortPath = '' + path.join(NESTED_OBJECT_DELIMITER);
+ // Link it to original path
+ result[keyWithShortPath] = '@:' + keyWithPath;
+ }
+ result[keyWithPath] = val;
+ }
+ }
+ return result;
+ };
+ flatObject.displayName = 'flatObject';
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#addInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Adds interpolation services to angular-translate, so it can manage them.
+ *
+ * @param {object} factory Interpolation service factory
+ */
+ this.addInterpolation = function (factory) {
+ $interpolatorFactories.push(factory);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use interpolation functionality of messageformat.js.
+ * This is useful when having high level pluralization and gender selection.
+ */
+ this.useMessageFormatInterpolation = function () {
+ return this.useInterpolation('$translateMessageFormatInterpolation');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useInterpolation
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate which interpolation style to use as default, application-wide.
+ * Simply pass a factory/service name. The interpolation service has to implement
+ * the correct interface.
+ *
+ * @param {string} factory Interpolation service name.
+ */
+ this.useInterpolation = function (factory) {
+ $interpolationFactory = factory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Simply sets a sanitation strategy type.
+ *
+ * @param {string} value Strategy type.
+ */
+ this.useSanitizeValueStrategy = function (value) {
+ $translateSanitizationProvider.useStrategy(value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#preferredLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which of the registered translation tables to use for translation
+ * at initial startup by passing a language key. Similar to `$translateProvider#use`
+ * only that it says which language to **prefer**.
+ *
+ * @param {string} langKey A language key.
+ *
+ */
+ this.preferredLanguage = function(langKey) {
+ setupPreferredLanguage(langKey);
+ return this;
+
+ };
+ var setupPreferredLanguage = function (langKey) {
+ if (langKey) {
+ $preferredLanguage = langKey;
+ }
+ return $preferredLanguage;
+ };
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found. E.g. when
+ * setting the indicator as 'X' and one tries to translate a translation id
+ * called `NOT_FOUND`, this will result in `X NOT_FOUND X`.
+ *
+ * Internally this methods sets a left indicator and a right indicator using
+ * `$translateProvider.translationNotFoundIndicatorLeft()` and
+ * `$translateProvider.translationNotFoundIndicatorRight()`.
+ *
+ * **Note**: These methods automatically add a whitespace between the indicators
+ * and the translation id.
+ *
+ * @param {string} indicator An indicator, could be any string.
+ */
+ this.translationNotFoundIndicator = function (indicator) {
+ this.translationNotFoundIndicatorLeft(indicator);
+ this.translationNotFoundIndicatorRight(indicator);
+ return this;
+ };
+
+ /**
+ * ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found left to the
+ * translation id.
+ *
+ * @param {string} indicator An indicator.
+ */
+ this.translationNotFoundIndicatorLeft = function (indicator) {
+ if (!indicator) {
+ return $notFoundIndicatorLeft;
+ }
+ $notFoundIndicatorLeft = indicator;
+ return this;
+ };
+
+ /**
+ * ngdoc function
+ * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets an indicator which is used when a translation isn't found right to the
+ * translation id.
+ *
+ * @param {string} indicator An indicator.
+ */
+ this.translationNotFoundIndicatorRight = function (indicator) {
+ if (!indicator) {
+ return $notFoundIndicatorRight;
+ }
+ $notFoundIndicatorRight = indicator;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#fallbackLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which of the registered translation tables to use when missing translations
+ * at initial startup by passing a language key. Similar to `$translateProvider#use`
+ * only that it says which language to **fallback**.
+ *
+ * @param {string||array} langKey A language key.
+ *
+ */
+ this.fallbackLanguage = function (langKey) {
+ fallbackStack(langKey);
+ return this;
+ };
+
+ var fallbackStack = function (langKey) {
+ if (langKey) {
+ if (angular.isString(langKey)) {
+ $fallbackWasString = true;
+ $fallbackLanguage = [ langKey ];
+ } else if (angular.isArray(langKey)) {
+ $fallbackWasString = false;
+ $fallbackLanguage = langKey;
+ }
+ if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) {
+ $fallbackLanguage.push($preferredLanguage);
+ }
+
+ return this;
+ } else {
+ if ($fallbackWasString) {
+ return $fallbackLanguage[0];
+ } else {
+ return $fallbackLanguage;
+ }
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#use
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Set which translation table to use for translation by given language key. When
+ * trying to 'use' a language which isn't provided, it'll throw an error.
+ *
+ * You actually don't have to use this method since `$translateProvider#preferredLanguage`
+ * does the job too.
+ *
+ * @param {string} langKey A language key.
+ */
+ this.use = function (langKey) {
+ if (langKey) {
+ if (!$translationTable[langKey] && (!$loaderFactory)) {
+ // only throw an error, when not loading translation data asynchronously
+ throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\'');
+ }
+ $uses = langKey;
+ return this;
+ }
+ return $uses;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#storageKey
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells the module which key must represent the choosed language by a user in the storage.
+ *
+ * @param {string} key A key for the storage.
+ */
+ var storageKey = function(key) {
+ if (!key) {
+ if ($storagePrefix) {
+ return $storagePrefix + $storageKey;
+ }
+ return $storageKey;
+ }
+ $storageKey = key;
+ return this;
+ };
+
+ this.storageKey = storageKey;
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useUrlLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateUrlLoader` extension service as loader.
+ *
+ * @param {string} url Url
+ * @param {Object=} options Optional configuration object
+ */
+ this.useUrlLoader = function (url, options) {
+ return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options));
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader.
+ *
+ * @param {Object=} options Optional configuration object
+ */
+ this.useStaticFilesLoader = function (options) {
+ return this.useLoader('$translateStaticFilesLoader', options);
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLoader
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use any other service as loader.
+ *
+ * @param {string} loaderFactory Factory name to use
+ * @param {Object=} options Optional configuration object
+ */
+ this.useLoader = function (loaderFactory, options) {
+ $loaderFactory = loaderFactory;
+ $loaderOptions = options || {};
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLocalStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateLocalStorage` service as storage layer.
+ *
+ */
+ this.useLocalStorage = function () {
+ return this.useStorage('$translateLocalStorage');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useCookieStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use `$translateCookieStorage` service as storage layer.
+ */
+ this.useCookieStorage = function () {
+ return this.useStorage('$translateCookieStorage');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useStorage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use custom service as storage layer.
+ */
+ this.useStorage = function (storageFactory) {
+ $storageFactory = storageFactory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#storagePrefix
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets prefix for storage key.
+ *
+ * @param {string} prefix Storage key prefix
+ */
+ this.storagePrefix = function (prefix) {
+ if (!prefix) {
+ return prefix;
+ }
+ $storagePrefix = prefix;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to use built-in log handler when trying to translate
+ * a translation Id which doesn't exist.
+ *
+ * This is actually a shortcut method for `useMissingTranslationHandler()`.
+ *
+ */
+ this.useMissingTranslationHandlerLog = function () {
+ return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog');
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Expects a factory name which later gets instantiated with `$injector`.
+ * This method can be used to tell angular-translate to use a custom
+ * missingTranslationHandler. Just build a factory which returns a function
+ * and expects a translation id as argument.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.useMissingTranslationHandler('customHandler');
+ * });
+ *
+ * app.factory('customHandler', function (dep1, dep2) {
+ * return function (translationId) {
+ * // something with translationId and dep1 and dep2
+ * };
+ * });
+ * </pre>
+ *
+ * @param {string} factory Factory name
+ */
+ this.useMissingTranslationHandler = function (factory) {
+ $missingTranslationHandlerFactory = factory;
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#usePostCompiling
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * If post compiling is enabled, all translated values will be processed
+ * again with AngularJS' $compile.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.usePostCompiling(true);
+ * });
+ * </pre>
+ *
+ * @param {string} factory Factory name
+ */
+ this.usePostCompiling = function (value) {
+ $postCompilingEnabled = !(!value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#forceAsyncReload
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * If force async reload is enabled, async loader will always be called
+ * even if $translationTable already contains the language key, adding
+ * possible new entries to the $translationTable.
+ *
+ * Example:
+ * <pre>
+ * app.config(function ($translateProvider) {
+ * $translateProvider.forceAsyncReload(true);
+ * });
+ * </pre>
+ *
+ * @param {boolean} value - valid values are true or false
+ */
+ this.forceAsyncReload = function (value) {
+ $forceAsyncReloadEnabled = !(!value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#uniformLanguageTag
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate which language tag should be used as a result when determining
+ * the current browser language.
+ *
+ * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}.
+ *
+ * <pre>
+ * $translateProvider
+ * .uniformLanguageTag('bcp47')
+ * .determinePreferredLanguage()
+ * </pre>
+ *
+ * The resolver currently supports:
+ * * default
+ * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US)
+ * en-US => en_US
+ * en_US => en_US
+ * en-us => en_us
+ * * java
+ * like default, but the second part will be always in uppercase
+ * en-US => en_US
+ * en_US => en_US
+ * en-us => en_US
+ * * BCP 47 (RFC 4646 & 4647)
+ * en-US => en-US
+ * en_US => en-US
+ * en-us => en-US
+ *
+ * See also:
+ * * http://en.wikipedia.org/wiki/IETF_language_tag
+ * * http://www.w3.org/International/core/langtags/
+ * * http://tools.ietf.org/html/bcp47
+ *
+ * @param {string|object} options - options (or standard)
+ * @param {string} options.standard - valid values are 'default', 'bcp47', 'java'
+ */
+ this.uniformLanguageTag = function (options) {
+
+ if (!options) {
+ options = {};
+ } else if (angular.isString(options)) {
+ options = {
+ standard: options
+ };
+ }
+
+ uniformLanguageTagResolver = options.standard;
+
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Tells angular-translate to try to determine on its own which language key
+ * to set as preferred language. When `fn` is given, angular-translate uses it
+ * to determine a language key, otherwise it uses the built-in `getLocale()`
+ * method.
+ *
+ * The `getLocale()` returns a language key in the format `[lang]_[country]` or
+ * `[lang]` depending on what the browser provides.
+ *
+ * Use this method at your own risk, since not all browsers return a valid
+ * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}).
+ *
+ * @param {Function=} fn Function to determine a browser's locale
+ */
+ this.determinePreferredLanguage = function (fn) {
+
+ var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale();
+
+ if (!$availableLanguageKeys.length) {
+ $preferredLanguage = locale;
+ } else {
+ $preferredLanguage = negotiateLocale(locale);
+ }
+
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a set of language keys the app will work with. Use this method in
+ * combination with
+ * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}.
+ * When available languages keys are registered, angular-translate
+ * tries to find the best fitting language key depending on the browsers locale,
+ * considering your language key convention.
+ *
+ * @param {object} languageKeys Array of language keys the your app will use
+ * @param {object=} aliases Alias map.
+ */
+ this.registerAvailableLanguageKeys = function (languageKeys, aliases) {
+ if (languageKeys) {
+ $availableLanguageKeys = languageKeys;
+ if (aliases) {
+ $languageKeyAliases = aliases;
+ }
+ return this;
+ }
+ return $availableLanguageKeys;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#useLoaderCache
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Registers a cache for internal $http based loaders.
+ * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}.
+ * When false the cache will be disabled (default). When true or undefined
+ * the cache will be a default (see $cacheFactory). When an object it will
+ * be treat as a cache object itself: the usage is $http({cache: cache})
+ *
+ * @param {object} cache boolean, string or cache-object
+ */
+ this.useLoaderCache = function (cache) {
+ if (cache === false) {
+ // disable cache
+ loaderCache = undefined;
+ } else if (cache === true) {
+ // enable cache using AJS defaults
+ loaderCache = true;
+ } else if (typeof(cache) === 'undefined') {
+ // enable cache using default
+ loaderCache = '$translationCache';
+ } else if (cache) {
+ // enable cache using given one (see $cacheFactory)
+ loaderCache = cache;
+ }
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#directivePriority
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Sets the default priority of the translate directive. The standard value is `0`.
+ * Calling this function without an argument will return the current value.
+ *
+ * @param {number} priority for the translate-directive
+ */
+ this.directivePriority = function (priority) {
+ if (priority === undefined) {
+ // getter
+ return directivePriority;
+ } else {
+ // setter with chaining
+ directivePriority = priority;
+ return this;
+ }
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateProvider#statefulFilter
+ * @methodOf pascalprecht.translate.$translateProvider
+ *
+ * @description
+ * Since AngularJS 1.3, filters which are not stateless (depending at the scope)
+ * have to explicit define this behavior.
+ * Sets whether the translate filter should be stateful or stateless. The standard value is `true`
+ * meaning being stateful.
+ * Calling this function without an argument will return the current value.
+ *
+ * @param {boolean} state - defines the state of the filter
+ */
+ this.statefulFilter = function (state) {
+ if (state === undefined) {
+ // getter
+ return statefulFilter;
+ } else {
+ // setter with chaining
+ statefulFilter = state;
+ return this;
+ }
+ };
+
+ /**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translate
+ * @requires $interpolate
+ * @requires $log
+ * @requires $rootScope
+ * @requires $q
+ *
+ * @description
+ * The `$translate` service is the actual core of angular-translate. It expects a translation id
+ * and optional interpolate parameters to translate contents.
+ *
+ * <pre>
+ * $translate('HEADLINE_TEXT').then(function (translation) {
+ * $scope.translatedText = translation;
+ * });
+ * </pre>
+ *
+ * @param {string|array} translationId A token which represents a translation id
+ * This can be optionally an array of translation ids which
+ * results that the function returns an object where each key
+ * is the translation id and the value the translation.
+ * @param {object=} interpolateParams An object hash for dynamic values
+ * @param {string} interpolationId The id of the interpolation to use
+ * @returns {object} promise
+ */
+ this.$get = [
+ '$log',
+ '$injector',
+ '$rootScope',
+ '$q',
+ function ($log, $injector, $rootScope, $q) {
+
+ var Storage,
+ defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'),
+ pendingLoader = false,
+ interpolatorHashMap = {},
+ langPromises = {},
+ fallbackIndex,
+ startFallbackIteration;
+
+ var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText) {
+
+ // Duck detection: If the first argument is an array, a bunch of translations was requested.
+ // The result is an object.
+ if (angular.isArray(translationId)) {
+ // Inspired by Q.allSettled by Kris Kowal
+ // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563
+ // This transforms all promises regardless resolved or rejected
+ var translateAll = function (translationIds) {
+ var results = {}; // storing the actual results
+ var promises = []; // promises to wait for
+ // Wraps the promise a) being always resolved and b) storing the link id->value
+ var translate = function (translationId) {
+ var deferred = $q.defer();
+ var regardless = function (value) {
+ results[translationId] = value;
+ deferred.resolve([translationId, value]);
+ };
+ // we don't care whether the promise was resolved or rejected; just store the values
+ $translate(translationId, interpolateParams, interpolationId, defaultTranslationText).then(regardless, regardless);
+ return deferred.promise;
+ };
+ for (var i = 0, c = translationIds.length; i < c; i++) {
+ promises.push(translate(translationIds[i]));
+ }
+ // wait for all (including storing to results)
+ return $q.all(promises).then(function () {
+ // return the results
+ return results;
+ });
+ };
+ return translateAll(translationId);
+ }
+
+ var deferred = $q.defer();
+
+ // trim off any whitespace
+ if (translationId) {
+ translationId = trim.apply(translationId);
+ }
+
+ var promiseToWaitFor = (function () {
+ var promise = $preferredLanguage ?
+ langPromises[$preferredLanguage] :
+ langPromises[$uses];
+
+ fallbackIndex = 0;
+
+ if ($storageFactory && !promise) {
+ // looks like there's no pending promise for $preferredLanguage or
+ // $uses. Maybe there's one pending for a language that comes from
+ // storage.
+ var langKey = Storage.get($storageKey);
+ promise = langPromises[langKey];
+
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ var index = indexOf($fallbackLanguage, langKey);
+ // maybe the language from storage is also defined as fallback language
+ // we increase the fallback language index to not search in that language
+ // as fallback, since it's probably the first used language
+ // in that case the index starts after the first element
+ fallbackIndex = (index === 0) ? 1 : 0;
+
+ // but we can make sure to ALWAYS fallback to preferred language at least
+ if (indexOf($fallbackLanguage, $preferredLanguage) < 0) {
+ $fallbackLanguage.push($preferredLanguage);
+ }
+ }
+ }
+ return promise;
+ }());
+
+ if (!promiseToWaitFor) {
+ // no promise to wait for? okay. Then there's no loader registered
+ // nor is a one pending for language that comes from storage.
+ // We can just translate.
+ determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject);
+ } else {
+ var promiseResolved = function () {
+ determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject);
+ };
+ promiseResolved.displayName = 'promiseResolved';
+
+ promiseToWaitFor['finally'](promiseResolved, deferred.reject);
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name applyNotFoundIndicators
+ * @private
+ *
+ * @description
+ * Applies not fount indicators to given translation id, if needed.
+ * This function gets only executed, if a translation id doesn't exist,
+ * which is why a translation id is expected as argument.
+ *
+ * @param {string} translationId Translation id.
+ * @returns {string} Same as given translation id but applied with not found
+ * indicators.
+ */
+ var applyNotFoundIndicators = function (translationId) {
+ // applying notFoundIndicators
+ if ($notFoundIndicatorLeft) {
+ translationId = [$notFoundIndicatorLeft, translationId].join(' ');
+ }
+ if ($notFoundIndicatorRight) {
+ translationId = [translationId, $notFoundIndicatorRight].join(' ');
+ }
+ return translationId;
+ };
+
+ /**
+ * @name useLanguage
+ * @private
+ *
+ * @description
+ * Makes actual use of a language by setting a given language key as used
+ * language and informs registered interpolators to also use the given
+ * key as locale.
+ *
+ * @param {key} Locale key.
+ */
+ var useLanguage = function (key) {
+ $uses = key;
+ $rootScope.$emit('$translateChangeSuccess', {language: key});
+
+ if ($storageFactory) {
+ Storage.put($translate.storageKey(), $uses);
+ }
+ // inform default interpolator
+ defaultInterpolator.setLocale($uses);
+
+ var eachInterpolator = function (interpolator, id) {
+ interpolatorHashMap[id].setLocale($uses);
+ };
+ eachInterpolator.displayName = 'eachInterpolatorLocaleSetter';
+
+ // inform all others too!
+ angular.forEach(interpolatorHashMap, eachInterpolator);
+ $rootScope.$emit('$translateChangeEnd', {language: key});
+ };
+
+ /**
+ * @name loadAsync
+ * @private
+ *
+ * @description
+ * Kicks of registered async loader using `$injector` and applies existing
+ * loader options. When resolved, it updates translation tables accordingly
+ * or rejects with given language key.
+ *
+ * @param {string} key Language key.
+ * @return {Promise} A promise.
+ */
+ var loadAsync = function (key) {
+ if (!key) {
+ throw 'No language key specified for loading.';
+ }
+
+ var deferred = $q.defer();
+
+ $rootScope.$emit('$translateLoadingStart', {language: key});
+ pendingLoader = true;
+
+ var cache = loaderCache;
+ if (typeof(cache) === 'string') {
+ // getting on-demand instance of loader
+ cache = $injector.get(cache);
+ }
+
+ var loaderOptions = angular.extend({}, $loaderOptions, {
+ key: key,
+ $http: angular.extend({}, {
+ cache: cache
+ }, $loaderOptions.$http)
+ });
+
+ var onLoaderSuccess = function (data) {
+ var translationTable = {};
+ $rootScope.$emit('$translateLoadingSuccess', {language: key});
+
+ if (angular.isArray(data)) {
+ angular.forEach(data, function (table) {
+ angular.extend(translationTable, flatObject(table));
+ });
+ } else {
+ angular.extend(translationTable, flatObject(data));
+ }
+ pendingLoader = false;
+ deferred.resolve({
+ key: key,
+ table: translationTable
+ });
+ $rootScope.$emit('$translateLoadingEnd', {language: key});
+ };
+ onLoaderSuccess.displayName = 'onLoaderSuccess';
+
+ var onLoaderError = function (key) {
+ $rootScope.$emit('$translateLoadingError', {language: key});
+ deferred.reject(key);
+ $rootScope.$emit('$translateLoadingEnd', {language: key});
+ };
+ onLoaderError.displayName = 'onLoaderError';
+
+ $injector.get($loaderFactory)(loaderOptions)
+ .then(onLoaderSuccess, onLoaderError);
+
+ return deferred.promise;
+ };
+
+ if ($storageFactory) {
+ Storage = $injector.get($storageFactory);
+
+ if (!Storage.get || !Storage.put) {
+ throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!');
+ }
+ }
+
+ // if we have additional interpolations that were added via
+ // $translateProvider.addInterpolation(), we have to map'em
+ if ($interpolatorFactories.length) {
+ var eachInterpolationFactory = function (interpolatorFactory) {
+ var interpolator = $injector.get(interpolatorFactory);
+ // setting initial locale for each interpolation service
+ interpolator.setLocale($preferredLanguage || $uses);
+ // make'em recognizable through id
+ interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator;
+ };
+ eachInterpolationFactory.displayName = 'interpolationFactoryAdder';
+
+ angular.forEach($interpolatorFactories, eachInterpolationFactory);
+ }
+
+ /**
+ * @name getTranslationTable
+ * @private
+ *
+ * @description
+ * Returns a promise that resolves to the translation table
+ * or is rejected if an error occurred.
+ *
+ * @param langKey
+ * @returns {Q.promise}
+ */
+ var getTranslationTable = function (langKey) {
+ var deferred = $q.defer();
+ if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) {
+ deferred.resolve($translationTable[langKey]);
+ } else if (langPromises[langKey]) {
+ var onResolve = function (data) {
+ translations(data.key, data.table);
+ deferred.resolve(data.table);
+ };
+ onResolve.displayName = 'translationTableResolver';
+ langPromises[langKey].then(onResolve, deferred.reject);
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name getFallbackTranslation
+ * @private
+ *
+ * @description
+ * Returns a promise that will resolve to the translation
+ * or be rejected if no translation was found for the language.
+ * This function is currently only used for fallback language translation.
+ *
+ * @param langKey The language to translate to.
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise}
+ */
+ var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) {
+ var deferred = $q.defer();
+
+ var onResolve = function (translationTable) {
+ if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) {
+ Interpolator.setLocale(langKey);
+ var translation = translationTable[translationId];
+ if (translation.substr(0, 2) === '@:') {
+ getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator)
+ .then(deferred.resolve, deferred.reject);
+ } else {
+ deferred.resolve(Interpolator.interpolate(translationTable[translationId], interpolateParams));
+ }
+ Interpolator.setLocale($uses);
+ } else {
+ deferred.reject();
+ }
+ };
+ onResolve.displayName = 'fallbackTranslationResolver';
+
+ getTranslationTable(langKey).then(onResolve, deferred.reject);
+
+ return deferred.promise;
+ };
+
+ /**
+ * @name getFallbackTranslationInstant
+ * @private
+ *
+ * @description
+ * Returns a translation
+ * This function is currently only used for fallback language translation.
+ *
+ * @param langKey The language to translate to.
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {string} translation
+ */
+ var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) {
+ var result, translationTable = $translationTable[langKey];
+
+ if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) {
+ Interpolator.setLocale(langKey);
+ result = Interpolator.interpolate(translationTable[translationId], interpolateParams);
+ if (result.substr(0, 2) === '@:') {
+ return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator);
+ }
+ Interpolator.setLocale($uses);
+ }
+
+ return result;
+ };
+
+
+ /**
+ * @name translateByHandler
+ * @private
+ *
+ * Translate by missing translation handler.
+ *
+ * @param translationId
+ * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is
+ * absent
+ */
+ var translateByHandler = function (translationId, interpolateParams) {
+ // If we have a handler factory - we might also call it here to determine if it provides
+ // a default text for a translationid that can't be found anywhere in our tables
+ if ($missingTranslationHandlerFactory) {
+ var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams);
+ if (resultString !== undefined) {
+ return resultString;
+ } else {
+ return translationId;
+ }
+ } else {
+ return translationId;
+ }
+ };
+
+ /**
+ * @name resolveForFallbackLanguage
+ * @private
+ *
+ * Recursive helper function for fallbackTranslation that will sequentially look
+ * for a translation in the fallbackLanguages starting with fallbackLanguageIndex.
+ *
+ * @param fallbackLanguageIndex
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise} Promise that will resolve to the translation.
+ */
+ var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) {
+ var deferred = $q.defer();
+
+ if (fallbackLanguageIndex < $fallbackLanguage.length) {
+ var langKey = $fallbackLanguage[fallbackLanguageIndex];
+ getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then(
+ deferred.resolve,
+ function () {
+ // Look in the next fallback language for a translation.
+ // It delays the resolving by passing another promise to resolve.
+ resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve);
+ }
+ );
+ } else {
+ // No translation found in any fallback language
+ // if a default translation text is set in the directive, then return this as a result
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ // if no default translation is set and an error handler is defined, send it to the handler
+ // and then return the result
+ deferred.resolve(translateByHandler(translationId, interpolateParams));
+ }
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @name resolveForFallbackLanguageInstant
+ * @private
+ *
+ * Recursive helper function for fallbackTranslation that will sequentially look
+ * for a translation in the fallbackLanguages starting with fallbackLanguageIndex.
+ *
+ * @param fallbackLanguageIndex
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {string} translation
+ */
+ var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) {
+ var result;
+
+ if (fallbackLanguageIndex < $fallbackLanguage.length) {
+ var langKey = $fallbackLanguage[fallbackLanguageIndex];
+ result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator);
+ if (!result) {
+ result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator);
+ }
+ }
+ return result;
+ };
+
+ /**
+ * Translates with the usage of the fallback languages.
+ *
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {Q.promise} Promise, that resolves to the translation.
+ */
+ var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) {
+ // Start with the fallbackLanguage with index 0
+ return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText);
+ };
+
+ /**
+ * Translates with the usage of the fallback languages.
+ *
+ * @param translationId
+ * @param interpolateParams
+ * @param Interpolator
+ * @returns {String} translation
+ */
+ var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) {
+ // Start with the fallbackLanguage with index 0
+ return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator);
+ };
+
+ var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText) {
+
+ var deferred = $q.defer();
+
+ var table = $uses ? $translationTable[$uses] : $translationTable,
+ Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator;
+
+ // if the translation id exists, we can just interpolate it
+ if (table && Object.prototype.hasOwnProperty.call(table, translationId)) {
+ var translation = table[translationId];
+
+ // If using link, rerun $translate with linked translationId and return it
+ if (translation.substr(0, 2) === '@:') {
+
+ $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText)
+ .then(deferred.resolve, deferred.reject);
+ } else {
+ deferred.resolve(Interpolator.interpolate(translation, interpolateParams));
+ }
+ } else {
+ var missingTranslationHandlerTranslation;
+ // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams);
+ }
+
+ // since we couldn't translate the inital requested translation id,
+ // we try it now with one or more fallback languages, if fallback language(s) is
+ // configured.
+ if ($uses && $fallbackLanguage && $fallbackLanguage.length) {
+ fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText)
+ .then(function (translation) {
+ deferred.resolve(translation);
+ }, function (_translationId) {
+ deferred.reject(applyNotFoundIndicators(_translationId));
+ });
+ } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) {
+ // looks like the requested translation id doesn't exists.
+ // Now, if there is a registered handler for missing translations and no
+ // asyncLoader is pending, we execute the handler
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ deferred.resolve(missingTranslationHandlerTranslation);
+ }
+ } else {
+ if (defaultTranslationText) {
+ deferred.resolve(defaultTranslationText);
+ } else {
+ deferred.reject(applyNotFoundIndicators(translationId));
+ }
+ }
+ }
+ return deferred.promise;
+ };
+
+ var determineTranslationInstant = function (translationId, interpolateParams, interpolationId) {
+
+ var result, table = $uses ? $translationTable[$uses] : $translationTable,
+ Interpolator = defaultInterpolator;
+
+ // if the interpolation id exists use custom interpolator
+ if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) {
+ Interpolator = interpolatorHashMap[interpolationId];
+ }
+
+ // if the translation id exists, we can just interpolate it
+ if (table && Object.prototype.hasOwnProperty.call(table, translationId)) {
+ var translation = table[translationId];
+
+ // If using link, rerun $translate with linked translationId and return it
+ if (translation.substr(0, 2) === '@:') {
+ result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId);
+ } else {
+ result = Interpolator.interpolate(translation, interpolateParams);
+ }
+ } else {
+ var missingTranslationHandlerTranslation;
+ // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams);
+ }
+
+ // since we couldn't translate the inital requested translation id,
+ // we try it now with one or more fallback languages, if fallback language(s) is
+ // configured.
+ if ($uses && $fallbackLanguage && $fallbackLanguage.length) {
+ fallbackIndex = 0;
+ result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator);
+ } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) {
+ // looks like the requested translation id doesn't exists.
+ // Now, if there is a registered handler for missing translations and no
+ // asyncLoader is pending, we execute the handler
+ result = missingTranslationHandlerTranslation;
+ } else {
+ result = applyNotFoundIndicators(translationId);
+ }
+ }
+
+ return result;
+ };
+
+ var clearNextLangAndPromise = function(key) {
+ if ($nextLang === key) {
+ $nextLang = undefined;
+ }
+ langPromises[key] = undefined;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#preferredLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key for the preferred language.
+ *
+ * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime)
+ *
+ * @return {string} preferred language key
+ */
+ $translate.preferredLanguage = function (langKey) {
+ if(langKey) {
+ setupPreferredLanguage(langKey);
+ }
+ return $preferredLanguage;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#cloakClassName
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the configured class name for `translate-cloak` directive.
+ *
+ * @return {string} cloakClassName
+ */
+ $translate.cloakClassName = function () {
+ return $cloakClassName;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#fallbackLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key for the fallback languages or sets a new fallback stack.
+ *
+ * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime)
+ *
+ * @return {string||array} fallback language key
+ */
+ $translate.fallbackLanguage = function (langKey) {
+ if (langKey !== undefined && langKey !== null) {
+ fallbackStack(langKey);
+
+ // as we might have an async loader initiated and a new translation language might have been defined
+ // we need to add the promise to the stack also. So - iterate.
+ if ($loaderFactory) {
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ if (!langPromises[$fallbackLanguage[i]]) {
+ langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]);
+ }
+ }
+ }
+ }
+ $translate.use($translate.use());
+ }
+ if ($fallbackWasString) {
+ return $fallbackLanguage[0];
+ } else {
+ return $fallbackLanguage;
+ }
+
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#useFallbackLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Sets the first key of the fallback language stack to be used for translation.
+ * Therefore all languages in the fallback array BEFORE this key will be skipped!
+ *
+ * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to
+ * get back to the whole stack
+ */
+ $translate.useFallbackLanguage = function (langKey) {
+ if (langKey !== undefined && langKey !== null) {
+ if (!langKey) {
+ startFallbackIteration = 0;
+ } else {
+ var langKeyPosition = indexOf($fallbackLanguage, langKey);
+ if (langKeyPosition > -1) {
+ startFallbackIteration = langKeyPosition;
+ }
+ }
+
+ }
+
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#proposedLanguage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the language key of language that is currently loaded asynchronously.
+ *
+ * @return {string} language key
+ */
+ $translate.proposedLanguage = function () {
+ return $nextLang;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#storage
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns registered storage.
+ *
+ * @return {object} Storage
+ */
+ $translate.storage = function () {
+ return Storage;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#use
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Tells angular-translate which language to use by given language key. This method is
+ * used to change language at runtime. It also takes care of storing the language
+ * key in a configured store to let your app remember the choosed language.
+ *
+ * When trying to 'use' a language which isn't available it tries to load it
+ * asynchronously with registered loaders.
+ *
+ * Returns promise object with loaded language file data
+ * @example
+ * $translate.use("en_US").then(function(data){
+ * $scope.text = $translate("HELLO");
+ * });
+ *
+ * @param {string} key Language key
+ * @return {string} Language key
+ */
+ $translate.use = function (key) {
+ if (!key) {
+ return $uses;
+ }
+
+ var deferred = $q.defer();
+
+ $rootScope.$emit('$translateChangeStart', {language: key});
+
+ // Try to get the aliased language key
+ var aliasedKey = negotiateLocale(key);
+ if (aliasedKey) {
+ key = aliasedKey;
+ }
+
+ // if there isn't a translation table for the language we've requested,
+ // we load it asynchronously
+ if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) {
+ $nextLang = key;
+ langPromises[key] = loadAsync(key).then(function (translation) {
+ translations(translation.key, translation.table);
+ deferred.resolve(translation.key);
+ useLanguage(translation.key);
+ return translation;
+ }, function (key) {
+ $rootScope.$emit('$translateChangeError', {language: key});
+ deferred.reject(key);
+ $rootScope.$emit('$translateChangeEnd', {language: key});
+ return $q.reject(key);
+ });
+ langPromises[key]['finally'](function () {
+ clearNextLangAndPromise(key);
+ });
+ } else if ($nextLang === key && langPromises[key]) {
+ // we are already loading this asynchronously
+ // resolve our new deferred when the old langPromise is resolved
+ langPromises[key].then(function (translation) {
+ deferred.resolve(translation.key);
+ return translation;
+ }, function (key) {
+ deferred.reject(key);
+ return $q.reject(key);
+ });
+ } else {
+ deferred.resolve(key);
+ useLanguage(key);
+ }
+
+ return deferred.promise;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#storageKey
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the key for the storage.
+ *
+ * @return {string} storage key
+ */
+ $translate.storageKey = function () {
+ return storageKey();
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isPostCompilingEnabled
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether post compiling is enabled or not
+ *
+ * @return {bool} storage key
+ */
+ $translate.isPostCompilingEnabled = function () {
+ return $postCompilingEnabled;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns whether force async reload is enabled or not
+ *
+ * @return {boolean} forceAsyncReload value
+ */
+ $translate.isForceAsyncReloadEnabled = function () {
+ return $forceAsyncReloadEnabled;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#refresh
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Refreshes a translation table pointed by the given langKey. If langKey is not specified,
+ * the module will drop all existent translation tables and load new version of those which
+ * are currently in use.
+ *
+ * Refresh means that the module will drop target translation table and try to load it again.
+ *
+ * In case there are no loaders registered the refresh() method will throw an Error.
+ *
+ * If the module is able to refresh translation tables refresh() method will broadcast
+ * $translateRefreshStart and $translateRefreshEnd events.
+ *
+ * @example
+ * // this will drop all currently existent translation tables and reload those which are
+ * // currently in use
+ * $translate.refresh();
+ * // this will refresh a translation table for the en_US language
+ * $translate.refresh('en_US');
+ *
+ * @param {string} langKey A language key of the table, which has to be refreshed
+ *
+ * @return {promise} Promise, which will be resolved in case a translation tables refreshing
+ * process is finished successfully, and reject if not.
+ */
+ $translate.refresh = function (langKey) {
+ if (!$loaderFactory) {
+ throw new Error('Couldn\'t refresh translation table, no loader registered!');
+ }
+
+ var deferred = $q.defer();
+
+ function resolve() {
+ deferred.resolve();
+ $rootScope.$emit('$translateRefreshEnd', {language: langKey});
+ }
+
+ function reject() {
+ deferred.reject();
+ $rootScope.$emit('$translateRefreshEnd', {language: langKey});
+ }
+
+ $rootScope.$emit('$translateRefreshStart', {language: langKey});
+
+ if (!langKey) {
+ // if there's no language key specified we refresh ALL THE THINGS!
+ var tables = [], loadingKeys = {};
+
+ // reload registered fallback languages
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ tables.push(loadAsync($fallbackLanguage[i]));
+ loadingKeys[$fallbackLanguage[i]] = true;
+ }
+ }
+
+ // reload currently used language
+ if ($uses && !loadingKeys[$uses]) {
+ tables.push(loadAsync($uses));
+ }
+
+ var allTranslationsLoaded = function (tableData) {
+ $translationTable = {};
+ angular.forEach(tableData, function (data) {
+ translations(data.key, data.table);
+ });
+ if ($uses) {
+ useLanguage($uses);
+ }
+ resolve();
+ };
+ allTranslationsLoaded.displayName = 'refreshPostProcessor';
+
+ $q.all(tables).then(allTranslationsLoaded, reject);
+
+ } else if ($translationTable[langKey]) {
+
+ var oneTranslationsLoaded = function (data) {
+ translations(data.key, data.table);
+ if (langKey === $uses) {
+ useLanguage($uses);
+ }
+ resolve();
+ };
+ oneTranslationsLoaded.displayName = 'refreshPostProcessor';
+
+ loadAsync(langKey).then(oneTranslationsLoaded, reject);
+
+ } else {
+ reject();
+ }
+ return deferred.promise;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#instant
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns a translation instantly from the internal state of loaded translation. All rules
+ * regarding the current language, the preferred language of even fallback languages will be
+ * used except any promise handling. If a language was not found, an asynchronous loading
+ * will be invoked in the background.
+ *
+ * @param {string|array} translationId A token which represents a translation id
+ * This can be optionally an array of translation ids which
+ * results that the function's promise returns an object where
+ * each key is the translation id and the value the translation.
+ * @param {object} interpolateParams Params
+ * @param {string} interpolationId The id of the interpolation to use
+ *
+ * @return {string|object} translation
+ */
+ $translate.instant = function (translationId, interpolateParams, interpolationId) {
+
+ // Detect undefined and null values to shorten the execution and prevent exceptions
+ if (translationId === null || angular.isUndefined(translationId)) {
+ return translationId;
+ }
+
+ // Duck detection: If the first argument is an array, a bunch of translations was requested.
+ // The result is an object.
+ if (angular.isArray(translationId)) {
+ var results = {};
+ for (var i = 0, c = translationId.length; i < c; i++) {
+ results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId);
+ }
+ return results;
+ }
+
+ // We discarded unacceptable values. So we just need to verify if translationId is empty String
+ if (angular.isString(translationId) && translationId.length < 1) {
+ return translationId;
+ }
+
+ // trim off any whitespace
+ if (translationId) {
+ translationId = trim.apply(translationId);
+ }
+
+ var result, possibleLangKeys = [];
+ if ($preferredLanguage) {
+ possibleLangKeys.push($preferredLanguage);
+ }
+ if ($uses) {
+ possibleLangKeys.push($uses);
+ }
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ possibleLangKeys = possibleLangKeys.concat($fallbackLanguage);
+ }
+ for (var j = 0, d = possibleLangKeys.length; j < d; j++) {
+ var possibleLangKey = possibleLangKeys[j];
+ if ($translationTable[possibleLangKey]) {
+ if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') {
+ result = determineTranslationInstant(translationId, interpolateParams, interpolationId);
+ } else if ($notFoundIndicatorLeft || $notFoundIndicatorRight) {
+ result = applyNotFoundIndicators(translationId);
+ }
+ }
+ if (typeof result !== 'undefined') {
+ break;
+ }
+ }
+
+ if (!result && result !== '') {
+ // Return translation of default interpolator if not found anything.
+ result = defaultInterpolator.interpolate(translationId, interpolateParams);
+ if ($missingTranslationHandlerFactory && !pendingLoader) {
+ result = translateByHandler(translationId, interpolateParams);
+ }
+ }
+
+ return result;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#versionInfo
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the current version information for the angular-translate library
+ *
+ * @return {string} angular-translate version
+ */
+ $translate.versionInfo = function () {
+ return version;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translate#loaderCache
+ * @methodOf pascalprecht.translate.$translate
+ *
+ * @description
+ * Returns the defined loaderCache.
+ *
+ * @return {boolean|string|object} current value of loaderCache
+ */
+ $translate.loaderCache = function () {
+ return loaderCache;
+ };
+
+ // internal purpose only
+ $translate.directivePriority = function () {
+ return directivePriority;
+ };
+
+ // internal purpose only
+ $translate.statefulFilter = function () {
+ return statefulFilter;
+ };
+
+ if ($loaderFactory) {
+
+ // If at least one async loader is defined and there are no
+ // (default) translations available we should try to load them.
+ if (angular.equals($translationTable, {})) {
+ $translate.use($translate.use());
+ }
+
+ // Also, if there are any fallback language registered, we start
+ // loading them asynchronously as soon as we can.
+ if ($fallbackLanguage && $fallbackLanguage.length) {
+ var processAsyncResult = function (translation) {
+ translations(translation.key, translation.table);
+ $rootScope.$emit('$translateChangeEnd', { language: translation.key });
+ return translation;
+ };
+ for (var i = 0, len = $fallbackLanguage.length; i < len; i++) {
+ var fallbackLanguageId = $fallbackLanguage[i];
+ if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) {
+ langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult);
+ }
+ }
+ }
+ }
+
+ return $translate;
+ }
+ ];
+}
+$translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider'];
+
+$translate.displayName = 'displayName';
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translateDefaultInterpolation
+ * @requires $interpolate
+ *
+ * @description
+ * Uses angular's `$interpolate` services to interpolate strings against some values.
+ *
+ * Be aware to configure a proper sanitization strategy.
+ *
+ * See also:
+ * * {@link pascalprecht.translate.$translateSanitization}
+ *
+ * @return {object} $translateDefaultInterpolation Interpolator service
+ */
+angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation);
+
+function $translateDefaultInterpolation ($interpolate, $translateSanitization) {
+
+ 'use strict';
+
+ var $translateInterpolator = {},
+ $locale,
+ $identifier = 'default';
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Sets current locale (this is currently not use in this interpolation).
+ *
+ * @param {string} locale Language key or locale.
+ */
+ $translateInterpolator.setLocale = function (locale) {
+ $locale = locale;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Returns an identifier for this interpolation service.
+ *
+ * @returns {string} $identifier
+ */
+ $translateInterpolator.getInterpolationIdentifier = function () {
+ return $identifier;
+ };
+
+ /**
+ * @deprecated will be removed in 3.0
+ * @see {@link pascalprecht.translate.$translateSanitization}
+ */
+ $translateInterpolator.useSanitizeValueStrategy = function (value) {
+ $translateSanitization.useStrategy(value);
+ return this;
+ };
+
+ /**
+ * @ngdoc function
+ * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate
+ * @methodOf pascalprecht.translate.$translateDefaultInterpolation
+ *
+ * @description
+ * Interpolates given string agains given interpolate params using angulars
+ * `$interpolate` service.
+ *
+ * @returns {string} interpolated string.
+ */
+ $translateInterpolator.interpolate = function (string, interpolationParams) {
+ interpolationParams = interpolationParams || {};
+ interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params');
+
+ var interpolatedText = $interpolate(string)(interpolationParams);
+ interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text');
+
+ return interpolatedText;
+ };
+
+ return $translateInterpolator;
+}
+$translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization'];
+
+$translateDefaultInterpolation.displayName = '$translateDefaultInterpolation';
+
+angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY');
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translate
+ * @requires $compile
+ * @requires $filter
+ * @requires $interpolate
+ * @restrict A
+ *
+ * @description
+ * Translates given translation id either through attribute or DOM content.
+ * Internally it uses `translate` filter to translate translation id. It possible to
+ * pass an optional `translate-values` object literal as string into translation id.
+ *
+ * @param {string=} translate Translation id which could be either string or interpolated string.
+ * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object.
+ * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute.
+ * @param {string=} translate-default will be used unless translation was successful
+ * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling}
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div ng-controller="TranslateCtrl">
+
+ <pre translate="TRANSLATION_ID"></pre>
+ <pre translate>TRANSLATION_ID</pre>
+ <pre translate translate-attr-title="TRANSLATION_ID"></pre>
+ <pre translate="{{translationId}}"></pre>
+ <pre translate>{{translationId}}</pre>
+ <pre translate="WITH_VALUES" translate-values="{value: 5}"></pre>
+ <pre translate translate-values="{value: 5}">WITH_VALUES</pre>
+ <pre translate="WITH_VALUES" translate-values="{{values}}"></pre>
+ <pre translate translate-values="{{values}}">WITH_VALUES</pre>
+ <pre translate translate-attr-title="WITH_VALUES" translate-values="{{values}}"></pre>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider.translations('en',{
+ 'TRANSLATION_ID': 'Hello there!',
+ 'WITH_VALUES': 'The following value is dynamic: {{value}}'
+ }).preferredLanguage('en');
+
+ });
+
+ angular.module('ngView').controller('TranslateCtrl', function ($scope) {
+ $scope.translationId = 'TRANSLATION_ID';
+
+ $scope.values = {
+ value: 78
+ };
+ });
+ </file>
+ <file name="scenario.js">
+ it('should translate', function () {
+ inject(function ($rootScope, $compile) {
+ $rootScope.translationId = 'TRANSLATION_ID';
+
+ element = $compile('<p translate="TRANSLATION_ID"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate="{{translationId}}"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate>TRANSLATION_ID</p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate>{{translationId}}</p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe('Hello there!');
+
+ element = $compile('<p translate translate-attr-title="TRANSLATION_ID"></p>')($rootScope);
+ $rootScope.$digest();
+ expect(element.attr('title')).toBe('Hello there!');
+ });
+ });
+ </file>
+ </example>
+ */
+.directive('translate', translateDirective);
+function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) {
+
+ 'use strict';
+
+ /**
+ * @name trim
+ * @private
+ *
+ * @description
+ * trim polyfill
+ *
+ * @returns {string} The string stripped of whitespace from both ends
+ */
+ var trim = function() {
+ return this.toString().replace(/^\s+|\s+$/g, '');
+ };
+
+ return {
+ restrict: 'AE',
+ scope: true,
+ priority: $translate.directivePriority(),
+ compile: function (tElement, tAttr) {
+
+ var translateValuesExist = (tAttr.translateValues) ?
+ tAttr.translateValues : undefined;
+
+ var translateInterpolation = (tAttr.translateInterpolation) ?
+ tAttr.translateInterpolation : undefined;
+
+ var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i);
+
+ var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)',
+ watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)';
+
+ return function linkFn(scope, iElement, iAttr) {
+
+ scope.interpolateParams = {};
+ scope.preText = '';
+ scope.postText = '';
+ var translationIds = {};
+
+ var initInterpolationParams = function (interpolateParams, iAttr, tAttr) {
+ // initial setup
+ if (iAttr.translateValues) {
+ angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent));
+ }
+ // initially fetch all attributes if existing and fill the params
+ if (translateValueExist) {
+ for (var attr in tAttr) {
+ if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') {
+ var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15);
+ interpolateParams[attributeName] = tAttr[attr];
+ }
+ }
+ }
+ };
+
+ // Ensures any change of the attribute "translate" containing the id will
+ // be re-stored to the scope's "translationId".
+ // If the attribute has no content, the element's text value (white spaces trimmed off) will be used.
+ var observeElementTranslation = function (translationId) {
+
+ // Remove any old watcher
+ if (angular.isFunction(observeElementTranslation._unwatchOld)) {
+ observeElementTranslation._unwatchOld();
+ observeElementTranslation._unwatchOld = undefined;
+ }
+
+ if (angular.equals(translationId , '') || !angular.isDefined(translationId)) {
+ // Resolve translation id by inner html if required
+ var interpolateMatches = trim.apply(iElement.text()).match(interpolateRegExp);
+ // Interpolate translation id if required
+ if (angular.isArray(interpolateMatches)) {
+ scope.preText = interpolateMatches[1];
+ scope.postText = interpolateMatches[3];
+ translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent);
+ var watcherMatches = iElement.text().match(watcherRegExp);
+ if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) {
+ observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) {
+ translationIds.translate = newValue;
+ updateTranslations();
+ });
+ }
+ } else {
+ translationIds.translate = iElement.text().replace(/^\s+|\s+$/g,'');
+ }
+ } else {
+ translationIds.translate = translationId;
+ }
+ updateTranslations();
+ };
+
+ var observeAttributeTranslation = function (translateAttr) {
+ iAttr.$observe(translateAttr, function (translationId) {
+ translationIds[translateAttr] = translationId;
+ updateTranslations();
+ });
+ };
+
+ // initial setup with values
+ initInterpolationParams(scope.interpolateParams, iAttr, tAttr);
+
+ var firstAttributeChangedEvent = true;
+ iAttr.$observe('translate', function (translationId) {
+ if (typeof translationId === 'undefined') {
+ // case of element "<translate>xyz</translate>"
+ observeElementTranslation('');
+ } else {
+ // case of regular attribute
+ if (translationId !== '' || !firstAttributeChangedEvent) {
+ translationIds.translate = translationId;
+ updateTranslations();
+ }
+ }
+ firstAttributeChangedEvent = false;
+ });
+
+ for (var translateAttr in iAttr) {
+ if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') {
+ observeAttributeTranslation(translateAttr);
+ }
+ }
+
+ iAttr.$observe('translateDefault', function (value) {
+ scope.defaultText = value;
+ });
+
+ if (translateValuesExist) {
+ iAttr.$observe('translateValues', function (interpolateParams) {
+ if (interpolateParams) {
+ scope.$parent.$watch(function () {
+ angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent));
+ });
+ }
+ });
+ }
+
+ if (translateValueExist) {
+ var observeValueAttribute = function (attrName) {
+ iAttr.$observe(attrName, function (value) {
+ var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15);
+ scope.interpolateParams[attributeName] = value;
+ });
+ };
+ for (var attr in iAttr) {
+ if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') {
+ observeValueAttribute(attr);
+ }
+ }
+ }
+
+ // Master update function
+ var updateTranslations = function () {
+ for (var key in translationIds) {
+
+ if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) {
+ updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText);
+ }
+ }
+ };
+
+ // Put translation processing function outside loop
+ var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText) {
+ if (translationId) {
+ $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText)
+ .then(function (translation) {
+ applyTranslation(translation, scope, true, translateAttr);
+ }, function (translationId) {
+ applyTranslation(translationId, scope, false, translateAttr);
+ });
+ } else {
+ // as an empty string cannot be translated, we can solve this using successful=false
+ applyTranslation(translationId, scope, false, translateAttr);
+ }
+ };
+
+ var applyTranslation = function (value, scope, successful, translateAttr) {
+ if (translateAttr === 'translate') {
+ // default translate into innerHTML
+ if (!successful && typeof scope.defaultText !== 'undefined') {
+ value = scope.defaultText;
+ }
+ iElement.html(scope.preText + value + scope.postText);
+ var globallyEnabled = $translate.isPostCompilingEnabled();
+ var locallyDefined = typeof tAttr.translateCompile !== 'undefined';
+ var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false';
+ if ((globallyEnabled && !locallyDefined) || locallyEnabled) {
+ $compile(iElement.contents())(scope);
+ }
+ } else {
+ // translate attribute
+ if (!successful && typeof scope.defaultText !== 'undefined') {
+ value = scope.defaultText;
+ }
+ var attributeName = iAttr.$attr[translateAttr];
+ if (attributeName.substr(0, 5) === 'data-') {
+ // ensure html5 data prefix is stripped
+ attributeName = attributeName.substr(5);
+ }
+ attributeName = attributeName.substr(15);
+ iElement.attr(attributeName, value);
+ }
+ };
+
+ if (translateValuesExist || translateValueExist || iAttr.translateDefault) {
+ scope.$watch('interpolateParams', updateTranslations, true);
+ }
+
+ // Ensures the text will be refreshed after the current language was changed
+ // w/ $translate.use(...)
+ var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations);
+
+ // ensure translation will be looked up at least one
+ if (iElement.text().length) {
+ if (iAttr.translate) {
+ observeElementTranslation(iAttr.translate);
+ } else {
+ observeElementTranslation('');
+ }
+ } else if (iAttr.translate) {
+ // ensure attribute will be not skipped
+ observeElementTranslation(iAttr.translate);
+ }
+ updateTranslations();
+ scope.$on('$destroy', unbind);
+ };
+ }
+ };
+}
+translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope'];
+
+translateDirective.displayName = 'translateDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc directive
+ * @name pascalprecht.translate.directive:translateCloak
+ * @requires $rootScope
+ * @requires $translate
+ * @restrict A
+ *
+ * $description
+ * Adds a `translate-cloak` class name to the given element where this directive
+ * is applied initially and removes it, once a loader has finished loading.
+ *
+ * This directive can be used to prevent initial flickering when loading translation
+ * data asynchronously.
+ *
+ * The class name is defined in
+ * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}.
+ *
+ * @param {string=} translate-cloak If a translationId is provided, it will be used for showing
+ * or hiding the cloak. Basically it relies on the translation
+ * resolve.
+ */
+.directive('translateCloak', translateCloakDirective);
+
+function translateCloakDirective($rootScope, $translate) {
+
+ 'use strict';
+
+ return {
+ compile: function (tElement) {
+ var applyCloak = function () {
+ tElement.addClass($translate.cloakClassName());
+ },
+ removeCloak = function () {
+ tElement.removeClass($translate.cloakClassName());
+ },
+ removeListener = $rootScope.$on('$translateChangeEnd', function () {
+ removeCloak();
+ removeListener();
+ removeListener = null;
+ });
+ applyCloak();
+
+ return function linkFn(scope, iElement, iAttr) {
+ // Register a watcher for the defined translation allowing a fine tuned cloak
+ if (iAttr.translateCloak && iAttr.translateCloak.length) {
+ iAttr.$observe('translateCloak', function (translationId) {
+ $translate(translationId).then(removeCloak, applyCloak);
+ });
+ }
+ };
+ }
+ };
+}
+translateCloakDirective.$inject = ['$rootScope', '$translate'];
+
+translateCloakDirective.displayName = 'translateCloakDirective';
+
+angular.module('pascalprecht.translate')
+/**
+ * @ngdoc filter
+ * @name pascalprecht.translate.filter:translate
+ * @requires $parse
+ * @requires pascalprecht.translate.$translate
+ * @function
+ *
+ * @description
+ * Uses `$translate` service to translate contents. Accepts interpolate parameters
+ * to pass dynamized values though translation.
+ *
+ * @param {string} translationId A translation id to be translated.
+ * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation.
+ *
+ * @returns {string} Translated text.
+ *
+ * @example
+ <example module="ngView">
+ <file name="index.html">
+ <div ng-controller="TranslateCtrl">
+
+ <pre>{{ 'TRANSLATION_ID' | translate }}</pre>
+ <pre>{{ translationId | translate }}</pre>
+ <pre>{{ 'WITH_VALUES' | translate:'{value: 5}' }}</pre>
+ <pre>{{ 'WITH_VALUES' | translate:values }}</pre>
+
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('ngView', ['pascalprecht.translate'])
+
+ .config(function ($translateProvider) {
+
+ $translateProvider.translations('en', {
+ 'TRANSLATION_ID': 'Hello there!',
+ 'WITH_VALUES': 'The following value is dynamic: {{value}}'
+ });
+ $translateProvider.preferredLanguage('en');
+
+ });
+
+ angular.module('ngView').controller('TranslateCtrl', function ($scope) {
+ $scope.translationId = 'TRANSLATION_ID';
+
+ $scope.values = {
+ value: 78
+ };
+ });
+ </file>
+ </example>
+ */
+.filter('translate', translateFilterFactory);
+
+function translateFilterFactory($parse, $translate) {
+
+ 'use strict';
+
+ var translateFilter = function (translationId, interpolateParams, interpolation) {
+
+ if (!angular.isObject(interpolateParams)) {
+ interpolateParams = $parse(interpolateParams)(this);
+ }
+
+ return $translate.instant(translationId, interpolateParams, interpolation);
+ };
+
+ if ($translate.statefulFilter()) {
+ translateFilter.$stateful = true;
+ }
+
+ return translateFilter;
+}
+translateFilterFactory.$inject = ['$parse', '$translate'];
+
+translateFilterFactory.displayName = 'translateFilterFactory';
+
+angular.module('pascalprecht.translate')
+
+/**
+ * @ngdoc object
+ * @name pascalprecht.translate.$translationCache
+ * @requires $cacheFactory
+ *
+ * @description
+ * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You
+ * can load translation tables directly into the cache by consuming the
+ * `$translationCache` service directly.
+ *
+ * @return {object} $cacheFactory object.
+ */
+ .factory('$translationCache', $translationCache);
+
+function $translationCache($cacheFactory) {
+
+ 'use strict';
+
+ return $cacheFactory('translations');
+}
+$translationCache.$inject = ['$cacheFactory'];
+
+$translationCache.displayName = '$translationCache';
+return 'pascalprecht.translate';
+
+}));
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js
new file mode 100644
index 0000000..619a819
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js
@@ -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';
+
+}));
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js
new file mode 100644
index 0000000..3ac4060
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js
@@ -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;
+}]);
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 89e896c..1a16fbc 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -1,3 +1,19 @@
+/*
+ * 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.login.freemarker;
import org.jboss.logging.Logger;
@@ -276,7 +292,10 @@ 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 cookiePath = Urls.localeCookiePath(baseUri, realm.getName());
+ LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath);
+
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
@@ -374,7 +393,9 @@ 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 cookiePath = Urls.localeCookiePath(baseUri, realm.getName());
+ LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath);
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
@@ -383,26 +404,32 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
+ @Override
public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN);
}
+ @Override
public Response createPasswordReset() {
return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
}
+ @Override
public Response createLoginTotp() {
return createResponse(LoginFormsPages.LOGIN_TOTP);
}
+ @Override
public Response createRegistration() {
return createResponse(LoginFormsPages.REGISTER);
}
+ @Override
public Response createInfoPage() {
return createResponse(LoginFormsPages.INFO);
}
+ @Override
public Response createErrorPage() {
if (status == null) {
status = Response.Status.INTERNAL_SERVER_ERROR;
@@ -410,7 +437,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.ERROR);
}
-
+ @Override
public Response createOAuthGrant(ClientSessionModel clientSession) {
this.clientSession = clientSession;
return createResponse(LoginFormsPages.OAUTH_GRANT);
@@ -494,11 +521,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return this;
}
+ @Override
public FreeMarkerLoginFormsProvider setUser(UserModel user) {
this.user = user;
return this;
}
+ @Override
public FreeMarkerLoginFormsProvider setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
index 8f62a52..c600322 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java
@@ -1,3 +1,19 @@
+/*
+ * 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.protocol.oidc;
import org.keycloak.constants.KerberosConstants;
@@ -19,6 +35,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -32,12 +49,14 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String GIVEN_NAME = "given name";
public static final String FAMILY_NAME = "family name";
public static final String FULL_NAME = "full name";
+ public static final String LOCALE = "locale";
public static final String USERNAME_CONSENT_TEXT = "${username}";
public static final String EMAIL_CONSENT_TEXT = "${email}";
public static final String EMAIL_VERIFIED_CONSENT_TEXT = "${emailVerified}";
public static final String GIVEN_NAME_CONSENT_TEXT = "${givenName}";
public static final String FAMILY_NAME_CONSENT_TEXT = "${familyName}";
public static final String FULL_NAME_CONSENT_TEXT = "${fullName}";
+ public static final String LOCALE_CONSENT_TEXT = "${locale}";
@Override
@@ -95,6 +114,12 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
false, EMAIL_VERIFIED_CONSENT_TEXT,
true, true);
builtins.add(model);
+ model = UserAttributeMapper.createClaimMapper(LOCALE,
+ "locale",
+ "locale", "String",
+ false, LOCALE_CONSENT_TEXT,
+ true, true, false);
+ builtins.add(model);
ProtocolMapperModel fullName = new ProtocolMapperModel();
fullName.setName(FULL_NAME);
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 3790d5a..5f1f9bf 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -1,3 +1,19 @@
+/*
+ * 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.managers;
import org.jboss.logging.Logger;
@@ -49,7 +65,9 @@ import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
+import org.keycloak.freemarker.LocaleHelper;
/**
* Stateless object that manages authentication
@@ -393,6 +411,9 @@ public class AuthenticationManager {
}
}
}
+
+ handleLoginLocale(realm, userSession, request, uriInfo);
+
// refresh the cookies!
createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN);
@@ -406,6 +427,17 @@ public class AuthenticationManager {
}
+ // If a locale has been set on the login screen, associate that locale with the user
+ private static void handleLoginLocale(RealmModel realm, UserSessionModel userSession,
+ HttpRequest request, UriInfo uriInfo) {
+ Cookie localeCookie = request.getHttpHeaders().getCookies().get(LocaleHelper.LOCALE_COOKIE);
+ if (localeCookie == null) return;
+
+ UserModel user = userSession.getUser();
+ Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, request.getHttpHeaders());
+ user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
+ }
+
public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
ClientConnection clientConnection,
HttpRequest request, UriInfo uriInfo, EventBuilder event) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index e3bde40..2f845e0 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -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>
@@ -283,8 +285,7 @@ public class AdminConsole {
map.put("resourceUrl", Urls.themeRoot(baseUri) + "/admin/" + adminTheme);
map.put("resourceVersion", Version.RESOURCES_VERSION);
- ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
- Theme theme = themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN);
+ Theme theme = getTheme();
map.put("properties", theme.getProperties());
@@ -296,10 +297,38 @@ public class AdminConsole {
}
}
+ private Theme getTheme() throws IOException {
+ ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
+ return themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN);
+ }
+
@GET
@Path("{indexhtml: index.html}") // this expression is a hack to get around jaxdoclet generation bug. Doesn't like index.html
public Response getIndexHtmlRedirect() {
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(getTheme(), lang);
+ if (msgs.isEmpty()) {
+ logger.warn("Message bundle not found for language code '" + lang + "'");
+ msgs = AdminMessagesLoader.getMessages(getTheme(), "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);
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java
new file mode 100644
index 0000000..43dfc3e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import org.keycloak.freemarker.Theme;
+
+/**
+ * Simple loader and cache 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(Theme theme, String strLocale) throws IOException {
+ String allMessagesKey = theme.getName() + "_" + strLocale;
+ Properties messages = allMessages.get(allMessagesKey);
+ if (messages != null) return messages;
+
+ Locale locale = Locale.forLanguageTag(strLocale);
+ messages = theme.getMessages("admin-messages", locale);
+ if (messages == null) return new Properties();
+
+ allMessages.put(allMessagesKey, messages);
+ return messages;
+ }
+}