diff --git a/README.md b/README.md
index 9eff9e0..858b137 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# intellij-i18n
+# easy-i18n
-
+
[](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
[](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
@@ -25,12 +25,12 @@ To keep everything working, do not remove `` sections.
- Using IDE built-in plugin system:
- Settings/Preferences > Plugins > Marketplace > Search for "intellij-i18n" >
+ Settings/Preferences > Plugins > Marketplace > Search for "easy-i18n" >
Install Plugin
- Manually:
- Download the [latest release](https://github.com/marhali/intellij-i18n/releases/latest) and install it manually using
+ Download the [latest release](https://github.com/marhali/easy-i18n/releases/latest) and install it manually using
Settings/Preferences > Plugins > ⚙️ > Install plugin from disk...
diff --git a/gradle.properties b/gradle.properties
index 0bc0dad..a23e919 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,7 +11,7 @@ pluginUntilBuild = 203.*
pluginVerifierIdeVersions = 2020.2.4, 2020.3.2, 2021.1
platformType = IC
-platformVersion = 2020.2.4
+platformVersion = 2020.3.2
platformDownloadSources = true
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
diff --git a/src/main/java/de/marhali/easyi18n/SettingsService.java b/src/main/java/de/marhali/easyi18n/SettingsService.java
new file mode 100644
index 0000000..d8bd269
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/SettingsService.java
@@ -0,0 +1,39 @@
+package de.marhali.easyi18n;
+
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.project.Project;
+
+import de.marhali.easyi18n.data.SettingsState;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Persistent settings storage at project level.
+ * @author marhali
+ */
+@State(name = "EasyI18nSettings")
+public class SettingsService implements PersistentStateComponent {
+
+ public static SettingsService getInstance(Project project) {
+ ServiceManager.getService(project, SettingsService.class).initializeComponent();
+ return ServiceManager.getService(project, SettingsService.class);
+ }
+
+ private SettingsState state;
+
+ public SettingsService() {
+ this.state = new SettingsState();
+ }
+
+ @Override
+ public @NotNull SettingsState getState() {
+ return state;
+ }
+
+ @Override
+ public void loadState(@NotNull SettingsState state) {
+ this.state = state;
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java b/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java
new file mode 100644
index 0000000..b0c8ddb
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/TranslatorToolWindowFactory.java
@@ -0,0 +1,60 @@
+package de.marhali.easyi18n;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+
+import de.marhali.easyi18n.data.DataStore;
+import de.marhali.easyi18n.ui.action.*;
+import de.marhali.easyi18n.ui.panel.TableView;
+import de.marhali.easyi18n.ui.panel.TreeView;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class TranslatorToolWindowFactory implements ToolWindowFactory {
+
+ @Override
+ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
+ ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
+
+ // Translations tree view
+ TreeView treeView = new TreeView(project);
+ Content treeContent = contentFactory.createContent(treeView.getRootPanel(),"TreeView", false);
+ toolWindow.getContentManager().addContent(treeContent);
+
+ // Translations table view
+ TableView tableView = new TableView(project);
+ Content tableContent = contentFactory.createContent(tableView.getRootPanel(), "TableView", false);
+ toolWindow.getContentManager().addContent(tableContent);
+
+ // ToolWindow Actions (Can be used for every view)
+ List actions = new ArrayList<>();
+ actions.add(new AddAction());
+ actions.add(new ReloadAction());
+ actions.add(new SettingsAction());
+ actions.add(new SearchAction((searchString) -> DataStore.getInstance(project).searchBeyKey(searchString)));
+ toolWindow.setTitleActions(actions);
+
+ // Initialize Window Manager
+ WindowManager.getInstance().initialize(toolWindow, treeView, tableView);
+
+ // Initialize data store and load from disk
+ DataStore store = DataStore.getInstance(project);
+ store.addSynchronizer(treeView);
+ store.addSynchronizer(tableView);
+
+ try {
+ store.reloadFromDisk();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/WindowManager.java b/src/main/java/de/marhali/easyi18n/WindowManager.java
new file mode 100644
index 0000000..c6dfb32
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/WindowManager.java
@@ -0,0 +1,39 @@
+package de.marhali.easyi18n;
+
+import com.intellij.openapi.wm.ToolWindow;
+
+import de.marhali.easyi18n.ui.panel.TableView;
+import de.marhali.easyi18n.ui.panel.TreeView;
+
+public class WindowManager {
+
+ private static WindowManager INSTANCE;
+
+ private ToolWindow toolWindow;
+ private TreeView treeView;
+ private TableView tableView;
+
+ public static WindowManager getInstance() {
+ return INSTANCE == null ? INSTANCE = new WindowManager() : INSTANCE;
+ }
+
+ private WindowManager() {}
+
+ public void initialize(ToolWindow toolWindow, TreeView treeView, TableView tableView) {
+ this.toolWindow = toolWindow;
+ this.treeView = treeView;
+ this.tableView = tableView;
+ }
+
+ public ToolWindow getToolWindow() {
+ return toolWindow;
+ }
+
+ public TreeView getTreeView() {
+ return treeView;
+ }
+
+ public TableView getTableView() {
+ return tableView;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/data/DataStore.java b/src/main/java/de/marhali/easyi18n/data/DataStore.java
new file mode 100644
index 0000000..482f06e
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/data/DataStore.java
@@ -0,0 +1,116 @@
+package de.marhali.easyi18n.data;
+
+import com.intellij.openapi.project.Project;
+
+import de.marhali.easyi18n.SettingsService;
+import de.marhali.easyi18n.io.translator.TranslatorIO;
+import de.marhali.easyi18n.model.DataSynchronizer;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationDelete;
+import de.marhali.easyi18n.model.TranslationUpdate;
+import de.marhali.easyi18n.util.IOUtil;
+import de.marhali.easyi18n.util.TranslationsUtil;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Singleton service to manage localized messages.
+ * @author marhali
+ */
+public class DataStore {
+
+ private static DataStore INSTANCE;
+
+ private final Project project;
+ private final List synchronizer;
+
+ private Translations translations;
+ private String searchQuery;
+
+ public static DataStore getInstance(Project project) {
+ return INSTANCE == null ? INSTANCE = new DataStore(project) : INSTANCE;
+ }
+
+ private DataStore(Project project) {
+ this.project = project;
+ this.synchronizer = new ArrayList<>();
+ }
+
+ public void addSynchronizer(DataSynchronizer synchronizer) {
+ this.synchronizer.add(synchronizer);
+ }
+
+ public void reloadFromDisk() throws IOException {
+ String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
+
+ if(localesPath == null || localesPath.isEmpty()) {
+ translations = new Translations(new ArrayList<>(),
+ new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>()));
+
+ } else {
+ TranslatorIO io = IOUtil.determineFormat(localesPath);
+ translations = io.read(localesPath);
+ }
+
+ // Propagate changes
+ synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, searchQuery));
+ }
+
+ public void saveToDisk() {
+ String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
+
+ if(localesPath == null || localesPath.isEmpty()) { // Cannot save without valid path
+ return;
+ }
+
+ TranslatorIO io = IOUtil.determineFormat(localesPath);
+ io.save(translations);
+ }
+
+ public void searchBeyKey(String fullPath) {
+ // Use synchronizer to propagate search instance to all views
+ synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, this.searchQuery = fullPath));
+ }
+
+ public void processUpdate(TranslationUpdate update) {
+ if(update.isDeletion() || update.isKeyChange()) { // Delete origin i18n key
+ String originKey = update.getOrigin().getKey();
+ List sections = TranslationsUtil.getSections(originKey);
+ String nodeKey = sections.remove(sections.size() - 1); // Remove last node, which needs to be removed by parent
+
+ LocalizedNode node = translations.getNodes();
+ for(String section : sections) {
+ if(node == null) { // Might be possible on multi-delete
+ break;
+ }
+
+ node = node.getChildren(section);
+ }
+
+ if(node != null) { // Only remove if parent exists. Might be already deleted on multi-delete
+ node.removeChildren(nodeKey);
+
+ // Parent is empty now, we need to remove it as well (except root)
+ if(node.getChildren().isEmpty() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
+ processUpdate(new TranslationDelete(new KeyedTranslation(
+ TranslationsUtil.sectionsToFullPath(sections), null)));
+ }
+ }
+ }
+
+ if(!update.isDeletion()) { // Recreate with changed val / create
+ LocalizedNode node = translations.getOrCreateNode(update.getChange().getKey());
+ node.setValue(update.getChange().getTranslations());
+ }
+
+ // Propagate changes and save them
+ synchronizer.forEach(synchronizer -> synchronizer.synchronize(translations, searchQuery));
+ saveToDisk();
+ }
+
+ public Translations getTranslations() {
+ return translations;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/data/LocalizedNode.java b/src/main/java/de/marhali/easyi18n/data/LocalizedNode.java
new file mode 100644
index 0000000..874b05f
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/data/LocalizedNode.java
@@ -0,0 +1,77 @@
+package de.marhali.easyi18n.data;
+
+import de.marhali.easyi18n.util.MapUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+/**
+ * Represents structured tree view for translated messages.
+ * @author marhali
+ */
+public class LocalizedNode {
+
+ public static final String ROOT_KEY = "root";
+
+ @NotNull
+ private final String key;
+
+ @NotNull
+ private TreeMap children;
+
+ @NotNull
+ private Map value;
+
+ public LocalizedNode(@NotNull String key, @NotNull List children) {
+ this.key = key;
+ this.children = MapUtil.convertToTreeMap(children);
+ this.value = new HashMap<>();
+ }
+
+ public LocalizedNode(@NotNull String key, @NotNull Map value) {
+ this.key = key;
+ this.children = new TreeMap<>();
+ this.value = value;
+ }
+
+ public @NotNull String getKey() {
+ return key;
+ }
+
+ public boolean isLeaf() {
+ return children.isEmpty();
+ }
+
+ public @NotNull Collection getChildren() {
+ return children.values();
+ }
+
+ public @Nullable LocalizedNode getChildren(@NotNull String key) {
+ return children.get(key);
+ }
+
+ public void setChildren(@NotNull LocalizedNode... children) {
+ this.value.clear();
+ this.children = MapUtil.convertToTreeMap(Arrays.asList(children));
+ }
+
+ public void addChildren(@NotNull LocalizedNode... children) {
+ this.value.clear();
+ Arrays.stream(children).forEach(e -> this.children.put(e.getKey(), e));
+ }
+
+ public void removeChildren(@NotNull String key) {
+ this.children.remove(key);
+ }
+
+ public @NotNull Map getValue() {
+ return value;
+ }
+
+ public void setValue(@NotNull Map value) {
+ this.children.clear();
+ this.value = value;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/data/SettingsState.java b/src/main/java/de/marhali/easyi18n/data/SettingsState.java
new file mode 100644
index 0000000..f206bbe
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/data/SettingsState.java
@@ -0,0 +1,33 @@
+package de.marhali.easyi18n.data;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * @author marhali
+ */
+public class SettingsState {
+
+ public static final String DEFAULT_PREVIEW_LOCALE = "en";
+
+ private String localesPath;
+ private String previewLocale;
+
+ public SettingsState() {}
+
+ public @Nullable String getLocalesPath() {
+ return localesPath;
+ }
+
+ public void setLocalesPath(String localesPath) {
+ this.localesPath = localesPath;
+ }
+
+ public @NotNull String getPreviewLocale() {
+ return previewLocale != null ? previewLocale : DEFAULT_PREVIEW_LOCALE;
+ }
+
+ public void setPreviewLocale(String previewLocale) {
+ this.previewLocale = previewLocale;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/data/Translations.java b/src/main/java/de/marhali/easyi18n/data/Translations.java
new file mode 100644
index 0000000..f40dee0
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/data/Translations.java
@@ -0,0 +1,92 @@
+package de.marhali.easyi18n.data;
+
+import de.marhali.easyi18n.util.TranslationsUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Translations {
+
+ private List locales;
+ private LocalizedNode nodes;
+
+ public Translations(List locales, LocalizedNode nodes) {
+ this.locales = locales;
+ this.nodes = nodes;
+ }
+
+ public List getLocales() {
+ return locales;
+ }
+
+ public LocalizedNode getNodes() {
+ return nodes;
+ }
+
+ public @Nullable LocalizedNode getNode(@NotNull String fullPath) {
+ List sections = TranslationsUtil.getSections(fullPath);
+
+ LocalizedNode node = nodes;
+
+ for(String section : sections) {
+ if(node == null) {
+ return null;
+ }
+ node = node.getChildren(section);
+ }
+
+ return node;
+ }
+
+ public @NotNull LocalizedNode getOrCreateNode(@NotNull String fullPath) {
+ List sections = TranslationsUtil.getSections(fullPath);
+
+ LocalizedNode node = nodes;
+
+ for(String section : sections) {
+ LocalizedNode subNode = node.getChildren(section);
+
+ if(subNode == null) {
+ subNode = new LocalizedNode(section, new ArrayList<>());
+ node.addChildren(subNode);
+ }
+
+ node = subNode;
+ }
+
+ return node;
+ }
+
+ public List getFullKeys() {
+ List keys = new ArrayList<>();
+
+ if(nodes.isLeaf()) { // Root has no children
+ return keys;
+ }
+
+ for(LocalizedNode children : nodes.getChildren()) {
+ keys.addAll(getFullKeys("", children));
+ }
+
+ return keys;
+ }
+
+ public List getFullKeys(String parentFullPath, LocalizedNode localizedNode) {
+ List keys = new ArrayList<>();
+
+ if(localizedNode.isLeaf()) {
+ keys.add(parentFullPath + (parentFullPath.isEmpty() ? "" : ".") + localizedNode.getKey());
+ return keys;
+ }
+
+ for(LocalizedNode children : localizedNode.getChildren()) {
+ String childrenPath = parentFullPath + (parentFullPath.isEmpty() ? "" : ".") + localizedNode.getKey();
+ keys.addAll(getFullKeys(childrenPath, children));
+ }
+
+ return keys;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/Filer.java b/src/main/java/de/marhali/easyi18n/io/Filer.java
new file mode 100644
index 0000000..31c2518
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/Filer.java
@@ -0,0 +1,31 @@
+package de.marhali.easyi18n.io;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import java.io.File;
+
+/**
+ * Singleton service for file io operations.
+ * @author marhali
+ */
+public class Filer {
+
+ private static Filer INSTANCE;
+
+ private final Project project;
+
+ public static Filer getInstance(Project project) {
+ return INSTANCE == null ? INSTANCE = new Filer(project) : INSTANCE;
+ }
+
+ private Filer(Project project) {
+ this.project = project;
+ }
+
+ public VirtualFile getFile() {
+ VirtualFile vfs = LocalFileSystem.getInstance().findFileByIoFile(new File(project.getBasePath() + "/src/lang/de.json"));
+ return vfs;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/io/translator/JsonTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/translator/JsonTranslatorIO.java
new file mode 100644
index 0000000..59e970b
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/translator/JsonTranslatorIO.java
@@ -0,0 +1,76 @@
+package de.marhali.easyi18n.io.translator;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.data.Translations;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+
+/**
+ * Implementation for JSON translation files.
+ * @author marhali
+ */
+public class JsonTranslatorIO implements TranslatorIO {
+
+ @Override
+ public Translations read(String directoryPath) throws IOException {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
+ }
+
+ VirtualFile[] files = directory.getChildren();
+
+ List locales = new ArrayList<>();
+ LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
+
+ for(VirtualFile file : files) {
+ locales.add(file.getNameWithoutExtension());
+ JsonObject tree = JsonParser.parseReader(new InputStreamReader(file.getInputStream())).getAsJsonObject();
+ readTree(file.getNameWithoutExtension(), tree, nodes);
+ }
+
+ return new Translations(locales, nodes);
+ }
+
+ @Override
+ public void save(Translations translations) {
+ System.out.println("TODO: save");
+ }
+
+ private void readTree(String locale, JsonObject json, LocalizedNode data) {
+ for(Map.Entry entry : json.entrySet()) {
+ String key = entry.getKey();
+
+ try {
+ // Try to go one level deeper
+ JsonObject childObject = entry.getValue().getAsJsonObject();
+
+ LocalizedNode childrenNode = new LocalizedNode(key, new ArrayList<>());
+ data.addChildren(childrenNode);
+ readTree(locale, childObject, childrenNode);
+
+ } catch(IllegalStateException e) { // Reached end for this node
+ LocalizedNode leafNode = data.getChildren(key);
+
+ if(leafNode == null) {
+ leafNode = new LocalizedNode(key, new HashMap<>());
+ data.addChildren(leafNode);
+ }
+
+ Map messages = leafNode.getValue();
+ messages.put(locale, entry.getValue().getAsString());
+ leafNode.setValue(messages);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/io/translator/TranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/translator/TranslatorIO.java
new file mode 100644
index 0000000..1ed8a7f
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/io/translator/TranslatorIO.java
@@ -0,0 +1,28 @@
+package de.marhali.easyi18n.io.translator;
+
+import de.marhali.easyi18n.data.Translations;
+
+import java.io.IOException;
+
+/**
+ * Interface to retrieve and save localized messages.
+ * Can be implemented by various standards. Such as JSON, Properties-Bundle and so on.
+ * @author marhali
+ */
+public interface TranslatorIO {
+
+ /**
+ * Reads localized messages from the persistence layer.
+ * @param directoryPath The full path from the parent directory which holds the different locale files.
+ * @return Translations model
+ * Example entry: username.title => [DE:Benutzername, EN:Username]
+ */
+ Translations read(String directoryPath) throws IOException;
+
+ /**
+ * Writes the provided messages to the persistence layer.
+ * @param translations Translatons model to save
+ * @see #read(String) More information regards the data map
+ */
+ void save(Translations translations);
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java b/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java
new file mode 100644
index 0000000..aab858d
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/DataSynchronizer.java
@@ -0,0 +1,20 @@
+package de.marhali.easyi18n.model;
+
+import de.marhali.easyi18n.data.Translations;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Interface to communicate data changes between data store and ui components.
+ * @author marhali
+ */
+public interface DataSynchronizer {
+
+ /**
+ * Propagates data changes to implementation classes.
+ * @param translations Updated translations model
+ * @param searchQuery Can be used to filter visible data. Like a search function for the full key path.
+ */
+ void synchronize(@NotNull Translations translations, @Nullable String searchQuery);
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/KeyedTranslation.java b/src/main/java/de/marhali/easyi18n/model/KeyedTranslation.java
new file mode 100644
index 0000000..8e5849b
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/KeyedTranslation.java
@@ -0,0 +1,42 @@
+package de.marhali.easyi18n.model;
+
+import java.util.Map;
+
+/**
+ * Translated messages for a dedicated key.
+ * @author marhali
+ */
+public class KeyedTranslation {
+
+ private String key;
+ private Map translations;
+
+ public KeyedTranslation(String key, Map translations) {
+ this.key = key;
+ this.translations = translations;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public Map getTranslations() {
+ return translations;
+ }
+
+ public void setTranslations(Map translations) {
+ this.translations = translations;
+ }
+
+ @Override
+ public String toString() {
+ return "KeyedTranslation{" +
+ "key='" + key + '\'' +
+ ", translations=" + translations +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/TranslationCreate.java b/src/main/java/de/marhali/easyi18n/model/TranslationCreate.java
new file mode 100644
index 0000000..9c955a9
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/TranslationCreate.java
@@ -0,0 +1,13 @@
+package de.marhali.easyi18n.model;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents update request to create a new translation.
+ * @author marhali
+ */
+public class TranslationCreate extends TranslationUpdate {
+ public TranslationCreate(@NotNull KeyedTranslation translation) {
+ super(null, translation);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/TranslationDelete.java b/src/main/java/de/marhali/easyi18n/model/TranslationDelete.java
new file mode 100644
index 0000000..763cb16
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/TranslationDelete.java
@@ -0,0 +1,13 @@
+package de.marhali.easyi18n.model;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents update request to delete a existing translation.
+ * @author marhali
+ */
+public class TranslationDelete extends TranslationUpdate {
+ public TranslationDelete(@NotNull KeyedTranslation translation) {
+ super(translation, null);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/TranslationUpdate.java b/src/main/java/de/marhali/easyi18n/model/TranslationUpdate.java
new file mode 100644
index 0000000..d4923b3
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/TranslationUpdate.java
@@ -0,0 +1,46 @@
+package de.marhali.easyi18n.model;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents an update for a translated I18n-Key. Supports key creation, manipulation and deletion.
+ * @author marhali
+ */
+public class TranslationUpdate {
+
+ private final @Nullable KeyedTranslation origin;
+ private final @Nullable KeyedTranslation change;
+
+ public TranslationUpdate(@Nullable KeyedTranslation origin, @Nullable KeyedTranslation change) {
+ this.origin = origin;
+ this.change = change;
+ }
+
+ public KeyedTranslation getOrigin() {
+ return origin;
+ }
+
+ public KeyedTranslation getChange() {
+ return change;
+ }
+
+ public boolean isCreation() {
+ return origin == null;
+ }
+
+ public boolean isDeletion() {
+ return change == null;
+ }
+
+ public boolean isKeyChange() {
+ return origin != null && change != null && !origin.getKey().equals(change.getKey());
+ }
+
+ @Override
+ public String toString() {
+ return "TranslationUpdate{" +
+ "origin=" + origin +
+ ", change=" + change +
+ '}';
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/model/table/TableModelTranslator.java b/src/main/java/de/marhali/easyi18n/model/table/TableModelTranslator.java
new file mode 100644
index 0000000..27265d0
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/table/TableModelTranslator.java
@@ -0,0 +1,122 @@
+package de.marhali.easyi18n.model.table;
+
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationUpdate;
+import de.marhali.easyi18n.data.Translations;
+
+import org.jetbrains.annotations.Nls;
+
+import javax.swing.event.TableModelListener;
+import javax.swing.table.TableModel;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Table model to represents localized messages.
+ * @author marhali
+ */
+public class TableModelTranslator implements TableModel {
+
+ private final Translations translations;
+ private final List locales;
+ private final List fullKeys;
+
+ private final Consumer updater;
+
+ /**
+ * @param translations Translations instance
+ * @param searchQuery Search / filter param
+ * @param updater Consumer which can be called on cell change / update
+ */
+ public TableModelTranslator(Translations translations, String searchQuery, Consumer updater) {
+ this.translations = translations;
+ this.locales = translations.getLocales();
+ this.updater = updater;
+
+ List fullKeys = translations.getFullKeys();
+
+ if(searchQuery != null && !searchQuery.isEmpty()) { // Filter keys by searchQuery
+ fullKeys.removeIf(key -> !key.startsWith(searchQuery));
+ }
+
+ this.fullKeys = fullKeys;
+ }
+
+ @Override
+ public int getRowCount() {
+ return fullKeys.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return locales.size() + 1; // Number of locales plus 1 for the Key's column
+ }
+
+ @Nls
+ @Override
+ public String getColumnName(int columnIndex) {
+ if(columnIndex == 0) {
+ return "Key";
+ }
+
+ return "" + locales.get(columnIndex - 1) + "";
+ }
+
+ @Override
+ public Class> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ return rowIndex > 0; // Everything should be editable except the headline
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ if(columnIndex == 0) { // Keys
+ return fullKeys.get(rowIndex);
+ }
+
+ String key = fullKeys.get(rowIndex);
+ String locale = locales.get(columnIndex - 1);
+ LocalizedNode node = translations.getNode(key);
+
+ return node == null ? null : node.getValue().get(locale);
+ }
+
+ @Override
+ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+ String key = String.valueOf(getValueAt(rowIndex, 0));
+ LocalizedNode node = translations.getNode(key);
+
+ if(node == null) { // Unknown cell
+ return;
+ }
+
+ String newKey = columnIndex == 0 ? String.valueOf(aValue) : key;
+ Map messages = node.getValue();
+
+ // Locale message update
+ if(columnIndex > 0) {
+ if(aValue == null || ((String) aValue).isEmpty()) {
+ messages.remove(locales.get(columnIndex - 1));
+ } else {
+ messages.put(locales.get(columnIndex - 1), String.valueOf(aValue));
+ }
+ }
+
+ TranslationUpdate update = new TranslationUpdate(new KeyedTranslation(key, messages),
+ new KeyedTranslation(newKey, messages));
+
+ updater.accept(update);
+ }
+
+ @Override
+ public void addTableModelListener(TableModelListener l) {}
+
+ @Override
+ public void removeTableModelListener(TableModelListener l) {}
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java b/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java
new file mode 100644
index 0000000..067408a
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/model/tree/TreeModelTranslator.java
@@ -0,0 +1,94 @@
+package de.marhali.easyi18n.model.tree;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.JBColor;
+
+import de.marhali.easyi18n.SettingsService;
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.data.Translations;
+import de.marhali.easyi18n.util.TranslationsUtil;
+import de.marhali.easyi18n.util.UiUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.tree.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * I18n key tree preparation.
+ * @author marhali
+ */
+public class TreeModelTranslator extends DefaultTreeModel {
+
+ private final @NotNull Project project;
+ private final @NotNull Translations translations;
+ private final @Nullable String searchQuery;
+
+
+ public TreeModelTranslator(
+ @NotNull Project project, @NotNull Translations translations, @Nullable String searchQuery) {
+ super(null);
+
+ this.project = project;
+ this.translations = translations;
+ this.searchQuery = searchQuery;
+
+ setRoot(generateNodes());
+ }
+
+ private DefaultMutableTreeNode generateNodes() {
+ DefaultMutableTreeNode root = new DefaultMutableTreeNode(LocalizedNode.ROOT_KEY);
+
+ if(translations.getNodes().isLeaf()) { // Empty tree
+ return root;
+ }
+
+ List searchSections = searchQuery == null ?
+ Collections.emptyList() : TranslationsUtil.getSections(searchQuery);
+
+ for(LocalizedNode children : translations.getNodes().getChildren()) {
+ generateSubNodes(root, children, new ArrayList<>(searchSections));
+ }
+
+ return root;
+ }
+
+ private void generateSubNodes(DefaultMutableTreeNode parent,
+ LocalizedNode localizedNode, List searchSections) {
+
+ String searchKey = searchSections.isEmpty() ? null : searchSections.remove(0);
+
+ if(searchKey != null && !localizedNode.getKey().startsWith(searchKey)) { // Filter node
+ return;
+ }
+
+ if(localizedNode.isLeaf()) {
+ String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
+
+ String title = localizedNode.getKey();
+ String sub = "(" + previewLocale + ": " + localizedNode.getValue().get(previewLocale) + ")";
+ String tooltip = UiUtil.generateHtmlTooltip(localizedNode.getValue());
+
+ PresentationData data = new PresentationData(title, sub, null, null);
+ data.setTooltip(tooltip);
+
+ if(localizedNode.getValue().size() != translations.getLocales().size()) {
+ data.setForcedTextForeground(JBColor.RED);
+ }
+
+ parent.add(new DefaultMutableTreeNode(data));
+
+ } else {
+ DefaultMutableTreeNode sub = new DefaultMutableTreeNode(localizedNode.getKey());
+ parent.add(sub);
+
+ for(LocalizedNode children : localizedNode.getChildren()) {
+ generateSubNodes(sub, children, new ArrayList<>(searchSections));
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.form b/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.form
new file mode 100644
index 0000000..76392c6
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.form
@@ -0,0 +1,18 @@
+
+
diff --git a/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.java b/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.java
new file mode 100644
index 0000000..2b77820
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/ActionsToolbar.java
@@ -0,0 +1,6 @@
+package de.marhali.easyi18n.ui;
+
+import javax.swing.*;
+
+public class ActionsToolbar {
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/AddAction.java b/src/main/java/de/marhali/easyi18n/ui/action/AddAction.java
new file mode 100644
index 0000000..916e20f
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/AddAction.java
@@ -0,0 +1,53 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+
+import de.marhali.easyi18n.WindowManager;
+import de.marhali.easyi18n.ui.dialog.AddDialog;
+import de.marhali.easyi18n.util.TreeUtil;
+
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.tree.TreePath;
+
+public class AddAction extends AnAction {
+
+ public AddAction() {
+ super("Add Translation", null, AllIcons.General.Add);
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ new AddDialog(e.getProject(), detectPreKey()).showAndHandle();
+ }
+
+ private String detectPreKey() {
+ WindowManager manager = WindowManager.getInstance();
+
+ if(manager == null) {
+ return null;
+ }
+
+ if(manager.getToolWindow().getContentManager().getSelectedContent().getDisplayName().equals("TreeView")) {
+
+ TreePath path = manager.getTreeView().getTree().getSelectionPath();
+
+ if(path != null) {
+ return TreeUtil.getFullPath(path) + ".";
+ }
+
+ } else { // Table View
+
+ int row = manager.getTableView().getTable().getSelectedRow();
+
+ if(row >= 0) {
+ String fullPath = String.valueOf(manager.getTableView().getTable().getValueAt(row, 0));
+ return fullPath + ".";
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/CollapseTreeViewAction.java b/src/main/java/de/marhali/easyi18n/ui/action/CollapseTreeViewAction.java
new file mode 100644
index 0000000..6ac8f2d
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/CollapseTreeViewAction.java
@@ -0,0 +1,25 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Action to collapse all tree nodes with children.
+ * @author marhali
+ */
+public class CollapseTreeViewAction extends AnAction {
+
+ private final Runnable collapseRunnable;
+
+ public CollapseTreeViewAction(Runnable collapseRunnable) {
+ super("Collapse Tree", null, AllIcons.Actions.Collapseall);
+ this.collapseRunnable = collapseRunnable;
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ collapseRunnable.run();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/ExpandTreeViewAction.java b/src/main/java/de/marhali/easyi18n/ui/action/ExpandTreeViewAction.java
new file mode 100644
index 0000000..a4f2a03
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/ExpandTreeViewAction.java
@@ -0,0 +1,25 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Action to expand the entire tree (open all nodes with children).
+ * @author marhali
+ */
+public class ExpandTreeViewAction extends AnAction {
+
+ private final Runnable expandRunnable;
+
+ public ExpandTreeViewAction(Runnable expandRunnable) {
+ super("Expand Tree", null, AllIcons.Actions.Expandall);
+ this.expandRunnable = expandRunnable;
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ expandRunnable.run();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/ReloadAction.java b/src/main/java/de/marhali/easyi18n/ui/action/ReloadAction.java
new file mode 100644
index 0000000..0d0584c
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/ReloadAction.java
@@ -0,0 +1,27 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+
+import de.marhali.easyi18n.data.DataStore;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+public class ReloadAction extends AnAction {
+
+ public ReloadAction() {
+ super("Reload From Disk", null, AllIcons.Actions.Refresh);
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ try {
+ DataStore.getInstance(e.getProject()).reloadFromDisk();
+ } catch (IOException ioException) {
+ ioException.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/SearchAction.java b/src/main/java/de/marhali/easyi18n/ui/action/SearchAction.java
new file mode 100644
index 0000000..2bebafd
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/SearchAction.java
@@ -0,0 +1,62 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.actionSystem.ex.CustomComponentAction;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.util.ui.JBUI;
+
+import org.jdesktop.swingx.prompt.PromptSupport;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.util.function.Consumer;
+
+public class SearchAction extends AnAction implements CustomComponentAction {
+
+ private final Consumer searchCallback;
+ private JBTextField textField;
+
+ public SearchAction(@NotNull Consumer searchCallback) {
+ super("Search");
+ this.searchCallback = searchCallback;
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {} // Should never be called
+
+ public void actionPerformed() {
+ searchCallback.accept(textField == null ? "" : textField.getText());
+ }
+
+ @Override
+ public @NotNull JComponent createCustomComponent(@NotNull Presentation presentation, @NotNull String place) {
+ textField = new JBTextField();
+ textField.setPreferredSize(new Dimension(160, 25));
+ PromptSupport.setPrompt("Search Key...", textField);
+
+ textField.addKeyListener(handleKeyListener());
+ textField.setBorder(JBUI.Borders.empty());
+
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.add(textField, BorderLayout.CENTER);
+
+ return panel;
+ }
+
+ private KeyAdapter handleKeyListener() {
+ return new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if(e.getKeyCode() == KeyEvent.VK_ENTER) {
+ e.consume();
+ actionPerformed();
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/action/SettingsAction.java b/src/main/java/de/marhali/easyi18n/ui/action/SettingsAction.java
new file mode 100644
index 0000000..65ecd00
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/action/SettingsAction.java
@@ -0,0 +1,19 @@
+package de.marhali.easyi18n.ui.action;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import de.marhali.easyi18n.ui.dialog.SettingsDialog;
+import org.jetbrains.annotations.NotNull;
+
+public class SettingsAction extends AnAction {
+
+ public SettingsAction() {
+ super("Settings", null, AllIcons.General.Settings);
+ }
+
+ @Override
+ public void actionPerformed(@NotNull AnActionEvent e) {
+ new SettingsDialog(e.getProject()).showAndHandle();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/dialog/AddDialog.java b/src/main/java/de/marhali/easyi18n/ui/dialog/AddDialog.java
new file mode 100644
index 0000000..b70defc
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/dialog/AddDialog.java
@@ -0,0 +1,100 @@
+package de.marhali.easyi18n.ui.dialog;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogBuilder;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.ui.components.JBTextField;
+
+import de.marhali.easyi18n.data.DataStore;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationCreate;
+
+import javax.swing.*;
+import javax.swing.border.EtchedBorder;
+import java.awt.*;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author marhali
+ */
+public class AddDialog {
+
+ private final Project project;
+ private String preKey;
+
+ private JBTextField keyTextField;
+ private Map valueTextFields;
+
+ public AddDialog(Project project, String preKey) {
+ this(project);
+ this.preKey = preKey;
+ }
+
+ public AddDialog(Project project) {
+ this.project = project;
+ }
+
+ public void showAndHandle() {
+ int code = prepare().show();
+
+ if(code == DialogWrapper.OK_EXIT_CODE) {
+ saveTranslation();
+ }
+ }
+
+ private void saveTranslation() {
+ Map messages = new HashMap<>();
+
+ valueTextFields.forEach((k, v) -> {
+ if(!v.getText().isEmpty()) {
+ messages.put(k, v.getText());
+ }
+ });
+
+ TranslationCreate creation = new TranslationCreate(new KeyedTranslation(keyTextField.getText(), messages));
+ DataStore.getInstance(project).processUpdate(creation);
+ }
+
+ private DialogBuilder prepare() {
+ JPanel rootPanel = new JPanel();
+ rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS));
+
+ JPanel keyPanel = new JPanel(new GridLayout(0, 1, 2, 2));
+ JBLabel keyLabel = new JBLabel("Key");
+ keyTextField = new JBTextField(this.preKey);
+ keyLabel.setLabelFor(keyTextField);
+ keyPanel.add(keyLabel);
+ keyPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
+ keyPanel.add(keyTextField);
+ rootPanel.add(keyPanel);
+
+ JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
+ valueTextFields = new HashMap<>();
+ for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
+ JBLabel localeLabel = new JBLabel(locale);
+ JBTextField localeText = new JBTextField();
+ localeLabel.setLabelFor(localeText);
+
+ valuePanel.add(localeLabel);
+ valuePanel.add(localeText);
+ valueTextFields.put(locale, localeText);
+ }
+
+ JBScrollPane valuePane = new JBScrollPane(valuePanel);
+ valuePane.setBorder(BorderFactory.createTitledBorder(new EtchedBorder(), "Locales"));
+ rootPanel.add(valuePane);
+
+ DialogBuilder builder = new DialogBuilder();
+ builder.setTitle("Add Translation");
+ builder.removeAllActions();
+ builder.addOkAction();
+ builder.addCancelAction();
+ builder.setCenterPanel(rootPanel);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/dialog/EditDialog.java b/src/main/java/de/marhali/easyi18n/ui/dialog/EditDialog.java
new file mode 100644
index 0000000..ee7c418
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/dialog/EditDialog.java
@@ -0,0 +1,96 @@
+package de.marhali.easyi18n.ui.dialog;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogBuilder;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.ui.components.JBTextField;
+import de.marhali.easyi18n.data.DataStore;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationDelete;
+import de.marhali.easyi18n.model.TranslationUpdate;
+import de.marhali.easyi18n.ui.dialog.descriptor.DeleteActionDescriptor;
+
+import javax.swing.*;
+import javax.swing.border.EtchedBorder;
+import java.awt.*;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EditDialog {
+
+ private final Project project;
+ private final KeyedTranslation origin;
+
+ private JBTextField keyTextField;
+ private Map valueTextFields;
+
+ public EditDialog(Project project, KeyedTranslation origin) {
+ this.project = project;
+ this.origin = origin;
+ }
+
+ public void showAndHandle() {
+ int code = prepare().show();
+
+ if(code == DialogWrapper.OK_EXIT_CODE) { // Edit
+ DataStore.getInstance(project).processUpdate(new TranslationUpdate(origin, getChanges()));
+
+ } else if(code == DeleteActionDescriptor.EXIT_CODE) { // Delete
+ DataStore.getInstance(project).processUpdate(new TranslationDelete(origin));
+ }
+ }
+
+ private KeyedTranslation getChanges() {
+ Map messages = new HashMap<>();
+
+ valueTextFields.forEach((k, v) -> {
+ if(!v.getText().isEmpty()) {
+ messages.put(k, v.getText());
+ }
+ });
+
+ return new KeyedTranslation(keyTextField.getText(), messages);
+ }
+
+ private DialogBuilder prepare() {
+ JPanel rootPanel = new JPanel();
+ rootPanel.setLayout(new BoxLayout(rootPanel, BoxLayout.PAGE_AXIS));
+
+ JPanel keyPanel = new JPanel(new GridLayout(0, 1, 2,2));
+ JBLabel keyLabel = new JBLabel("Key");
+ keyTextField = new JBTextField(this.origin.getKey());
+ keyLabel.setLabelFor(keyTextField);
+ keyPanel.add(keyLabel);
+ keyPanel.add(keyTextField);
+ keyPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
+ rootPanel.add(keyPanel);
+
+ JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
+ valueTextFields = new HashMap<>();
+ for(String locale : DataStore.getInstance(project).getTranslations().getLocales()) {
+ JBLabel localeLabel = new JBLabel(locale);
+ JBTextField localeText = new JBTextField(this.origin.getTranslations().get(locale));
+ localeLabel.setLabelFor(localeText);
+
+ valuePanel.add(localeLabel);
+ valuePanel.add(localeText);
+ valueTextFields.put(locale, localeText);
+ }
+
+ JBScrollPane valuePane = new JBScrollPane(valuePanel);
+ valuePane.setBorder(BorderFactory.createTitledBorder(new EtchedBorder(), "Locales"));
+ rootPanel.add(valuePane);
+
+ DialogBuilder builder = new DialogBuilder();
+ builder.setTitle("Edit Translation");
+ builder.removeAllActions();
+ builder.addCancelAction();
+ builder.addActionDescriptor(new DeleteActionDescriptor());
+ builder.addOkAction();
+ builder.setCenterPanel(rootPanel);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/dialog/SettingsDialog.java b/src/main/java/de/marhali/easyi18n/ui/dialog/SettingsDialog.java
new file mode 100644
index 0000000..5bcc2c8
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/dialog/SettingsDialog.java
@@ -0,0 +1,79 @@
+package de.marhali.easyi18n.ui.dialog;
+
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogBuilder;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBTextField;
+
+import de.marhali.easyi18n.SettingsService;
+import de.marhali.easyi18n.data.DataStore;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.IOException;
+
+/**
+ * Plugin configuration dialog.
+ * @author marhali
+ */
+public class SettingsDialog {
+
+ private final Project project;
+
+ private TextFieldWithBrowseButton pathText;
+ private JBTextField previewText;
+
+ public SettingsDialog(Project project) {
+ this.project = project;
+ }
+
+ public void showAndHandle() {
+ String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
+ String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
+
+ if(prepare(localesPath, previewLocale).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
+ SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText());
+ SettingsService.getInstance(project).getState().setPreviewLocale(previewText.getText());
+
+ // Reload instance
+ try {
+ DataStore.getInstance(project).reloadFromDisk();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private DialogBuilder prepare(String localesPath, String previewLocale) {
+ JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
+
+ JBLabel pathLabel = new JBLabel("Locales directory");
+ pathText = new TextFieldWithBrowseButton(new JTextField(localesPath));
+
+ pathLabel.setLabelFor(pathText);
+ pathText.addBrowseFolderListener("Locales Directory", null, project, new FileChooserDescriptor(
+ false, true, false, false, false, false));
+
+ rootPanel.add(pathLabel);
+ rootPanel.add(pathText);
+
+ JBLabel previewLabel = new JBLabel("Preview locale");
+ previewText = new JBTextField(previewLocale);
+ previewLabel.setLabelFor(previewText);
+
+ rootPanel.add(previewLabel);
+ rootPanel.add(previewText);
+
+ DialogBuilder builder = new DialogBuilder();
+ builder.setTitle("Settings");
+ builder.removeAllActions();
+ builder.addCancelAction();
+ builder.addOkAction();
+ builder.setCenterPanel(rootPanel);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/dialog/descriptor/DeleteActionDescriptor.java b/src/main/java/de/marhali/easyi18n/ui/dialog/descriptor/DeleteActionDescriptor.java
new file mode 100644
index 0000000..4c991be
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/dialog/descriptor/DeleteActionDescriptor.java
@@ -0,0 +1,36 @@
+package de.marhali.easyi18n.ui.dialog.descriptor;
+
+import com.intellij.openapi.ui.DialogBuilder;
+import com.intellij.openapi.ui.DialogWrapper;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * Delete action which represents the delete button on the edit translation dialog.
+ * Action can be monitored using the exit code for the opened dialog. See EXIT_CODE.
+ * @author marhali
+ */
+public class DeleteActionDescriptor extends AbstractAction implements DialogBuilder.ActionDescriptor {
+
+ public static final int EXIT_CODE = 10;
+
+ private DialogWrapper dialogWrapper;
+
+ public DeleteActionDescriptor() {
+ super("Delete");
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if(dialogWrapper != null) {
+ dialogWrapper.close(EXIT_CODE, false);
+ }
+ }
+
+ @Override
+ public Action getAction(DialogWrapper dialogWrapper) {
+ this.dialogWrapper = dialogWrapper;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/listener/DeleteKeyListener.java b/src/main/java/de/marhali/easyi18n/ui/listener/DeleteKeyListener.java
new file mode 100644
index 0000000..283976d
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/listener/DeleteKeyListener.java
@@ -0,0 +1,30 @@
+package de.marhali.easyi18n.ui.listener;
+
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+
+/**
+ * Delete (DEL) keystroke listener.
+ * @author marhali
+ */
+public class DeleteKeyListener implements KeyListener {
+
+ private final Runnable deleteRunnable;
+
+ public DeleteKeyListener(Runnable deleteRunnable) {
+ this.deleteRunnable = deleteRunnable;
+ }
+
+ @Override
+ public void keyTyped(KeyEvent e) {
+ if(e.getKeyChar() == KeyEvent.VK_DELETE) {
+ deleteRunnable.run();
+ }
+ }
+
+ @Override
+ public void keyPressed(KeyEvent e) {}
+
+ @Override
+ public void keyReleased(KeyEvent e) {}
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/listener/PopupClickListener.java b/src/main/java/de/marhali/easyi18n/ui/listener/PopupClickListener.java
new file mode 100644
index 0000000..4d2845f
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/listener/PopupClickListener.java
@@ -0,0 +1,42 @@
+package de.marhali.easyi18n.ui.listener;
+
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.util.function.Consumer;
+
+/**
+ * Popup click listener for awt {@link MouseListener}.
+ * Emits consumer defined in constructor on popup open action.
+ * @author marhali
+ */
+public class PopupClickListener implements MouseListener {
+
+ private final Consumer callback;
+
+ public PopupClickListener(Consumer callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {}
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if(e.isPopupTrigger()) {
+ this.callback.accept(e);
+ }
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if(e.isPopupTrigger()) {
+ this.callback.accept(e);
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {}
+
+ @Override
+ public void mouseExited(MouseEvent e) {}
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TableRenderer.java b/src/main/java/de/marhali/easyi18n/ui/panel/TableRenderer.java
new file mode 100644
index 0000000..79a86c5
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TableRenderer.java
@@ -0,0 +1,41 @@
+package de.marhali.easyi18n.ui.panel;
+
+import com.intellij.ui.JBColor;
+
+import javax.swing.*;
+import javax.swing.table.DefaultTableCellRenderer;
+import java.awt.*;
+
+/**
+ * Similar to {@link DefaultTableCellRenderer} but will mark the first column red if any column is empty.
+ * @author marhali
+ */
+public class TableRenderer extends DefaultTableCellRenderer {
+
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
+ Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+
+ if(column == 0 && missesValues(row, table)) {
+ component.setForeground(JBColor.RED);
+ } else { // Reset color
+ component.setForeground(null);
+ }
+
+ return component;
+ }
+
+ private boolean missesValues(int row, JTable table) {
+ int columns = table.getColumnCount();
+
+ for(int i = 1; i < columns; i++) {
+ Object value = table.getValueAt(row, i);
+
+ if(value == null || value.toString().isEmpty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TableView.form b/src/main/java/de/marhali/easyi18n/ui/panel/TableView.form
new file mode 100644
index 0000000..00b4c80
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TableView.form
@@ -0,0 +1,29 @@
+
+
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TableView.java b/src/main/java/de/marhali/easyi18n/ui/panel/TableView.java
new file mode 100644
index 0000000..640dd9e
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TableView.java
@@ -0,0 +1,80 @@
+package de.marhali.easyi18n.ui.panel;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.ui.table.JBTable;
+import de.marhali.easyi18n.data.DataStore;
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.model.DataSynchronizer;
+import de.marhali.easyi18n.data.Translations;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationDelete;
+import de.marhali.easyi18n.model.table.TableModelTranslator;
+import de.marhali.easyi18n.ui.dialog.EditDialog;
+import de.marhali.easyi18n.ui.listener.DeleteKeyListener;
+import de.marhali.easyi18n.ui.listener.PopupClickListener;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.event.MouseEvent;
+
+public class TableView implements DataSynchronizer {
+
+ private final Project project;
+
+ private JPanel rootPanel;
+ private JPanel containerPanel;
+
+ private JBTable table;
+
+ public TableView(Project project) {
+ this.project = project;
+
+ table = new JBTable();
+ table.getEmptyText().setText("Empty");
+ table.addMouseListener(new PopupClickListener(this::handlePopup));
+ table.addKeyListener(new DeleteKeyListener(handleDeleteKey()));
+ table.setDefaultRenderer(String.class, new TableRenderer());
+
+ containerPanel.add(new JBScrollPane(table));
+ }
+
+ private void handlePopup(MouseEvent e) {
+ int row = table.rowAtPoint(e.getPoint());
+
+ if(row >= 0) {
+ String fullPath = String.valueOf(table.getValueAt(row, 0));
+ LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(fullPath);
+
+ if(node != null) {
+ new EditDialog(project, new KeyedTranslation(fullPath, node.getValue())).showAndHandle();
+ }
+ }
+ }
+
+ private Runnable handleDeleteKey() {
+ return () -> {
+ for (int selectedRow : table.getSelectedRows()) {
+ String fullPath = String.valueOf(table.getValueAt(selectedRow, 0));
+
+ DataStore.getInstance(project).processUpdate(
+ new TranslationDelete(new KeyedTranslation(fullPath, null)));
+ }
+ };
+ }
+
+ @Override
+ public void synchronize(@NotNull Translations translations, @Nullable String searchQuery) {
+ table.setModel(new TableModelTranslator(translations, searchQuery, update ->
+ DataStore.getInstance(project).processUpdate(update)));
+ }
+
+ public JPanel getRootPanel() {
+ return rootPanel;
+ }
+
+ public JBTable getTable() {
+ return table;
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.form b/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.form
new file mode 100644
index 0000000..55d85b9
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.form
@@ -0,0 +1,130 @@
+
+
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.java b/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.java
new file mode 100644
index 0000000..97d75c8
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TestPanel.java
@@ -0,0 +1,12 @@
+package de.marhali.easyi18n.ui.panel;
+
+import javax.swing.*;
+
+public class TestPanel {
+ private JPanel panel1;
+ private JTextField textField1;
+ private JTextField textField2;
+ private JTextField textField3;
+ private JTextField textField4;
+ private JTextField textField5;
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TreeRenderer.java b/src/main/java/de/marhali/easyi18n/ui/panel/TreeRenderer.java
new file mode 100644
index 0000000..4b26dd0
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TreeRenderer.java
@@ -0,0 +1,29 @@
+package de.marhali.easyi18n.ui.panel;
+
+import com.intellij.ide.util.treeView.NodeRenderer;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.openapi.util.NlsSafe;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.TreeCellRenderer;
+import java.awt.*;
+
+public class TreeRenderer extends NodeRenderer {
+
+ @Override
+ public void customizeCellRenderer(@NotNull JTree tree, @NlsSafe Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+ super.customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus);
+ }
+
+ @Override
+ protected @Nullable ItemPresentation getPresentation(Object node) {
+ if(node instanceof ItemPresentation) {
+ return (ItemPresentation) node;
+ } else {
+ return super.getPresentation(node);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.form b/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.form
new file mode 100644
index 0000000..d93a66e
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.form
@@ -0,0 +1,29 @@
+
+
diff --git a/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.java b/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.java
new file mode 100644
index 0000000..701d752
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/panel/TreeView.java
@@ -0,0 +1,132 @@
+package de.marhali.easyi18n.ui.panel;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.treeStructure.Tree;
+
+import de.marhali.easyi18n.data.DataStore;
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.model.DataSynchronizer;
+import de.marhali.easyi18n.data.Translations;
+import de.marhali.easyi18n.model.KeyedTranslation;
+import de.marhali.easyi18n.model.TranslationDelete;
+import de.marhali.easyi18n.model.tree.TreeModelTranslator;
+import de.marhali.easyi18n.ui.action.CollapseTreeViewAction;
+import de.marhali.easyi18n.ui.action.ExpandTreeViewAction;
+import de.marhali.easyi18n.ui.dialog.EditDialog;
+import de.marhali.easyi18n.ui.listener.DeleteKeyListener;
+import de.marhali.easyi18n.ui.listener.PopupClickListener;
+import de.marhali.easyi18n.util.TreeUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreePath;
+import java.awt.event.MouseEvent;
+
+public class TreeView implements DataSynchronizer {
+
+ private final Project project;
+
+ private JPanel rootPanel;
+ private JPanel toolBarPanel;
+ private JPanel containerPanel;
+
+ private Tree tree;
+
+ public TreeView(Project project) {
+ this.project = project;
+
+ tree = new Tree();
+ tree.setCellRenderer(new TreeRenderer());
+ tree.setRootVisible(false);
+ tree.getEmptyText().setText("Empty");
+ tree.addMouseListener(new PopupClickListener(this::handlePopup));
+ tree.addKeyListener(new DeleteKeyListener(handleDeleteKey()));
+
+ containerPanel.add(tree);
+ placeActions();
+ }
+
+ private void placeActions() {
+ DefaultActionGroup group = new DefaultActionGroup("TranslationsGroup", false);
+
+ ExpandTreeViewAction expand = new ExpandTreeViewAction(expandAll());
+ CollapseTreeViewAction collapse = new CollapseTreeViewAction(collapseAll());
+
+ group.add(collapse);
+ group.add(expand);
+
+ JComponent actionToolbar = ActionManager.getInstance()
+ .createActionToolbar("TranslationsActions", group, false).getComponent();
+
+ toolBarPanel.add(actionToolbar);
+ }
+
+ @Override
+ public void synchronize(@NotNull Translations translations, @Nullable String searchQuery) {
+ tree.setModel(new TreeModelTranslator(project, translations, searchQuery));
+ }
+
+ private void handlePopup(MouseEvent e) {
+ TreePath path = tree.getPathForLocation(e.getX(), e.getY());
+
+ if(path != null) {
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+
+ if(node.getUserObject() instanceof PresentationData) {
+ String fullPath = TreeUtil.getFullPath(path);
+ LocalizedNode localizedNode = DataStore.getInstance(project).getTranslations().getNode(fullPath);
+
+ if(localizedNode != null) {
+ new EditDialog(project,new KeyedTranslation(fullPath, localizedNode.getValue())).showAndHandle();
+ }
+ }
+ }
+ }
+
+ private Runnable handleDeleteKey() {
+ return () -> {
+ TreePath[] paths = tree.getSelectionPaths();
+
+ if (paths == null) {
+ return;
+ }
+
+ for (TreePath path : tree.getSelectionPaths()) {
+ String fullPath = TreeUtil.getFullPath(path);
+
+ DataStore.getInstance(project).processUpdate(
+ new TranslationDelete(new KeyedTranslation(fullPath, null)));
+ }
+ };
+ }
+
+ private Runnable expandAll() {
+ return () -> {
+ for(int i = 0; i < tree.getRowCount(); i++) {
+ tree.expandRow(i);
+ }
+ };
+ }
+
+ private Runnable collapseAll() {
+ return () -> {
+ for(int i = 0; i < tree.getRowCount(); i++) {
+ tree.collapseRow(i);
+ }
+ };
+ }
+
+ public JPanel getRootPanel() {
+ return rootPanel;
+ }
+
+ public Tree getTree() {
+ return tree;
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/ui/table/CustomTableModel.java b/src/main/java/de/marhali/easyi18n/ui/table/CustomTableModel.java
new file mode 100644
index 0000000..2c0a991
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/ui/table/CustomTableModel.java
@@ -0,0 +1,65 @@
+package de.marhali.easyi18n.ui.table;
+
+import org.jetbrains.annotations.Nls;
+
+import javax.swing.event.TableModelListener;
+import javax.swing.table.TableModel;
+
+public class CustomTableModel implements TableModel {
+
+
+ @Override
+ public int getRowCount() {
+ return 2;
+ }
+
+ @Override
+ public int getColumnCount() {
+ return 3;
+ }
+
+ @Nls
+ @Override
+ public String getColumnName(int columnIndex) {
+ switch (columnIndex) {
+ case 0:
+ return "key";
+ case 1:
+ return "de";
+ case 2:
+ return "en";
+ }
+
+ return null;
+ }
+
+ @Override
+ public Class> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ return false;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ return columnIndex == 0 ? "key" : "val";
+ }
+
+ @Override
+ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+
+ }
+
+ @Override
+ public void addTableModelListener(TableModelListener l) {
+
+ }
+
+ @Override
+ public void removeTableModelListener(TableModelListener l) {
+
+ }
+}
diff --git a/src/main/java/de/marhali/easyi18n/util/IOUtil.java b/src/main/java/de/marhali/easyi18n/util/IOUtil.java
new file mode 100644
index 0000000..0ad9162
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/util/IOUtil.java
@@ -0,0 +1,39 @@
+package de.marhali.easyi18n.util;
+
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import de.marhali.easyi18n.io.translator.JsonTranslatorIO;
+import de.marhali.easyi18n.io.translator.TranslatorIO;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Optional;
+
+public class IOUtil {
+
+ public static TranslatorIO determineFormat(String directoryPath) {
+ VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
+
+ if(directory == null || directory.getChildren() == null) {
+ throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
+ }
+
+ Optional any = Arrays.stream(directory.getChildren()).findAny();
+
+ if(!any.isPresent()) {
+ throw new IllegalStateException("Could not determine format");
+ }
+
+ switch (any.get().getFileType().getDefaultExtension().toLowerCase()) {
+ case "json":
+ return new JsonTranslatorIO();
+
+ case "properties":
+ throw new UnsupportedOperationException();
+
+ default:
+ throw new UnsupportedOperationException("Unsupported format: " +
+ any.get().getFileType().getDefaultExtension());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/util/MapUtil.java b/src/main/java/de/marhali/easyi18n/util/MapUtil.java
new file mode 100644
index 0000000..9c89fc8
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/util/MapUtil.java
@@ -0,0 +1,23 @@
+package de.marhali.easyi18n.util;
+
+import de.marhali.easyi18n.data.LocalizedNode;
+
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * Map utilities.
+ * @author marhali
+ */
+public class MapUtil {
+
+ public static TreeMap convertToTreeMap(List list) {
+ TreeMap map = new TreeMap<>();
+
+ for(LocalizedNode item : list) {
+ map.put(item.getKey(), item);
+ }
+
+ return map;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/util/TranslationsUtil.java b/src/main/java/de/marhali/easyi18n/util/TranslationsUtil.java
new file mode 100644
index 0000000..6dcca0b
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/util/TranslationsUtil.java
@@ -0,0 +1,33 @@
+package de.marhali.easyi18n.util;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class TranslationsUtil {
+
+ public static @NotNull List getSections(@NotNull String path) {
+ if(!path.contains(".")) {
+ return new ArrayList<>(Collections.singletonList(path));
+ }
+
+ return new ArrayList<>(Arrays.asList(path.split("\\.")));
+ }
+
+ public static @NotNull String sectionsToFullPath(@NotNull List sections) {
+ StringBuilder builder = new StringBuilder();
+
+ for (String section : sections) {
+ if(builder.length() > 0) {
+ builder.append(".");
+ }
+
+ builder.append(section);
+ }
+
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/util/TreeUtil.java b/src/main/java/de/marhali/easyi18n/util/TreeUtil.java
new file mode 100644
index 0000000..d0b35f2
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/util/TreeUtil.java
@@ -0,0 +1,35 @@
+package de.marhali.easyi18n.util;
+
+import com.intellij.ide.projectView.PresentationData;
+import de.marhali.easyi18n.data.LocalizedNode;
+import de.marhali.easyi18n.model.tree.TreeModelTranslator;
+
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreePath;
+
+public class TreeUtil {
+
+ public static String getFullPath(TreePath path) {
+ StringBuilder builder = new StringBuilder();
+
+
+ for (Object obj : path.getPath()) {
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode) obj;
+ Object value = node.getUserObject();
+ String section = value instanceof PresentationData ?
+ ((PresentationData) value).getPresentableText() : String.valueOf(value);
+
+ if(section == null || section.equals(LocalizedNode.ROOT_KEY)) { // Skip root node
+ continue;
+ }
+
+ if(builder.length() != 0) {
+ builder.append(".");
+ }
+
+ builder.append(section);
+ }
+
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/marhali/easyi18n/util/UiUtil.java b/src/main/java/de/marhali/easyi18n/util/UiUtil.java
new file mode 100644
index 0000000..5fdad39
--- /dev/null
+++ b/src/main/java/de/marhali/easyi18n/util/UiUtil.java
@@ -0,0 +1,28 @@
+package de.marhali.easyi18n.util;
+
+import java.util.Map;
+
+/**
+ * User interface utilities.
+ * @author marhali
+ */
+public class UiUtil {
+
+ public static String generateHtmlTooltip(Map messages) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("");
+
+ for(Map.Entry entry : messages.entrySet()) {
+ builder.append("");
+ builder.append(entry.getKey()).append(":");
+ builder.append(" ");
+ builder.append(entry.getValue());
+ builder.append("
");
+ }
+
+ builder.append("");
+
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/marhali/intelliji18n/MyBundle.kt b/src/main/kotlin/com/github/marhali/intelliji18n/MyBundle.kt
deleted file mode 100644
index d53a1ed..0000000
--- a/src/main/kotlin/com/github/marhali/intelliji18n/MyBundle.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.marhali.intelliji18n
-
-import com.intellij.AbstractBundle
-import org.jetbrains.annotations.NonNls
-import org.jetbrains.annotations.PropertyKey
-
-@NonNls
-private const val BUNDLE = "messages.MyBundle"
-
-object MyBundle : AbstractBundle(BUNDLE) {
-
- @Suppress("SpreadOperator")
- @JvmStatic
- fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) =
- getMessage(key, *params)
-
- @Suppress("SpreadOperator")
- @JvmStatic
- fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) =
- getLazyMessage(key, *params)
-}
diff --git a/src/main/kotlin/com/github/marhali/intelliji18n/listeners/MyProjectManagerListener.kt b/src/main/kotlin/com/github/marhali/intelliji18n/listeners/MyProjectManagerListener.kt
deleted file mode 100644
index 1dd57af..0000000
--- a/src/main/kotlin/com/github/marhali/intelliji18n/listeners/MyProjectManagerListener.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.marhali.intelliji18n.listeners
-
-import com.github.marhali.intelliji18n.services.MyProjectService
-import com.intellij.openapi.components.service
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.project.ProjectManagerListener
-
-internal class MyProjectManagerListener : ProjectManagerListener {
-
- override fun projectOpened(project: Project) {
- project.service()
- }
-}
diff --git a/src/main/kotlin/com/github/marhali/intelliji18n/services/MyApplicationService.kt b/src/main/kotlin/com/github/marhali/intelliji18n/services/MyApplicationService.kt
deleted file mode 100644
index f7df029..0000000
--- a/src/main/kotlin/com/github/marhali/intelliji18n/services/MyApplicationService.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.github.marhali.intelliji18n.services
-
-import com.github.marhali.intelliji18n.MyBundle
-
-class MyApplicationService {
-
- init {
- println(MyBundle.message("applicationService"))
- }
-}
diff --git a/src/main/kotlin/com/github/marhali/intelliji18n/services/MyProjectService.kt b/src/main/kotlin/com/github/marhali/intelliji18n/services/MyProjectService.kt
deleted file mode 100644
index 467a1ae..0000000
--- a/src/main/kotlin/com/github/marhali/intelliji18n/services/MyProjectService.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.github.marhali.intelliji18n.services
-
-import com.github.marhali.intelliji18n.MyBundle
-import com.intellij.openapi.project.Project
-
-class MyProjectService(project: Project) {
-
- init {
- println(MyBundle.message("projectService", project.name))
- }
-}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index a2ad852..cffda90 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,19 +1,14 @@
- com.github.marhali.intelliji18n
- intellij-i18n
+ de.marhali.easyi18n
+ easy-i18n
marhali
- com.intellij.modules.platform
+ com.intellij.modules.lang
-
-
+
+
-
-
-
-