diff --git a/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidget.java b/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidget.java
new file mode 100644
index 000000000..17dba5dae
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidget.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.ui;
+
+import com.vaadin.data.provider.QuerySortOrder;
+import com.vaadin.icons.VaadinIcons;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CustomComponent;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.TextArea;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+import com.vaadin.ui.components.grid.HeaderCell;
+import com.vaadin.ui.components.grid.HeaderRow;
+import com.vaadin.ui.themes.ValoTheme;
+import org.libreccm.l10n.LocalizedString;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * A editor component for properties of type {@link LocalizedString}.
+ *
+ * The component contains a {@link Grid} which shows all values of the localized
+ * string. The Grid provides buttons editing, removing and adding values. The
+ * add and remove buttons can be enabled and disabled by setting
+ * {@link #addAndRemoveEnabled}.
+ *
+ * Please note that this widget does not save the modifications done
+ * the the edited {@link LocalizedString} to the database. That is the
+ * responsibility of the caller.
+ *
+ * @author Jens Pelzetter
+ */
+public class LocalizedStringWidget extends CustomComponent {
+
+ private static final long serialVersionUID = -3089484568782865789L;
+
+ private static final String COL_LOCALE = "col_locale";
+ private static final String COL_VALUE = "col_value";
+ private static final String COL_EDIT = "col_edit";
+ private static final String COL_REMOVE = "col_remove";
+
+ /**
+ * CDI bean providing access to some other CDI beans which provide
+ * functionality required by this widget.
+ */
+ private final LocalizedStringWidgetController controller;
+ /**
+ * Implementation of {@link TextEditorBuilder} which is used to create the
+ * editor for the texts if {@link #multiline} is set to {@code true}.
+ */
+ private final TextEditorBuilder textEditorBuilder;
+
+ /**
+ * The localised string to edit.
+ */
+ private LocalizedString localizedString;
+
+ /**
+ * Enable the add and remove buttons? (default: {@code false})
+ */
+ private boolean addAndRemoveEnabled = false;
+ /**
+ * Is the text to edit a multi line text or simple single line string?
+ */
+ private boolean multiline;
+ /**
+ * Is HTML permitted in the string. This property is only useful for multi
+ * line strings. The value is passed to the {@link #textEditorBuilder}.
+ */
+ private boolean htmlPermitted;
+
+ /**
+ * The {@link Grid} showing the values of the {@link #localizedString}.
+ */
+ private Grid> valuesGrid;
+ /**
+ * The header row containing the add button.
+ */
+ private HorizontalLayout actionsRow;
+
+ /**
+ * Creates a new {@code LocalizedStringWidget}.
+ *
+ * @param controller
+ */
+ public LocalizedStringWidget(
+ final LocalizedStringWidgetController controller) {
+
+ super();
+
+ this.controller = controller;
+ textEditorBuilder = htmlPermitted -> new SimpleTextEditor("Text");
+
+ valuesGrid = new Grid<>();
+ valuesGrid.setDataProvider(this::fetchValues, this::countValues);
+ valuesGrid
+ .addColumn(Map.Entry::getKey)
+ .setId(COL_LOCALE)
+ .setCaption("Locale");
+ valuesGrid
+ .addColumn(Map.Entry::getValue)
+ .setId(COL_VALUE)
+ .setCaption("Value");
+ valuesGrid.addComponentColumn(value -> {
+ final Button button = new Button("Edit", VaadinIcons.EDIT);
+ button.addClickListener(event -> showTextEditor(value.getKey()));
+ return button;
+ })
+ .setId(COL_EDIT);
+ valuesGrid.addComponentColumn(value -> {
+ final Button button = new Button("Remove",
+ VaadinIcons.MINUS_CIRCLE_O);
+ button.addClickListener(event -> {
+ });
+ return button;
+ })
+ .setId(COL_REMOVE);
+
+ valuesGrid.getColumn(COL_REMOVE).setHidden(!addAndRemoveEnabled);
+
+ final NativeSelect localeSelect = new NativeSelect<>();
+ final Button addButton = new Button("Add value",
+ VaadinIcons.PLUS_CIRCLE_O);
+ addButton.addClickListener(event -> addValue(localeSelect.getValue()));
+ addButton.setEnabled(false);
+ localeSelect.addSelectionListener(event -> {
+ addButton.setEnabled(event.getSelectedItem().isPresent());
+ });
+ actionsRow = new HorizontalLayout(localeSelect, addButton);
+
+ final HeaderRow headerRow = valuesGrid.prependHeaderRow();
+ final HeaderCell actionsCell = headerRow.join(COL_LOCALE,
+ COL_VALUE,
+ COL_EDIT,
+ COL_REMOVE);
+ actionsCell.setComponent(actionsRow);
+
+ actionsRow.setVisible(addAndRemoveEnabled);
+ }
+
+ public LocalizedStringWidget(
+ final LocalizedStringWidgetController controller,
+ final boolean addAndRemoveEnabled) {
+
+ this(controller);
+ this.addAndRemoveEnabled = addAndRemoveEnabled;
+ valuesGrid.getColumn(COL_REMOVE).setHidden(!addAndRemoveEnabled);
+ actionsRow.setVisible(addAndRemoveEnabled);
+ }
+
+ public LocalizedStringWidget(
+ final LocalizedStringWidgetController controller,
+ final LocalizedString localizedString) {
+
+ this(controller);
+ this.localizedString = localizedString;
+ valuesGrid.getDataProvider().refreshAll();
+ }
+
+ public LocalizedStringWidget(
+ final LocalizedStringWidgetController controller,
+ final LocalizedString localizedString,
+ final boolean addAndRemoveEnabled) {
+
+ this(controller, localizedString);
+ this.addAndRemoveEnabled = addAndRemoveEnabled;
+ valuesGrid.getColumn(COL_REMOVE).setHidden(!addAndRemoveEnabled);
+ actionsRow.setVisible(addAndRemoveEnabled);
+ }
+
+ public LocalizedString getLocalizedString() {
+ return this.localizedString;
+ }
+
+ public void setLocalizedString(final LocalizedString localizedString) {
+ this.localizedString = localizedString;
+ valuesGrid.getDataProvider().refreshAll();
+ }
+
+ public boolean isAddAndRemoveEnabled() {
+ return addAndRemoveEnabled;
+ }
+
+ public void setAddAndRemoveEnabled(final boolean addAndRemoveEnabled) {
+ this.addAndRemoveEnabled = addAndRemoveEnabled;
+ valuesGrid.getColumn(COL_REMOVE).setHidden(!addAndRemoveEnabled);
+ actionsRow.setVisible(addAndRemoveEnabled);
+ }
+
+ public boolean isMultiline() {
+ return multiline;
+ }
+
+ public void setMultiline(final boolean multiline) {
+ this.multiline = multiline;
+ }
+
+ public boolean isHtmlPermitted() {
+ return htmlPermitted;
+ }
+
+ public void setHtmlPermitted(final boolean htmlPermitted) {
+ this.htmlPermitted = htmlPermitted;
+ }
+
+ private Stream> fetchValues(
+ final List sortOrder,
+ final int offset,
+ final int limit) {
+
+ if (localizedString == null) {
+ return Stream.empty();
+ } else {
+ final List> values = new ArrayList<>(
+ localizedString.getValues().entrySet());
+ if (limit >= values.size()) {
+ return values.stream();
+ } else {
+ return values.subList(offset, limit).stream();
+ }
+
+ }
+ }
+
+ private Integer countValues() {
+ if (localizedString == null) {
+ return 0;
+ } else {
+ return localizedString.getValues().size();
+ }
+ }
+
+ private void addValue(final Locale forLocale) {
+
+ final String value;
+ if (localizedString.hasValue(controller.getDefaultLocale())) {
+ value = localizedString.getValue(controller.getDefaultLocale());
+ } else {
+ final Set availableLocales = localizedString
+ .getAvailableLocales();
+ if (availableLocales.isEmpty()) {
+ value = "";
+ } else {
+ final Optional firstLocale = availableLocales
+ .stream()
+ .sorted((locale1, locale2) -> {
+ return locale1.toString().compareTo(locale2.toString());
+ })
+ .findFirst();
+
+ if (firstLocale.isPresent()) {
+ value = localizedString.getValue(firstLocale.get());
+ } else {
+ value = "";
+ }
+ }
+ }
+
+ localizedString.addValue(forLocale, value);
+ }
+
+ private void showTextEditor(final Locale locale) {
+
+ final Window window = new Window();
+
+ final Component editComponent;
+ final Button saveButton;
+ if (multiline) {
+ final TextEditor textEditor = textEditorBuilder.buildTextEditor(
+ htmlPermitted);
+ textEditor.setText(localizedString.getValue(locale));
+ editComponent = textEditor;
+
+ saveButton = new Button("Save", VaadinIcons.CHECK_CIRCLE_O);
+ saveButton.addClickListener(event -> {
+ localizedString.addValue(locale, textEditor.getText());
+ valuesGrid.getDataProvider().refreshAll();
+ window.close();
+ });
+ } else {
+ final TextField textField = new TextField("Text");
+ textField.setValue(localizedString.getValue(locale));
+ editComponent = textField;
+
+ saveButton = new Button("Save", VaadinIcons.CHECK_CIRCLE_O);
+ saveButton.addClickListener(event -> {
+ localizedString.addValue(locale, textField.getValue());
+ valuesGrid.getDataProvider().refreshAll();
+ window.close();
+ });
+ }
+
+ final Button cancelButton = new Button("Cancel");
+ cancelButton.addStyleName(ValoTheme.BUTTON_DANGER);
+ cancelButton.addClickListener(event -> window.close());
+
+ final HorizontalLayout buttonsLayout = new HorizontalLayout(saveButton,
+ cancelButton);
+
+ final VerticalLayout layout = new VerticalLayout(editComponent,
+ buttonsLayout);
+
+ window.setContent(layout);
+ window.setCaption(String.format("Edit text for locale \"%s\"",
+ locale.toString()));
+ window.setModal(true);
+
+ UI.getCurrent().addWindow(window);
+ }
+
+ private class SimpleTextEditor extends TextArea implements TextEditor {
+
+ private static final long serialVersionUID = -1189747199799719077L;
+
+ public SimpleTextEditor() {
+ super();
+ }
+
+ public SimpleTextEditor(final String caption) {
+ super(caption);
+ }
+
+ @Override
+ public String getText() {
+ return getValue();
+ }
+
+ @Override
+ public void setText(final String text) {
+ setValue(text);
+ }
+
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidgetController.java b/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidgetController.java
new file mode 100644
index 000000000..6e1df89d2
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/LocalizedStringWidgetController.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.ui;
+
+import com.arsdigita.kernel.KernelConfig;
+
+import org.libreccm.configuration.ConfigurationManager;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+
+/**
+ * Provides access to the CDI controlled beans used by the
+ * {@link LocalizedStringWidget}.
+ *
+ * @see LocalizedStringWidget
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+public class LocalizedStringWidgetController {
+
+ @Inject
+ private ConfigurationManager confManager;
+
+ public Locale getDefaultLocale() {
+
+ final KernelConfig kernelConfig = confManager
+ .findConfiguration(KernelConfig.class);
+
+ return kernelConfig.getDefaultLocale();
+ }
+
+ public List getSupportedLocales() {
+
+ final KernelConfig kernelConfig = confManager
+ .findConfiguration(KernelConfig.class);
+
+ return kernelConfig
+ .getSupportedLanguages()
+ .stream()
+ .sorted((lang1, lang2) -> lang1.compareTo(lang2))
+ .map(Locale::new)
+ .collect(Collectors.toList());
+
+ }
+
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/TextEditor.java b/ccm-core/src/main/java/org/libreccm/ui/TextEditor.java
new file mode 100644
index 000000000..dc8b139c0
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/TextEditor.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.ui;
+
+import com.vaadin.ui.Component;
+
+/**
+ * Interface which adds basic methods for a editor component for texts
+ * (multi line) to the {@link Component} interface of Vaadin.
+ *
+ * @author Jens Pelzetter
+ */
+public interface TextEditor extends Component {
+
+ String getText();
+
+ void setText(final String text);
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/TextEditorBuilder.java b/ccm-core/src/main/java/org/libreccm/ui/TextEditorBuilder.java
new file mode 100644
index 000000000..eca52e439
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/TextEditorBuilder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.ui;
+
+/**
+ * A functional interface which can be used by other components which embed
+ * a text editor.
+ *
+ * @author Jens Pelzetter
+ */
+@FunctionalInterface
+public interface TextEditorBuilder {
+
+ /**
+ * Create the text editor component.
+ *
+ * @param htmlPermitted If HTML input is permitted.
+ *
+ * @return The text editor component.
+ */
+ TextEditor buildTextEditor(boolean htmlPermitted);
+
+}
+