KEYCLOAK-14855 Added realm-specific localization texts which affect texts in every part of the UI (admin console / login page / personal info page / email templates). Also new API endpoints and a new UI screen to manage the realm-specific localization texts were introduced.

Co-authored-by: Daniel Fesenmeyer <daniel.fesenmeyer@bosch.io>
This commit is contained in:
Christoph Leistert 2020-08-05 15:21:47 +02:00 committed by Pedro Igor
parent 785f2e78bc
commit e131de9574
41 changed files with 1428 additions and 46 deletions

View file

@ -0,0 +1,62 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.admin.client.resource;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
public interface RealmLocalizationResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
List<String> getRealmSpecificLocales();
@Path("{locale}")
@GET
@Produces(MediaType.APPLICATION_JSON)
Map<String, String> getRealmLocalizationTexts(final @PathParam("locale") String locale);
@Path("{locale}/{key}")
@GET
@Produces(MediaType.TEXT_PLAIN)
String getRealmLocalizationText(final @PathParam("locale") String locale, final @PathParam("key") String key);
@Path("{locale}")
@DELETE
void deleteRealmLocalizationTexts(@PathParam("locale") String locale);
@Path("{locale}/{key}")
@DELETE
void deleteRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key);
@Path("{locale}/{key}")
@PUT
@Consumes(MediaType.TEXT_PLAIN)
void saveRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key, String text);
}

View file

@ -279,4 +279,7 @@ public interface RealmResource {
@Path("keys") @Path("keys")
KeyResource keys(); KeyResource keys();
@Path("localization")
RealmLocalizationResource localization();
} }

View file

@ -1645,6 +1645,35 @@ public class RealmAdapter implements CachedRealmModel {
return cached.getAttributes(); return cached.getAttributes();
} }
@Override
public void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts) {
getDelegateForUpdate();
updated.patchRealmLocalizationTexts(locale, localizationTexts);
}
@Override
public boolean removeRealmLocalizationTexts(String locale) {
getDelegateForUpdate();
return updated.removeRealmLocalizationTexts(locale);
}
@Override
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
if (isUpdated()) return updated.getRealmLocalizationTexts();
return cached.getRealmLocalizationTexts();
}
@Override
public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
if (isUpdated()) return updated.getRealmLocalizationTextsByLocale(locale);
Map<String, String> localizationTexts = Collections.emptyMap();
if (cached.getRealmLocalizationTexts() != null && cached.getRealmLocalizationTexts().containsKey(locale)) {
localizationTexts = cached.getRealmLocalizationTexts().get(locale);
}
return Collections.unmodifiableMap(localizationTexts);
}
@Override @Override
public String toString() { public String toString() {
return String.format("%s@%08x", getId(), hashCode()); return String.format("%s@%08x", getId(), hashCode());

View file

@ -171,7 +171,6 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupDelegate; return groupDelegate;
} }
@Override @Override
public void registerRealmInvalidation(String id, String name) { public void registerRealmInvalidation(String id, String name) {
cache.realmUpdated(id, name, invalidations); cache.realmUpdated(id, name, invalidations);
@ -1247,4 +1246,51 @@ public class RealmCacheSession implements CacheRealmProvider {
getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess); getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess);
} }
@Override
public void saveLocalizationText(RealmModel realm, String locale, String key, String text) {
getRealmDelegate().saveLocalizationText(realm, locale, key, text);
registerRealmInvalidation(realm.getId(), locale);
}
@Override
public void saveLocalizationTexts(RealmModel realm, String locale, Map<String, String> localizationTexts) {
getRealmDelegate().saveLocalizationTexts(realm, locale, localizationTexts);
registerRealmInvalidation(realm.getId(), locale);
}
@Override
public boolean updateLocalizationText(RealmModel realm, String locale, String key, String text) {
boolean wasFound = getRealmDelegate().updateLocalizationText(realm, locale, key, text);
if (wasFound) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasFound;
}
@Override
public boolean deleteLocalizationTextsByLocale(RealmModel realm, String locale) {
boolean wasDeleted = getRealmDelegate().deleteLocalizationTextsByLocale(realm, locale);
if(wasDeleted) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasDeleted;
}
@Override
public boolean deleteLocalizationText(RealmModel realm, String locale, String key) {
boolean wasFound = getRealmDelegate().deleteLocalizationText(realm, locale, key);
if (wasFound) {
registerRealmInvalidation(realm.getId(), locale);
}
return wasFound;
}
@Override
public String getLocalizationTextsById(RealmModel realm, String locale, String key) {
Map<String, String> localizationTexts = getRealm(realm.getId()).getRealmLocalizationTextsByLocale(locale);
if(localizationTexts != null) {
return localizationTexts.get(key);
}
return null;
}
} }

View file

@ -163,6 +163,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
private Map<String, Integer> userActionTokenLifespans; private Map<String, Integer> userActionTokenLifespans;
protected Map<String, Map<String,String>> realmLocalizationTexts;
public CachedRealm(Long revision, RealmModel model) { public CachedRealm(Long revision, RealmModel model) {
super(revision, model.getId()); super(revision, model.getId());
name = model.getName(); name = model.getName();
@ -301,6 +303,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
} catch (UnsupportedOperationException ex) { } catch (UnsupportedOperationException ex) {
} }
realmLocalizationTexts = model.getRealmLocalizationTexts();
} }
protected void cacheClientScopes(RealmModel model) { protected void cacheClientScopes(RealmModel model) {
@ -718,4 +721,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public boolean isAllowUserManagedAccess() { public boolean isAllowUserManagedAccess() {
return allowUserManagedAccess; return allowUserManagedAccess;
} }
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
return realmLocalizationTexts;
}
} }

View file

@ -39,12 +39,16 @@ import org.keycloak.models.jpa.entities.ClientInitialAccessEntity;
import org.keycloak.models.jpa.entities.ClientScopeEntity; import org.keycloak.models.jpa.entities.ClientScopeEntity;
import org.keycloak.models.jpa.entities.GroupEntity; import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.RealmEntity; import org.keycloak.models.jpa.entities.RealmEntity;
import org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity;
import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.Root;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -858,6 +862,84 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
.executeUpdate(); .executeUpdate();
} }
private RealmLocalizationTextsEntity getRealmLocalizationTextsEntity(String locale, String realmId) {
RealmLocalizationTextsEntity.RealmLocalizationTextEntityKey key = new RealmLocalizationTextsEntity.RealmLocalizationTextEntityKey();
key.setRealmId(realmId);
key.setLocale(locale);
return em.find(RealmLocalizationTextsEntity.class, key);
}
@Override
public boolean updateLocalizationText(RealmModel realm, String locale, String key, String text) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
entity.getTexts().put(key, text);
em.persist(entity);
return true;
} else {
return false;
}
}
@Override
public void saveLocalizationText(RealmModel realm, String locale, String key, String text) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if(entity == null) {
entity = new RealmLocalizationTextsEntity();
entity.setRealmId(realm.getId());
entity.setLocale(locale);
entity.setTexts(new HashMap<>());
}
entity.getTexts().put(key, text);
em.persist(entity);
}
@Override
public void saveLocalizationTexts(RealmModel realm, String locale, Map<String, String> localizationTexts) {
RealmLocalizationTextsEntity entity = new RealmLocalizationTextsEntity();
entity.setTexts(localizationTexts);
entity.setLocale(locale);
entity.setRealmId(realm.getId());
em.merge(entity);
}
@Override
public boolean deleteLocalizationTextsByLocale(RealmModel realm, String locale) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaDelete<RealmLocalizationTextsEntity> criteriaDelete =
builder.createCriteriaDelete(RealmLocalizationTextsEntity.class);
Root<RealmLocalizationTextsEntity> root = criteriaDelete.from(RealmLocalizationTextsEntity.class);
criteriaDelete.where(builder.and(
builder.equal(root.get("realmId"), realm.getId()),
builder.equal(root.get("locale"), locale)));
int linesUpdated = em.createQuery(criteriaDelete).executeUpdate();
return linesUpdated == 1?true:false;
}
@Override
public String getLocalizationTextsById(RealmModel realm, String locale, String key) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
return entity.getTexts().get(key);
}
return null;
}
@Override
public boolean deleteLocalizationText(RealmModel realm, String locale, String key) {
RealmLocalizationTextsEntity entity = getRealmLocalizationTextsEntity(locale, realm.getId());
if (entity != null && entity.getTexts() != null && entity.getTexts().containsKey(key)) {
entity.getTexts().remove(key);
em.persist(entity);
return true;
} else {
return false;
}
}
private ClientInitialAccessModel entityToModel(ClientInitialAccessEntity entity) { private ClientInitialAccessModel entityToModel(ClientInitialAccessEntity entity) {
ClientInitialAccessModel model = new ClientInitialAccessModel(); ClientInitialAccessModel model = new ClientInitialAccessModel();
model.setId(entity.getId()); model.setId(entity.getId());

View file

@ -2225,6 +2225,53 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return c; return c;
} }
@Override
public void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts) {
Map<String, RealmLocalizationTextsEntity> currentLocalizationTexts = realm.getRealmLocalizationTexts();
if(currentLocalizationTexts.containsKey(locale)) {
RealmLocalizationTextsEntity localizationTextsEntity = currentLocalizationTexts.get(locale);
localizationTextsEntity.getTexts().putAll(localizationTexts);
em.persist(localizationTextsEntity);
}
else {
RealmLocalizationTextsEntity realmLocalizationTextsEntity = new RealmLocalizationTextsEntity();
realmLocalizationTextsEntity.setRealmId(realm.getId());
realmLocalizationTextsEntity.setLocale(locale);
realmLocalizationTextsEntity.setTexts(localizationTexts);
em.persist(realmLocalizationTextsEntity);
}
}
@Override
public boolean removeRealmLocalizationTexts(String locale) {
if (locale == null) return false;
if (realm.getRealmLocalizationTexts().containsKey(locale))
{
em.remove(realm.getRealmLocalizationTexts().get(locale));
return true;
}
return false;
}
@Override
public Map<String, Map<String, String>> getRealmLocalizationTexts() {
Map<String, Map<String, String>> localizationTexts = new HashMap<>();
realm.getRealmLocalizationTexts().forEach((locale, localizationTextsEntity) -> {
localizationTexts.put(localizationTextsEntity.getLocale(), localizationTextsEntity.getTexts());
});
return localizationTexts;
}
@Override
public Map<String, String> getRealmLocalizationTextsByLocale(String locale) {
if (realm.getRealmLocalizationTexts().containsKey(locale)) {
return realm.getRealmLocalizationTexts().get(locale).getTexts();
}
return Collections.emptyMap();
}
@Override @Override
public String toString() { public String toString() {
return String.format("%s@%08x", getId(), hashCode()); return String.format("%s@%08x", getId(), hashCode());

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models.jpa.converter;
import java.io.IOException;
import java.util.Map;
import javax.persistence.AttributeConverter;
import org.jboss.logging.Logger;
import org.keycloak.util.JsonSerialization;
public class MapStringConverter implements AttributeConverter<Map<String, String>, String> {
private static final Logger logger = Logger.getLogger(MapStringConverter.class);
@Override
public String convertToDatabaseColumn(Map<String, String> attribute) {
try {
return JsonSerialization.writeValueAsString(attribute);
} catch (IOException e) {
logger.error("Error while converting Map to JSON String: ", e);
return null;
}
}
@Override
public Map<String, String> convertToEntityAttribute(String dbData) {
try {
return JsonSerialization.readValue(dbData, Map.class);
} catch (IOException e) {
logger.error("Error while converting JSON String to Map: ", e);
return null;
}
}
}

View file

@ -28,6 +28,7 @@ import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.JoinTable; import javax.persistence.JoinTable;
import javax.persistence.MapKey;
import javax.persistence.MapKeyColumn; import javax.persistence.MapKeyColumn;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
@ -242,6 +243,9 @@ public class RealmEntity {
@Column(name="ALLOW_USER_MANAGED_ACCESS") @Column(name="ALLOW_USER_MANAGED_ACCESS")
private boolean allowUserManagedAccess; private boolean allowUserManagedAccess;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realmId")
@MapKey(name="locale")
Map<String, RealmLocalizationTextsEntity> realmLocalizationTexts;
public String getId() { public String getId() {
return id; return id;
@ -834,6 +838,17 @@ public class RealmEntity {
return allowUserManagedAccess; return allowUserManagedAccess;
} }
public Map<String, RealmLocalizationTextsEntity> getRealmLocalizationTexts() {
if (realmLocalizationTexts == null) {
realmLocalizationTexts = new HashMap<>();
}
return realmLocalizationTexts;
}
public void setRealmLocalizationTexts(Map<String, RealmLocalizationTextsEntity> realmLocalizationTexts) {
this.realmLocalizationTexts = realmLocalizationTexts;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -0,0 +1,129 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models.jpa.entities;
import java.io.Serializable;
import java.util.Map;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import org.keycloak.models.jpa.converter.MapStringConverter;
@Entity
@IdClass(RealmLocalizationTextsEntity.RealmLocalizationTextEntityKey.class)
@Table(name = "REALM_LOCALIZATIONS")
public class RealmLocalizationTextsEntity {
static public class RealmLocalizationTextEntityKey implements Serializable {
private String realmId;
private String locale;
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RealmLocalizationTextEntityKey that = (RealmLocalizationTextEntityKey) o;
return Objects.equals(realmId, that.realmId) &&
Objects.equals(locale, that.locale);
}
@Override
public int hashCode() {
return Objects.hash(realmId, locale);
}
}
@Id
@Column(name = "REALM_ID")
private String realmId;
@Id
@Column(name = "LOCALE")
private String locale;
@Column(name = "TEXTS")
@Convert(converter = MapStringConverter.class)
private Map<String,String> texts;
public Map<String,String> getTexts() {
return texts;
}
public void setTexts(Map<String,String> texts) {
this.texts = texts;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
@Override
public String toString() {
return "LocalizationTextEntity{" +
", text='" + texts + '\'' +
", locale='" + locale + '\'' +
", realmId='" + realmId + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RealmLocalizationTextsEntity that = (RealmLocalizationTextsEntity) o;
return Objects.equals(realmId, that.realmId) &&
Objects.equals(locale, that.locale) &&
Objects.equals(texts, that.texts);
}
@Override
public int hashCode() {
return Objects.hash(realmId, locale, texts);
}
}

View file

@ -26,4 +26,20 @@
<dropForeignKeyConstraint baseTableName="SCOPE_MAPPING" constraintName="FK_P3RH9GRKU11KQFRS4FLTT7RNQ"/> <dropForeignKeyConstraint baseTableName="SCOPE_MAPPING" constraintName="FK_P3RH9GRKU11KQFRS4FLTT7RNQ"/>
</changeSet> </changeSet>
<changeSet author="keycloak" id="12.1.0-add-realm-localization-table">
<createTable tableName="REALM_LOCALIZATIONS">
<column name="REALM_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="LOCALE" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="TEXTS" type="CLOB">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="REALM_ID, LOCALE" tableName="REALM_LOCALIZATIONS"/>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -35,6 +35,7 @@
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class> <class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
<class>org.keycloak.models.jpa.entities.MigrationModelEntity</class> <class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class> <class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity</class>
<class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class> <class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
<class>org.keycloak.models.jpa.entities.UserAttributeEntity</class> <class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class> <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>

View file

@ -770,6 +770,15 @@ public interface RealmModel extends RoleContainerModel {
void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope); void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope);
void removeDefaultClientScope(ClientScopeModel clientScope); void removeDefaultClientScope(ClientScopeModel clientScope);
/**
* Patches the realm-specific localization texts. This method will not delete any text.
* It updates texts, which are already stored or create new ones if the key does not exist yet.
*/
void patchRealmLocalizationTexts(String locale, Map<String, String> localizationTexts);
boolean removeRealmLocalizationTexts(String locale);
Map<String, Map<String, String>> getRealmLocalizationTexts();
Map<String, String> getRealmLocalizationTextsByLocale(String locale);
/** /**
* @deprecated Use {@link #getDefaultClientScopesStream(boolean) getDefaultClientScopesStream} instead. * @deprecated Use {@link #getDefaultClientScopesStream(boolean) getDefaultClientScopesStream} instead.
*/ */

View file

@ -17,6 +17,7 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.Map;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
@ -80,6 +81,18 @@ public interface RealmProvider extends Provider /* TODO: Remove in future versio
void removeExpiredClientInitialAccess(); void removeExpiredClientInitialAccess();
void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update
void saveLocalizationText(RealmModel realm, String locale, String key, String text);
void saveLocalizationTexts(RealmModel realm, String locale, Map<String, String> localizationTexts);
boolean updateLocalizationText(RealmModel realm, String locale, String key, String text);
boolean deleteLocalizationTextsByLocale(RealmModel realm, String locale);
boolean deleteLocalizationText(RealmModel realm, String locale, String key);
String getLocalizationTextsById(RealmModel realm, String locale, String key);
// The methods below are going to be removed in future version of Keycloak // The methods below are going to be removed in future version of Keycloak
// Sadly, we have to copy-paste the declarations from the respective interfaces // Sadly, we have to copy-paste the declarations from the respective interfaces
// including the "default" body to be able to add a note on deprecation // including the "default" body to be able to add a note on deprecation

View file

@ -28,7 +28,6 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import org.jboss.logging.Logger;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.ObjectUtil;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
@ -209,6 +208,8 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
attributes.put("locale", locale); attributes.put("locale", locale);
Properties rb = theme.getMessages(locale); Properties rb = theme.getMessages(locale);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
rb.putAll(localizationTexts);
attributes.put("msg", new MessageFormatterMethod(locale, rb)); attributes.put("msg", new MessageFormatterMethod(locale, rb));
attributes.put("properties", theme.getProperties()); attributes.put("properties", theme.getProperties());
String subject = new MessageFormat(rb.getProperty(subjectKey, subjectKey), locale).format(subjectAttributes.toArray()); String subject = new MessageFormat(rb.getProperty(subjectKey, subjectKey), locale).format(subjectAttributes.toArray());

View file

@ -129,6 +129,8 @@ public class FreeMarkerAccountProvider implements AccountProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale, attributes); Properties messagesBundle = handleThemeResources(theme, locale, attributes);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
messagesBundle.putAll(localizationTexts);
URI baseUri = uriInfo.getBaseUri(); URI baseUri = uriInfo.getBaseUri();
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();

View file

@ -182,6 +182,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale); Properties messagesBundle = handleThemeResources(theme, locale);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
messagesBundle.putAll(localizationTexts);
handleMessages(locale, messagesBundle); handleMessages(locale, messagesBundle);
@ -248,6 +250,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale); Properties messagesBundle = handleThemeResources(theme, locale);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.getCountry());
messagesBundle.putAll(localizationTexts);
handleMessages(locale, messagesBundle); handleMessages(locale, messagesBundle);
@ -353,6 +357,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale); Properties messagesBundle = handleThemeResources(theme, locale);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.getCountry());
messagesBundle.putAll(localizationTexts);
FormMessage msg = new FormMessage(null, message); FormMessage msg = new FormMessage(null, message);
return formatMessage(msg, messagesBundle, locale); return formatMessage(msg, messagesBundle, locale);
} }
@ -369,6 +375,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Locale locale = session.getContext().resolveLocale(user); Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale); Properties messagesBundle = handleThemeResources(theme, locale);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.getCountry());
messagesBundle.putAll(localizationTexts);
FormMessage msg = new FormMessage(message, (Object[]) parameters); FormMessage msg = new FormMessage(message, (Object[]) parameters);
return formatMessage(msg, messagesBundle, locale); return formatMessage(msg, messagesBundle, locale);
} }

View file

@ -220,6 +220,15 @@ public class RealmAdminResource {
return clientScopesResource; return clientScopesResource;
} }
/**
* Base path for managing localization under this realm.
*/
@Path("localization")
public RealmLocalizationResource getLocalization() {
RealmLocalizationResource resource = new RealmLocalizationResource(realm, auth);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
/** /**
* Get realm default client scopes. Only name and ids are returned. * Get realm default client scopes. Only name and ids are returned.

View file

@ -0,0 +1,164 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 com.fasterxml.jackson.core.type.TypeReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.keycloak.util.JsonSerialization;
public class RealmLocalizationResource {
private final RealmModel realm;
private final AdminPermissionEvaluator auth;
@Context
protected KeycloakSession session;
public RealmLocalizationResource(RealmModel realm, AdminPermissionEvaluator auth) {
this.realm = realm;
this.auth = auth;
}
@Path("{locale}/{key}")
@PUT
@Consumes(MediaType.TEXT_PLAIN)
public void saveRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key,
String text) {
this.auth.realm().requireManageRealm();
try {
session.realms().saveLocalizationText(realm, locale, key, text);
} catch (ModelDuplicateException e) {
throw new BadRequestException(
String.format("Localization text %s for the locale %s and realm %s already exists.",
key, locale, realm.getId()));
}
}
/**
* Import localization from uploaded JSON file
*/
@POST
@Path("{locale}")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public void patchRealmLocalizationTextsFromFile(@PathParam("locale") String locale, MultipartFormDataInput input)
throws IOException {
this.auth.realm().requireManageRealm();
Map<String, List<InputPart>> formDataMap = input.getFormDataMap();
if (!formDataMap.containsKey("file")) {
throw new BadRequestException();
}
InputPart file = formDataMap.get("file").get(0);
try (InputStream inputStream = file.getBody(InputStream.class, null)) {
TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {
};
Map<String, String> rep = JsonSerialization.readValue(inputStream, typeRef);
realm.patchRealmLocalizationTexts(locale, rep);
} catch (IOException e) {
throw new BadRequestException("Could not read file.");
}
}
@PATCH
@Path("{locale}")
@Consumes(MediaType.APPLICATION_JSON)
public void patchRealmLocalizationTexts(@PathParam("locale") String locale, Map<String, String> loclizationTexts) {
this.auth.realm().requireManageRealm();
realm.patchRealmLocalizationTexts(locale, loclizationTexts);
}
@Path("{locale}")
@DELETE
public void deleteRealmLocalizationTexts(@PathParam("locale") String locale) {
this.auth.realm().requireManageRealm();
if(!realm.removeRealmLocalizationTexts(locale)) {
throw new NotFoundException("No localization texts for locale " + locale + " found.");
}
}
@Path("{locale}/{key}")
@DELETE
public void deleteRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key) {
this.auth.realm().requireManageRealm();
if (!session.realms().deleteLocalizationText(realm, locale, key)) {
throw new NotFoundException("Localization text not found");
}
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<String> getRealmLocalizationLocales() {
this.auth.realm().requireViewRealm();
List<String> realmLocalesList = new ArrayList<>(realm.getRealmLocalizationTexts().keySet());
Collections.sort(realmLocalesList);
return realmLocalesList;
}
@Path("{locale}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Map<String, String> getRealmLocalizationTexts(@PathParam("locale") String locale) {
this.auth.realm().requireViewRealm();
return realm.getRealmLocalizationTextsByLocale(locale);
}
@Path("{locale}/{key}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key) {
this.auth.realm().requireViewRealm();
String text = session.realms().getLocalizationTextsById(realm, locale, key);
if (text != null) {
return text;
} else {
throw new NotFoundException("Localization text not found");
}
}
}

View file

@ -26,9 +26,8 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.util.ConcurrentMultivaluedHashMap; import org.keycloak.common.util.ConcurrentMultivaluedHashMap;
import org.keycloak.testsuite.arquillian.TestContext; import org.keycloak.testsuite.arquillian.TestContext;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
* Enlist resources to be cleaned after test method * Enlist resources to be cleaned after test method
@ -46,6 +45,7 @@ public class TestCleanup {
private static final String GROUP_IDS = "GROUP_IDS"; private static final String GROUP_IDS = "GROUP_IDS";
private static final String AUTH_FLOW_IDS = "AUTH_FLOW_IDS"; private static final String AUTH_FLOW_IDS = "AUTH_FLOW_IDS";
private static final String AUTH_CONFIG_IDS = "AUTH_CONFIG_IDS"; private static final String AUTH_CONFIG_IDS = "AUTH_CONFIG_IDS";
private static final String LOCALIZATION_LANGUAGES = "LOCALIZATION_LANGUAGES";
private final TestContext testContext; private final TestContext testContext;
private final String realmName; private final String realmName;
@ -115,6 +115,9 @@ public class TestCleanup {
entities.add(AUTH_FLOW_IDS, flowId); entities.add(AUTH_FLOW_IDS, flowId);
} }
public void addLocalization(String language) {
entities.add(LOCALIZATION_LANGUAGES, language);
}
public void addAuthenticationConfigId(String executionConfigId) { public void addAuthenticationConfigId(String executionConfigId) {
entities.add(AUTH_CONFIG_IDS, executionConfigId); entities.add(AUTH_CONFIG_IDS, executionConfigId);
@ -225,6 +228,17 @@ public class TestCleanup {
} }
} }
} }
List<String> localizationLanguages = entities.get(LOCALIZATION_LANGUAGES);
if (localizationLanguages != null) {
for (String localizationLanguage : localizationLanguages) {
try {
realm.localization().deleteRealmLocalizationTexts(localizationLanguage);
} catch (NotFoundException nfe) {
// Localization texts might be already deleted in the test
}
}
}
} }
private Keycloak getAdminClient() { private Keycloak getAdminClient() {

View file

@ -0,0 +1,130 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.testsuite.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmLocalizationResource;
import java.util.List;
import java.util.Map;
import javax.ws.rs.NotFoundException;
public class RealmRealmLocalizationResourceTest extends AbstractAdminTest {
private RealmLocalizationResource resource;
@Before
public void before() {
adminClient.realm(REALM_NAME).localization().saveRealmLocalizationText("en", "key-a", "text-a_en");
adminClient.realm(REALM_NAME).localization().saveRealmLocalizationText("en", "key-b", "text-b_en");
adminClient.realm(REALM_NAME).localization().saveRealmLocalizationText("de", "key-a", "text-a_de");
getCleanup().addLocalization("en");
getCleanup().addLocalization("de");
resource = adminClient.realm(REALM_NAME).localization();
}
@Test
public void getRealmSpecificLocales() {
List<String> languages = resource.getRealmSpecificLocales();
assertEquals(2, languages.size());
assertThat(languages, CoreMatchers.hasItems("en", "de"));
}
@Test
public void getRealmLocalizationTexts() {
Map<String, String> localizations = resource.getRealmLocalizationTexts("en");
assertNotNull(localizations);
assertEquals(2, localizations.size());
assertEquals("text-a_en", localizations.get("key-a"));
assertEquals("text-b_en", localizations.get("key-b"));
}
@Test
public void getRealmLocalizationsNotExists() {
Map<String, String> localizations = resource.getRealmLocalizationTexts("zz");
assertNotNull(localizations);
assertEquals(0, localizations.size());
}
@Test
public void getRealmLocalizationText() {
String localizationText = resource.getRealmLocalizationText("en", "key-a");
assertNotNull(localizationText);
assertEquals("text-a_en", localizationText);
}
@Test(expected = NotFoundException.class)
public void getRealmLocalizationTextNotExists() {
resource.getRealmLocalizationText("en", "key-zz");
}
@Test
public void addRealmLocalizationText() {
resource.saveRealmLocalizationText("en", "key-c", "text-c");
String localizationText = resource.getRealmLocalizationText("en", "key-c");
assertNotNull(localizationText);
assertEquals("text-c", localizationText);
}
@Test
public void updateRealmLocalizationText() {
resource.saveRealmLocalizationText("en", "key-b", "text-b-new");
String localizationText = resource.getRealmLocalizationText("en", "key-b");
assertNotNull(localizationText);
assertEquals("text-b-new", localizationText);
}
@Test
public void deleteRealmLocalizationText() {
resource.deleteRealmLocalizationText("en", "key-a");
Map<String, String> localizations = resource.getRealmLocalizationTexts("en");
assertEquals(1, localizations.size());
assertEquals("text-b_en", localizations.get("key-b"));
}
@Test(expected = NotFoundException.class)
public void deleteRealmLocalizationTextNotExists() {
resource.deleteRealmLocalizationText("en", "zz");
}
@Test
public void deleteRealmLocalizationTexts() {
resource.deleteRealmLocalizationTexts("en");
List<String> localizations = resource.getRealmSpecificLocales();
assertEquals(1, localizations.size());
assertThat(localizations, CoreMatchers.hasItems("de"));
}
}

View file

@ -62,6 +62,17 @@ i18n-enabled=Internacionalitzaci\u00F3 activa
supported-locales=Idiomes suportats supported-locales=Idiomes suportats
supported-locales.placeholder=Indica l''idioma i prem Intro supported-locales.placeholder=Indica l''idioma i prem Intro
default-locale=Idioma per defecte default-locale=Idioma per defecte
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-enabled=Cach\u00E9 de domini habilitada realm-cache-enabled=Cach\u00E9 de domini habilitada
realm-cache-enabled.tooltip=Activar/desactivar la cach\u00E9 per al domini, client i dades de rols. realm-cache-enabled.tooltip=Activar/desactivar la cach\u00E9 per al domini, client i dades de rols.
user-cache-enabled=Cach\u00E9 d''usuari habilitada user-cache-enabled=Cach\u00E9 d''usuari habilitada
@ -107,6 +118,7 @@ realm-tab-login=Inici de sessi\u00F3
realm-tab-keys=Claus realm-tab-keys=Claus
realm-tab-email=Email realm-tab-email=Email
realm-tab-themes=Temes realm-tab-themes=Temes
#realm-tab-localization=Localization
realm-tab-cache=Cach\u00E9 realm-tab-cache=Cach\u00E9
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-security-defenses=Defenses de seguretat realm-tab-security-defenses=Defenses de seguretat

View file

@ -91,6 +91,18 @@ i18n-enabled=Internationalisierung aktiv
supported-locales=Unterst\u00FCtzte Sprachen supported-locales=Unterst\u00FCtzte Sprachen
#supported-locales.placeholder=Type a locale and enter #supported-locales.placeholder=Type a locale and enter
#default-locale=Default Locale #default-locale=Default Locale
localization-upload-file=Hochladen einer JSON Datei mit Lokalisierungstexten
missing-locale=Locale fehlt.
missing-file=Datei fehlt. Bitte eine Datei f\u00FCr den Upload ausw\u00E4hlen.
localization-file.upload.success=Die Internationalisierungstexte wurden importiert.
localization-file.upload.error=Die Datei konnte nicht hochgeladen werden. Bitte \u00FCberpr\u00FCfen Sie die Datei.
localization-show=Realm-spezifische Lokalisierungstexte
no-localizations-configured=Es sind zur Zeit keine realm-spezifischen Lokalisierungstexte vorhanden.
add-localization-text=Lokalisierungstext hinzuf\u00FCgen
locale.create.success=Die Locale wurde ertellt.
localization-text.create.success=Der Lokalisierungstext wurde erstellt.
localization-text.update.success=Der Lokalisierungstext wurde aktualisiert.
localization-text.remove.success=Der Lokalisierungstext wurde gel\u00F6scht.
#realm-cache-clear=Realm Cache #realm-cache-clear=Realm Cache
#realm-cache-clear.tooltip=Clears all entries from the realm cache (this will clear entries for all realms) #realm-cache-clear.tooltip=Clears all entries from the realm cache (this will clear entries for all realms)
#user-cache-clear=User Cache #user-cache-clear=User Cache
@ -175,6 +187,7 @@ days=Tage
#realm-tab-keys=Keys #realm-tab-keys=Keys
#realm-tab-email=Email #realm-tab-email=Email
#realm-tab-themes=Themes #realm-tab-themes=Themes
realm-tab-localization=Internationalisierung
#realm-tab-cache=Cache #realm-tab-cache=Cache
#realm-tab-tokens=Tokens #realm-tab-tokens=Tokens
#realm-tab-client-registration=Client Registration #realm-tab-client-registration=Client Registration
@ -318,7 +331,7 @@ add-client=Client hinzuf\u00FCgen
#idp-sso-relay-state=IDP Initiated SSO Relay State #idp-sso-relay-state=IDP Initiated SSO Relay State
#idp-sso-relay-state.tooltip=Relay state you want to send with SAML request when you want to do IDP Initiated SSO. #idp-sso-relay-state.tooltip=Relay state you want to send with SAML request when you want to do IDP Initiated SSO.
web-origins=Web Origins web-origins=Web Origins
web-origins.tooltip=Erlaubte CORS Origins. Um alle Origins der Valid Redirect URIs zu erlauben, fügen Sie ein '+' hinzu. Dabei wird der '*' Platzhalter nicht mit übernommen. Um alle Origins zu erlauben, geben Sie explizit einen Eintrag mit '*' an. web-origins.tooltip=Erlaubte CORS Origins. Um alle Origins der Valid Redirect URIs zu erlauben, f\u00FCgen Sie ein '+' hinzu. Dabei wird der '*' Platzhalter nicht mit \u00FCbernommen. Um alle Origins zu erlauben, geben Sie explizit einen Eintrag mit '*' an.
#fine-oidc-endpoint-conf=Fine Grain OpenID Connect Configuration #fine-oidc-endpoint-conf=Fine Grain OpenID Connect Configuration
#fine-oidc-endpoint-conf.tooltip=Expand this section to configure advanced settings of this client related to OpenID Connect protocol #fine-oidc-endpoint-conf.tooltip=Expand this section to configure advanced settings of this client related to OpenID Connect protocol
#user-info-signed-response-alg=User Info Signed Response Algorithm #user-info-signed-response-alg=User Info Signed Response Algorithm
@ -505,13 +518,13 @@ last-refresh=Letzte Aktualisierung
#first-broker-login-flow=First Login Flow #first-broker-login-flow=First Login Flow
#post-broker-login-flow=Post Login Flow #post-broker-login-flow=Post Login Flow
sync-mode=Synchronisationsmodus sync-mode=Synchronisationsmodus
sync-mode.tooltip=Standardsyncmodus für alle Mapper. Mögliche Werte sind: 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu importieren. sync-mode.tooltip=Standardsyncmodus f\u00FCr alle Mapper. M\u00F6gliche Werte sind: 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu importieren.
sync-mode.inherit=Standard erben sync-mode.inherit=Standard erben
sync-mode.legacy=Legacy sync-mode.legacy=Legacy
sync-mode.import=Importieren sync-mode.import=Importieren
sync-mode.force=Erzwingen sync-mode.force=Erzwingen
sync-mode-override=Ãœberschriebene Synchronisation sync-mode-override=\u00DCberschriebene Synchronisation
sync-mode-override.tooltip=Überschreibt den normalen Synchronisationsmodus des IDP für diesen Mapper. Were sind 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu updaten. sync-mode-override.tooltip=\u00DCberschreibt den normalen Synchronisationsmodus des IDP f\u00FCr diesen Mapper. Were sind 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu updaten.
#redirect-uri=Redirect URI #redirect-uri=Redirect URI
#redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. #redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
#alias=Alias #alias=Alias
@ -706,16 +719,16 @@ group.assigned-roles.tooltip=Realm-Rollen die zur Gruppe zugeordnet sind
#group.effective-roles-client.tooltip=Role mappings for this client. Some roles here might be inherited from a mapped composite role. #group.effective-roles-client.tooltip=Role mappings for this client. Some roles here might be inherited from a mapped composite role.
group.move.success=Gruppe verschoben. group.move.success=Gruppe verschoben.
group.remove.confirm.title=Gruppe löschen group.remove.confirm.title=Gruppe l\u00F6schen
group.remove.confirm.message=Sind Sie sicher, dass Sie die Gruppe \u201E{{name}}\u201C löschen möchten? group.remove.confirm.message=Sind Sie sicher, dass Sie die Gruppe \u201E{{name}}\u201C l\u00F6schen m\u00F6chten?
group.remove.success=Die Gruppe wurde gelöscht. group.remove.success=Die Gruppe wurde gel\u00F6scht.
group.fetch.fail=Fehler beim Laden: {{params}} group.fetch.fail=Fehler beim Laden: {{params}}
group.create.success=Gruppe erstellt. group.create.success=Gruppe erstellt.
group.edit.success=Die Änderungen wurde gespeichert. group.edit.success=Die \u00C4nderungen wurde gespeichert.
group.roles.add.success=Rollenzuweisung hinzugefügt. group.roles.add.success=Rollenzuweisung hinzugef\u00FCgt.
group.roles.remove.success=Rollenzuweisung entfernt. group.roles.remove.success=Rollenzuweisung entfernt.
group.default.add.error=Bitte eine Gruppe auswählen. group.default.add.error=Bitte eine Gruppe ausw\u00E4hlen.
group.default.add.success=Standardgruppe hinzugefügt. group.default.add.success=Standardgruppe hinzugef\u00FCgt.
group.default.remove.success=Standardgruppe entfernt. group.default.remove.success=Standardgruppe entfernt.
default-roles=Standardrollen default-roles=Standardrollen
@ -729,49 +742,49 @@ user.effective-roles.tooltip=Alle Realm-Rollen-Zuweisungen. Einige Rollen hier k
#user.assigned-roles-client.tooltip=Role mappings for this client. #user.assigned-roles-client.tooltip=Role mappings for this client.
#user.effective-roles-client.tooltip=Role mappings for this client. Some roles here might be inherited from a mapped composite role. #user.effective-roles-client.tooltip=Role mappings for this client. Some roles here might be inherited from a mapped composite role.
user.roles.add.success=Rollenzuweisung hinzugefügt. user.roles.add.success=Rollenzuweisung hinzugef\u00FCgt.
user.roles.remove.success=Rollenzuweisung entfernt. user.roles.remove.success=Rollenzuweisung entfernt.
user.logout.all.success=Benutzer von allen Sitzungen abgemeldet. user.logout.all.success=Benutzer von allen Sitzungen abgemeldet.
user.logout.session.success=Benutzer von Sitzung abgemeldet. user.logout.session.success=Benutzer von Sitzung abgemeldet.
user.fedid.link.remove.confirm.title=Verknüpfung mit Identity Provider entfernen user.fedid.link.remove.confirm.title=Verkn\u00FCpfung mit Identity Provider entfernen
user.fedid.link.remove.confirm.message=Sind Sie sicher, dass Sie die Verknüpfung mit dem Identity Provider \u201E{{name}}\u201C entfernen möchten? user.fedid.link.remove.confirm.message=Sind Sie sicher, dass Sie die Verkn\u00FCpfung mit dem Identity Provider \u201E{{name}}\u201C entfernen m\u00F6chten?
user.fedid.link.remove.success=Verknüpfung mit Identity Provider entfernt. user.fedid.link.remove.success=Verkn\u00FCpfung mit Identity Provider entfernt.
user.fedid.link.add.success=Verknüpfung mit Identity Provider angelegt. user.fedid.link.add.success=Verkn\u00FCpfung mit Identity Provider angelegt.
user.consent.revoke.success=Einwilligung widerrufen. user.consent.revoke.success=Einwilligung widerrufen.
user.consent.revoke.error=Einwilligung konnte nicht widerrufen werden. user.consent.revoke.error=Einwilligung konnte nicht widerrufen werden.
user.unlock.success=Alle vorübergehend gesperrten Benutzer wurden entsperrt. user.unlock.success=Alle vor\u00FCbergehend gesperrten Benutzer wurden entsperrt.
user.remove.confirm.title=Benutzer löschen user.remove.confirm.title=Benutzer l\u00F6schen
user.remove.confirm.message=Sind Sie sicher, dass Sie den Benutzer \u201E{{name}}\u201C löschen möchten? user.remove.confirm.message=Sind Sie sicher, dass Sie den Benutzer \u201E{{name}}\u201C l\u00F6schen m\u00F6chten?
user.remove.success=Der Benutzer wurde gelöscht. user.remove.success=Der Benutzer wurde gel\u00F6scht.
user.remove.error=Der Benutzer konnte nicht gelöscht werden. user.remove.error=Der Benutzer konnte nicht gel\u00F6scht werden.
user.create.success=Der Benutzer wurde angelegt. user.create.success=Der Benutzer wurde angelegt.
user.edit.success=Die Änderungen wurden gespeichert. user.edit.success=Die \u00C4nderungen wurden gespeichert.
user.credential.update.success=Die Zugangsdaten wurdern gespeichert. user.credential.update.success=Die Zugangsdaten wurdern gespeichert.
user.credential.update.error=Beim Speichern der Zugangsdaten ist ein Fehler aufgetreten. user.credential.update.error=Beim Speichern der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.remove.confirm.title=Zugangsdaten löschen user.credential.remove.confirm.title=Zugangsdaten l\u00F6schen
user.credential.remove.confirm.message=Sind Sie sicher, dass Sie die Zugangsdaten löschen löschen möchten? user.credential.remove.confirm.message=Sind Sie sicher, dass Sie die Zugangsdaten l\u00F6schen m\u00F6chten?
user.credential.remove.success=Die Zugangsdaten wurden gelöscht. user.credential.remove.success=Die Zugangsdaten wurden gel\u00F6scht.
user.credential.remove.error=Beim Löschen der Zugangsdaten ist ein Fehler aufgetreten. user.credential.remove.error=Beim L\u00F6schen der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.move-top.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten. user.credential.move-top.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.move-up.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten. user.credential.move-up.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.move-down.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten. user.credential.move-down.error=Beim Verschieben der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.fetch.error=Beim Laden der Zugangsdaten ist ein Fehler aufgetreten. user.credential.fetch.error=Beim Laden der Zugangsdaten ist ein Fehler aufgetreten.
#user.credential.storage.fetch.error=Error while loading user storage credentials. See console for more information. #user.credential.storage.fetch.error=Error while loading user storage credentials. See console for more information.
user.password.error.not-matching=Die Passwörter stimmen nicht überein. user.password.error.not-matching=Die Passw\u00F6rter stimmen nicht \u00FCberein.
user.password.reset.confirm.title=Passwort zurücksetzen user.password.reset.confirm.title=Passwort zur\u00FCcksetzen
user.password.reset.confirm.message=Sind Sie sicher, dass Sie das Passwort für diesen Benutzer zurücksetzen möchten? user.password.reset.confirm.message=Sind Sie sicher, dass Sie das Passwort f\u00FCr diesen Benutzer zur\u00FCcksetzen m\u00F6chten?
user.password.reset.success=Das Passwort wurde zurückgesetzt. user.password.reset.success=Das Passwort wurde zur\u00FCckgesetzt.
user.password.set.confirm.title=Passwort setzen user.password.set.confirm.title=Passwort setzen
user.password.set.confirm.message=Sind Sie sicher, dass Sie ein Passwort für diesen Benutzer setzen möchten? user.password.set.confirm.message=Sind Sie sicher, dass Sie ein Passwort f\u00FCr diesen Benutzer setzen m\u00F6chten?
user.password.set.success=Das Passwort wurde gesetzt. user.password.set.success=Das Passwort wurde gesetzt.
user.credential.disable.confirm.title=Zugangsdaten deaktivieren user.credential.disable.confirm.title=Zugangsdaten deaktivieren
user.credential.disable.confirm.message=Sind Sie sicher, dass Sie diese Zugangsdaten deaktivieren möchten? user.credential.disable.confirm.message=Sind Sie sicher, dass Sie diese Zugangsdaten deaktivieren m\u00F6chten?
user.credential.disable.confirm.success=Zugangsdaten deaktiviert. user.credential.disable.confirm.success=Zugangsdaten deaktiviert.
user.credential.disable.confirm.error=Fehler beim Deaktivieren der Zugangsdaten user.credential.disable.confirm.error=Fehler beim Deaktivieren der Zugangsdaten
user.actions-email.send.pending-changes.title=E-Mail kann nicht gesendet werden. user.actions-email.send.pending-changes.title=E-Mail kann nicht gesendet werden.
user.actions-email.send.pending-changes.message=Bitte speichern Sie Ihre Änderungen bevor Sie die E-Mail senden. user.actions-email.send.pending-changes.message=Bitte speichern Sie Ihre \u00C4nderungen bevor Sie die E-Mail senden.
user.actions-email.send.confirm.title=E-Mail senden user.actions-email.send.confirm.title=E-Mail senden
user.actions-email.send.confirm.message=Sind Sie sicher, dass Sie die E-Mail an den Benutzer senden möchten? user.actions-email.send.confirm.message=Sind Sie sicher, dass Sie die E-Mail an den Benutzer senden m\u00F6chten?
user.actions-email.send.confirm.success=E-Mail an Benutzer gesendet. user.actions-email.send.confirm.success=E-Mail an Benutzer gesendet.
user.actions-email.send.confirm.error=Fehler beim Senden der E-Mail user.actions-email.send.confirm.error=Fehler beim Senden der E-Mail
#user.storage.remove.confirm.title=Delete User storage provider #user.storage.remove.confirm.title=Delete User storage provider
@ -787,10 +800,10 @@ user.actions-email.send.confirm.error=Fehler beim Senden der E-Mail
#user.storage.unlink.error=Error during unlink #user.storage.unlink.error=Error during unlink
user.groups.fetch.all.error=Fehler beim Laden alle Gruppen: {{params}} user.groups.fetch.all.error=Fehler beim Laden alle Gruppen: {{params}}
user.groups.fetch.error=Fehler beim Laden: {{params}} user.groups.fetch.error=Fehler beim Laden: {{params}}
user.groups.join.error.no-group-selected=Bitte wählen Sie eine Gruppe aus! user.groups.join.error.no-group-selected=Bitte w\u00E4hlen Sie eine Gruppe aus!
user.groups.join.error.already-added=Benutzer gehört der Gruppe bereits an. user.groups.join.error.already-added=Benutzer geh\u00F6rt der Gruppe bereits an.
user.groups.join.success=Zur Gruppe hinzugefügt. user.groups.join.success=Zur Gruppe hinzugef\u00FCgt.
user.groups.leave.error.no-group-selected=Bitte wählen Sie eine Gruppe aus! user.groups.leave.error.no-group-selected=Bitte w\u00E4hlen Sie eine Gruppe aus!
user.groups.leave.success=Aus Gruppe entfernt. user.groups.leave.success=Aus Gruppe entfernt.
#default.available-roles.tooltip=Realm level roles that can be assigned. #default.available-roles.tooltip=Realm level roles that can be assigned.
@ -1608,8 +1621,8 @@ notifications.success.header=Erfolg!
notifications.error.header=Fehler! notifications.error.header=Fehler!
notifications.warn.header=Warnung! notifications.warn.header=Warnung!
dialogs.delete.title={{type}} löschen dialogs.delete.title={{type}} l\u00F6schen
dialogs.delete.message=Sind Sie sicher, dass Sie {{type}} {{name}} löschen möchten? dialogs.delete.message=Sind Sie sicher, dass Sie {{type}} {{name}} l\u00F6schen m\u00F6chten?
dialogs.delete.confirm=Löschen dialogs.delete.confirm=L\u00F6schen
dialogs.cancel=Abbrechen dialogs.cancel=Abbrechen
dialogs.ok=OK dialogs.ok=OK

View file

@ -62,6 +62,18 @@ i18n-enabled=Internacionalizaci\u00F3n activa
supported-locales=Idiomas soportados supported-locales=Idiomas soportados
supported-locales.placeholder=Indica el idioma y pulsa Intro supported-locales.placeholder=Indica el idioma y pulsa Intro
default-locale=Idioma por defecto default-locale=Idioma por defecto
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-enabled=Cach\u00E9 de dominio habilitada realm-cache-enabled=Cach\u00E9 de dominio habilitada
realm-cache-enabled.tooltip=Activar/desactivar la cach\u00E9 para el dominio, cliente y datos de roles. realm-cache-enabled.tooltip=Activar/desactivar la cach\u00E9 para el dominio, cliente y datos de roles.
user-cache-enabled=Cach\u00E9 de usuario habilitada user-cache-enabled=Cach\u00E9 de usuario habilitada
@ -107,6 +119,7 @@ realm-tab-login=Inicio de sesi\u00F3n
realm-tab-keys=Claves realm-tab-keys=Claves
realm-tab-email=Email realm-tab-email=Email
realm-tab-themes=Temas realm-tab-themes=Temas
#realm-tab-localization=Localization
realm-tab-cache=Cach\u00E9 realm-tab-cache=Cach\u00E9
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-security-defenses=Defensas de seguridad realm-tab-security-defenses=Defensas de seguridad

View file

@ -82,6 +82,18 @@ i18n-enabled=Internationalisation activ\u00e9e
supported-locales=Locales support\u00e9es supported-locales=Locales support\u00e9es
supported-locales.placeholder=Entrez la locale et validez supported-locales.placeholder=Entrez la locale et validez
default-locale=Locale par d\u00e9faut default-locale=Locale par d\u00e9faut
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-enabled=Cache du domaine activ\u00e9 realm-cache-enabled=Cache du domaine activ\u00e9
realm-cache-enabled.tooltip=Activer/D\u00e9sactiver le cache pour le domaine, client et donn\u00e9es. realm-cache-enabled.tooltip=Activer/D\u00e9sactiver le cache pour le domaine, client et donn\u00e9es.
user-cache-enabled=Cache utilisateur activ\u00e9 user-cache-enabled=Cache utilisateur activ\u00e9
@ -123,6 +135,7 @@ realm-tab-login=Connexion
realm-tab-keys=Clefs realm-tab-keys=Clefs
realm-tab-email=Courriels realm-tab-email=Courriels
realm-tab-themes=Th\u00e8mes realm-tab-themes=Th\u00e8mes
#realm-tab-localization=Localization
realm-tab-cache=Cache realm-tab-cache=Cache
realm-tab-tokens=Jetons realm-tab-tokens=Jetons
realm-tab-security-defenses=Mesures de s\u00e9curit\u00e9 realm-tab-security-defenses=Mesures de s\u00e9curit\u00e9

View file

@ -93,6 +93,18 @@ i18n-enabled=国際化の有効
supported-locales=サポートされるロケール supported-locales=サポートされるロケール
supported-locales.placeholder=ロケールを入力し、Enterキーを押してください supported-locales.placeholder=ロケールを入力し、Enterキーを押してください
default-locale=デフォルト・ロケール default-locale=デフォルト・ロケール
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=レルムキャッシュ realm-cache-clear=レルムキャッシュ
realm-cache-clear.tooltip=レルムキャッシュからすべてのエントリーをクリアする(これにより、すべてのレルムのエントリーがクリアされます)。 realm-cache-clear.tooltip=レルムキャッシュからすべてのエントリーをクリアする(これにより、すべてのレルムのエントリーがクリアされます)。
user-cache-clear=ユーザー・キャッシュ user-cache-clear=ユーザー・キャッシュ
@ -192,6 +204,7 @@ realm-tab-login=ログイン
realm-tab-keys= realm-tab-keys=
realm-tab-email=Eメール realm-tab-email=Eメール
realm-tab-themes=テーマ realm-tab-themes=テーマ
#realm-tab-localization=Localization
realm-tab-cache=キャッシュ realm-tab-cache=キャッシュ
realm-tab-tokens=トークン realm-tab-tokens=トークン
realm-tab-client-registration=クライアント登録 realm-tab-client-registration=クライアント登録

View file

@ -69,6 +69,18 @@ i18n-enabled=Daugiakalbystės palaikymas
supported-locales=Palaikomos kalbos supported-locales=Palaikomos kalbos
supported-locales.placeholder=Pasirinkite arba įrašykite kalbos pavadinimą supported-locales.placeholder=Pasirinkite arba įrašykite kalbos pavadinimą
default-locale=Numatyta kalba default-locale=Numatyta kalba
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=Srities podėlis realm-cache-clear=Srities podėlis
realm-cache-clear.tooltip=Iš visų sričių pašalinama visa podėlyje (cache) esanti informacija realm-cache-clear.tooltip=Iš visų sričių pašalinama visa podėlyje (cache) esanti informacija
user-cache-clear=Naudotojų podėlis user-cache-clear=Naudotojų podėlis
@ -119,6 +131,7 @@ realm-tab-login=Prisijungimas
realm-tab-keys=Raktai realm-tab-keys=Raktai
realm-tab-email=El. paštas realm-tab-email=El. paštas
realm-tab-themes=Temos realm-tab-themes=Temos
#realm-tab-localization=Localization
realm-tab-cache=Podėlis realm-tab-cache=Podėlis
realm-tab-tokens=Raktai realm-tab-tokens=Raktai
realm-tab-client-registration=Klientų registracija realm-tab-client-registration=Klientų registracija

View file

@ -68,6 +68,18 @@ i18n-enabled=Internasjonalisering aktivert
supported-locales=St\u00F8ttede lokaliteter supported-locales=St\u00F8ttede lokaliteter
supported-locales.placeholder=Skriv inn en lokalitet og klikk enter supported-locales.placeholder=Skriv inn en lokalitet og klikk enter
default-locale=Standard lokalitet default-locale=Standard lokalitet
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=Cache for sikkerhetsdomenet realm-cache-clear=Cache for sikkerhetsdomenet
realm-cache-clear.tooltip=T\u00F8m sikkerhetsdomenecache (Dette vil fjerne oppf\u00F8ringer for alle sikkerhetsdomener) realm-cache-clear.tooltip=T\u00F8m sikkerhetsdomenecache (Dette vil fjerne oppf\u00F8ringer for alle sikkerhetsdomener)
user-cache-clear=Brukercache user-cache-clear=Brukercache
@ -120,6 +132,7 @@ realm-tab-login=Innlogging
realm-tab-keys=N\u00F8kler realm-tab-keys=N\u00F8kler
realm-tab-email=E-post realm-tab-email=E-post
realm-tab-themes=Tema realm-tab-themes=Tema
#realm-tab-localization=Localization
realm-tab-cache=Cache realm-tab-cache=Cache
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-client-initial-access=F\u00F8rste access token realm-tab-client-initial-access=F\u00F8rste access token

View file

@ -69,6 +69,18 @@ i18n-enabled=Habilitar internacionalização
supported-locales=Locais disponíveis supported-locales=Locais disponíveis
supported-locales.placeholder=Digite um local e pressione Enter supported-locales.placeholder=Digite um local e pressione Enter
default-locale=Local padrão default-locale=Local padrão
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=Realm Cache realm-cache-clear=Realm Cache
realm-cache-clear.tooltip=Remove todas as entradas do cache de realm (isto irá remover as entradas para todos os realms) realm-cache-clear.tooltip=Remove todas as entradas do cache de realm (isto irá remover as entradas para todos os realms)
user-cache-clear=Cache de usuário user-cache-clear=Cache de usuário
@ -119,6 +131,7 @@ realm-tab-login=Login
realm-tab-keys=Chaves realm-tab-keys=Chaves
realm-tab-email=E-mail realm-tab-email=E-mail
realm-tab-themes=Temas realm-tab-themes=Temas
#realm-tab-localization=Localization
realm-tab-cache=Cache realm-tab-cache=Cache
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-client-initial-access=Tokens de Acesso inicial realm-tab-client-initial-access=Tokens de Acesso inicial

View file

@ -75,6 +75,18 @@ i18n-enabled=Интернационализация
supported-locales=Поддерживаемые языки supported-locales=Поддерживаемые языки
supported-locales.placeholder=Выберите язык и нажмите Enter supported-locales.placeholder=Выберите язык и нажмите Enter
default-locale=Язык по умолчанию default-locale=Язык по умолчанию
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=Кэш Realm realm-cache-clear=Кэш Realm
realm-cache-clear.tooltip=Удалить все записи в кэше realm (удалит все записи для всех realm) realm-cache-clear.tooltip=Удалить все записи в кэше realm (удалит все записи для всех realm)
user-cache-clear=Кэш пользователей user-cache-clear=Кэш пользователей
@ -127,6 +139,7 @@ realm-tab-login=Вход
realm-tab-keys=Ключи realm-tab-keys=Ключи
realm-tab-email=E-mail realm-tab-email=E-mail
realm-tab-themes=Темы realm-tab-themes=Темы
#realm-tab-localization=Localization
realm-tab-cache=Кэш realm-tab-cache=Кэш
realm-tab-tokens=Токены realm-tab-tokens=Токены
realm-tab-client-initial-access=Первоначальные токены доступа realm-tab-client-initial-access=Первоначальные токены доступа

View file

@ -69,6 +69,18 @@ i18n-enabled=启用国际化
supported-locales=支持的语言 supported-locales=支持的语言
supported-locales.placeholder=输入一个locale并按回车 supported-locales.placeholder=输入一个locale并按回车
default-locale=默认语言 default-locale=默认语言
#localization-upload-file=Upload localization JSON file
#missing-locale=Missing locale.
#missing-file=Missing file. Please select a file to upload.
#localization-file.upload.success=The localization data has been loaded from file.
#localization-file.upload.error=The file can not be uploaded. Please verify the file.
#localization-show=Show realm specific localizations
#no-localizations-configured=No realm specific localizations configured
#add-localization-text=Add localization text
#locale.create.success=The Locale has been created.
#localization-text.create.success=The localization text has been created.
#localization-text.update.success=The localization text has been updated.
#localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=域缓存 realm-cache-clear=域缓存
realm-cache-clear.tooltip=从域缓存中清理所有条目(这会清理所有域的条目) realm-cache-clear.tooltip=从域缓存中清理所有条目(这会清理所有域的条目)
user-cache-clear=用户缓存 user-cache-clear=用户缓存
@ -119,6 +131,7 @@ realm-tab-login=登录
realm-tab-keys=秘钥 realm-tab-keys=秘钥
realm-tab-email=Email realm-tab-email=Email
realm-tab-themes=主题 realm-tab-themes=主题
#realm-tab-localization=Localization
realm-tab-cache=缓存 realm-tab-cache=缓存
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-client-registration=客户端注册 realm-tab-client-registration=客户端注册

View file

@ -96,6 +96,17 @@ i18n-enabled=Internationalization Enabled
supported-locales=Supported Locales supported-locales=Supported Locales
supported-locales.placeholder=Type a locale and enter supported-locales.placeholder=Type a locale and enter
default-locale=Default Locale default-locale=Default Locale
localization-upload-file=Upload localization JSON file
missing-locale=Missing locale.
missing-file=Missing file. Please select a file to upload.
localization-file.upload.success=The localization data has been loaded from file.
localization-file.upload.error=The file can not be uploaded. Please verify the file.
localization-show=Show realm specific localizations
no-localizations-configured=No realm specific localizations configured
add-localization-text=Add localization text
localization-text.create.success=The localization text has been created.
localization-text.update.success=The localization text has been updated.
localization-text.remove.success=The localization text has been deleted.
realm-cache-clear=Realm Cache realm-cache-clear=Realm Cache
realm-cache-clear.tooltip=Clears all entries from the realm cache (this will clear entries for all realms) realm-cache-clear.tooltip=Clears all entries from the realm cache (this will clear entries for all realms)
user-cache-clear=User Cache user-cache-clear=User Cache
@ -199,6 +210,7 @@ realm-tab-login=Login
realm-tab-keys=Keys realm-tab-keys=Keys
realm-tab-email=Email realm-tab-email=Email
realm-tab-themes=Themes realm-tab-themes=Themes
realm-tab-localization=Localization
realm-tab-cache=Cache realm-tab-cache=Cache
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-client-registration=Client Registration realm-tab-client-registration=Client Registration

View file

@ -7,6 +7,8 @@ var locale = 'en';
var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'angularTreeview', 'pascalprecht.translate', 'ngCookies', 'ngSanitize', 'ui.ace']); var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'angularTreeview', 'pascalprecht.translate', 'ngCookies', 'ngSanitize', 'ui.ace']);
var resourceRequests = 0; var resourceRequests = 0;
var loadingTimer = -1; var loadingTimer = -1;
var translateProvider = null;
var currentRealm = null;
angular.element(document).ready(function () { angular.element(document).ready(function () {
var keycloakAuth = new Keycloak(consoleBaseUrl + 'config'); var keycloakAuth = new Keycloak(consoleBaseUrl + 'config');
@ -146,6 +148,7 @@ module.factory('authInterceptor', function($q, Auth) {
}); });
module.config(['$translateProvider', function($translateProvider) { module.config(['$translateProvider', function($translateProvider) {
translateProvider = $translateProvider;
$translateProvider.useSanitizeValueStrategy('sanitizeParameters'); $translateProvider.useSanitizeValueStrategy('sanitizeParameters');
$translateProvider.preferredLanguage(locale); $translateProvider.preferredLanguage(locale);
$translateProvider.translations(locale, resourceBundle); $translateProvider.translations(locale, resourceBundle);
@ -178,6 +181,33 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'RealmDetailCtrl' controller : 'RealmDetailCtrl'
}) })
.when('/realms/:realm/localization', {
templateUrl : resourceUrl + '/partials/realm-localization.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
},
realmSpecificLocales : function(RealmSpecificLocalesLoader) {
return RealmSpecificLocalesLoader();
}
},
controller : 'RealmLocalizationCtrl'
})
.when('/realms/:realm/localization/upload', {
templateUrl : resourceUrl + '/partials/realm-localization-upload.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'RealmLocalizationUploadCtrl'
})
.when('/realms/:realm/login-settings', { .when('/realms/:realm/login-settings', {
templateUrl : resourceUrl + '/partials/realm-login-settings.html', templateUrl : resourceUrl + '/partials/realm-login-settings.html',
resolve : { resolve : {
@ -2085,6 +2115,42 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'AuthenticationConfigCreateCtrl' controller : 'AuthenticationConfigCreateCtrl'
}) })
.when('/create/localization/:realm/:locale', {
templateUrl : resourceUrl + '/partials/realm-localization-detail.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
locale: function($route) {
return $route.current.params.locale;
},
key: function() {
return null
},
localizationText : function() {
return null;
}
},
controller : 'RealmLocalizationDetailCtrl'
})
.when('/realms/:realm/localization/:locale/:key', {
templateUrl : resourceUrl + '/partials/realm-localization-detail.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
locale: function($route) {
return $route.current.params.locale;
},
key: function($route) {
return $route.current.params.key;
},
localizationText : function(RealmSpecificlocalizationTextLoader) {
return RealmSpecificlocalizationTextLoader();
}
},
controller : 'RealmLocalizationDetailCtrl'
})
.when('/server-info', { .when('/server-info', {
templateUrl : resourceUrl + '/partials/server-info.html', templateUrl : resourceUrl + '/partials/server-info.html',
resolve : { resolve : {

View file

@ -83,7 +83,7 @@ function getAccessObject(Auth, Current) {
} }
module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location, Notifications, ServerInfo) { module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location, Notifications, ServerInfo, RealmSpecificLocalizationTexts) {
$scope.authUrl = authUrl; $scope.authUrl = authUrl;
$scope.resourceUrl = resourceUrl; $scope.resourceUrl = resourceUrl;
$scope.auth = Auth; $scope.auth = Auth;
@ -97,6 +97,18 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location
$scope.fragment = $location.path(); $scope.fragment = $location.path();
$scope.path = $location.path().substring(1).split("/"); $scope.path = $location.path().substring(1).split("/");
}); });
$scope.$watch(function() {
return Current.realm;
}, function() {
if(Current.realm !== null && currentRealm !== Current.realm.id) {
currentRealm = Current.realm.id;
translateProvider.translations(locale, resourceBundle);
RealmSpecificLocalizationTexts.get({id: currentRealm, locale: locale}, function (localizationTexts) {
translateProvider.translations(locale, localizationTexts.toJSON());
})
}
})
}); });
module.controller('HomeCtrl', function(Realm, Auth, Current, $location) { module.controller('HomeCtrl', function(Realm, Auth, Current, $location) {
@ -499,6 +511,131 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
$scope.$watch('realm.internationalizationEnabled', updateSupported); $scope.$watch('realm.internationalizationEnabled', updateSupported);
}); });
module.controller('RealmLocalizationCtrl', function($scope, Current, $location, Realm, realm, serverInfo, Notifications, RealmSpecificLocales, realmSpecificLocales, RealmSpecificLocalizationTexts, RealmSpecificLocalizationText, Dialog, $translate){
$scope.realm = realm;
$scope.realmSpecificLocales = realmSpecificLocales;
$scope.newLocale = null;
$scope.selectedRealmSpecificLocales = null;
$scope.localizationTexts = null;
$scope.createLocale = function() {
if(!$scope.newLocale) {
Notifications.error($translate.instant('missing-locale'));
return;
}
$scope.realmSpecificLocales.push($scope.newLocale)
$scope.selectedRealmSpecificLocales = $scope.newLocale;
$scope.newLocale = null;
$location.url('/create/localization/' + realm.realm + '/' + $scope.selectedRealmSpecificLocales);
}
$scope.$watch(function() {
return $scope.selectedRealmSpecificLocales;
}, function() {
if($scope.selectedRealmSpecificLocales != null) {
$scope.updateRealmSpecificLocalizationTexts();
}
})
$scope.updateRealmSpecificLocales = function() {
RealmSpecificLocales.get({id: realm.realm}, function (updated) {
$scope.realmSpecificLocales = updated;
})
}
$scope.updateRealmSpecificLocalizationTexts = function() {
RealmSpecificLocalizationTexts.get({id: realm.realm, locale: $scope.selectedRealmSpecificLocales }, function (updated) {
$scope.localizationTexts = updated;
})
}
$scope.removeLocalizationText = function(key) {
Dialog.confirmDelete(key, 'localization text', function() {
RealmSpecificLocalizationText.remove({
realm: realm.realm,
locale: $scope.selectedRealmSpecificLocales,
key: key
}, function () {
$scope.updateRealmSpecificLocalizationTexts();
Notifications.success($translate.instant('localization-text.remove.success'));
});
});
}
});
module.controller('RealmLocalizationUploadCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, $upload, $translate){
$scope.realm = realm;
$scope.locale = null;
$scope.files = [];
$scope.onFileSelect = function($files) {
$scope.files = $files;
};
$scope.reset = function() {
$scope.locale = null;
$scope.files = null;
};
$scope.save = function() {
if(!$scope.files || $scope.files.length === 0) {
Notifications.error($translate.instant('missing-file'));
return;
}
//$files: an array of files selected, each file has name, size, and type.
for (var i = 0; i < $scope.files.length; i++) {
var $file = $scope.files[i];
$scope.upload = $upload.upload({
url: authUrl + '/admin/realms/' + realm.realm + '/localization/' + $scope.locale,
file: $file
}).then(function(response) {
$scope.reset();
Notifications.success($translate.instant('localization-file.upload.success'));
}).catch(function() {
Notifications.error($translate.instant('localization-file.upload.error'));
});
}
};
});
module.controller('RealmLocalizationDetailCtrl', function($scope, Current, $location, Realm, realm, Notifications, locale, key, RealmSpecificLocalizationText, localizationText, $translate){
$scope.realm = realm;
$scope.locale = locale;
$scope.key = key;
$scope.value = ((localizationText)? localizationText.content : null);
$scope.create = !key;
$scope.save = function() {
if ($scope.create) {
RealmSpecificLocalizationText.save({
realm: realm.realm,
locale: $scope.locale,
key: $scope.key
}, $scope.value, function (data, headers) {
$location.url("/realms/" + realm.realm + "/localization");
Notifications.success($translate.instant('localization-text.create.success'));
});
} else {
RealmSpecificLocalizationText.save({
realm: realm.realm,
locale: $scope.locale,
key: $scope.key
}, $scope.value, function (data, headers) {
$location.url("/realms/" + realm.realm + "/localization");
Notifications.success($translate.instant('localization-text.update.success'));
});
}
};
$scope.cancel = function () {
$location.url("/realms/" + realm.realm + "/localization");
};
});
module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache, RealmClearRealmCache, RealmClearKeysCache, Notifications) { module.controller('RealmCacheCtrl', function($scope, realm, RealmClearUserCache, RealmClearRealmCache, RealmClearKeysCache, Notifications) {
$scope.realm = angular.copy(realm); $scope.realm = angular.copy(realm);

View file

@ -57,6 +57,24 @@ module.factory('RealmKeysLoader', function(Loader, RealmKeys, $route, $q) {
}); });
}); });
module.factory('RealmSpecificLocalesLoader', function(Loader, RealmSpecificLocales, $route, $q) {
return Loader.get(RealmSpecificLocales, function() {
return {
id : $route.current.params.realm
}
});
});
module.factory('RealmSpecificlocalizationTextLoader', function(Loader, RealmSpecificLocalizationText, $route, $q) {
return Loader.get(RealmSpecificLocalizationText, function() {
return {
realm : $route.current.params.realm,
locale : $route.current.params.locale,
key: $route.current.params.key
}
});
});
module.factory('RealmEventsConfigLoader', function(Loader, RealmEventsConfig, $route, $q) { module.factory('RealmEventsConfigLoader', function(Loader, RealmEventsConfig, $route, $q) {
return Loader.get(RealmEventsConfig, function() { return Loader.get(RealmEventsConfig, function() {
return { return {

View file

@ -377,6 +377,41 @@ module.factory('RealmKeys', function($resource) {
}); });
}); });
module.factory('RealmSpecificLocales', function($resource) {
return $resource(authUrl + '/admin/realms/:id/localization', {
id : '@realm'
},{'get': {method:'GET', isArray:true}});
});
module.factory('RealmSpecificLocalizationTexts', function($resource) {
return $resource(authUrl + '/admin/realms/:id/localization/:locale', {
id : '@realm',
locale : '@locale'
});
});
module.factory('RealmSpecificLocalizationText', function ($resource) {
return $resource(authUrl + '/admin/realms/:realm/localization/:locale/:key', {
realm: '@realm',
locale: '@locale',
key: '@key'
}, {
// wrap plain text response as AngularJS $resource will convert it into a char array otherwise.
get: {
method: 'GET',
transformResponse: function (data) {
return {content: data};
}
},
save: {
method: 'PUT',
headers: {
'Content-Type': 'text/plain;charset=utf-8'
}
}
});
});
module.factory('RealmEventsConfig', function($resource) { module.factory('RealmEventsConfig', function($resource) {
return $resource(authUrl + '/admin/realms/:id/events/config', { return $resource(authUrl + '/admin/realms/:id/events/config', {
id : '@realm' id : '@realm'

View file

@ -0,0 +1,50 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/localization">{{:: 'localization' | translate}}</a></li>
<li data-ng-hide="create">{{key}}</li>
<li data-ng-show="create">{{:: 'add-localization-text' | translate}}</li>
</ol>
<form class="form-horizontal clearfix" name="localizationForm" novalidate>
<fieldset>
<div class="form-group">
<label class="col-md-2 control-label" for="locale">{{:: 'locale' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="text" id="locale" name="locale" data-ng-model="locale"
readonly>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="key">
<span class="required" data-ng-show="create">*</span> {{:: 'key' | translate}}
</label>
<div class="col-md-6">
<input class="form-control" type="text" id="key" name="key" data-ng-model="key" autofocus
required data-ng-readonly="!create">
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="value">
<span class="required" data-ng-show="create">*</span> {{:: 'value' | translate}}
</label>
<div class="col-md-6">
<input class="form-control" type="text" id="value" name="value" required data-ng-model="value">
</div>
</div>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save>{{:: 'save' | translate}}</button>
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,37 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
<li><a href="#/realms/{{realm.realm}}/localization">{{:: 'lookup' | translate}}</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/localization/upload">{{:: 'localization-upload-file' | translate}}</a></li>
</ul>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group">
<label class="col-md-2 control-label" for="locale"><span class="required">*</span> {{:: 'locale' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="locale" type="text" ng-model="locale" placeholder="{{:: 'locale' | translate}}" required>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label"><span class="required">*</span> {{:: 'file' | translate}}</label>
<div class="col-md-6">
<div data-ng-show="!files || files.length == 0">
<label for="import-file" class="btn btn-default">{{:: 'select-file' | translate}} <i class="pficon pficon-import"></i></label>
<input id="import-file" type="file" class="hidden" ng-file-select="onFileSelect($files)" required>
</div>
<span class="kc-uploaded-file" data-ng-show="files.length > 0">
{{files[0].name}}
</span>
</div>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button data-kc-save>{{:: 'import' | translate}}</button>
<button data-kc-reset>{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,61 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-realm></kc-tabs-realm>
<ul class="nav nav-tabs nav-tabs-pf">
<li class="active"><a href="#/realms/{{realm.realm}}/localization">{{:: 'lookup' | translate}}</a></li>
<li><a href="#/realms/{{realm.realm}}/localization/upload">{{:: 'localization-upload-file' | translate}}</a></li>
</ul>
<form class="form-horizontal" name="realmForm" novalidate>
<div class="form-group">
<label class="col-md-2 control-label" for="selectedRealmSpecificLocales">{{:: 'locale' | translate}}</label>
<div class="col-md-6" ng-show="realmSpecificLocales.length > 0">
<select class="form-control" id="selectedRealmSpecificLocales"
ng-model="selectedRealmSpecificLocales"
ng-options="o as o for o in realmSpecificLocales">
<option value="" disabled selected>{{:: 'select-one.placeholder' | translate}}</option>
</select>
</div>
<div class="col-md-6" ng-show="realmSpecificLocales.length < 1">
{{:: 'no-localizations-configured' | translate}}
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label"></label>
<div class="col-md-6">
<input class="form-control ng-pristine ng-untouched ng-empty ng-invalid" id="newLocale" type="text" ng-model="newLocale" placeholder="locale">
</div>
<div class="col-sm-4">
<button class="btn btn-primary ng-binding" type="submit" data-ng-click="createLocale()">Create</button>
</div>
</div>
</form>
<table class="table table-striped table-bordered" data-ng-show="localizationTexts">
<thead>
<tr>
<th class="kc-table-actions" colspan="4">
<div class="form-inline">
<div class="pull-right" data-ng-show="access.manageRealm">
<a id="createLocalizationText" class="btn btn-default" href="#/create/localization/{{realm.realm}}/{{selectedRealmSpecificLocales}}">{{:: 'add-localization-text' | translate}}</a>
</div>
</div>
</th>
</tr>
<tr >
<th>{{:: 'key' | translate}}</th>
<th>{{:: 'value' | translate}}</th>
<th colspan="2">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, value) in localizationTexts">
<td>{{key}}</td>
<td>{{value}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/localization/{{selectedRealmSpecificLocales}}/{{key}}">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-click="removeLocalizationText(key)">{{:: 'delete' | translate}}</td>
</tr>
</tbody>
</table>
</div>
<kc-menu></kc-menu>

View file

@ -24,6 +24,7 @@
|| path[2] == 'login-settings' || path[2] == 'login-settings'
|| path[2] == 'keys' || path[2] == 'keys'
|| path[2] == 'theme-settings' || path[2] == 'theme-settings'
|| path[2] == 'localization'
|| path[2] == 'token-settings' || path[2] == 'token-settings'
|| path[2] == 'client-registration' || path[2] == 'client-registration'
|| path[2] == 'cache-settings' || path[2] == 'cache-settings'

View file

@ -11,6 +11,7 @@
<li ng-class="{active: path[2] == 'keys'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys">{{:: 'realm-tab-keys' | translate}}</a></li> <li ng-class="{active: path[2] == 'keys'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys">{{:: '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] == '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] == '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] == 'localization'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/localization">{{:: 'realm-tab-localization' | 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] == '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] == '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] == 'client-registration'}" data-ng-show="access.viewClients"><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'realm-tab-client-registration' | translate}}</a></li> <li ng-class="{active: path[2] == 'client-registration'}" data-ng-show="access.viewClients"><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'realm-tab-client-registration' | translate}}</a></li>