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")
KeyResource keys();
@Path("localization")
RealmLocalizationResource localization();
}

View file

@ -1645,6 +1645,35 @@ public class RealmAdapter implements CachedRealmModel {
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
public String toString() {
return String.format("%s@%08x", getId(), hashCode());

View file

@ -171,7 +171,6 @@ public class RealmCacheSession implements CacheRealmProvider {
return groupDelegate;
}
@Override
public void registerRealmInvalidation(String id, String name) {
cache.realmUpdated(id, name, invalidations);
@ -1247,4 +1246,51 @@ public class RealmCacheSession implements CacheRealmProvider {
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;
protected Map<String, Map<String,String>> realmLocalizationTexts;
public CachedRealm(Long revision, RealmModel model) {
super(revision, model.getId());
name = model.getName();
@ -301,6 +303,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
} catch (UnsupportedOperationException ex) {
}
realmLocalizationTexts = model.getRealmLocalizationTexts();
}
protected void cacheClientScopes(RealmModel model) {
@ -718,4 +721,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public boolean isAllowUserManagedAccess() {
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.GroupEntity;
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.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
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.stream.Collectors;
@ -858,6 +862,84 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
.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) {
ClientInitialAccessModel model = new ClientInitialAccessModel();
model.setId(entity.getId());

View file

@ -2225,6 +2225,53 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
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
public String toString() {
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.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.MapKey;
import javax.persistence.MapKeyColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
@ -242,6 +243,9 @@ public class RealmEntity {
@Column(name="ALLOW_USER_MANAGED_ACCESS")
private boolean allowUserManagedAccess;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realmId")
@MapKey(name="locale")
Map<String, RealmLocalizationTextsEntity> realmLocalizationTexts;
public String getId() {
return id;
@ -834,6 +838,17 @@ public class RealmEntity {
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
public boolean equals(Object o) {
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"/>
</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>

View file

@ -35,6 +35,7 @@
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
<class>org.keycloak.models.jpa.entities.MigrationModelEntity</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.UserAttributeEntity</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 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.
*/

View file

@ -17,6 +17,7 @@
package org.keycloak.models;
import java.util.Map;
import org.keycloak.migration.MigrationModel;
import org.keycloak.provider.Provider;
@ -80,6 +81,18 @@ public interface RealmProvider extends Provider /* TODO: Remove in future versio
void removeExpiredClientInitialAccess();
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
// 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

View file

@ -28,7 +28,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.jboss.logging.Logger;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.email.EmailException;
@ -209,6 +208,8 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
Locale locale = session.getContext().resolveLocale(user);
attributes.put("locale", 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("properties", theme.getProperties());
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);
Properties messagesBundle = handleThemeResources(theme, locale, attributes);
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
messagesBundle.putAll(localizationTexts);
URI baseUri = uriInfo.getBaseUri();
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();

View file

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

View file

@ -220,6 +220,15 @@ public class RealmAdminResource {
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.

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.testsuite.arquillian.TestContext;
import com.google.common.collect.Streams;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* 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 AUTH_FLOW_IDS = "AUTH_FLOW_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 String realmName;
@ -115,6 +115,9 @@ public class TestCleanup {
entities.add(AUTH_FLOW_IDS, flowId);
}
public void addLocalization(String language) {
entities.add(LOCALIZATION_LANGUAGES, language);
}
public void addAuthenticationConfigId(String 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() {

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.placeholder=Indica l''idioma i prem Intro
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.tooltip=Activar/desactivar la cach\u00E9 per al domini, client i dades de rols.
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-email=Email
realm-tab-themes=Temes
#realm-tab-localization=Localization
realm-tab-cache=Cach\u00E9
realm-tab-tokens=Tokens
realm-tab-security-defenses=Defenses de seguretat

View file

@ -91,6 +91,18 @@ i18n-enabled=Internationalisierung aktiv
supported-locales=Unterst\u00FCtzte Sprachen
#supported-locales.placeholder=Type a locale and enter
#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.tooltip=Clears all entries from the realm cache (this will clear entries for all realms)
#user-cache-clear=User Cache
@ -175,6 +187,7 @@ days=Tage
#realm-tab-keys=Keys
#realm-tab-email=Email
#realm-tab-themes=Themes
realm-tab-localization=Internationalisierung
#realm-tab-cache=Cache
#realm-tab-tokens=Tokens
#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.tooltip=Relay state you want to send with SAML request when you want to do IDP Initiated SSO.
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.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
@ -505,13 +518,13 @@ last-refresh=Letzte Aktualisierung
#first-broker-login-flow=First Login Flow
#post-broker-login-flow=Post Login Flow
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.legacy=Legacy
sync-mode.import=Importieren
sync-mode.force=Erzwingen
sync-mode-override=Ãœberschriebene 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=\u00DCberschriebene Synchronisation
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.tooltip=The redirect uri to use when configuring the identity provider.
#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.move.success=Gruppe verschoben.
group.remove.confirm.title=Gruppe löschen
group.remove.confirm.message=Sind Sie sicher, dass Sie die Gruppe \u201E{{name}}\u201C löschen möchten?
group.remove.success=Die Gruppe wurde gelöscht.
group.remove.confirm.title=Gruppe l\u00F6schen
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\u00F6scht.
group.fetch.fail=Fehler beim Laden: {{params}}
group.create.success=Gruppe erstellt.
group.edit.success=Die Änderungen wurde gespeichert.
group.roles.add.success=Rollenzuweisung hinzugefügt.
group.edit.success=Die \u00C4nderungen wurde gespeichert.
group.roles.add.success=Rollenzuweisung hinzugef\u00FCgt.
group.roles.remove.success=Rollenzuweisung entfernt.
group.default.add.error=Bitte eine Gruppe auswählen.
group.default.add.success=Standardgruppe hinzugefügt.
group.default.add.error=Bitte eine Gruppe ausw\u00E4hlen.
group.default.add.success=Standardgruppe hinzugef\u00FCgt.
group.default.remove.success=Standardgruppe entfernt.
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.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.logout.all.success=Benutzer von allen Sitzungen 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.message=Sind Sie sicher, dass Sie die Verknüpfung mit dem Identity Provider \u201E{{name}}\u201C entfernen möchten?
user.fedid.link.remove.success=Verknüpfung mit Identity Provider entfernt.
user.fedid.link.add.success=Verknüpfung mit Identity Provider angelegt.
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\u00FCpfung mit dem Identity Provider \u201E{{name}}\u201C entfernen m\u00F6chten?
user.fedid.link.remove.success=Verkn\u00FCpfung mit Identity Provider entfernt.
user.fedid.link.add.success=Verkn\u00FCpfung mit Identity Provider angelegt.
user.consent.revoke.success=Einwilligung widerrufen.
user.consent.revoke.error=Einwilligung konnte nicht widerrufen werden.
user.unlock.success=Alle vorübergehend gesperrten Benutzer wurden entsperrt.
user.remove.confirm.title=Benutzer löschen
user.remove.confirm.message=Sind Sie sicher, dass Sie den Benutzer \u201E{{name}}\u201C löschen möchten?
user.remove.success=Der Benutzer wurde gelöscht.
user.remove.error=Der Benutzer konnte nicht gelöscht werden.
user.unlock.success=Alle vor\u00FCbergehend gesperrten Benutzer wurden entsperrt.
user.remove.confirm.title=Benutzer l\u00F6schen
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\u00F6scht.
user.remove.error=Der Benutzer konnte nicht gel\u00F6scht werden.
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.error=Beim Speichern der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.remove.confirm.title=Zugangsdaten löschen
user.credential.remove.confirm.message=Sind Sie sicher, dass Sie die Zugangsdaten löschen löschen möchten?
user.credential.remove.success=Die Zugangsdaten wurden gelöscht.
user.credential.remove.error=Beim Löschen der Zugangsdaten ist ein Fehler aufgetreten.
user.credential.remove.confirm.title=Zugangsdaten l\u00F6schen
user.credential.remove.confirm.message=Sind Sie sicher, dass Sie die Zugangsdaten l\u00F6schen m\u00F6chten?
user.credential.remove.success=Die Zugangsdaten wurden gel\u00F6scht.
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-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.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.password.error.not-matching=Die Passwörter stimmen nicht überein.
user.password.reset.confirm.title=Passwort zurücksetzen
user.password.reset.confirm.message=Sind Sie sicher, dass Sie das Passwort für diesen Benutzer zurücksetzen möchten?
user.password.reset.success=Das Passwort wurde zurückgesetzt.
user.password.error.not-matching=Die Passw\u00F6rter stimmen nicht \u00FCberein.
user.password.reset.confirm.title=Passwort zur\u00FCcksetzen
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\u00FCckgesetzt.
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.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.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.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.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.error=Fehler beim Senden der E-Mail
#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.groups.fetch.all.error=Fehler beim Laden alle Gruppen: {{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.already-added=Benutzer gehört der Gruppe bereits an.
user.groups.join.success=Zur Gruppe hinzugefügt.
user.groups.leave.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\u00F6rt der Gruppe bereits an.
user.groups.join.success=Zur Gruppe hinzugef\u00FCgt.
user.groups.leave.error.no-group-selected=Bitte w\u00E4hlen Sie eine Gruppe aus!
user.groups.leave.success=Aus Gruppe entfernt.
#default.available-roles.tooltip=Realm level roles that can be assigned.
@ -1608,8 +1621,8 @@ notifications.success.header=Erfolg!
notifications.error.header=Fehler!
notifications.warn.header=Warnung!
dialogs.delete.title={{type}} löschen
dialogs.delete.message=Sind Sie sicher, dass Sie {{type}} {{name}} löschen möchten?
dialogs.delete.confirm=Löschen
dialogs.delete.title={{type}} l\u00F6schen
dialogs.delete.message=Sind Sie sicher, dass Sie {{type}} {{name}} l\u00F6schen m\u00F6chten?
dialogs.delete.confirm=L\u00F6schen
dialogs.cancel=Abbrechen
dialogs.ok=OK

View file

@ -62,6 +62,18 @@ i18n-enabled=Internacionalizaci\u00F3n activa
supported-locales=Idiomas soportados
supported-locales.placeholder=Indica el idioma y pulsa Intro
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.tooltip=Activar/desactivar la cach\u00E9 para el dominio, cliente y datos de roles.
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-email=Email
realm-tab-themes=Temas
#realm-tab-localization=Localization
realm-tab-cache=Cach\u00E9
realm-tab-tokens=Tokens
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.placeholder=Entrez la locale et validez
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.tooltip=Activer/D\u00e9sactiver le cache pour le domaine, client et donn\u00e9es.
user-cache-enabled=Cache utilisateur activ\u00e9
@ -123,6 +135,7 @@ realm-tab-login=Connexion
realm-tab-keys=Clefs
realm-tab-email=Courriels
realm-tab-themes=Th\u00e8mes
#realm-tab-localization=Localization
realm-tab-cache=Cache
realm-tab-tokens=Jetons
realm-tab-security-defenses=Mesures de s\u00e9curit\u00e9

View file

@ -93,6 +93,18 @@ i18n-enabled=国際化の有効
supported-locales=サポートされるロケール
supported-locales.placeholder=ロケールを入力し、Enterキーを押してください
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.tooltip=レルムキャッシュからすべてのエントリーをクリアする(これにより、すべてのレルムのエントリーがクリアされます)。
user-cache-clear=ユーザー・キャッシュ
@ -192,6 +204,7 @@ realm-tab-login=ログイン
realm-tab-keys=
realm-tab-email=Eメール
realm-tab-themes=テーマ
#realm-tab-localization=Localization
realm-tab-cache=キャッシュ
realm-tab-tokens=トークン
realm-tab-client-registration=クライアント登録

View file

@ -69,6 +69,18 @@ i18n-enabled=Daugiakalbystės palaikymas
supported-locales=Palaikomos kalbos
supported-locales.placeholder=Pasirinkite arba įrašykite kalbos pavadinimą
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.tooltip=Iš visų sričių pašalinama visa podėlyje (cache) esanti informacija
user-cache-clear=Naudotojų podėlis
@ -119,6 +131,7 @@ realm-tab-login=Prisijungimas
realm-tab-keys=Raktai
realm-tab-email=El. paštas
realm-tab-themes=Temos
#realm-tab-localization=Localization
realm-tab-cache=Podėlis
realm-tab-tokens=Raktai
realm-tab-client-registration=Klientų registracija

View file

@ -68,6 +68,18 @@ i18n-enabled=Internasjonalisering aktivert
supported-locales=St\u00F8ttede lokaliteter
supported-locales.placeholder=Skriv inn en lokalitet og klikk enter
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.tooltip=T\u00F8m sikkerhetsdomenecache (Dette vil fjerne oppf\u00F8ringer for alle sikkerhetsdomener)
user-cache-clear=Brukercache
@ -120,6 +132,7 @@ realm-tab-login=Innlogging
realm-tab-keys=N\u00F8kler
realm-tab-email=E-post
realm-tab-themes=Tema
#realm-tab-localization=Localization
realm-tab-cache=Cache
realm-tab-tokens=Tokens
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.placeholder=Digite um local e pressione Enter
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.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
@ -119,6 +131,7 @@ realm-tab-login=Login
realm-tab-keys=Chaves
realm-tab-email=E-mail
realm-tab-themes=Temas
#realm-tab-localization=Localization
realm-tab-cache=Cache
realm-tab-tokens=Tokens
realm-tab-client-initial-access=Tokens de Acesso inicial

View file

@ -75,6 +75,18 @@ i18n-enabled=Интернационализация
supported-locales=Поддерживаемые языки
supported-locales.placeholder=Выберите язык и нажмите Enter
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.tooltip=Удалить все записи в кэше realm (удалит все записи для всех realm)
user-cache-clear=Кэш пользователей
@ -127,6 +139,7 @@ realm-tab-login=Вход
realm-tab-keys=Ключи
realm-tab-email=E-mail
realm-tab-themes=Темы
#realm-tab-localization=Localization
realm-tab-cache=Кэш
realm-tab-tokens=Токены
realm-tab-client-initial-access=Первоначальные токены доступа

View file

@ -69,6 +69,18 @@ i18n-enabled=启用国际化
supported-locales=支持的语言
supported-locales.placeholder=输入一个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.tooltip=从域缓存中清理所有条目(这会清理所有域的条目)
user-cache-clear=用户缓存
@ -119,6 +131,7 @@ realm-tab-login=登录
realm-tab-keys=秘钥
realm-tab-email=Email
realm-tab-themes=主题
#realm-tab-localization=Localization
realm-tab-cache=缓存
realm-tab-tokens=Tokens
realm-tab-client-registration=客户端注册

View file

@ -96,6 +96,17 @@ i18n-enabled=Internationalization Enabled
supported-locales=Supported Locales
supported-locales.placeholder=Type a locale and 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
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.tooltip=Clears all entries from the realm cache (this will clear entries for all realms)
user-cache-clear=User Cache
@ -199,6 +210,7 @@ realm-tab-login=Login
realm-tab-keys=Keys
realm-tab-email=Email
realm-tab-themes=Themes
realm-tab-localization=Localization
realm-tab-cache=Cache
realm-tab-tokens=Tokens
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 resourceRequests = 0;
var loadingTimer = -1;
var translateProvider = null;
var currentRealm = null;
angular.element(document).ready(function () {
var keycloakAuth = new Keycloak(consoleBaseUrl + 'config');
@ -146,6 +148,7 @@ module.factory('authInterceptor', function($q, Auth) {
});
module.config(['$translateProvider', function($translateProvider) {
translateProvider = $translateProvider;
$translateProvider.useSanitizeValueStrategy('sanitizeParameters');
$translateProvider.preferredLanguage(locale);
$translateProvider.translations(locale, resourceBundle);
@ -178,6 +181,33 @@ module.config([ '$routeProvider', function($routeProvider) {
},
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', {
templateUrl : resourceUrl + '/partials/realm-login-settings.html',
resolve : {
@ -2085,6 +2115,42 @@ module.config([ '$routeProvider', function($routeProvider) {
},
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', {
templateUrl : resourceUrl + '/partials/server-info.html',
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.resourceUrl = resourceUrl;
$scope.auth = Auth;
@ -97,6 +97,18 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location
$scope.fragment = $location.path();
$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) {
@ -499,6 +511,131 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
$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) {
$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) {
return Loader.get(RealmEventsConfig, function() {
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) {
return $resource(authUrl + '/admin/realms/:id/events/config', {
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] == 'keys'
|| path[2] == 'theme-settings'
|| path[2] == 'localization'
|| path[2] == 'token-settings'
|| path[2] == 'client-registration'
|| 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] == '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] == '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] == '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>