Initial commit

This commit is contained in:
Marcel Haßlinger 2021-03-13 17:17:59 +01:00
parent 0e16479a03
commit f434643ca1
52 changed files with 2274 additions and 70 deletions

View File

@ -1,6 +1,6 @@
# intellij-i18n
# easy-i18n
![Build](https://github.com/marhali/intellij-i18n/workflows/Build/badge.svg)
![Build](https://github.com/marhali/easy-i18n/workflows/Build/badge.svg)
[![Version](https://img.shields.io/jetbrains/plugin/v/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
[![Downloads](https://img.shields.io/jetbrains/plugin/d/PLUGIN_ID.svg)](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:
<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > <kbd>Marketplace</kbd> > <kbd>Search for "intellij-i18n"</kbd> >
<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > <kbd>Marketplace</kbd> > <kbd>Search for "easy-i18n"</kbd> >
<kbd>Install Plugin</kbd>
- 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
<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > <kbd>⚙️</kbd> > <kbd>Install plugin from disk...</kbd>

View File

@ -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

View File

@ -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<SettingsState> {
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;
}
}

View File

@ -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<AnAction> 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();
}
}
}

View File

@ -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;
}
}

View File

@ -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<DataSynchronizer> 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<String> 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;
}
}

View File

@ -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<String, LocalizedNode> children;
@NotNull
private Map<String, String> value;
public LocalizedNode(@NotNull String key, @NotNull List<LocalizedNode> children) {
this.key = key;
this.children = MapUtil.convertToTreeMap(children);
this.value = new HashMap<>();
}
public LocalizedNode(@NotNull String key, @NotNull Map<String, String> 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<LocalizedNode> 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<String, String> getValue() {
return value;
}
public void setValue(@NotNull Map<String, String> value) {
this.children.clear();
this.value = value;
}
}

View File

@ -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;
}
}

View File

@ -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<String> locales;
private LocalizedNode nodes;
public Translations(List<String> locales, LocalizedNode nodes) {
this.locales = locales;
this.nodes = nodes;
}
public List<String> getLocales() {
return locales;
}
public LocalizedNode getNodes() {
return nodes;
}
public @Nullable LocalizedNode getNode(@NotNull String fullPath) {
List<String> 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<String> 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<String> getFullKeys() {
List<String> 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<String> getFullKeys(String parentFullPath, LocalizedNode localizedNode) {
List<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<String> 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<String, JsonElement> 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<String, String> messages = leafNode.getValue();
messages.put(locale, entry.getValue().getAsString());
leafNode.setValue(messages);
}
}
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<String, String> translations;
public KeyedTranslation(String key, Map<String, String> translations) {
this.key = key;
this.translations = translations;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Map<String, String> getTranslations() {
return translations;
}
public void setTranslations(Map<String, String> translations) {
this.translations = translations;
}
@Override
public String toString() {
return "KeyedTranslation{" +
"key='" + key + '\'' +
", translations=" + translations +
'}';
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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<String> locales;
private final List<String> fullKeys;
private final Consumer<TranslationUpdate> 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<TranslationUpdate> updater) {
this.translations = translations;
this.locales = translations.getLocales();
this.updater = updater;
List<String> 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 "<html><b>Key</b></html>";
}
return "<html><b>" + locales.get(columnIndex - 1) + "</b></html>";
}
@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<String, String> 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) {}
}

View File

@ -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<String> 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<String> 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));
}
}
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="de.marhali.easyi18n.ui.ActionsToolbar">
<grid id="27dc6" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<vspacer id="c33fe">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>

View File

@ -0,0 +1,6 @@
package de.marhali.easyi18n.ui;
import javax.swing.*;
public class ActionsToolbar {
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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<String> searchCallback;
private JBTextField textField;
public SearchAction(@NotNull Consumer<String> 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();
}
}
};
}
}

View File

@ -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();
}
}

View File

@ -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<String, JBTextField> 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<String, String> 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;
}
}

View File

@ -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<String, JBTextField> 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<String, String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {}
}

View File

@ -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<MouseEvent> callback;
public PopupClickListener(Consumer<MouseEvent> 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) {}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="de.marhali.easyi18n.ui.panel.TableView">
<grid id="27dc6" binding="rootPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="6c001" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<grid id="ea7f6" binding="containerPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
</form>

View File

@ -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;
}
}

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="de.marhali.easyi18n.ui.panel.TestPanel">
<grid id="27dc6" binding="panel1" default-binding="true" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="10" left="10" bottom="10" right="10"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="empty"/>
<children>
<grid id="2384b" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="2226" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Label"/>
</properties>
</component>
<vspacer id="f5394">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="298a2" class="javax.swing.JTextField" binding="textField1" default-binding="true">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
</children>
</grid>
<scrollpane id="a867e">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="e3db9" layout-manager="GridLayoutManager" row-count="9" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints/>
<properties/>
<border type="none"/>
<children>
<component id="519ad" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Label"/>
</properties>
</component>
<vspacer id="3f792">
<constraints>
<grid row="8" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="da2f8" class="javax.swing.JTextField" binding="textField2" default-binding="true">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="47bfb" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Label"/>
</properties>
</component>
<component id="a0189" class="javax.swing.JTextField" binding="textField3" default-binding="true">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="5ba8a" class="javax.swing.JLabel">
<constraints>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Label"/>
</properties>
</component>
<component id="79bc7" class="javax.swing.JTextField" binding="textField4" default-binding="true">
<constraints>
<grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="532d3" class="javax.swing.JLabel">
<constraints>
<grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Label"/>
</properties>
</component>
<component id="ddee" class="javax.swing.JTextField" binding="textField5" default-binding="true">
<constraints>
<grid row="7" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
</children>
</grid>
</children>
</scrollpane>
</children>
</grid>
</form>

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="de.marhali.easyi18n.ui.panel.TreeView">
<grid id="27dc6" binding="rootPanel" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="79692" binding="toolBarPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<grid id="4e80a" binding="containerPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</grid>
</form>

View File

@ -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;
}
}

View File

@ -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 "<html><b>key</b></html>";
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) {
}
}

View File

@ -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<VirtualFile> 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());
}
}
}

View File

@ -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<String, LocalizedNode> convertToTreeMap(List<LocalizedNode> list) {
TreeMap<String, LocalizedNode> map = new TreeMap<>();
for(LocalizedNode item : list) {
map.put(item.getKey(), item);
}
return map;
}
}

View File

@ -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<String> 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<String> sections) {
StringBuilder builder = new StringBuilder();
for (String section : sections) {
if(builder.length() > 0) {
builder.append(".");
}
builder.append(section);
}
return builder.toString();
}
}

View File

@ -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();
}
}

View File

@ -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<String, String> messages) {
StringBuilder builder = new StringBuilder();
builder.append("<html>");
for(Map.Entry<String, String> entry : messages.entrySet()) {
builder.append("<b>");
builder.append(entry.getKey()).append(":");
builder.append("</b> ");
builder.append(entry.getValue());
builder.append("<br>");
}
builder.append("</html>");
return builder.toString();
}
}

View File

@ -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)
}

View File

@ -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<MyProjectService>()
}
}

View File

@ -1,10 +0,0 @@
package com.github.marhali.intelliji18n.services
import com.github.marhali.intelliji18n.MyBundle
class MyApplicationService {
init {
println(MyBundle.message("applicationService"))
}
}

View File

@ -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))
}
}

View File

@ -1,19 +1,14 @@
<idea-plugin>
<id>com.github.marhali.intelliji18n</id>
<name>intellij-i18n</name>
<id>de.marhali.easyi18n</id>
<name>easy-i18n</name>
<vendor>marhali</vendor>
<!-- Product and plugin compatibility requirements -->
<!-- https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.github.marhali.intelliji18n.services.MyApplicationService"/>
<projectService serviceImplementation="com.github.marhali.intelliji18n.services.MyProjectService"/>
<toolWindow id="Translator" anchor="bottom" factoryClass="de.marhali.easyi18n.TranslatorToolWindowFactory" />
<projectService serviceImplementation="de.marhali.easyi18n.SettingsService" />
</extensions>
<applicationListeners>
<listener class="com.github.marhali.intelliji18n.listeners.MyProjectManagerListener"
topic="com.intellij.openapi.project.ProjectManagerListener"/>
</applicationListeners>
</idea-plugin>