Merge pull request #108 from marhali/feat/next

Feat/next
This commit is contained in:
Marcel 2022-04-22 10:38:53 +02:00 committed by GitHub
commit bad6c27386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 3276 additions and 1557 deletions

View File

@ -3,12 +3,30 @@
# easy-i18n Changelog # easy-i18n Changelog
## [Unreleased] ## [Unreleased]
### BREAKING CHANGES
- Configuration rework. Existing settings will be lost and must be configured via the new configuration page
### Added
- Key delimiters (namespace / section) can be configured
- Extract translation intention
- Full language support for Java, Kotlin, JavaScript / TypeScript, Vue and PHP
- Expand already expanded nodes after data update
- Experimental option to force translation key folding
- Individual icon for tool-window and lookup items
- Dedicated configuration file (easy-i18n.xml) inside <kbd>.idea</kbd> folder
### Changed
- Editor assistance has been reengineered. This will affect key suggestion and annotation
- Moved configuration dialog into own page inside <kbd>IDE Settings</kbd>
### Fixed
- AlreadyDisposedException on FileChangeListener after project dispose
## [3.2.0] ## [3.2.0]
### Added ### Added
- Support for IntelliJ 2022.1 - Support for IntelliJ 2022.1
### Changed ### Changed
- Updated dependencies - Updated dependencies
## [3.1.0] ## [3.1.0]

View File

@ -91,6 +91,9 @@ _For more examples, please refer to the [Examples Directory](https://github.com/
## Roadmap ## Roadmap
- [X] JSON5 Support - [X] JSON5 Support
- [X] Configurable namespace and section separators
- [X] Define default namespace to use if none was provided
- [X] Enhance editor code assistance
- [ ] XML Support - [ ] XML Support
- [ ] Mark duplicate translation values - [ ] Mark duplicate translation values

View File

@ -4,7 +4,7 @@
pluginGroup = de.marhali.easyi18n pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n pluginName = easy-i18n
# SemVer format -> https://semver.org # SemVer format -> https://semver.org
pluginVersion = 3.2.0 pluginVersion = 4.0.0-rc.1
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions. # for insight into build numbers and IntelliJ Platform versions.
@ -17,7 +17,7 @@ platformVersion = 2021.3
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = org.jetbrains.kotlin, JavaScriptLanguage, org.jetbrains.plugins.vue:213.5744.223 platformPlugins = org.jetbrains.kotlin, JavaScriptLanguage, org.jetbrains.plugins.vue:213.5744.223, com.jetbrains.php:213.5744.279
# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
javaVersion = 11 javaVersion = 11

View File

@ -1,9 +1,9 @@
package de.marhali.easyi18n; package de.marhali.easyi18n;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.bus.BusListener; import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.KeyPath;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;

View File

@ -7,10 +7,10 @@ import com.intellij.openapi.vfs.*;
import de.marhali.easyi18n.exception.EmptyLocalesDirException; import de.marhali.easyi18n.exception.EmptyLocalesDirException;
import de.marhali.easyi18n.io.IOHandler; import de.marhali.easyi18n.io.IOHandler;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.service.FileChangeListener; import de.marhali.easyi18n.service.FileChangeListener;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.NotificationHelper; import de.marhali.easyi18n.util.NotificationHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -35,7 +35,7 @@ public class DataStore {
this.changeListener = new FileChangeListener(project); this.changeListener = new FileChangeListener(project);
VirtualFileManager.getInstance().addAsyncFileListener( VirtualFileManager.getInstance().addAsyncFileListener(
this.changeListener, Disposer.newDisposable("EasyI18n")); this.changeListener, Disposer.newDisposable(project, "EasyI18n"));
} }
public @NotNull TranslationData getData() { public @NotNull TranslationData getData() {
@ -48,18 +48,18 @@ public class DataStore {
* @param successResult Consumer will inform if operation was successful * @param successResult Consumer will inform if operation was successful
*/ */
public void loadFromPersistenceLayer(@NotNull Consumer<Boolean> successResult) { public void loadFromPersistenceLayer(@NotNull Consumer<Boolean> successResult) {
SettingsState settings = SettingsService.getInstance(this.project).getState(); ProjectSettings settings = ProjectSettingsService.get(project).getState();
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added) ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> { ApplicationManager.getApplication().runReadAction(() -> {
try { try {
this.data = new IOHandler(settings).read(); this.data = new IOHandler(settings).read();
this.changeListener.updateLocalesPath(settings.getLocalesPath()); this.changeListener.updateLocalesPath(settings.getLocalesDirectory());
successResult.accept(true); successResult.accept(true);
} catch (Exception ex) { } catch (Exception ex) {
this.data = new TranslationData(settings.isSortKeys()); this.data = new TranslationData(settings.isSorting());
successResult.accept(false); successResult.accept(false);
if(ex instanceof EmptyLocalesDirException) { if(ex instanceof EmptyLocalesDirException) {
@ -76,7 +76,7 @@ public class DataStore {
* @param successResult Consumer will inform if operation was successful * @param successResult Consumer will inform if operation was successful
*/ */
public void saveToPersistenceLayer(@NotNull Consumer<Boolean> successResult) { public void saveToPersistenceLayer(@NotNull Consumer<Boolean> successResult) {
SettingsState settings = SettingsService.getInstance(this.project).getState(); ProjectSettings settings = ProjectSettingsService.get(project).getState();
ApplicationManager.getApplication().runWriteAction(() -> { ApplicationManager.getApplication().runWriteAction(() -> {
try { try {

View File

@ -3,7 +3,7 @@ package de.marhali.easyi18n;
import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.TranslationUpdate; import de.marhali.easyi18n.model.action.TranslationUpdate;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -52,13 +52,22 @@ public class InstanceManager {
return this.bus; return this.bus;
} }
/**
* Reloads the plugin instance. Unsaved cached data will be deleted.
* Fetches data from persistence layer and notifies all endpoints via {@link DataBus}.
*/
public void reload() {
store.loadFromPersistenceLayer((success) ->
bus.propagate().onUpdateData(store.getData()));
}
public void processUpdate(TranslationUpdate update) { public void processUpdate(TranslationUpdate update) {
if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation
this.store.getData().setTranslation(update.getOrigin().getKey(), null); this.store.getData().setTranslation(update.getOrigin().getKey(), null);
} }
if(!update.isDeletion()) { // Create or re-create translation with changed data if(!update.isDeletion()) { // Create or re-create translation with changed data
this.store.getData().setTranslation(update.getChange().getKey(), update.getChange().getTranslation()); this.store.getData().setTranslation(update.getChange().getKey(), update.getChange().getValue());
} }
this.store.saveToPersistenceLayer(success -> { this.store.saveToPersistenceLayer(success -> {

View File

@ -6,10 +6,10 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.ui.content.Content; import com.intellij.ui.content.Content;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.dialog.AddDialog; import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.util.KeyPathConverter;
import de.marhali.easyi18n.util.TreeUtil; import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -32,7 +32,7 @@ public class AddAction extends AnAction {
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
new AddDialog(Objects.requireNonNull(e.getProject()), detectPreKey(e.getProject())).showAndHandle(); new AddDialog(Objects.requireNonNull(e.getProject()), detectPreKey(e.getProject()), null).showAndHandle();
} }
private @Nullable KeyPath detectPreKey(@NotNull Project project) { private @Nullable KeyPath detectPreKey(@NotNull Project project) {
@ -58,7 +58,7 @@ public class AddAction extends AnAction {
if(row >= 0) { if(row >= 0) {
String path = String.valueOf(window.getTableView().getTable().getValueAt(row, 0)); String path = String.valueOf(window.getTableView().getTable().getValueAt(row, 0));
return converter.split(path); return converter.fromString(path);
} }
} }

View File

@ -8,6 +8,7 @@ import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@ -23,9 +24,6 @@ public class ReloadAction extends AnAction {
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
InstanceManager manager = InstanceManager.get(e.getProject()); InstanceManager.get(Objects.requireNonNull(e.getProject())).reload();
manager.store().loadFromPersistenceLayer((success) -> {
manager.bus().propagate().onUpdateData(manager.store().getData());
});
} }
} }

View File

@ -3,7 +3,10 @@ package de.marhali.easyi18n.action;
import com.intellij.icons.AllIcons; import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.AnActionEvent;
import de.marhali.easyi18n.dialog.SettingsDialog; import com.intellij.openapi.options.ShowSettingsUtil;
import de.marhali.easyi18n.settings.ProjectSettingsConfigurable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@ -21,6 +24,6 @@ public class SettingsAction extends AnAction {
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
new SettingsDialog(e.getProject()).showAndHandle(); ShowSettingsUtil.getInstance().showSettingsDialog(e.getProject(), ProjectSettingsConfigurable.class);
} }
} }

View File

@ -0,0 +1,17 @@
package de.marhali.easyi18n.assistance;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
/**
* Used to define editor hooks as assistable.
* @author marhali
*/
public interface OptionalAssistance {
default boolean isAssistance(@NotNull Project project) {
return ProjectSettingsService.get(project).getState().isAssistance();
}
}

View File

@ -0,0 +1,17 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiLiteralExpression;
/**
* Java specific completion contributor.
* @author marhali
*/
public class JavaCompletionContributor extends CompletionContributor {
public JavaCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralExpression.class),
new KeyCompletionProvider());
}
}

View File

@ -0,0 +1,17 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.patterns.PlatformPatterns;
/**
* JavaScript specific completion contributor.
* @author marhali
*/
public class JsCompletionContributor extends CompletionContributor {
public JsCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(JSLiteralExpression.class),
new KeyCompletionProvider());
}
}

View File

@ -0,0 +1,60 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.IconLoader;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.Set;
/**
* Provides existing translation keys for code completion.
* @author marhali
*/
class KeyCompletionProvider extends CompletionProvider<CompletionParameters> implements OptionalAssistance {
private static final Icon icon = IconLoader.getIcon("/icons/translate13.svg", KeyCompletionProvider.class);
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
Project project = parameters.getOriginalFile().getProject();
if(!isAssistance(project)) {
return;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
TranslationData data = InstanceManager.get(project).store().getData();
Set<KeyPath> fullKeys = data.getFullKeys();
for (KeyPath key : fullKeys) {
result.addElement(constructLookup(new Translation(key, data.getTranslation(key)), settings));
}
}
private LookupElement constructLookup(Translation translation, ProjectSettings settings) {
KeyPathConverter converter = new KeyPathConverter(settings);
return LookupElementBuilder
.create(converter.toString(translation.getKey()))
.withTailText(" " + translation.getValue().get(settings.getPreviewLocale()), true)
.withIcon(icon);
}
}

View File

@ -1,19 +1,16 @@
package de.marhali.easyi18n.editor.kotlin; package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType; import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PlatformPatterns;
import de.marhali.easyi18n.editor.KeyCompletionProvider;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry; import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/** /**
* Kotlin specific translation key completion contributor. * Kotlin specific completion contributor.
* @author marhali * @author marhali
*/ */
public class KotlinKeyCompletionContributor extends CompletionContributor { public class KtCompletionContributor extends CompletionContributor {
public KtCompletionContributor() {
public KotlinKeyCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(KtLiteralStringTemplateEntry.class), extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(KtLiteralStringTemplateEntry.class),
new KeyCompletionProvider()); new KeyCompletionProvider());
} }

View File

@ -0,0 +1,17 @@
package de.marhali.easyi18n.assistance.completion;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
/**
* Php specific completion contributor.
* @author marhali
*/
public class PhpCompletionContributor extends CompletionContributor {
public PhpCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(StringLiteralExpression.class),
new KeyCompletionProvider());
}
}

View File

@ -0,0 +1,104 @@
package de.marhali.easyi18n.assistance.documentation;
import com.intellij.lang.documentation.DocumentationMarkup;
import com.intellij.lang.documentation.DocumentationProvider;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.ResourceBundle;
/**
* Provides locale values as documentation for translation keys.
* @author marhali
*/
abstract class AbstractDocumentationProvider implements DocumentationProvider, OptionalAssistance {
private static final ResourceBundle bundle = ResourceBundle.getBundle("messages");
/**
* Checks if the provided key is a valid translation-key and generates the equivalent documentation for it.
* @param project Opened project
* @param key Designated translation key
* @return Generated documentation or null if not responsible
*/
protected @Nullable String generateDoc(@NotNull Project project, @Nullable String key) {
if(key == null || !isAssistance(project)) {
return null;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
KeyPath path = converter.fromString(key);
// So we want to take care of context and pluralization here
// we should check the last key section for plural / context delims and if so provide all leafs within the last node
if(path.isEmpty()) {
return null;
}
TranslationData data = InstanceManager.get(project).store().getData();
String leaf = path.remove(path.size() - 1);
TranslationNode leafNode = data.getRootNode();
for(String section : path) {
leafNode = leafNode.getChildren().get(section);
if(leafNode == null) { // Cannot resolve last node before leaf
return null;
}
}
Map<String, String> results = new LinkedHashMap<>();
// Filter results for matching leafs (contextual and pluralization support)
for (Map.Entry<String, TranslationNode> entry : leafNode.getChildren().entrySet()) {
if(entry.getKey().startsWith(leaf) && entry.getValue().isLeaf()) {
results.put(entry.getKey(), entry.getValue().getValue().get(settings.getPreviewLocale()));
}
}
if(results.isEmpty()) { // No results to show
return null;
}
StringBuilder builder = new StringBuilder();
builder.append(DocumentationMarkup.DEFINITION_START);
builder.append(bundle.getString("documentation"));
builder.append(DocumentationMarkup.DEFINITION_END);
if(results.size() == 1) { // Single value
builder.append(DocumentationMarkup.CONTENT_START);
builder.append("<strong>").append(results.values().toArray()[0]).append("</strong>");
builder.append(DocumentationMarkup.CONTENT_END);
} else { // Pluralization | Contextual relevant values
builder.append(DocumentationMarkup.SECTIONS_START);
for (Map.Entry<String, String> entry : results.entrySet()) {
builder.append(DocumentationMarkup.SECTION_HEADER_START);
builder.append(entry.getKey()).append(":");
builder.append(DocumentationMarkup.SECTION_SEPARATOR);
builder.append("<p>");
builder.append("<strong>").append(entry.getValue()).append("</strong>");
}
builder.append(DocumentationMarkup.SECTIONS_END);
}
return builder.toString();
}
}

View File

@ -0,0 +1,26 @@
package de.marhali.easyi18n.assistance.documentation;
import com.intellij.psi.PsiElement;
import de.marhali.easyi18n.assistance.reference.PsiKeyReference;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;
/**
* Language unspecific documentation provider. Every supported language should register an extension to this EP.
* @author marhali
*/
public class CommonDocumentationProvider extends AbstractDocumentationProvider {
@Override
public @Nullable
@Nls String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
if(!(element instanceof PsiKeyReference.TranslationReference)) {
return null;
}
PsiKeyReference.TranslationReference keyReference = (PsiKeyReference.TranslationReference) element;
String value = keyReference.getName();
return generateDoc(element.getProject(), value);
}
}

View File

@ -0,0 +1,104 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Language specific translation key folding with representative locale value.
* @author marhali
*/
abstract class AbstractFoldingBuilder extends FoldingBuilderEx implements OptionalAssistance {
/**
* Extract all relevant folding regions for the desired root element.
* The implementation does not need to verify if the character literal is a valid translation.
* @param root Root element
* @return found regions
*/
abstract @NotNull List<Pair<String, PsiElement>> extractRegions(@NotNull PsiElement root);
/**
* Extract the text from the given node.
* @param node Node
* @return extracted text or null if not applicable
*/
abstract @Nullable String extractText(@NotNull ASTNode node);
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
if(quick || !isAssistance(root.getProject())) {
return FoldingDescriptor.EMPTY;
}
List<FoldingDescriptor> descriptors = new ArrayList<>();
ProjectSettings settings = ProjectSettingsService.get(root.getProject()).getState();
TranslationData data = InstanceManager.get(root.getProject()).store().getData();
KeyPathConverter converter = new KeyPathConverter(settings);
for(Pair<String, PsiElement> region : extractRegions(root)) {
if(data.getTranslation(converter.fromString(region.first)) == null) {
continue;
}
TextRange range = new TextRange(region.second.getTextRange().getStartOffset() + 1,
region.second.getTextRange().getEndOffset() - 1);
// Some language implementations like [Vue Template] does not support FoldingGroup's
FoldingDescriptor descriptor = new FoldingDescriptor(region.second.getNode(), range,
null, Set.of(), settings.isAlwaysFold());
descriptors.add(descriptor);
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
@Override
public @Nullable String getPlaceholderText(@NotNull ASTNode node) {
String text = extractText(node);
if(text == null) {
return null;
}
Project project = node.getPsi().getProject();
DataStore store = InstanceManager.get(project).store();
KeyPathConverter converter = new KeyPathConverter(project);
TranslationValue localeValues = store.getData().getTranslation(converter.fromString(text));
if(localeValues == null) {
return null;
}
String previewLocale = ProjectSettingsService.get(project).getState().getPreviewLocale();
return localeValues.get(previewLocale);
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return true;
}
}

View File

@ -0,0 +1,32 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
/**
* Java specific translation key folding.
* @author marhali
*/
public class JavaFoldingBuilder extends AbstractFoldingBuilder {
@Override
@NotNull List<Pair<String, PsiElement>> extractRegions(@NotNull PsiElement root) {
return PsiTreeUtil.findChildrenOfType(root, PsiLiteralExpression.class).stream().map(literalExpression ->
Pair.pair(String.valueOf(literalExpression.getValue()), (PsiElement) literalExpression))
.collect(Collectors.toList());
}
@Override
@Nullable String extractText(@NotNull ASTNode node) {
PsiLiteralExpression literalExpression = node.getPsi(PsiLiteralExpression.class);
return String.valueOf(literalExpression.getValue());
}
}

View File

@ -0,0 +1,32 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
/**
* JavaScript specific translation key folding.
* @author marhali
*/
public class JsFoldingBuilder extends AbstractFoldingBuilder {
@Override
@NotNull List<Pair<String, PsiElement>> extractRegions(@NotNull PsiElement root) {
return PsiTreeUtil.findChildrenOfType(root, JSLiteralExpression.class).stream().map(literalExpression ->
Pair.pair(literalExpression.getStringValue(), (PsiElement) literalExpression))
.collect(Collectors.toList());
}
@Override
@Nullable String extractText(@NotNull ASTNode node) {
JSLiteralExpression literalExpression = node.getPsi(JSLiteralExpression.class);
return literalExpression.getStringValue();
}
}

View File

@ -0,0 +1,45 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.psi.KtStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
import java.util.ArrayList;
import java.util.List;
/**
* Kotlin specific translation-key folding.
* @author marhali
*/
public class KtFoldingBuilder extends AbstractFoldingBuilder {
@Override
@NotNull List<Pair<String, PsiElement>> extractRegions(@NotNull PsiElement root) {
List<Pair<String, PsiElement>> regions = new ArrayList<>();
for (KtStringTemplateExpression templateExpression : PsiTreeUtil.findChildrenOfType(root, KtStringTemplateExpression.class)) {
for (KtStringTemplateEntry entry : templateExpression.getEntries()) {
regions.add(Pair.pair(entry.getText(), templateExpression));
break;
}
}
return regions;
}
@Override
@Nullable String extractText(@NotNull ASTNode node) {
KtStringTemplateExpression templateExpression = node.getPsi(KtStringTemplateExpression.class);
for (KtStringTemplateEntry entry : templateExpression.getEntries()) {
return entry.getText();
}
return null;
}
}

View File

@ -0,0 +1,31 @@
package de.marhali.easyi18n.assistance.folding;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Collectors;
/**
* Php specific translation key folding.
* @author marhali
*/
public class PhpFoldingBuilder extends AbstractFoldingBuilder {
@Override
@NotNull List<Pair<String, PsiElement>> extractRegions(@NotNull PsiElement root) {
return PsiTreeUtil.findChildrenOfType(root, StringLiteralExpression.class).stream().map(literalExpression ->
Pair.pair(literalExpression.getContents(), (PsiElement) literalExpression))
.collect(Collectors.toList());
}
@Override
@Nullable String extractText(@NotNull ASTNode node) {
StringLiteralExpression literalExpression = node.getPsi(StringLiteralExpression.class);
return literalExpression.getContents();
}
}

View File

@ -0,0 +1,138 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.util.IncorrectOperationException;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ResourceBundle;
/**
* Intention for translation related use-cases.
* Can be used to extract (create) translations or to edit existing ones.
* @author marhali
*/
abstract class AbstractTranslationIntention extends BaseElementAtCaretIntentionAction implements OptionalAssistance {
protected static final ResourceBundle bundle = ResourceBundle.getBundle("messages");
private boolean existingTranslation = false;
@Override
public @IntentionName @NotNull String getText() {
return existingTranslation
? bundle.getString("action.edit")
: bundle.getString("action.extract");
}
@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return "EasyI18n";
}
@Override
public boolean startInWriteAction() {
return false;
}
/**
* This is the only method a language-specific translation intention needs to implement.
* The implementation needs to verify element type and extract the relevant key literal or value.
* @param element Element at caret
* @return extract translation key (not verified!) or null if intention is not applicable for this element
*/
protected abstract @Nullable String extractText(@NotNull PsiElement element);
@NotNull TextRange convertRange(@NotNull TextRange input) {
return new TextRange(input.getStartOffset(), input.getEndOffset());
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
if(!isAssistance(project)) {
return false;
}
String text = extractText(element);
if(text != null) {
KeyPathConverter converter = new KeyPathConverter(project);
existingTranslation = InstanceManager.get(project).store().getData()
.getTranslation(converter.fromString(text)) != null;
}
return text != null;
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element)
throws IncorrectOperationException {
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
String text = extractText(element);
if(text == null) {
throw new IncorrectOperationException("Cannot extract translation intention at caret");
}
TranslationData data = InstanceManager.get(project).store().getData();
KeyPath path = converter.fromString(text);
TranslationValue existingTranslation = data.getTranslation(path);
// Existing translation - edit dialog
if(existingTranslation != null) {
new EditDialog(project, new Translation(path, existingTranslation)).showAndHandle();
return;
}
// Extract translation by key
// We assume that a text is a translation-key if it contains section delimiters and does not end with them
if(text.contains(settings.getSectionDelimiter()) && !text.endsWith(settings.getSectionDelimiter())) {
new AddDialog(project, path, null).showAndHandle();
return;
}
// Extract translation by preview locale value
AddDialog dialog = new AddDialog(project, new KeyPath(), text);
dialog.registerCallback(translationUpdate -> { // Replace text at caret with chosen translation key
if(editor != null) {
Document doc = editor.getDocument();
Caret caret = editor.getCaretModel().getPrimaryCaret();
TextRange range = convertRange(element.getTextRange());
WriteCommandAction.runWriteCommandAction(project, () ->
doc.replaceString(range.getStartOffset(), range.getEndOffset(),
converter.toString(translationUpdate.getChange().getKey())));
caret.removeSelection();
}
});
dialog.showAndHandle();
}
}

View File

@ -0,0 +1,27 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Java specific translation intention.
* @author marhali
*/
public class JavaTranslationIntention extends AbstractTranslationIntention {
@Override
protected @Nullable String extractText(@NotNull PsiElement element) {
if(!(element.getParent() instanceof PsiLiteralExpression)) {
return null;
}
return String.valueOf(((PsiLiteralExpression) element.getParent()).getValue());
}
@Override
@NotNull TextRange convertRange(@NotNull TextRange input) {
return new TextRange(input.getStartOffset() + 1, input.getEndOffset() - 1);
}
}

View File

@ -0,0 +1,28 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* JavaScript specific translation key intention.
* @author marhali
*/
public class JsTranslationIntention extends AbstractTranslationIntention {
@Override
protected @Nullable String extractText(@NotNull PsiElement element) {
if(!(element.getParent() instanceof JSLiteralExpression)) {
return null;
}
return ((JSLiteralExpression) element.getParent()).getStringValue();
}
@Override
@NotNull TextRange convertRange(@NotNull TextRange input) {
return new TextRange(input.getStartOffset() + 1, input.getEndOffset() - 1);
}
}

View File

@ -0,0 +1,23 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/**
* Kotlin specific translation key intention.
* @author marhali
*/
public class KtTranslationIntention extends AbstractTranslationIntention {
@Override
protected @Nullable String extractText(@NotNull PsiElement element) {
if(!(element.getParent() instanceof KtLiteralStringTemplateEntry)) {
return null;
}
KtLiteralStringTemplateEntry expression = (KtLiteralStringTemplateEntry) element.getParent();
return expression.getText();
}
}

View File

@ -0,0 +1,23 @@
package de.marhali.easyi18n.assistance.intention;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Php specific translation intention
* @author marhali
*/
public class PhpTranslationIntention extends AbstractTranslationIntention {
@Override
protected @Nullable String extractText(@NotNull PsiElement element) {
if(!(element.getParent() instanceof StringLiteralExpression)) {
return null;
}
return ((StringLiteralExpression) element.getParent()).getContents();
}
}

View File

@ -0,0 +1,53 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceContributor;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.assistance.OptionalAssistance;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Language specific translation key reference contributor.
* @author marhali
*/
abstract class AbstractKeyReferenceContributor extends PsiReferenceContributor implements OptionalAssistance {
/**
* Searches for relevant translation-key references
* @param project Opened project
* @param element Targeted element
* @param text Designated translation key
* @return Matched translation-key reference(s)
*/
protected @NotNull PsiReference[] getReferences(
@NotNull Project project, @NotNull PsiElement element, @Nullable String text) {
if(text == null || text.isEmpty() || !isAssistance(project)) {
return PsiReference.EMPTY_ARRAY;
}
ProjectSettings settings = ProjectSettingsService.get(project).getState();
KeyPathConverter converter = new KeyPathConverter(settings);
// TODO: We should provide multiple references if not a leaf node was provided (contextual / plurals support)
KeyPath path = converter.fromString(text);
TranslationValue values = InstanceManager.get(project).store().getData().getTranslation(path);
if(values == null) { // We only reference valid and existing translations
return PsiReference.EMPTY_ARRAY;
}
return new PsiReference[] {
new PsiKeyReference(converter, new Translation(path, values), element)
};
}
}

View File

@ -0,0 +1,41 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
/**
* Java specific key reference binding.
* @author marhali
*/
public class JavaKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(PsiLiteralExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Project project = element.getProject();
PsiLiteralExpression literalExpression = (PsiLiteralExpression) element;
String value = literalExpression.getValue() instanceof String
? (String) literalExpression.getValue()
: null;
return getReferences(project, element, value);
}
};
}
}

View File

@ -0,0 +1,39 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.psi.PsiReferenceRegistrar;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
/**
* JavaScript specific translation-key reference binding.
* @author marhali
*/
public class JsKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(JSLiteralExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Project project = element.getProject();
JSLiteralExpression literalExpression = (JSLiteralExpression) element;
String value = literalExpression.getStringValue();
return getReferences(project, element, value);
}
};
}
}

View File

@ -0,0 +1,46 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.psi.PsiReferenceRegistrar;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
import java.util.Arrays;
import java.util.Optional;
/**
* Kotlin specific translation-key reference binding.
* @author marhali
*/
public class KtKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement().inside(KtStringTemplateExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Optional<PsiElement> targetElement = Arrays.stream(element.getChildren()).filter(child ->
child instanceof KtLiteralStringTemplateEntry).findAny();
if(targetElement.isEmpty()) {
return PsiReference.EMPTY_ARRAY;
}
return getReferences(element.getProject(), element, targetElement.get().getText());
}
};
}
}

View File

@ -0,0 +1,37 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.psi.PsiReferenceRegistrar;
import com.intellij.util.ProcessingContext;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import org.jetbrains.annotations.NotNull;
/**
* Php specific key reference binding
*/
public class PhpKeyReferenceContributor extends AbstractKeyReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(StringLiteralExpression.class),
getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
Project project = element.getProject();
StringLiteralExpression literalExpression = (StringLiteralExpression) element;
return getReferences(project, element, literalExpression.getContents());
}
};
}
}

View File

@ -0,0 +1,72 @@
package de.marhali.easyi18n.assistance.reference;
import com.intellij.navigation.ItemPresentation;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReferenceBase;
import com.intellij.psi.SyntheticElement;
import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* References translation keys inside editor with corresponding {@link EditDialog} / {@link AddDialog}.
* @author marhali
*/
public class PsiKeyReference extends PsiReferenceBase<PsiElement> {
private final @NotNull Translation translation;
private final @NotNull KeyPathConverter converter;
protected PsiKeyReference(
@NotNull KeyPathConverter converter, @NotNull Translation translation, @NotNull PsiElement element) {
super(element, true);
this.translation = translation;
this.converter = converter;
}
public @NotNull String getKey() {
return converter.toString(translation.getKey());
}
@Override
public @Nullable PsiElement resolve() {
return new TranslationReference();
}
public class TranslationReference extends FakePsiElement implements SyntheticElement {
@Override
public PsiElement getParent() {
return myElement;
}
@Override
public void navigate(boolean requestFocus) {
new EditDialog(getProject(), translation).showAndHandle();
}
@Override
public String getPresentableText() {
return getKey();
}
@Override
public String getName() {
return getKey();
}
@Override
public @Nullable TextRange getTextRange() {
TextRange rangeInElement = getRangeInElement();
TextRange elementRange = myElement.getTextRange();
return elementRange != null ? rangeInElement.shiftRight(elementRange.getStartOffset()) : rangeInElement;
}
}
}

View File

@ -3,112 +3,65 @@ package de.marhali.easyi18n.dialog;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogBuilder; import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper; 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.InstanceManager; import de.marhali.easyi18n.model.action.TranslationCreate;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.model.action.TranslationUpdate;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
/** /**
* Create translation dialog. * Dialog to create a new translation with all associated locale values.
* Supports optional prefill technique for translation key or locale value.
* @author marhali * @author marhali
*/ */
public class AddDialog { public class AddDialog extends TranslationDialog {
private final @NotNull Project project; /**
private final @NotNull KeyPathConverter converter; * Constructs a new create dialog with prefilled fields
* @param project Opened project
private @NotNull KeyPath preKey; * @param prefillKey Prefill translation key
* @param prefillLocale Prefill preview locale value
private JBTextField keyTextField; */
private Map<String, JBTextField> valueTextFields; public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale) {
super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(),
public AddDialog(@NotNull Project project, @Nullable KeyPath preKey) { prefillLocale != null
this(project); ? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale)
this.preKey = preKey == null ? new KeyPath() : preKey; : null)
);
} }
/**
* Constructs a new create dialog without prefilled fields.
* @param project Opened project
*/
public AddDialog(@NotNull Project project) { public AddDialog(@NotNull Project project) {
this.project = project; this(project, new KeyPath(), "");
this.converter = new KeyPathConverter(project);
this.preKey = new KeyPath();
} }
public void showAndHandle() {
int code = prepare().show();
if(code == DialogWrapper.OK_EXIT_CODE) {
saveTranslation();
}
}
private void saveTranslation() {
Translation translation = new Translation();
valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) {
translation.put(k, v.getText());
}
});
KeyedTranslation keyedTranslation = new KeyedTranslation(converter.split(keyTextField.getText()), translation);
TranslationCreate creation = new TranslationCreate(keyedTranslation);
InstanceManager.get(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(ResourceBundle.getBundle("messages").getString("translation.key"));
keyTextField = new JBTextField(this.converter.concat(this.preKey));
keyLabel.setLabelFor(keyTextField);
keyPanel.add(keyLabel);
keyPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
keyPanel.add(keyTextField);
rootPanel.add(keyPanel);
if(!this.preKey.isEmpty()) { // Add delimiter if pre key is defined
keyTextField.setText(keyTextField.getText() + KeyPath.DELIMITER);
}
JPanel valuePanel = new JPanel(new GridLayout(0, 1, 2, 2));
valueTextFields = new HashMap<>();
for(String locale : InstanceManager.get(project).store().getData().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(),
ResourceBundle.getBundle("messages").getString("translation.locales")));
rootPanel.add(valuePane);
@Override
protected @NotNull DialogBuilder configure(@NotNull JComponent centerPanel) {
DialogBuilder builder = new DialogBuilder(); DialogBuilder builder = new DialogBuilder();
builder.setTitle(ResourceBundle.getBundle("messages").getString("action.add")); builder.setTitle(bundle.getString("action.add"));
builder.removeAllActions(); builder.removeAllActions();
builder.addOkAction(); builder.addOkAction();
builder.addCancelAction(); builder.addCancelAction();
builder.setCenterPanel(rootPanel); builder.setCenterPanel(centerPanel);
return builder; return builder;
} }
}
@Override
protected @Nullable TranslationUpdate handleExit(int exitCode) {
if(exitCode == DialogWrapper.OK_EXIT_CODE) {
return new TranslationCreate(getState());
}
return null;
}
}

View File

@ -3,102 +3,53 @@ package de.marhali.easyi18n.dialog;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogBuilder; import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper; 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.InstanceManager;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor; import de.marhali.easyi18n.dialog.descriptor.DeleteActionDescriptor;
import de.marhali.easyi18n.model.action.TranslationDelete;
import de.marhali.easyi18n.model.action.TranslationUpdate;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
/** /**
* Edit translation dialog. * Dialog to edit or delete an existing translation.
* @author marhali * @author marhali
*/ */
public class EditDialog { public class EditDialog extends TranslationDialog {
private final Project project; /**
private final KeyPathConverter converter; * Constructs a new edit dialog with the provided translation
* @param project Opened project
private final KeyedTranslation origin; * @param origin Translation to edit
*/
private JBTextField keyTextField; public EditDialog(@NotNull Project project, @NotNull Translation origin) {
private Map<String, JBTextField> valueTextFields; super(project, origin);
public EditDialog(Project project, KeyedTranslation origin) {
this.project = project;
this.converter = new KeyPathConverter(project);
this.origin = origin;
} }
public void showAndHandle() { @Override
int code = prepare().show(); protected @NotNull DialogBuilder configure(@NotNull JComponent centerPanel) {
if(code == DialogWrapper.OK_EXIT_CODE) { // Edit
InstanceManager.get(project).processUpdate(new TranslationUpdate(origin, getChanges()));
} else if(code == DeleteActionDescriptor.EXIT_CODE) { // Delete
InstanceManager.get(project).processUpdate(new TranslationDelete(origin));
}
}
private KeyedTranslation getChanges() {
Translation translation = new Translation();
valueTextFields.forEach((k, v) -> {
if(!v.getText().isEmpty()) {
translation.put(k, v.getText());
}
});
return new KeyedTranslation(converter.split(keyTextField.getText()), translation);
}
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(ResourceBundle.getBundle("messages").getString("translation.key"));
keyTextField = new JBTextField(this.converter.concat(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 : InstanceManager.get(project).store().getData().getLocales()) {
JBLabel localeLabel = new JBLabel(locale);
JBTextField localeText = new JBTextField(this.origin.getTranslation().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(),
ResourceBundle.getBundle("messages").getString("translation.locales")));
rootPanel.add(valuePane);
DialogBuilder builder = new DialogBuilder(); DialogBuilder builder = new DialogBuilder();
builder.setTitle(ResourceBundle.getBundle("messages").getString("action.edit")); builder.setTitle(bundle.getString("action.edit"));
builder.removeAllActions(); builder.removeAllActions();
builder.addCancelAction(); builder.addCancelAction();
builder.addActionDescriptor(new DeleteActionDescriptor()); builder.addActionDescriptor(new DeleteActionDescriptor());
builder.addOkAction(); builder.addOkAction();
builder.setCenterPanel(rootPanel); builder.setCenterPanel(centerPanel);
return builder; return builder;
} }
@Override
protected @Nullable TranslationUpdate handleExit(int exitCode) {
switch (exitCode) {
case DialogWrapper.OK_EXIT_CODE:
return new TranslationUpdate(origin, getState());
case DeleteActionDescriptor.EXIT_CODE:
return new TranslationDelete(origin);
default:
return null;
}
}
} }

View File

@ -1,173 +0,0 @@
package de.marhali.easyi18n.dialog;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBPanel;
import com.intellij.ui.components.JBTextField;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.io.parser.ArrayMapper;
import de.marhali.easyi18n.model.FolderStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.service.SettingsService;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ResourceBundle;
/**
* Plugin configuration dialog.
* @author marhali
*/
public class SettingsDialog {
private final Project project;
private TextFieldWithBrowseButton pathText;
private ComboBox<String> folderStrategyComboBox;
private ComboBox<String> parserStrategyComboBox;
private JBTextField filePatternText;
private JBTextField previewLocaleText;
private JBTextField pathPrefixText;
private JBCheckBox sortKeysCheckbox;
private JBCheckBox nestedKeysCheckbox;
private JBCheckBox codeAssistanceCheckbox;
public SettingsDialog(Project project) {
this.project = project;
}
public void showAndHandle() {
SettingsState state = SettingsService.getInstance(project).getState();
if(prepare(state).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
state.setLocalesPath(pathText.getText());
state.setFolderStrategy(FolderStrategyType.fromIndex(folderStrategyComboBox.getSelectedIndex()));
state.setParserStrategy(ParserStrategyType.fromIndex(parserStrategyComboBox.getSelectedIndex()));
state.setFilePattern(filePatternText.getText());
state.setPreviewLocale(previewLocaleText.getText());
state.setPathPrefix(pathPrefixText.getText());
state.setSortKeys(sortKeysCheckbox.isSelected());
state.setNestedKeys(nestedKeysCheckbox.isSelected());
state.setCodeAssistance(codeAssistanceCheckbox.isSelected());
// Reload instance
InstanceManager manager = InstanceManager.get(project);
manager.store().loadFromPersistenceLayer((success) ->
manager.bus().propagate().onUpdateData(manager.store().getData()));
}
}
private DialogBuilder prepare(SettingsState state) {
ResourceBundle bundle = ResourceBundle.getBundle("messages");
JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
/* path */
JBLabel pathLabel = new JBLabel(bundle.getString("settings.path.text"));
pathText = new TextFieldWithBrowseButton(new JTextField(state.getLocalesPath()));
pathLabel.setLabelFor(pathText);
pathText.addBrowseFolderListener(bundle.getString("settings.path.title"), null, project, new FileChooserDescriptor(
false, true, false, false, false, false));
rootPanel.add(pathLabel);
rootPanel.add(pathText);
JBLabel strategyLabel = new JBLabel(bundle.getString("settings.strategy.title"));
rootPanel.add(strategyLabel);
JPanel strategyPanel = new JBPanel<>(new GridBagLayout());
rootPanel.add(strategyPanel);
GridBagConstraints constraints = new GridBagConstraints();
/* folder strategy */
folderStrategyComboBox = new ComboBox<>(bundle.getString("settings.strategy.folder").split(ArrayMapper.SPLITERATOR_REGEX));
folderStrategyComboBox.setSelectedIndex(state.getFolderStrategy().toIndex());
folderStrategyComboBox.setToolTipText(bundle.getString("settings.strategy.folder.tooltip"));
folderStrategyComboBox.setMinimumAndPreferredWidth(256);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 0;
constraints.gridy = 0;
strategyPanel.add(folderStrategyComboBox, constraints);
/* parser strategy */
parserStrategyComboBox = new ComboBox<>(bundle.getString("settings.strategy.parser").split(ArrayMapper.SPLITERATOR_REGEX));
parserStrategyComboBox.setSelectedIndex(state.getParserStrategy().toIndex());
parserStrategyComboBox.setToolTipText(bundle.getString("settings.strategy.parser.tooltip"));
parserStrategyComboBox.addItemListener(handleParserChange());
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.gridy = 0;
strategyPanel.add(parserStrategyComboBox, constraints);
/* file pattern strategy */
filePatternText = new JBTextField(state.getFilePattern());
filePatternText.setToolTipText(bundle.getString("settings.strategy.file-pattern.tooltip"));
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 2;
constraints.gridy = 0;
constraints.weightx = 1;
strategyPanel.add(filePatternText, constraints);
/* preview locale */
JBLabel previewLocaleLabel = new JBLabel(bundle.getString("settings.preview"));
previewLocaleText = new JBTextField(state.getPreviewLocale());
previewLocaleLabel.setLabelFor(previewLocaleText);
rootPanel.add(previewLocaleLabel);
rootPanel.add(previewLocaleText);
/* path prefix */
JBLabel pathPrefixLabel = new JBLabel(bundle.getString("settings.path.prefix"));
pathPrefixText = new JBTextField(state.getPathPrefix());
rootPanel.add(pathPrefixLabel);
rootPanel.add(pathPrefixText);
/* sort keys */
sortKeysCheckbox = new JBCheckBox(bundle.getString("settings.keys.sort"));
sortKeysCheckbox.setSelected(state.isSortKeys());
rootPanel.add(sortKeysCheckbox);
/* nested keys */
nestedKeysCheckbox = new JBCheckBox(bundle.getString("settings.keys.nested"));
nestedKeysCheckbox.setSelected(state.isNestedKeys());
rootPanel.add(nestedKeysCheckbox);
/* code assistance */
codeAssistanceCheckbox = new JBCheckBox(bundle.getString("settings.editor.assistance"));
codeAssistanceCheckbox.setSelected(state.isCodeAssistance());
rootPanel.add(codeAssistanceCheckbox);
DialogBuilder builder = new DialogBuilder();
builder.setTitle(bundle.getString("action.settings"));
builder.removeAllActions();
builder.addCancelAction();
builder.addOkAction();
builder.setCenterPanel(rootPanel);
return builder;
}
private ItemListener handleParserChange() {
return e -> {
if(e.getStateChange() == ItemEvent.SELECTED) {
// Automatically suggest file pattern option on parser change
ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategyComboBox.getSelectedIndex());
filePatternText.setText(newStrategy.getExampleFilePattern());
}
};
}
}

View File

@ -0,0 +1,148 @@
package de.marhali.easyi18n.dialog;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogBuilder;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
import com.intellij.util.Consumer;
import com.intellij.util.ui.FormBuilder;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.model.action.TranslationUpdate;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.EtchedBorder;
import java.awt.*;
import java.util.*;
/**
* Base for add and edit translation dialogs.
* @author marhali
*/
abstract class TranslationDialog {
protected static final ResourceBundle bundle = ResourceBundle.getBundle("messages");
protected final @NotNull Project project;
protected final @NotNull ProjectSettings settings;
protected final @NotNull KeyPathConverter converter;
protected final @NotNull Translation origin;
protected final JTextField keyField;
protected final Map<String, JTextField> localeValueFields;
private final Set<Consumer<TranslationUpdate>> callbacks;
/**
* Constructs a new translation dialog.
* @param project Opened project
* @param origin Prefill translation
*/
protected TranslationDialog(@NotNull Project project, @NotNull Translation origin) {
this.project = project;
this.settings = ProjectSettingsService.get(project).getState();
this.converter = new KeyPathConverter(settings);
this.origin = origin;
this.callbacks = new HashSet<>();
// Fields
TranslationValue value = origin.getValue();
this.keyField = new JBTextField(converter.toString(origin.getKey()));
this.localeValueFields = new HashMap<>();
for(String locale : InstanceManager.get(project).store().getData().getLocales()) {
localeValueFields.put(locale, new JBTextField(value != null ? value.get(locale) : null));
}
}
/**
* Registers a callback that is called on dialog close with the final state.
* If the user aborts the dialog no callback is called.
* @param callback Callback to register
*/
public void registerCallback(Consumer<TranslationUpdate> callback) {
callbacks.add(callback);
}
/**
* Implementation needs to configure the dialog. E.g. title, actions, ...
* The implementation needs to set the provided centerPanel as the view panel.
* @param centerPanel GUI to set on the dialog builder
* @return configured dialog builder
*/
protected abstract @NotNull DialogBuilder configure(@NotNull JComponent centerPanel);
/**
* Implementation needs to handle exit
* @param exitCode See {@link com.intellij.openapi.ui.DialogWrapper} for exit codes
* @return update conclusion, null if aborted
*/
protected abstract @Nullable TranslationUpdate handleExit(int exitCode);
/**
* Opens the translation modal and applies the appropriate logic on modal close.
* Internally, the {@link #handleExit(int)} method will be called to determine finalization logic.
*/
public void showAndHandle() {
int exitCode = createDialog().show();
TranslationUpdate update = handleExit(exitCode);
if(update != null) {
InstanceManager.get(project).processUpdate(update);
callbacks.forEach(callback -> callback.consume(update));
}
}
/**
* Retrieve current modal state.
* @return Translation
*/
protected @NotNull Translation getState() {
KeyPath key = converter.fromString(keyField.getText());
TranslationValue value = new TranslationValue();
for(Map.Entry<String, JTextField> entry : localeValueFields.entrySet()) {
value.put(entry.getKey(), entry.getValue().getText());
}
return new Translation(key, value);
}
private DialogBuilder createDialog() {
JPanel panel = FormBuilder.createFormBuilder()
.addLabeledComponent(bundle.getString("translation.key"), keyField, true)
.addComponent(createLocalesPanel(), 12)
.getPanel();
panel.setMinimumSize(new Dimension(200, 150));
return configure(panel);
}
private JComponent createLocalesPanel() {
FormBuilder builder = FormBuilder.createFormBuilder();
for(Map.Entry<String, JTextField> localeEntry : localeValueFields.entrySet()) {
builder.addLabeledComponent(localeEntry.getKey(), localeEntry.getValue(), 6, true);
}
JScrollPane scrollPane = new JBScrollPane(builder.getPanel());
scrollPane.setBorder(BorderFactory.createTitledBorder(
new EtchedBorder(), bundle.getString("translation.locales")));
return scrollPane;
}
}

View File

@ -1,59 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
/**
* Superclass for managing key annotations.
* @author marhali
*/
public class KeyAnnotator {
/**
* Adds annotations for i18n keys with content preview for preferred locale.
* @param key I18n key extracted by psi element
* @param project Project instance
* @param holder Annotation holder
*/
protected void annotate(@NotNull String key, @NotNull Project project, @NotNull AnnotationHolder holder) {
// Do not annotate keys if service is disabled
if(!SettingsService.getInstance(project).getState().isCodeAssistance()) {
return;
}
SettingsState state = SettingsService.getInstance(project).getState();
String pathPrefix = state.getPathPrefix();
String previewLocale = state.getPreviewLocale();
KeyPathConverter converter = new KeyPathConverter(project);
String searchKey = key.length() >= pathPrefix.length()
? key.substring(pathPrefix.length())
: key;
if(searchKey.startsWith(KeyPath.DELIMITER)) {
searchKey = searchKey.substring(KeyPath.DELIMITER.length());
}
TranslationNode node = InstanceManager.get(project).store().getData().getNode(converter.split(searchKey));
if(node == null) { // Unknown translation. Just ignore it
return;
}
String tooltip = node.isLeaf() ? "I18n(" + previewLocale + ": " + node.getValue().get(previewLocale) + ")"
: "I18n ([])";
holder.newAnnotation(HighlightSeverity.INFORMATION, tooltip).create();
}
}

View File

@ -1,62 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.*;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.project.*;
import com.intellij.util.*;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.service.*;
import org.jetbrains.annotations.*;
import java.util.*;
/**
* I18n translation key completion provider.
* @author marhali
*/
public class KeyCompletionProvider extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
@NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
Project project = parameters.getOriginalFile().getProject();
// Do not annotate keys if service is disabled
if(!SettingsService.getInstance(project).getState().isCodeAssistance()) {
return;
}
DataStore store = InstanceManager.get(project).store();
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
if(pathPrefix.length() > 0 && !pathPrefix.endsWith(KeyPath.DELIMITER)) {
pathPrefix += KeyPath.DELIMITER;
}
Set<KeyPath> fullKeys = store.getData().getFullKeys();
for(KeyPath currentKey : fullKeys) {
result.addElement(createElement(
pathPrefix,
currentKey,
previewLocale,
Objects.requireNonNull(store.getData().getTranslation(currentKey))
));
}
}
private LookupElement createElement(String prefix, KeyPath path, String locale, Translation translation) {
return LookupElementBuilder.create(prefix + path.toSimpleString())
.withIcon(AllIcons.Actions.PreserveCaseHover)
.appendTailText(" I18n(" + locale + ": " + translation.get(locale) + ")", true);
}
}

View File

@ -1,93 +0,0 @@
package de.marhali.easyi18n.editor;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.AddDialog;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.KeyedTranslation;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Go to declaration reference for i18n keys.
* @author marhali
*/
public class KeyReference extends PsiReferenceBase<PsiElement> {
@Nullable private final String myKey;
public KeyReference(@NotNull final PsiElement element) {
this(element, (String)null);
}
public KeyReference(@NotNull final PsiElement element, @Nullable final String myKey) {
super(element, true);
this.myKey = myKey;
}
public KeyReference(@NotNull final PsiElement element, @NotNull TextRange textRange) {
this(element, textRange, null);
}
public KeyReference(@NotNull PsiElement element, TextRange textRange, @Nullable String myKey) {
super(element, textRange, true);
this.myKey = myKey;
}
@Override
public @Nullable PsiElement resolve() {
return new TranslationKey();
}
public String getKey() {
return myKey != null ? myKey : getValue();
}
class TranslationKey extends FakePsiElement implements SyntheticElement {
@Override
public PsiElement getParent() {
return myElement;
}
@Override
public void navigate(boolean requestFocus) {
KeyPathConverter converter = new KeyPathConverter(getProject());
KeyPath path = converter.split(getKey());
Translation translation = InstanceManager.get(getProject()).store().getData().getTranslation(path);
if(translation != null) {
new EditDialog(getProject(), new KeyedTranslation(path, translation)).showAndHandle();
} else {
new AddDialog(getProject(), path).showAndHandle();
}
}
@Override
public String getPresentableText() {
return getKey();
}
@Override
public String getName() {
return getKey();
}
@Override
public @Nullable TextRange getTextRange() {
final TextRange rangeInElement = getRangeInElement();
final TextRange elementRange = myElement.getTextRange();
return elementRange != null ? rangeInElement.shiftRight(elementRange.getStartOffset()) : rangeInElement;
}
}
public static boolean isReferencable(String value) {
return value.matches("^[^\\s:/]+$");
}
}

View File

@ -1,87 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.FoldingBuilderEx;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralValue;
import com.intellij.psi.util.PsiTreeUtil;
import de.marhali.easyi18n.DataStore;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Translation key folding with actual value based on i18n instance.
* @author marhali
*/
public class GenericFoldingBuilder extends FoldingBuilderEx {
@Override
public FoldingDescriptor @NotNull [] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
Collection<PsiLiteralValue> literalValues = PsiTreeUtil.findChildrenOfType(root, PsiLiteralValue.class);
List<FoldingDescriptor> descriptors = new ArrayList<>();
if(!SettingsService.getInstance(root.getProject()).getState().isCodeAssistance()) {
return FoldingDescriptor.EMPTY;
}
DataStore store = InstanceManager.get(root.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(root.getProject());
for(final PsiLiteralValue literalValue : literalValues) {
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
// Undefined string literal or not a translation
if(value == null || store.getData().getTranslation(converter.split(value)) == null) {
continue;
}
descriptors.add(new FoldingDescriptor(literalValue.getNode(),
new TextRange(literalValue.getTextRange().getStartOffset() + 1,
literalValue.getTextRange().getEndOffset() - 1)));
}
return descriptors.toArray(new FoldingDescriptor[0]);
}
@Nullable
@Override
public String getPlaceholderText(@NotNull ASTNode node) {
PsiLiteralValue literalValue = node.getPsi(PsiLiteralValue.class);
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return null;
}
DataStore store = InstanceManager.get(literalValue.getProject()).store();
KeyPathConverter converter = new KeyPathConverter(literalValue.getProject());
Translation translation = store.getData().getTranslation(converter.split(value));
if(translation == null) {
return null;
}
String previewLocale = SettingsService.getInstance(literalValue.getProject()).getState().getPreviewLocale();
return translation.get(previewLocale);
}
@Override
public boolean isCollapsedByDefault(@NotNull ASTNode node) {
return true;
}
}

View File

@ -1,33 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralValue;
import de.marhali.easyi18n.editor.KeyAnnotator;
import org.jetbrains.annotations.NotNull;
/**
* Translation key annotator for generic languages which support {@link PsiLiteralValue}.
* @author marhali
*/
public class GenericKeyAnnotator extends KeyAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if(!(element instanceof PsiLiteralValue)) {
return;
}
PsiLiteralValue literalValue = (PsiLiteralValue) element;
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return;
}
annotate(value, element.getProject(), holder);
}
}

View File

@ -1,24 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.*;
import com.intellij.psi.*;
import com.intellij.psi.xml.*;
import de.marhali.easyi18n.editor.KeyCompletionProvider;
/**
* Translation key completion for generic languages which support {@link PsiLiteralValue}.
* @author marhali
*/
public class GenericKeyCompletionContributor extends CompletionContributor {
public GenericKeyCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement(PlainTextTokenTypes.PLAIN_TEXT),
new KeyCompletionProvider());
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(XmlElement.class),
new KeyCompletionProvider());
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralValue.class),
new KeyCompletionProvider());
}
}

View File

@ -1,54 +0,0 @@
package de.marhali.easyi18n.editor.generic;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
/**
* Generic translation key reference contributor.
* @author marhali
*/
public class GenericKeyReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement(PsiLiteralValue.class), getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(
@NotNull PsiElement element, @NotNull ProcessingContext context) {
PsiLiteralValue literalValue = (PsiLiteralValue) element;
String value = literalValue.getValue() instanceof String ? (String) literalValue.getValue() : null;
if(value == null) {
return PsiReference.EMPTY_ARRAY;
}
// Do not reference keys if service is disabled
if(!SettingsService.getInstance(element.getProject()).getState().isCodeAssistance()) {
return PsiReference.EMPTY_ARRAY;
}
KeyPathConverter converter = new KeyPathConverter(element.getProject());
if(InstanceManager.get(element.getProject()).store().getData().getTranslation(converter.split(value)) == null) {
if(!KeyReference.isReferencable(value)) { // Creation policy
return PsiReference.EMPTY_ARRAY;
}
}
return new PsiReference[] { new KeyReference(element, value) };
}
};
}
}

View File

@ -1,31 +0,0 @@
package de.marhali.easyi18n.editor.kotlin;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.psi.PsiElement;
import de.marhali.easyi18n.editor.KeyAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/**
* Kotlin specific translation key annotator
* @author marhali
*/
public class KotlinKeyAnnotator extends KeyAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if(!(element instanceof KtLiteralStringTemplateEntry)) {
return;
}
String value = element.getText();
if(value == null) {
return;
}
annotate(value, element.getProject(), holder);
}
}

View File

@ -1,59 +0,0 @@
package de.marhali.easyi18n.editor.kotlin;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.util.ProcessingContext;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.editor.KeyReference;
import de.marhali.easyi18n.model.KeyPathConverter;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
import org.jetbrains.kotlin.psi.KtStringTemplateExpression;
/**
* Kotlin translation key reference contributor.
* @author marhali
*/
public class KotlinKeyReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
registrar.registerReferenceProvider(PlatformPatterns.psiElement().inside(KtStringTemplateExpression.class), getProvider());
}
private PsiReferenceProvider getProvider() {
return new PsiReferenceProvider() {
@Override
public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
String value = null;
for (PsiElement child : element.getChildren()) {
if(child instanceof KtLiteralStringTemplateEntry) {
value = child.getText();
}
}
if(value == null) {
return PsiReference.EMPTY_ARRAY;
}
// Do not reference keys if service is disabled
if(!SettingsService.getInstance(element.getProject()).getState().isCodeAssistance()) {
return PsiReference.EMPTY_ARRAY;
}
KeyPathConverter converter = new KeyPathConverter(element.getProject());
if(InstanceManager.get(element.getProject()).store().getData().getNode(converter.split(value)) == null) {
return PsiReference.EMPTY_ARRAY;
}
return new PsiReference[] { new KeyReference(element, value) };
}
};
}
}

View File

@ -9,6 +9,7 @@ import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.io.parser.ParserStrategyType; import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
@ -21,23 +22,23 @@ import java.util.List;
*/ */
public class IOHandler { public class IOHandler {
private final @NotNull SettingsState settings; private final @NotNull ProjectSettings settings;
private final @NotNull FolderStrategy folderStrategy; private final @NotNull FolderStrategy folderStrategy;
private final @NotNull ParserStrategyType parserStrategyType; private final @NotNull ParserStrategyType parserStrategyType;
private final @NotNull ParserStrategy parserStrategy; private final @NotNull ParserStrategy parserStrategy;
public IOHandler(@NotNull SettingsState settings) throws Exception { public IOHandler(@NotNull ProjectSettings settings) throws Exception {
this.settings = settings; this.settings = settings;
this.folderStrategy = settings.getFolderStrategy().getStrategy() this.folderStrategy = settings.getFolderStrategy().getStrategy()
.getDeclaredConstructor(SettingsState.class).newInstance(settings); .getDeclaredConstructor(ProjectSettings.class).newInstance(settings);
this.parserStrategyType = settings.getParserStrategy(); this.parserStrategyType = settings.getParserStrategy();
this.parserStrategy = parserStrategyType.getStrategy() this.parserStrategy = parserStrategyType.getStrategy()
.getDeclaredConstructor(SettingsState.class).newInstance(settings); .getDeclaredConstructor(ProjectSettings.class).newInstance(settings);
} }
/** /**
@ -47,7 +48,7 @@ public class IOHandler {
* @throws IOException Could not read translation data * @throws IOException Could not read translation data
*/ */
public @NotNull TranslationData read() throws IOException { public @NotNull TranslationData read() throws IOException {
String localesPath = this.settings.getLocalesPath(); String localesPath = this.settings.getLocalesDirectory();
if(localesPath == null || localesPath.isEmpty()) { if(localesPath == null || localesPath.isEmpty()) {
throw new EmptyLocalesDirException("Locales path must not be empty"); throw new EmptyLocalesDirException("Locales path must not be empty");
@ -59,7 +60,7 @@ public class IOHandler {
throw new IllegalArgumentException("Specified locales path is invalid (" + localesPath + ")"); throw new IllegalArgumentException("Specified locales path is invalid (" + localesPath + ")");
} }
TranslationData data = new TranslationData(this.settings.isSortKeys()); TranslationData data = new TranslationData(this.settings.isSorting());
List<TranslationFile> translationFiles = this.folderStrategy.analyzeFolderStructure(localesDirectory); List<TranslationFile> translationFiles = this.folderStrategy.analyzeFolderStructure(localesDirectory);
for(TranslationFile file : translationFiles) { for(TranslationFile file : translationFiles) {
@ -80,7 +81,7 @@ public class IOHandler {
* @throws IOException Write action failed * @throws IOException Write action failed
*/ */
public void write(@NotNull TranslationData data) throws IOException { public void write(@NotNull TranslationData data) throws IOException {
String localesPath = this.settings.getLocalesPath(); String localesPath = this.settings.getLocalesDirectory();
if(localesPath == null || localesPath.isEmpty()) { if(localesPath == null || localesPath.isEmpty()) {
throw new EmptyLocalesDirException("Locales path must not be empty"); throw new EmptyLocalesDirException("Locales path must not be empty");

View File

@ -4,10 +4,10 @@ import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategyType; import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -22,9 +22,9 @@ import java.util.Objects;
*/ */
public abstract class FolderStrategy { public abstract class FolderStrategy {
protected final @NotNull SettingsState settings; protected final @NotNull ProjectSettings settings;
public FolderStrategy(@NotNull SettingsState settings) { public FolderStrategy(@NotNull ProjectSettings settings) {
this.settings = settings; this.settings = settings;
} }

View File

@ -1,23 +1,24 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.io.folder;
import de.marhali.easyi18n.io.folder.FolderStrategy;
import de.marhali.easyi18n.io.folder.ModularLocaleFolderStrategy;
import de.marhali.easyi18n.io.folder.ModularNamespaceFolderStrategy;
import de.marhali.easyi18n.io.folder.SingleFolderStrategy;
/** /**
* Represents all supported folder strategies. * Represents all supported folder strategies.
* @author marhali * @author marhali
*/ */
public enum FolderStrategyType { public enum FolderStrategyType {
SINGLE(SingleFolderStrategy.class), SINGLE(SingleFolderStrategy.class, false),
MODULARIZED_LOCALE(ModularLocaleFolderStrategy.class), MODULARIZED_LOCALE(ModularLocaleFolderStrategy.class, true),
MODULARIZED_NAMESPACE(ModularNamespaceFolderStrategy.class); MODULARIZED_NAMESPACE(ModularNamespaceFolderStrategy.class, true);
private final Class<? extends FolderStrategy> strategy; private final Class<? extends FolderStrategy> strategy;
private final boolean namespaceMode;
FolderStrategyType(Class<? extends FolderStrategy> strategy) { /**
* @param strategy Strategy implementation
* @param namespaceMode Does this strategy use namespaces?
*/
FolderStrategyType(Class<? extends FolderStrategy> strategy, boolean namespaceMode) {
this.strategy = strategy; this.strategy = strategy;
this.namespaceMode = namespaceMode;
} }
public Class<? extends FolderStrategy> getStrategy() { public Class<? extends FolderStrategy> getStrategy() {
@ -38,6 +39,10 @@ public enum FolderStrategyType {
throw new NullPointerException(); throw new NullPointerException();
} }
public boolean isNamespaceMode() {
return namespaceMode;
}
public static FolderStrategyType fromIndex(int index) { public static FolderStrategyType fromIndex(int index) {
return values()[index]; return values()[index];
} }

View File

@ -3,9 +3,9 @@ package de.marhali.easyi18n.io.folder;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategyType; import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -20,7 +20,7 @@ import java.util.List;
*/ */
public class ModularLocaleFolderStrategy extends FolderStrategy { public class ModularLocaleFolderStrategy extends FolderStrategy {
public ModularLocaleFolderStrategy(@NotNull SettingsState settings) { public ModularLocaleFolderStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -3,9 +3,9 @@ package de.marhali.easyi18n.io.folder;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategyType; import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -20,7 +20,7 @@ import java.util.List;
*/ */
public class ModularNamespaceFolderStrategy extends FolderStrategy { public class ModularNamespaceFolderStrategy extends FolderStrategy {
public ModularNamespaceFolderStrategy(@NotNull SettingsState settings) { public ModularNamespaceFolderStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -3,9 +3,9 @@ package de.marhali.easyi18n.io.folder;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategyType; import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -22,7 +22,7 @@ import java.util.List;
*/ */
public class SingleFolderStrategy extends FolderStrategy { public class SingleFolderStrategy extends FolderStrategy {
public SingleFolderStrategy(@NotNull SettingsState settings) { public SingleFolderStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -2,6 +2,8 @@ package de.marhali.easyi18n.io.parser;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
@ -12,9 +14,9 @@ import java.util.Objects;
*/ */
public abstract class ParserStrategy { public abstract class ParserStrategy {
protected final @NotNull SettingsState settings; protected final @NotNull ProjectSettings settings;
public ParserStrategy(@NotNull SettingsState settings) { public ParserStrategy(@NotNull ProjectSettings settings) {
this.settings = settings; this.settings = settings;
} }
@ -45,10 +47,10 @@ public abstract class ParserStrategy {
if(file.getNamespace() != null) { if(file.getNamespace() != null) {
String moduleName = file.getNamespace(); String moduleName = file.getNamespace();
TranslationNode moduleNode = data.getNode(KeyPath.of(moduleName)); TranslationNode moduleNode = data.getNode(new KeyPath(moduleName));
if(moduleNode == null) { if(moduleNode == null) {
moduleNode = new TranslationNode(this.settings.isSortKeys()); moduleNode = new TranslationNode(this.settings.isSorting());
data.getRootNode().setChildren(moduleName, moduleNode); data.getRootNode().setChildren(moduleName, moduleNode);
} }
@ -68,7 +70,7 @@ public abstract class ParserStrategy {
TranslationNode targetNode = data.getRootNode(); TranslationNode targetNode = data.getRootNode();
if(file.getNamespace() != null) { if(file.getNamespace() != null) {
targetNode = data.getNode(KeyPath.of(file.getNamespace())); targetNode = data.getNode(new KeyPath(file.getNamespace()));
} }
return Objects.requireNonNull(targetNode); return Objects.requireNonNull(targetNode);

View File

@ -4,8 +4,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
@ -30,7 +30,7 @@ public class JsonMapper {
// Nested element - run recursively // Nested element - run recursively
read(locale, value.getAsJsonObject(), childNode); read(locale, value.getAsJsonObject(), childNode);
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = entry.getValue().isJsonArray() String content = entry.getValue().isJsonArray()
? JsonArrayMapper.read(value.getAsJsonArray()) ? JsonArrayMapper.read(value.getAsJsonArray())
@ -55,7 +55,7 @@ public class JsonMapper {
json.add(key, childJson); json.add(key, childJson);
} }
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = translation.get(locale); String content = translation.get(locale);
if(content != null) { if(content != null) {

View File

@ -3,14 +3,15 @@ package de.marhali.easyi18n.io.parser.json;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategy; import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.util.Objects; import java.util.Objects;
@ -23,7 +24,7 @@ public class JsonParserStrategy extends ParserStrategy {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
public JsonParserStrategy(@NotNull SettingsState settings) { public JsonParserStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -1,12 +1,12 @@
package de.marhali.easyi18n.io.parser.json5; package de.marhali.easyi18n.io.parser.json5;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.StringUtil;
import de.marhali.json5.Json5Element; import de.marhali.json5.Json5Element;
import de.marhali.json5.Json5Object; import de.marhali.json5.Json5Object;
import de.marhali.json5.Json5Primitive; import de.marhali.json5.Json5Primitive;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang.math.NumberUtils;
@ -28,7 +28,7 @@ public class Json5Mapper {
// Nested element - run recursively // Nested element - run recursively
read(locale, value.getAsJson5Object(), childNode); read(locale, value.getAsJson5Object(), childNode);
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = value.isJson5Array() String content = value.isJson5Array()
? Json5ArrayMapper.read(value.getAsJson5Array()) ? Json5ArrayMapper.read(value.getAsJson5Array())
@ -54,7 +54,7 @@ public class Json5Mapper {
} }
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = translation.get(locale); String content = translation.get(locale);
if(content != null) { if(content != null) {
if(Json5ArrayMapper.isArray(content)) { if(Json5ArrayMapper.isArray(content)) {

View File

@ -3,10 +3,10 @@ package de.marhali.easyi18n.io.parser.json5;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategy; import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.json5.Json5; import de.marhali.json5.Json5;
import de.marhali.json5.Json5Element; import de.marhali.json5.Json5Element;
import de.marhali.json5.Json5Object; import de.marhali.json5.Json5Object;
@ -26,7 +26,7 @@ public class Json5ParserStrategy extends ParserStrategy {
private static final Json5 JSON5 = Json5.builder(builder -> private static final Json5 JSON5 = Json5.builder(builder ->
builder.allowInvalidSurrogate().trailingComma().indentFactor(4).build()); builder.allowInvalidSurrogate().trailingComma().indentFactor(4).build());
public Json5ParserStrategy(@NotNull SettingsState settings) { public Json5ParserStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -1,8 +1,9 @@
package de.marhali.easyi18n.io.parser.properties; package de.marhali.easyi18n.io.parser.properties;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.KeyPathConverter;
import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
@ -16,15 +17,17 @@ import java.util.Map;
*/ */
public class PropertiesMapper { public class PropertiesMapper {
public static void read(String locale, SortableProperties properties, TranslationData data) { public static void read(String locale, SortableProperties properties,
TranslationData data, KeyPathConverter converter) {
for(Map.Entry<Object, Object> entry : properties.entrySet()) { for(Map.Entry<Object, Object> entry : properties.entrySet()) {
KeyPath key = new KeyPath(String.valueOf(entry.getKey())); KeyPath key = converter.fromString(String.valueOf(entry.getKey()));
Object value = entry.getValue(); Object value = entry.getValue();
Translation translation = data.getTranslation(key); TranslationValue translation = data.getTranslation(key);
if(translation == null) { if(translation == null) {
translation = new Translation(); translation = new TranslationValue();
} }
String content = value instanceof String[] String content = value instanceof String[]
@ -36,12 +39,14 @@ public class PropertiesMapper {
} }
} }
public static void write(String locale, SortableProperties properties, TranslationData data) { public static void write(String locale, SortableProperties properties,
for(KeyPath key : data.getFullKeys()) { TranslationData data, KeyPathConverter converter) {
Translation translation = data.getTranslation(key);
if(translation != null && translation.containsKey(locale)) { for(KeyPath key : data.getFullKeys()) {
String simpleKey = key.toSimpleString(); TranslationValue translation = data.getTranslation(key);
if(translation != null && translation.containsLocale(locale)) {
String simpleKey = converter.toString(key);
String content = translation.get(locale); String content = translation.get(locale);
if(PropertiesArrayMapper.isArray(content)) { if(PropertiesArrayMapper.isArray(content)) {

View File

@ -3,10 +3,11 @@ package de.marhali.easyi18n.io.parser.properties;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategy; import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -20,8 +21,11 @@ import java.io.StringWriter;
*/ */
public class PropertiesParserStrategy extends ParserStrategy { public class PropertiesParserStrategy extends ParserStrategy {
public PropertiesParserStrategy(@NotNull SettingsState settings) { private final @NotNull KeyPathConverter converter;
public PropertiesParserStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
this.converter = new KeyPathConverter(settings);
} }
@Override @Override
@ -33,9 +37,9 @@ public class PropertiesParserStrategy extends ParserStrategy {
TranslationData targetData = new TranslationData(data.getLocales(), targetNode); TranslationData targetData = new TranslationData(data.getLocales(), targetNode);
try(Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) { try(Reader reader = new InputStreamReader(vf.getInputStream(), vf.getCharset())) {
SortableProperties input = new SortableProperties(this.settings.isSortKeys()); SortableProperties input = new SortableProperties(this.settings.isSorting());
input.load(reader); input.load(reader);
PropertiesMapper.read(file.getLocale(), input, targetData); PropertiesMapper.read(file.getLocale(), input, targetData, converter);
} }
} }
@ -44,8 +48,8 @@ public class PropertiesParserStrategy extends ParserStrategy {
TranslationNode targetNode = super.getTargetNode(data, file); TranslationNode targetNode = super.getTargetNode(data, file);
TranslationData targetData = new TranslationData(data.getLocales(), targetNode); TranslationData targetData = new TranslationData(data.getLocales(), targetNode);
SortableProperties output = new SortableProperties(this.settings.isSortKeys()); SortableProperties output = new SortableProperties(this.settings.isSorting());
PropertiesMapper.write(file.getLocale(), output, targetData); PropertiesMapper.write(file.getLocale(), output, targetData, converter);
try(StringWriter writer = new StringWriter()) { try(StringWriter writer = new StringWriter()) {
output.store(writer, null); output.store(writer, null);

View File

@ -1,7 +1,7 @@
package de.marhali.easyi18n.io.parser.yaml; package de.marhali.easyi18n.io.parser.yaml;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringEscapeUtils;
@ -28,7 +28,7 @@ public class YamlMapper {
// Nested element - run recursively // Nested element - run recursively
read(locale, (MapSection) value, childNode); read(locale, (MapSection) value, childNode);
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = value instanceof ListSection String content = value instanceof ListSection
? YamlArrayMapper.read((ListSection) value) ? YamlArrayMapper.read((ListSection) value)
@ -53,7 +53,7 @@ public class YamlMapper {
section.setInScope(key, childSection); section.setInScope(key, childSection);
} }
} else { } else {
Translation translation = childNode.getValue(); TranslationValue translation = childNode.getValue();
String content = translation.get(locale); String content = translation.get(locale);
if(content != null) { if(content != null) {

View File

@ -3,10 +3,10 @@ package de.marhali.easyi18n.io.parser.yaml;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategy; import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile; import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.model.TranslationNode; import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -22,7 +22,7 @@ import java.io.Reader;
*/ */
public class YamlParserStrategy extends ParserStrategy { public class YamlParserStrategy extends ParserStrategy {
public YamlParserStrategy(@NotNull SettingsState settings) { public YamlParserStrategy(@NotNull ProjectSettings settings) {
super(settings); super(settings);
} }

View File

@ -1,58 +1,37 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
/** /**
* Represents a full translation key with all sections. * Represents the absolute key path for a desired translation.
* Implementations can use single section or variable section length variants. * The key could be based one or many sections.
* The respective layer (io, presentation) is responsible for using the correct mapping mechanism. * Classes implementing this structure need to take care on how to layer translations paths.
* @author marhali * @author marhali
*/ */
public class KeyPath extends ArrayList<String> { public class KeyPath extends ArrayList<String> {
public static final String DELIMITER = "."; public KeyPath() {}
public static KeyPath of(@NotNull String... path) { public KeyPath(@Nullable String... path) {
return new KeyPath(List.of(path)); super.addAll(List.of(path));
} }
public KeyPath() { public KeyPath(@NotNull List<String> path) {
super(); super(path);
} }
public KeyPath(@NotNull KeyPath path, String... pathToAppend) { public KeyPath(@NotNull KeyPath path, @Nullable String... pathToAppend) {
this(path); this(path);
this.addAll(List.of(pathToAppend)); super.addAll(List.of(pathToAppend));
} }
public KeyPath(@NotNull Collection<? extends String> c) { @Override
super(c); public String toString() {
// Just a simple array view (e.g. [first, second]) - use KeyPathConverter to properly convert a key path
return super.toString();
} }
}
public KeyPath(@NotNull String simplePath) {
this(List.of(simplePath.split(Pattern.quote(DELIMITER))));
}
/**
* <b>Note: </b>Use {@link KeyPathConverter} if you want to keep hierarchy.
* @return simple path representation by adding delimiter between the secton nodes
*/
public String toSimpleString() {
StringBuilder builder = new StringBuilder();
for(String section : this) {
if(builder.length() > 0) {
builder.append(DELIMITER);
}
builder.append(section);
}
return builder.toString();
}
}

View File

@ -1,69 +0,0 @@
package de.marhali.easyi18n.model;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.service.SettingsService;
import org.jetbrains.annotations.NotNull;
import java.util.regex.Pattern;
/**
* Responsible for mapping {@link KeyPath} into single string and backwards.
* If nesting is enabled the delimiter within a section is escaped otherwise the delimiter between the key sections.
* @author marhali
*/
public class KeyPathConverter {
private final boolean nestKeys;
public KeyPathConverter(boolean nestKeys) {
this.nestKeys = nestKeys;
}
public KeyPathConverter(@NotNull Project project) {
this(SettingsService.getInstance(project).getState().isNestedKeys());
}
public @NotNull String concat(@NotNull KeyPath path) {
StringBuilder builder = new StringBuilder();
for(String section : path) {
if(builder.length() > 0) {
if(!this.nestKeys) {
builder.append("\\\\");
}
builder.append(KeyPath.DELIMITER);
}
if(this.nestKeys) {
builder.append(section.replace(KeyPath.DELIMITER, "\\\\" + KeyPath.DELIMITER));
} else {
builder.append(section);
}
}
return builder.toString();
}
public @NotNull KeyPath split(@NotNull String concatPath) {
String[] sections = concatPath.split(this.nestKeys ?
"(?<!\\\\)" + Pattern.quote(KeyPath.DELIMITER) : Pattern.quote("\\\\" + KeyPath.DELIMITER));
KeyPath path = new KeyPath();
for(String section : sections) {
path.add(section.replace("\\\\" + KeyPath.DELIMITER, KeyPath.DELIMITER));
}
return path;
}
@Override
public String toString() {
return "KeyPathConverter{" +
"nestKeys=" + nestKeys +
'}';
}
}

View File

@ -1,43 +0,0 @@
package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* I18n translation with associated key path (full-key).
* @author marhali
*/
public class KeyedTranslation {
private @NotNull KeyPath key;
private @Nullable Translation translation;
public KeyedTranslation(@NotNull KeyPath key, @Nullable Translation translation) {
this.key = key;
this.translation = translation;
}
public KeyPath getKey() {
return key;
}
public void setKey(KeyPath key) {
this.key = key;
}
public @Nullable Translation getTranslation() {
return translation;
}
public void setTranslation(@NotNull Translation translation) {
this.translation = translation;
}
@Override
public String toString() {
return "KeyedTranslation{" +
"key='" + key + '\'' +
", translation=" + translation +
'}';
}
}

View File

@ -1,105 +0,0 @@
package de.marhali.easyi18n.model;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents the persistent settings which can be configured.
* @author marhali
*/
public class SettingsState {
public static final String DEFAULT_PREVIEW_LOCALE = "en";
public static final FolderStrategyType DEFAULT_FOLDER_STRATEGY = FolderStrategyType.SINGLE;
public static final ParserStrategyType DEFAULT_PARSER_STRATEGY = ParserStrategyType.JSON;
public static final String DEFAULT_FILE_PATTERN = "*.*";
public static final String DEFAULT_PATH_PREFIX = "";
public static final boolean DEFAULT_SORT_KEYS = true;
public static final boolean DEFAULT_NESTED_KEYS = true;
public static final boolean DEFAULT_CODE_ASSISTANCE = true;
private String localesPath;
private FolderStrategyType folderStrategy;
private ParserStrategyType parserStrategy;
private String filePattern;
private String previewLocale;
private String pathPrefix;
private Boolean sortKeys;
private Boolean nestedKeys;
private Boolean codeAssistance;
public SettingsState() {}
public @Nullable String getLocalesPath() {
return localesPath;
}
public void setLocalesPath(String localesPath) {
this.localesPath = localesPath;
}
public @NotNull FolderStrategyType getFolderStrategy() {
return folderStrategy != null ? folderStrategy : DEFAULT_FOLDER_STRATEGY;
}
public void setFolderStrategy(FolderStrategyType folderStrategy) {
this.folderStrategy = folderStrategy;
}
public @NotNull ParserStrategyType getParserStrategy() {
return parserStrategy != null ? parserStrategy : DEFAULT_PARSER_STRATEGY;
}
public void setParserStrategy(ParserStrategyType parserStrategy) {
this.parserStrategy = parserStrategy;
}
public @NotNull String getFilePattern() {
return filePattern != null ? filePattern : DEFAULT_FILE_PATTERN;
}
public void setFilePattern(String filePattern) {
this.filePattern = filePattern;
}
public @NotNull String getPreviewLocale() {
return previewLocale != null ? previewLocale : DEFAULT_PREVIEW_LOCALE;
}
public void setPreviewLocale(String previewLocale) {
this.previewLocale = previewLocale;
}
public @NotNull String getPathPrefix() {
return pathPrefix != null ? pathPrefix : DEFAULT_PATH_PREFIX;
}
public void setPathPrefix(String pathPrefix) {
this.pathPrefix = pathPrefix;
}
public boolean isSortKeys() {
return sortKeys == null ? DEFAULT_SORT_KEYS : sortKeys;
}
public void setSortKeys(boolean sortKeys) {
this.sortKeys = sortKeys;
}
public boolean isNestedKeys() {
return nestedKeys == null ? DEFAULT_NESTED_KEYS : nestedKeys;
}
public void setNestedKeys(boolean nestedKeys) {
this.nestedKeys = nestedKeys;
}
public boolean isCodeAssistance() {
return codeAssistance == null ? DEFAULT_CODE_ASSISTANCE : codeAssistance;
}
public void setCodeAssistance(boolean codeAssistance) {
this.codeAssistance = codeAssistance;
}
}

View File

@ -1,29 +1,54 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model;
import java.util.HashMap; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Represents all translations for an element. The assignment to an element is done in the using class. * Represents a translation with defined key and locale values.
* This class contains only the translations for this unspecific element. *
* @author marhali * @author marhali
*/ */
public class Translation extends HashMap<String, String> { public class Translation {
public Translation() {
super(); private final @NotNull KeyPath key;
private @Nullable TranslationValue value;
/**
* Constructs a new translation instance.
* @param key Absolute key path
* @param value Values to set - nullable to indicate removal
*/
public Translation(@NotNull KeyPath key, @Nullable TranslationValue value) {
this.key = key;
this.value = value;
} }
public Translation(String locale, String content) { /**
this(); * @return Absolute key path
super.put(locale, content); */
public @NotNull KeyPath getKey() {
return key;
} }
public Translation add(String locale, String content) { /**
super.put(locale, content); * @return values - nullable to indicate removal
return this; */
public @Nullable TranslationValue getValue() {
return value;
}
/**
* @param value Values to set - nullable to indicate removal
*/
public void setValue(@Nullable TranslationValue value) {
this.value = value;
} }
@Override @Override
public String toString() { public String toString() {
return super.toString(); return "Translation{" +
"key=" + key +
", value=" + value +
'}';
} }
} }

View File

@ -93,7 +93,7 @@ public class TranslationData {
* @param fullPath Absolute translation key path * @param fullPath Absolute translation key path
* @return Found translation. Can be null if path is empty or is not a leaf element * @return Found translation. Can be null if path is empty or is not a leaf element
*/ */
public @Nullable Translation getTranslation(@NotNull KeyPath fullPath) { public @Nullable TranslationValue getTranslation(@NotNull KeyPath fullPath) {
TranslationNode node = this.getNode(fullPath); TranslationNode node = this.getNode(fullPath);
if(node == null || !node.isLeaf()) { if(node == null || !node.isLeaf()) {
@ -109,7 +109,7 @@ public class TranslationData {
* @param fullPath Absolute translation key path * @param fullPath Absolute translation key path
* @param translation Translation to set. Can be null to delete the corresponding node * @param translation Translation to set. Can be null to delete the corresponding node
*/ */
public void setTranslation(@NotNull KeyPath fullPath, @Nullable Translation translation) { public void setTranslation(@NotNull KeyPath fullPath, @Nullable TranslationValue translation) {
if(fullPath.isEmpty()) { if(fullPath.isEmpty()) {
throw new IllegalArgumentException("Key path cannot be empty"); throw new IllegalArgumentException("Key path cannot be empty");
} }

View File

@ -27,7 +27,7 @@ public class TranslationNode {
private Map<String, TranslationNode> children; private Map<String, TranslationNode> children;
@NotNull @NotNull
private Translation value; private TranslationValue value;
public TranslationNode(boolean sort) { public TranslationNode(boolean sort) {
this(sort ? new TreeMap<>() : new LinkedHashMap<>()); this(sort ? new TreeMap<>() : new LinkedHashMap<>());
@ -40,7 +40,7 @@ public class TranslationNode {
public TranslationNode(@NotNull Map<String, TranslationNode> children) { public TranslationNode(@NotNull Map<String, TranslationNode> children) {
this.parent = null; this.parent = null;
this.children = children; this.children = children;
this.value = new Translation(); this.value = new TranslationValue();
} }
/** /**
@ -62,11 +62,11 @@ public class TranslationNode {
this.parent = parent; this.parent = parent;
} }
public @NotNull Translation getValue() { public @NotNull TranslationValue getValue() {
return value; return value;
} }
public void setValue(@NotNull Translation value) { public void setValue(@NotNull TranslationValue value) {
this.children.clear(); this.children.clear();
this.value = value; this.value = value;
} }
@ -93,7 +93,7 @@ public class TranslationNode {
} }
} }
public void setChildren(@NotNull String key, @NotNull Translation translation) { public void setChildren(@NotNull String key, @NotNull TranslationValue translation) {
this.setChildren(key).setValue(translation); this.setChildren(key).setValue(translation);
} }

View File

@ -0,0 +1,70 @@
package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Represents the set values behind a specific translation.
* @author marhali
*/
public class TranslationValue {
private @NotNull Map<String, String> localeValues;
public TranslationValue() {
this.localeValues = new HashMap<>();
}
public TranslationValue(@NotNull String locale, @NotNull String content) {
this();
localeValues.put(locale, content);
}
public Set<Map.Entry<String, String>> getEntries() {
return this.localeValues.entrySet();
}
public Collection<String> getLocaleContents() {
return this.localeValues.values();
}
public void setLocaleValues(@NotNull Map<String, String> localeValues) {
this.localeValues = localeValues;
}
public @Nullable String get(@NotNull String locale) {
return this.localeValues.get(locale);
}
public void put(@NotNull String locale, @NotNull String content) {
this.localeValues.put(locale, content);
}
public void remove(@NotNull String locale) {
this.localeValues.remove(locale);
}
public boolean containsLocale(@NotNull String locale) {
return this.localeValues.containsKey(locale);
}
public int size() {
return this.localeValues.size();
}
public void clear() {
this.localeValues.clear();
}
@Override
public String toString() {
return "TranslationValue{" +
"localeValues=" + localeValues +
'}';
}
}

View File

@ -1,4 +1,6 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model.action;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -7,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
* @author marhali * @author marhali
*/ */
public class TranslationCreate extends TranslationUpdate { public class TranslationCreate extends TranslationUpdate {
public TranslationCreate(@NotNull KeyedTranslation translation) { public TranslationCreate(@NotNull Translation translation) {
super(null, translation); super(null, translation);
} }
} }

View File

@ -1,4 +1,6 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model.action;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -7,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
* @author marhali * @author marhali
*/ */
public class TranslationDelete extends TranslationUpdate { public class TranslationDelete extends TranslationUpdate {
public TranslationDelete(@NotNull KeyedTranslation translation) { public TranslationDelete(@NotNull Translation translation) {
super(translation, null); super(translation, null);
} }
} }

View File

@ -1,5 +1,6 @@
package de.marhali.easyi18n.model; package de.marhali.easyi18n.model.action;
import de.marhali.easyi18n.model.Translation;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
@ -10,19 +11,19 @@ import org.jetbrains.annotations.Nullable;
*/ */
public class TranslationUpdate { public class TranslationUpdate {
private final @Nullable KeyedTranslation origin; private final @Nullable Translation origin;
private final @Nullable KeyedTranslation change; private final @Nullable Translation change;
public TranslationUpdate(@Nullable KeyedTranslation origin, @Nullable KeyedTranslation change) { public TranslationUpdate(@Nullable Translation origin, @Nullable Translation change) {
this.origin = origin; this.origin = origin;
this.change = change; this.change = change;
} }
public @Nullable KeyedTranslation getOrigin() { public @Nullable Translation getOrigin() {
return origin; return origin;
} }
public @Nullable KeyedTranslation getChange() { public @Nullable Translation getChange() {
return change; return change;
} }

View File

@ -1,37 +0,0 @@
package de.marhali.easyi18n.service;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.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) {
return project.getService(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,35 @@
package de.marhali.easyi18n.settings;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* API to access the project-specific configuration for this plugin.
* @author marhaliu
*/
public interface ProjectSettings {
// Resource Configuration
@Nullable String getLocalesDirectory();
@NotNull FolderStrategyType getFolderStrategy();
@NotNull ParserStrategyType getParserStrategy();
@NotNull String getFilePattern();
boolean isSorting();
// Editor Configuration
@Nullable String getNamespaceDelimiter();
@NotNull String getSectionDelimiter();
@Nullable String getContextDelimiter();
@Nullable String getPluralDelimiter();
@Nullable String getDefaultNamespace();
@NotNull String getPreviewLocale();
boolean isNestedKeys();
boolean isAssistance();
// Experimental Configuration
boolean isAlwaysFold();
}

View File

@ -0,0 +1,228 @@
package de.marhali.easyi18n.settings;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.ui.JBColor;
import com.intellij.ui.TitledSeparator;
import com.intellij.ui.components.*;
import com.intellij.ui.components.fields.ExtendableTextField;
import com.intellij.util.ui.FormBuilder;
import de.marhali.easyi18n.io.parser.ArrayMapper;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.settings.presets.Preset;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ResourceBundle;
/**
* Configuration panel with all possible options for this plugin.
* @author marhali
*/
public class ProjectSettingsComponent extends ProjectSettingsComponentState {
private final Project project;
private final ResourceBundle bundle;
private final JPanel mainPanel;
// Data fields are provided by the underlying state class
public ProjectSettingsComponent(Project project) {
this.project = project;
this.bundle = ResourceBundle.getBundle("messages");
this.mainPanel = FormBuilder.createFormBuilder()
.addComponent(new JBLabel(bundle.getString("settings.hint.text")))
.addComponent(new ActionLink(bundle.getString("settings.hint.action"),
(ActionListener) (var) -> BrowserUtil.browse("https://github.com/marhali/easy-i18n")))
.addVerticalGap(24)
.addLabeledComponent(bundle.getString("settings.preset.title"), constructPresetField(), 1, false)
.addVerticalGap(12)
.addComponent(new TitledSeparator(bundle.getString("settings.resource.title")))
.addLabeledComponent(bundle.getString("settings.resource.path.title"), constructLocalesDirectoryField(), 1, false)
.addLabeledComponent(bundle.getString("settings.resource.strategy"), constructFileStrategyPanel(), 1, false)
.addVerticalGap(12)
.addComponent(constructSortingField())
.addVerticalGap(24)
.addComponent(new TitledSeparator(bundle.getString("settings.editor.title")))
.addLabeledComponent(bundle.getString("settings.editor.key.title"), constructKeyStrategyPanel(), 1, false)
.addLabeledComponent(bundle.getString("settings.editor.default-namespace.title"), constructDefaultNamespaceField(), 1, false)
.addLabeledComponent(bundle.getString("settings.editor.preview.title"), constructPreviewLocaleField(), 1, false)
.addVerticalGap(12)
.addComponent(constructNestedKeysField())
.addComponent(constructAssistanceField())
.addVerticalGap(24)
.addComponent(new TitledSeparator(bundle.getString("settings.experimental.title")))
.addComponent(constructAlwaysFoldField())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
private JComponent constructPresetField() {
preset = new ComboBox<>(Preset.values());
preset.setToolTipText(bundle.getString("settings.preset.tooltip"));
preset.setMinimumAndPreferredWidth(196);
preset.addActionListener(e -> setState(preset.getItem().config())); // Listen to selection change
return preset;
}
private JComponent constructLocalesDirectoryField() {
localesDirectory = new TextFieldWithBrowseButton();
localesDirectory.setToolTipText(bundle.getString("settings.resource.path.tooltip"));
localesDirectory.addBrowseFolderListener(bundle.getString("settings.resource.path.window"),
bundle.getString("settings.resource.path.tooltip"), project,
new FileChooserDescriptor(false, true,
false, false, false, false));
// Listen to value change
localesDirectory.getTextField().getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
@Override
public void removeUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
@Override
public void changedUpdate(DocumentEvent e) {
validateLocalesDirectory();
}
});
validateLocalesDirectory();
return localesDirectory;
}
private void validateLocalesDirectory() {
// Paint red border to indicate missing value
localesDirectory.setBorder(localesDirectory.getText().isEmpty()
? BorderFactory.createLineBorder(JBColor.red) : null);
}
private JPanel constructFileStrategyPanel() {
JPanel panel = new JBPanel<>(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
/* folder strategy */
folderStrategy = new ComboBox<>(bundle.getString("settings.resource.folder.items").split(ArrayMapper.SPLITERATOR_REGEX));
folderStrategy.setToolTipText(bundle.getString("settings.resource.folder.tooltip"));
folderStrategy.setMinimumAndPreferredWidth(256);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 0;
constraints.gridy = 0;
panel.add(folderStrategy, constraints);
/* parser strategy */
parserStrategy = new ComboBox<>(bundle.getString("settings.resource.parser.items").split(ArrayMapper.SPLITERATOR_REGEX));
parserStrategy.setToolTipText(bundle.getString("settings.resource.parser.tooltip"));
parserStrategy.addItemListener(handleParserChange());
parserStrategy.setMinimumAndPreferredWidth(128);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
constraints.gridy = 0;
panel.add(parserStrategy, constraints);
/* file pattern strategy */
filePattern = new JBTextField();
filePattern.setToolTipText(bundle.getString("settings.resource.file-pattern.tooltip"));
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 2;
constraints.gridy = 0;
constraints.weightx = 1;
panel.add(filePattern, constraints);
return panel;
}
private JComponent constructSortingField() {
sorting = new JBCheckBox(bundle.getString("settings.resource.sorting.title"));
sorting.setToolTipText(bundle.getString("settings.resource.sorting.tooltip"));
return sorting;
}
private JPanel constructKeyStrategyPanel() {
JPanel panel = new JBPanel<>(new FlowLayout(FlowLayout.LEFT));
panel.add(new JBLabel(bundle.getString("settings.editor.key.namespace.title")));
panel.add(namespaceDelimiter = createDelimiterField(bundle.getString("settings.editor.key.namespace.tooltip")));
panel.add(new JBLabel(bundle.getString("settings.editor.key.section.title")));
panel.add(sectionDelimiter = createDelimiterField(bundle.getString("settings.editor.key.section.tooltip")));
panel.add(createBoldLabel(bundle.getString("settings.editor.key.leaf.title")));
panel.add(contextDelimiter = createDelimiterField(bundle.getString("settings.editor.key.context.tooltip")));
panel.add(createBoldLabel(bundle.getString("settings.editor.key.context.title")));
panel.add(pluralDelimiter = createDelimiterField(bundle.getString("settings.editor.key.plural.tooltip")));
panel.add(createBoldLabel(bundle.getString("settings.editor.key.plural.title")));
return panel;
}
private JLabel createBoldLabel(String title) {
JBLabel label = new JBLabel(title);
Font font = label.getFont();
label.setFont(font.deriveFont(font.getStyle() | Font.BOLD));
return label;
}
private JTextField createDelimiterField(String tooltip) {
JBTextField field = new JBTextField();
field.setHorizontalAlignment(JTextField.CENTER);
field.setToolTipText(tooltip);
return field;
}
private JComponent constructDefaultNamespaceField() {
defaultNamespace = new ExtendableTextField(20);
defaultNamespace.setToolTipText(bundle.getString("settings.editor.default-namespace.tooltip"));
return defaultNamespace;
}
private JComponent constructPreviewLocaleField() {
previewLocale = new ExtendableTextField(12);
previewLocale.setToolTipText(bundle.getString("settings.editor.preview.tooltip"));
return previewLocale;
}
private JComponent constructNestedKeysField() {
nestedKeys = new JBCheckBox(bundle.getString("settings.editor.key.nesting.title"));
nestedKeys.setToolTipText(bundle.getString("settings.editor.key.nesting.tooltip"));
return nestedKeys;
}
private JComponent constructAssistanceField() {
assistance = new JBCheckBox(bundle.getString("settings.editor.assistance.title"));
assistance.setToolTipText(bundle.getString("settings.editor.assistance.tooltip"));
return assistance;
}
private JComponent constructAlwaysFoldField() {
alwaysFold = new JBCheckBox(bundle.getString("settings.experimental.always-fold.title"));
alwaysFold.setToolTipText(bundle.getString("settings.experimental.always-fold.tooltip"));
return alwaysFold;
}
private ItemListener handleParserChange() {
return e -> {
if(e.getStateChange() == ItemEvent.SELECTED) {
// Automatically suggest file pattern option on parser change
ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex());
filePattern.setText(newStrategy.getExampleFilePattern());
}
};
}
public JPanel getMainPanel() {
return mainPanel;
}
}

View File

@ -0,0 +1,89 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import de.marhali.easyi18n.settings.presets.Preset;
import javax.swing.*;
/**
* Mandatory for state management for the project settings component.
* @author marhali
*/
public class ProjectSettingsComponentState {
protected ComboBox<Preset> preset;
// Resource Configuration
protected TextFieldWithBrowseButton localesDirectory;
protected ComboBox<String> folderStrategy;
protected ComboBox<String> parserStrategy;
protected JTextField filePattern;
protected JCheckBox sorting;
// Editor configuration
protected JTextField namespaceDelimiter;
protected JTextField sectionDelimiter;
protected JTextField contextDelimiter;
protected JTextField pluralDelimiter;
protected JTextField defaultNamespace;
protected JTextField previewLocale;
protected JCheckBox nestedKeys;
protected JCheckBox assistance;
// Experimental configuration
protected JCheckBox alwaysFold;
protected ProjectSettingsState getState() {
// Every field needs to provide its state
ProjectSettingsState state = new ProjectSettingsState();
state.setLocalesDirectory(localesDirectory.getText());
state.setFolderStrategy(FolderStrategyType.fromIndex(folderStrategy.getSelectedIndex()));
state.setParserStrategy(ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex()));
state.setFilePattern(filePattern.getText());
state.setSorting(sorting.isSelected());
state.setNamespaceDelimiter(namespaceDelimiter.getText());
state.setSectionDelimiter(sectionDelimiter.getText());
state.setContextDelimiter(contextDelimiter.getText());
state.setPluralDelimiter(pluralDelimiter.getText());
state.setDefaultNamespace(defaultNamespace.getText());
state.setPreviewLocale(previewLocale.getText());
state.setNestedKeys(nestedKeys.isSelected());
state.setAssistance(assistance.isSelected());
state.setAlwaysFold(alwaysFold.isSelected());
return state;
}
protected void setState(ProjectSettings state) {
// Update every field with the new state
localesDirectory.setText(state.getLocalesDirectory());
folderStrategy.setSelectedIndex(state.getFolderStrategy().toIndex());
parserStrategy.setSelectedIndex((state.getParserStrategy().toIndex()));
filePattern.setText(state.getFilePattern());
sorting.setSelected(state.isSorting());
namespaceDelimiter.setText(state.getNamespaceDelimiter());
sectionDelimiter.setText(state.getSectionDelimiter());
contextDelimiter.setText(state.getContextDelimiter());
pluralDelimiter.setText(state.getPluralDelimiter());
defaultNamespace.setText(state.getDefaultNamespace());
previewLocale.setText(state.getPreviewLocale());
nestedKeys.setSelected(state.isNestedKeys());
assistance.setSelected(state.isAssistance());
alwaysFold.setSelected(state.isAlwaysFold());
}
}

View File

@ -0,0 +1,59 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.InstanceManager;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
/**
* IDE settings panel for this plugin
* @author marhali
*/
public class ProjectSettingsConfigurable implements Configurable {
private final Project project;
private ProjectSettingsComponent component;
public ProjectSettingsConfigurable(Project project) {
this.project = project;
}
@Override
public String getDisplayName() {
return "Easy I18n";
}
@Override
public @Nullable JComponent createComponent() {
component = new ProjectSettingsComponent(project);
component.setState(ProjectSettingsService.get(project).getState());
return component.getMainPanel();
}
@Override
public boolean isModified() {
ProjectSettingsState originState = ProjectSettingsService.get(project).getState();
return !originState.equals(component.getState());
}
@Override
public void apply() {
ProjectSettingsService.get(project).setState(component.getState());
InstanceManager.get(project).reload();
}
@Override
public void reset() {
component.setState(ProjectSettingsService.get(project).getState());
}
@Override
public void disposeUIResources() {
component = null;
}
}

View File

@ -0,0 +1,47 @@
package de.marhali.easyi18n.settings;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
/**
* Persistent storage for project-specific settings.
* @author marhali
*/
@State(
name = "ProjectSettingsService",
storages = @Storage("easy-i18n.xml")
)
public class ProjectSettingsService implements PersistentStateComponent<ProjectSettingsState> {
public static @NotNull ProjectSettingsService get(@NotNull Project project) {
return project.getService(ProjectSettingsService.class);
}
private ProjectSettingsState state;
public ProjectSettingsService() {
this.state = new ProjectSettingsState();
}
/**
* Sets the provided configuration and invalidates the merged state.
* @param state New configuration
*/
protected void setState(@NotNull ProjectSettingsState state) {
this.state = state;
}
@Override
public @NotNull ProjectSettingsState getState() {
return state;
}
@Override
public void loadState(@NotNull ProjectSettingsState state) {
this.state = state;
}
}

View File

@ -0,0 +1,168 @@
package de.marhali.easyi18n.settings;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import de.marhali.easyi18n.settings.presets.DefaultPreset;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents the project-specific configuration of this plugin.
* @author marhali
*/
public class ProjectSettingsState implements ProjectSettings {
private static final ProjectSettings defaults = new DefaultPreset();
// Resource Configuration
private String localesDirectory;
private FolderStrategyType folderStrategy;
private ParserStrategyType parserStrategy;
private String filePattern;
private Boolean sorting;
// Editor configuration
private String namespaceDelimiter;
private String sectionDelimiter;
private String contextDelimiter;
private String pluralDelimiter;
private String defaultNamespace;
private String previewLocale;
private Boolean nestedKeys;
private Boolean assistance;
// Experimental configuration
private Boolean alwaysFold;
public ProjectSettingsState() {}
@Override
public @Nullable String getLocalesDirectory() {
return localesDirectory != null ? localesDirectory : defaults.getLocalesDirectory();
}
@Override
public @NotNull FolderStrategyType getFolderStrategy() {
return folderStrategy != null ? folderStrategy : defaults.getFolderStrategy();
}
@Override
public @NotNull ParserStrategyType getParserStrategy() {
return parserStrategy != null ? parserStrategy : defaults.getParserStrategy();
}
@Override
public @NotNull String getFilePattern() {
return filePattern != null ? filePattern : defaults.getFilePattern();
}
@Override
public boolean isSorting() {
return sorting != null ? sorting : defaults.isSorting();
}
@Override
public @Nullable String getNamespaceDelimiter() {
return namespaceDelimiter != null ? namespaceDelimiter : defaults.getNamespaceDelimiter();
}
@Override
public @NotNull String getSectionDelimiter() {
return sectionDelimiter != null ? sectionDelimiter : defaults.getSectionDelimiter();
}
@Override
public @Nullable String getContextDelimiter() {
return contextDelimiter != null ? contextDelimiter : defaults.getContextDelimiter();
}
@Override
public @Nullable String getPluralDelimiter() {
return pluralDelimiter != null ? pluralDelimiter : defaults.getPluralDelimiter();
}
@Nullable
@Override
public String getDefaultNamespace() {
return defaultNamespace;
}
@Override
public @NotNull String getPreviewLocale() {
return previewLocale != null ? previewLocale : defaults.getPreviewLocale();
}
@Override
public boolean isNestedKeys() {
return nestedKeys != null ? nestedKeys : defaults.isNestedKeys();
}
@Override
public boolean isAssistance() {
return assistance != null ? assistance : defaults.isAssistance();
}
@Override
public boolean isAlwaysFold() {
return alwaysFold != null ? alwaysFold : defaults.isAlwaysFold();
}
public void setLocalesDirectory(String localesDirectory) {
this.localesDirectory = localesDirectory;
}
public void setFolderStrategy(FolderStrategyType folderStrategy) {
this.folderStrategy = folderStrategy;
}
public void setParserStrategy(ParserStrategyType parserStrategy) {
this.parserStrategy = parserStrategy;
}
public void setFilePattern(String filePattern) {
this.filePattern = filePattern;
}
public void setSorting(Boolean sorting) {
this.sorting = sorting;
}
public void setNamespaceDelimiter(String namespaceDelimiter) {
this.namespaceDelimiter = namespaceDelimiter;
}
public void setSectionDelimiter(String sectionDelimiter) {
this.sectionDelimiter = sectionDelimiter;
}
public void setContextDelimiter(String contextDelimiter) {
this.contextDelimiter = contextDelimiter;
}
public void setPluralDelimiter(String pluralDelimiter) {
this.pluralDelimiter = pluralDelimiter;
}
public void setDefaultNamespace(String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
}
public void setPreviewLocale(String previewLocale) {
this.previewLocale = previewLocale;
}
public void setNestedKeys(Boolean nestedKeys) {
this.nestedKeys = nestedKeys;
}
public void setAssistance(Boolean assistance) {
this.assistance = assistance;
}
public void setAlwaysFold(Boolean alwaysFold) {
this.alwaysFold = alwaysFold;
}
}

View File

@ -0,0 +1,84 @@
package de.marhali.easyi18n.settings.presets;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Default preset. Used if none has been defined.
* @author marhali
*/
public class DefaultPreset implements ProjectSettings {
@Override
public String getLocalesDirectory() {
return null;
}
@Override
public @NotNull FolderStrategyType getFolderStrategy() {
return FolderStrategyType.SINGLE;
}
@Override
public @NotNull ParserStrategyType getParserStrategy() {
return ParserStrategyType.JSON;
}
@Override
public @NotNull String getFilePattern() {
return "*.*";
}
@Override
public boolean isSorting() {
return true;
}
@Override
public String getNamespaceDelimiter() {
return ":";
}
@Override
public @NotNull String getSectionDelimiter() {
return ".";
}
@Override
public String getContextDelimiter() {
return "_";
}
@Override
public String getPluralDelimiter() {
return "_";
}
@Override
public @Nullable String getDefaultNamespace() {
return null;
}
@Override
public @NotNull String getPreviewLocale() {
return "en";
}
@Override
public boolean isNestedKeys() {
return true;
}
@Override
public boolean isAssistance() {
return true;
}
@Override
public boolean isAlwaysFold() {
return false;
}
}

View File

@ -0,0 +1,33 @@
package de.marhali.easyi18n.settings.presets;
import de.marhali.easyi18n.settings.ProjectSettings;
/**
* Enumeration of all available configuration presets.
* Every preset needs to be registered here to be properly recognized.
* @author marhali
*/
public enum Preset {
DEFAULT(DefaultPreset.class),
VUE_I18N(VueI18nPreset.class),
REACT_I18NEXT(ReactI18NextPreset.class);
private final Class<? extends ProjectSettings> clazz;
Preset(Class<? extends ProjectSettings> clazz) {
this.clazz = clazz;
}
public ProjectSettings config() {
try {
return this.clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return super.name().toLowerCase();
}
}

View File

@ -0,0 +1,84 @@
package de.marhali.easyi18n.settings.presets;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Preset for React - i18n-next
* @author marhali
*/
public class ReactI18NextPreset implements ProjectSettings {
@Override
public @Nullable String getLocalesDirectory() {
return null;
}
@Override
public @NotNull FolderStrategyType getFolderStrategy() {
return FolderStrategyType.MODULARIZED_NAMESPACE;
}
@Override
public @NotNull ParserStrategyType getParserStrategy() {
return ParserStrategyType.JSON;
}
@Override
public @NotNull String getFilePattern() {
return "*.json";
}
@Override
public boolean isSorting() {
return true;
}
@Override
public @Nullable String getNamespaceDelimiter() {
return ":";
}
@Override
public @NotNull String getSectionDelimiter() {
return ".";
}
@Override
public @Nullable String getContextDelimiter() {
return "_";
}
@Override
public @Nullable String getPluralDelimiter() {
return "_";
}
@Override
public @Nullable String getDefaultNamespace() {
return "common";
}
@Override
public @NotNull String getPreviewLocale() {
return "en";
}
@Override
public boolean isNestedKeys() {
return false;
}
@Override
public boolean isAssistance() {
return true;
}
@Override
public boolean isAlwaysFold() {
return false;
}
}

View File

@ -0,0 +1,83 @@
package de.marhali.easyi18n.settings.presets;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.io.folder.FolderStrategyType;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Preset for Vue.js - vue-i18n
* @author marhali
*/
public class VueI18nPreset implements ProjectSettings {
@Override
public @Nullable String getLocalesDirectory() {
return null;
}
@Override
public @NotNull FolderStrategyType getFolderStrategy() {
return FolderStrategyType.SINGLE;
}
@Override
public @NotNull ParserStrategyType getParserStrategy() {
return ParserStrategyType.JSON;
}
@Override
public @NotNull String getFilePattern() {
return "*.json";
}
@Override
public boolean isSorting() {
return true;
}
@Override
public @Nullable String getNamespaceDelimiter() {
return null;
}
@Override
public @NotNull String getSectionDelimiter() {
return ".";
}
@Override
public @Nullable String getContextDelimiter() {
return null;
}
@Override
public @Nullable String getPluralDelimiter() {
return null;
}
@Override
public @Nullable String getDefaultNamespace() {
return null;
}
@Override
public @NotNull String getPreviewLocale() {
return "en";
}
@Override
public boolean isNestedKeys() {
return true;
}
@Override
public boolean isAssistance() {
return true;
}
@Override
public boolean isAlwaysFold() {
return false;
}
}

View File

@ -5,14 +5,19 @@ import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable; import com.intellij.ui.table.JBTable;
import de.marhali.easyi18n.InstanceManager; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.listener.ReturnKeyListener;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.dialog.EditDialog; import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.ReturnKeyListener;
import de.marhali.easyi18n.listener.DeleteKeyListener; import de.marhali.easyi18n.listener.DeleteKeyListener;
import de.marhali.easyi18n.listener.PopupClickListener; import de.marhali.easyi18n.listener.PopupClickListener;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.action.TranslationDelete;
import de.marhali.easyi18n.model.bus.BusListener; import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.renderer.TableRenderer; import de.marhali.easyi18n.renderer.TableRenderer;
import de.marhali.easyi18n.tabs.mapper.TableModelMapper; import de.marhali.easyi18n.tabs.mapper.TableModelMapper;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -56,20 +61,20 @@ public class TableView implements BusListener {
return; return;
} }
KeyPath fullPath = this.converter.split(String.valueOf(this.table.getValueAt(row, 0))); KeyPath fullPath = this.converter.fromString(String.valueOf(this.table.getValueAt(row, 0)));
Translation translation = InstanceManager.get(project).store().getData().getTranslation(fullPath); TranslationValue value = InstanceManager.get(project).store().getData().getTranslation(fullPath);
if (translation != null) { if (value != null) {
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle(); new EditDialog(project, new Translation(fullPath, value)).showAndHandle();
} }
} }
private void deleteSelectedRows() { private void deleteSelectedRows() {
for (int selectedRow : table.getSelectedRows()) { for (int selectedRow : table.getSelectedRows()) {
KeyPath fullPath = this.converter.split(String.valueOf(table.getValueAt(selectedRow, 0))); KeyPath fullPath = this.converter.fromString(String.valueOf(table.getValueAt(selectedRow, 0)));
InstanceManager.get(project).processUpdate( InstanceManager.get(project).processUpdate(
new TranslationDelete(new KeyedTranslation(fullPath, null)) new TranslationDelete(new Translation(fullPath, null))
); );
} }
} }
@ -84,7 +89,7 @@ public class TableView implements BusListener {
@Override @Override
public void onFocusKey(@NotNull KeyPath key) { public void onFocusKey(@NotNull KeyPath key) {
String concatKey = this.converter.concat(key); String concatKey = this.converter.toString(key);
int row = -1; int row = -1;
for (int i = 0; i < table.getRowCount(); i++) { for (int i = 0; i < table.getRowCount(); i++) {

View File

@ -9,16 +9,20 @@ import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree; import com.intellij.ui.treeStructure.Tree;
import de.marhali.easyi18n.InstanceManager; import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.ReturnKeyListener; import de.marhali.easyi18n.listener.ReturnKeyListener;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.action.TranslationDelete;
import de.marhali.easyi18n.model.bus.BusListener; import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.action.treeview.CollapseTreeViewAction; import de.marhali.easyi18n.action.treeview.CollapseTreeViewAction;
import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction; import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.DeleteKeyListener; import de.marhali.easyi18n.listener.DeleteKeyListener;
import de.marhali.easyi18n.listener.PopupClickListener; import de.marhali.easyi18n.listener.PopupClickListener;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.renderer.TreeRenderer; import de.marhali.easyi18n.renderer.TreeRenderer;
import de.marhali.easyi18n.service.SettingsService; import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.tabs.mapper.TreeModelMapper; import de.marhali.easyi18n.tabs.mapper.TreeModelMapper;
import de.marhali.easyi18n.util.TreeUtil; import de.marhali.easyi18n.util.TreeUtil;
@ -28,6 +32,8 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@ -80,7 +86,21 @@ public class TreeView implements BusListener {
@Override @Override
public void onUpdateData(@NotNull TranslationData data) { public void onUpdateData(@NotNull TranslationData data) {
tree.setModel(this.currentMapper = new TreeModelMapper(data, SettingsService.getInstance(project).getState())); List<Integer> expanded = getExpandedRows();
tree.setModel(this.currentMapper = new TreeModelMapper(data, ProjectSettingsService.get(project).getState()));
expanded.forEach(tree::expandRow);
}
private List<Integer> getExpandedRows() {
List<Integer> expanded = new ArrayList<>();
for(int i = 0; i < tree.getRowCount(); i++) {
if(tree.isExpanded(i)) {
expanded.add(i);
}
}
return expanded;
} }
@Override @Override
@ -127,13 +147,13 @@ public class TreeView implements BusListener {
} }
KeyPath fullPath = TreeUtil.getFullPath(path); KeyPath fullPath = TreeUtil.getFullPath(path);
Translation translation = InstanceManager.get(project).store().getData().getTranslation(fullPath); TranslationValue value = InstanceManager.get(project).store().getData().getTranslation(fullPath);
if (translation == null) { if (value == null) {
return; return;
} }
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle(); new EditDialog(project, new Translation(fullPath, value)).showAndHandle();
} }
private void deleteSelectedNodes() { private void deleteSelectedNodes() {
@ -147,7 +167,7 @@ public class TreeView implements BusListener {
KeyPath fullPath = TreeUtil.getFullPath(path); KeyPath fullPath = TreeUtil.getFullPath(path);
InstanceManager.get(project).processUpdate( InstanceManager.get(project).processUpdate(
new TranslationDelete(new KeyedTranslation(fullPath, null)) new TranslationDelete(new Translation(fullPath, null))
); );
} }
} }

View File

@ -1,8 +1,13 @@
package de.marhali.easyi18n.tabs.mapper; package de.marhali.easyi18n.tabs.mapper;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.action.TranslationUpdate;
import de.marhali.easyi18n.model.bus.FilterMissingTranslationsListener; import de.marhali.easyi18n.model.bus.FilterMissingTranslationsListener;
import de.marhali.easyi18n.model.bus.SearchQueryListener; import de.marhali.easyi18n.model.bus.SearchQueryListener;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -50,10 +55,10 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
List<KeyPath> matches = new ArrayList<>(); List<KeyPath> matches = new ArrayList<>();
for(KeyPath key : this.data.getFullKeys()) { for(KeyPath key : this.data.getFullKeys()) {
if(this.converter.concat(key).toLowerCase().contains(query)) { if(this.converter.toString(key).toLowerCase().contains(query)) {
matches.add(key); matches.add(key);
} else { } else {
for(String content : this.data.getTranslation(key).values()) { for(String content : this.data.getTranslation(key).getLocaleContents()) {
if(content.toLowerCase().contains(query)) { if(content.toLowerCase().contains(query)) {
matches.add(key); matches.add(key);
} }
@ -74,7 +79,7 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
List<KeyPath> matches = new ArrayList<>(); List<KeyPath> matches = new ArrayList<>();
for(KeyPath key : this.data.getFullKeys()) { for(KeyPath key : this.data.getFullKeys()) {
if(this.data.getTranslation(key).values().size() != this.locales.size()) { if(this.data.getTranslation(key).getLocaleContents().size() != this.locales.size()) {
matches.add(key); matches.add(key);
} }
} }
@ -117,25 +122,25 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
KeyPath key = this.fullKeys.get(rowIndex); KeyPath key = this.fullKeys.get(rowIndex);
if(columnIndex == 0) { // Keys if(columnIndex == 0) { // Keys
return this.converter.concat(key); return this.converter.toString(key);
} }
String locale = this.locales.get(columnIndex - 1); String locale = this.locales.get(columnIndex - 1);
Translation translation = this.data.getTranslation(key); TranslationValue value = this.data.getTranslation(key);
return translation == null ? null : translation.get(locale); return value == null ? null : value.get(locale);
} }
@Override @Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) { public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
KeyPath key = this.fullKeys.get(rowIndex); KeyPath key = this.fullKeys.get(rowIndex);
Translation translation = this.data.getTranslation(key); TranslationValue translation = this.data.getTranslation(key);
if(translation == null) { // Unknown cell if(translation == null) { // Unknown cell
return; return;
} }
KeyPath newKey = columnIndex == 0 ? this.converter.split(String.valueOf(aValue)) : key; KeyPath newKey = columnIndex == 0 ? this.converter.fromString(String.valueOf(aValue)) : key;
// Translation content update // Translation content update
if(columnIndex > 0) { if(columnIndex > 0) {
@ -146,8 +151,8 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
} }
} }
TranslationUpdate update = new TranslationUpdate(new KeyedTranslation(key, translation), TranslationUpdate update = new TranslationUpdate(new Translation(key, translation),
new KeyedTranslation(newKey, translation)); new Translation(newKey, translation));
this.updater.accept(update); this.updater.accept(update);
} }

View File

@ -3,9 +3,14 @@ package de.marhali.easyi18n.tabs.mapper;
import com.intellij.ide.projectView.PresentationData; import com.intellij.ide.projectView.PresentationData;
import com.intellij.ui.JBColor; import com.intellij.ui.JBColor;
import de.marhali.easyi18n.model.*; import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.bus.FilterMissingTranslationsListener; import de.marhali.easyi18n.model.bus.FilterMissingTranslationsListener;
import de.marhali.easyi18n.model.bus.SearchQueryListener; import de.marhali.easyi18n.model.bus.SearchQueryListener;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.util.KeyPathConverter;
import de.marhali.easyi18n.util.UiUtil; import de.marhali.easyi18n.util.UiUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -24,13 +29,13 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
private final TranslationData data; private final TranslationData data;
private final KeyPathConverter converter; private final KeyPathConverter converter;
private final SettingsState state; private final ProjectSettings state;
public TreeModelMapper(TranslationData data, SettingsState state) { public TreeModelMapper(TranslationData data, ProjectSettings state) {
super(null); super(null);
this.data = data; this.data = data;
this.converter = new KeyPathConverter(state.isNestedKeys()); this.converter = new KeyPathConverter(state);
this.state = state; this.state = state;
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
@ -41,7 +46,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
@Override @Override
public void onSearchQuery(@Nullable String query) { public void onSearchQuery(@Nullable String query) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
TranslationData shadow = new TranslationData(this.state.isSortKeys()); TranslationData shadow = new TranslationData(this.state.isSorting());
if(query == null) { // Reset if(query == null) { // Reset
this.generateNodes(rootNode, this.data.getRootNode()); this.generateNodes(rootNode, this.data.getRootNode());
@ -52,15 +57,15 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
query = query.toLowerCase(); query = query.toLowerCase();
for(KeyPath currentKey : this.data.getFullKeys()) { for(KeyPath currentKey : this.data.getFullKeys()) {
Translation translation = this.data.getTranslation(currentKey); TranslationValue translation = this.data.getTranslation(currentKey);
String loweredKey = this.converter.concat(currentKey).toLowerCase(); String loweredKey = this.converter.toString(currentKey).toLowerCase();
if(query.contains(loweredKey) || loweredKey.contains(query)) { if(query.contains(loweredKey) || loweredKey.contains(query)) {
shadow.setTranslation(currentKey, translation); shadow.setTranslation(currentKey, translation);
continue; continue;
} }
for(String currentContent : translation.values()) { for(String currentContent : translation.getLocaleContents()) {
if(currentContent.toLowerCase().contains(query)) { if(currentContent.toLowerCase().contains(query)) {
shadow.setTranslation(currentKey, translation); shadow.setTranslation(currentKey, translation);
break; break;
@ -75,7 +80,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
@Override @Override
public void onFilterMissingTranslations(boolean filter) { public void onFilterMissingTranslations(boolean filter) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
TranslationData shadow = new TranslationData(this.state.isSortKeys()); TranslationData shadow = new TranslationData(this.state.isSorting());
if(!filter) { // Reset if(!filter) { // Reset
this.generateNodes(rootNode, this.data.getRootNode()); this.generateNodes(rootNode, this.data.getRootNode());
@ -84,9 +89,9 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
} }
for(KeyPath currentKey : this.data.getFullKeys()) { for(KeyPath currentKey : this.data.getFullKeys()) {
Translation translation = this.data.getTranslation(currentKey); TranslationValue translation = this.data.getTranslation(currentKey);
if(translation.values().size() != this.data.getLocales().size()) { if(translation.getLocaleContents().size() != this.data.getLocales().size()) {
shadow.setTranslation(currentKey, translation); shadow.setTranslation(currentKey, translation);
} }
} }
@ -124,7 +129,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
} else { } else {
String previewLocale = this.state.getPreviewLocale(); String previewLocale = this.state.getPreviewLocale();
String sub = "(" + previewLocale + ": " + childTranslationNode.getValue().get(previewLocale) + ")"; String sub = "(" + previewLocale + ": " + childTranslationNode.getValue().get(previewLocale) + ")";
String tooltip = UiUtil.generateHtmlTooltip(childTranslationNode.getValue()); String tooltip = UiUtil.generateHtmlTooltip(childTranslationNode.getValue().getEntries());
PresentationData data = new PresentationData(key, sub, null, null); PresentationData data = new PresentationData(key, sub, null, null);
data.setTooltip(tooltip); data.setTooltip(tooltip);

View File

@ -0,0 +1,159 @@
package de.marhali.easyi18n.util;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.settings.ProjectSettings;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Stateful utility to transform absolute translation keys into their character literal representation and backwards.
* @author marhali
*/
public class KeyPathConverter {
private final ProjectSettings settings;
/**
* Constructs a new converter instance
* @param settings Delimiter configuration
*/
public KeyPathConverter(@NotNull ProjectSettings settings) {
this.settings = settings;
}
/**
* @see #KeyPathConverter(ProjectSettings)
* @param project Opened project
*/
public KeyPathConverter(@NotNull Project project) {
this(ProjectSettingsService.get(project).getState());
}
/**
* Transform to character literal representation
* @param path Absolute key path
* @return Character literal
*/
public @NotNull String toString(@NotNull KeyPath path) {
StringBuilder builder = new StringBuilder();
for(int i = 0; i < path.size(); i++) {
if(i > 0) { // Delimiters
if(i == 1 && settings.getFolderStrategy().isNamespaceMode() && settings.getNamespaceDelimiter() != null) {
builder.append(quoteDelimiter(settings.getNamespaceDelimiter()));
} else {
builder.append(quoteDelimiter(settings.getSectionDelimiter()));
}
}
// Section content
builder.append(quoteSection(path.get(i)));
}
return builder.toString();
}
/**
* Splits provided character literal into key path sections.
* If namespace mode is activated and none was provided, the default namespace will be added.
* @return Layered key path sections
*/
public @NotNull KeyPath fromString(@NotNull String literalPath) {
KeyPath path = new KeyPath();
int i = 0;
for(String section : literalPath.split(getSplitRegex())) {
// Missing namespace
if(i == 0 && settings.getFolderStrategy().isNamespaceMode() && hasDefaultNamespace()) {
String namespaceDelim = (settings.isNestedKeys() ? "" : "\\") + settings.getNamespaceDelimiter();
if(section.length() == literalPath.length() || !literalPath.substring(section.length()).startsWith(namespaceDelim)) {
path.add(settings.getDefaultNamespace());
}
}
path.add(unquoteSection(section));
i++;
}
return path;
}
@Override
public String toString() {
return "KeyPathConverter{" +
"settings=" + settings +
'}';
}
/*
* INTERNAL METHODS
*/
private boolean hasDefaultNamespace() {
return settings.getDefaultNamespace() != null && !settings.getDefaultNamespace().isEmpty();
}
private String getSplitRegex() {
return settings.isNestedKeys()
? ("(?<!" + Pattern.quote("\\") + ")" + getSplitCharsRegex())
: Pattern.quote("\\") + getSplitCharsRegex();
}
private String getSplitCharsRegex() {
StringBuilder builder = new StringBuilder();
builder.append("(");
builder.append(Pattern.quote(settings.getSectionDelimiter()));
// Add optional namespace delimiter if present
if(settings.getNamespaceDelimiter() != null && !settings.getNamespaceDelimiter().isEmpty()) {
builder.append("|");
builder.append(Pattern.quote(Objects.requireNonNull(settings.getNamespaceDelimiter())));
}
builder.append(")");
return builder.toString();
}
/**
* Securely escape found delimiters inside provided section according to the configured policy.
*/
private String quoteSection(String section) {
String quoted = section;
if(!settings.isNestedKeys()) {
return quoted;
}
if(hasDefaultNamespace()) {
quoted = quoted.replace(settings.getNamespaceDelimiter(), "\\" + settings.getNamespaceDelimiter());
}
quoted = quoted.replace(settings.getSectionDelimiter(), "\\" + settings.getSectionDelimiter());
return quoted;
}
private String unquoteSection(String section) {
String unquoted = section;
if(hasDefaultNamespace()) {
unquoted = unquoted.replace("\\" + settings.getNamespaceDelimiter(), settings.getNamespaceDelimiter());
}
unquoted = unquoted.replace("\\" + settings.getSectionDelimiter(), settings.getSectionDelimiter());
return unquoted;
}
/**
* Securely escape provided delimiter according to the configured policy.
*/
private String quoteDelimiter(String delimiter) {
return settings.isNestedKeys() ? delimiter : delimiter.replace(delimiter, "\\" + delimiter);
}
}

View File

@ -5,7 +5,7 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.action.SettingsAction; import de.marhali.easyi18n.action.SettingsAction;
import de.marhali.easyi18n.io.IOHandler; import de.marhali.easyi18n.io.IOHandler;
import de.marhali.easyi18n.model.SettingsState; import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.text.MessageFormat; import java.text.MessageFormat;
@ -17,11 +17,11 @@ import java.util.ResourceBundle;
*/ */
public class NotificationHelper { public class NotificationHelper {
public static void createIOError(@NotNull SettingsState state, Exception ex) { public static void createIOError(@NotNull ProjectSettings state, Exception ex) {
ResourceBundle bundle = ResourceBundle.getBundle("messages"); ResourceBundle bundle = ResourceBundle.getBundle("messages");
String message = MessageFormat.format(bundle.getString("error.io"), String message = MessageFormat.format(bundle.getString("error.io"),
state.getFolderStrategy(), state.getParserStrategy(), state.getFilePattern(), state.getLocalesPath()); state.getFolderStrategy(), state.getParserStrategy(), state.getFilePattern(), state.getLocalesDirectory());
Logger.getInstance(IOHandler.class).error(message, ex); Logger.getInstance(IOHandler.class).error(message, ex);
} }

View File

@ -1,6 +1,7 @@
package de.marhali.easyi18n.util; package de.marhali.easyi18n.util;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* User interface utilities. * User interface utilities.
@ -13,12 +14,12 @@ public class UiUtil {
* @param messages Contains locales with desired translation * @param messages Contains locales with desired translation
* @return String with html format * @return String with html format
*/ */
public static String generateHtmlTooltip(Map<String, String> messages) { public static String generateHtmlTooltip(Set<Map.Entry<String, String>> messages) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("<html>"); builder.append("<html>");
for(Map.Entry<String, String> entry : messages.entrySet()) { for(Map.Entry<String, String> entry : messages) {
builder.append("<b>"); builder.append("<b>");
builder.append(entry.getKey()).append(":"); builder.append(entry.getKey()).append(":");
builder.append("</b> "); builder.append("</b> ");

View File

@ -1,5 +1,27 @@
<idea-plugin> <idea-plugin>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="JAVA" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" /> <intentionAction>
<className>de.marhali.easyi18n.assistance.intention.JavaTranslationIntention</className>
</intentionAction>
<psi.referenceContributor
language="JAVA"
implementation="de.marhali.easyi18n.assistance.reference.JavaKeyReferenceContributor"
/>
<lang.foldingBuilder
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.folding.JavaFoldingBuilder"
/>
<lang.documentationProvider
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<completion.contributor
language="JAVA"
implementationClass="de.marhali.easyi18n.assistance.completion.JavaCompletionContributor"
/>
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View File

@ -1,6 +1,45 @@
<idea-plugin> <idea-plugin>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="JavaScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" /> <psi.referenceContributor
<lang.foldingBuilder language="TypeScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" /> language="JavaScript"
implementation="de.marhali.easyi18n.assistance.reference.JsKeyReferenceContributor"
/>
<lang.documentationProvider
language="JavaScript"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<!-- JavaScript plugin also includes TypeScript -->
<lang.documentationProvider
language="TypeScript"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<!-- JavaScript plugin also includes TypeScript -->
<lang.foldingBuilder
language="TypeScript"
implementationClass="de.marhali.easyi18n.assistance.folding.JsFoldingBuilder"
/>
<!-- JavaScript plugin also includes TypeScript JSX -->
<lang.foldingBuilder
language="TypeScript JSX"
implementationClass="de.marhali.easyi18n.assistance.folding.JsFoldingBuilder"
/>
<lang.foldingBuilder
language="JavaScript"
implementationClass="de.marhali.easyi18n.assistance.folding.JsFoldingBuilder"
/>
<intentionAction>
<className>de.marhali.easyi18n.assistance.intention.JsTranslationIntention</className>
</intentionAction>
<completion.contributor
language="JavaScript"
implementationClass="de.marhali.easyi18n.assistance.completion.JsCompletionContributor"
/>
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View File

@ -1,11 +1,27 @@
<idea-plugin> <idea-plugin>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<annotator language="kotlin" implementationClass="de.marhali.easyi18n.editor.kotlin.KotlinKeyAnnotator" /> <intentionAction>
<className>de.marhali.easyi18n.assistance.intention.KtTranslationIntention</className>
</intentionAction>
<completion.contributor language="kotlin" <psi.referenceContributor
implementationClass="de.marhali.easyi18n.editor.kotlin.KotlinKeyCompletionContributor" /> language="kotlin"
implementation="de.marhali.easyi18n.assistance.reference.KtKeyReferenceContributor"
/>
<psi.referenceContributor language="kotlin" <lang.documentationProvider
implementation="de.marhali.easyi18n.editor.kotlin.KotlinKeyReferenceContributor" /> language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<completion.contributor
language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.completion.KtCompletionContributor"
/>
<lang.foldingBuilder
language="kotlin"
implementationClass="de.marhali.easyi18n.assistance.folding.KtFoldingBuilder"
/>
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View File

@ -0,0 +1,27 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<intentionAction>
<className>de.marhali.easyi18n.assistance.intention.PhpTranslationIntention</className>
</intentionAction>
<psi.referenceContributor
language="PHP"
implementation="de.marhali.easyi18n.assistance.reference.PhpKeyReferenceContributor"
/>
<lang.foldingBuilder
language="PHP"
implementationClass="de.marhali.easyi18n.assistance.folding.PhpFoldingBuilder"
/>
<lang.documentationProvider
language="PHP"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<completion.contributor
language="PHP"
implementationClass="de.marhali.easyi18n.assistance.completion.PhpCompletionContributor"
/>
</extensions>
</idea-plugin>

View File

@ -1,6 +1,15 @@
<idea-plugin> <idea-plugin>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="HTML" <lang.foldingBuilder language="VueJS" implementationClass="de.marhali.easyi18n.assistance.folding.JsFoldingBuilder" />
implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<lang.documentationProvider
language="VueJS"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<lang.documentationProvider
language="Vue"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View File

@ -13,6 +13,7 @@
<depends optional="true" config-file="de.marhali.easyi18n-javascript.xml">JavaScript</depends> <depends optional="true" config-file="de.marhali.easyi18n-javascript.xml">JavaScript</depends>
<depends optional="true" config-file="de.marhali.easyi18n-java.xml">com.intellij.java</depends> <depends optional="true" config-file="de.marhali.easyi18n-java.xml">com.intellij.java</depends>
<depends optional="true" config-file="de.marhali.easyi18n-vue.xml">org.jetbrains.plugins.vue</depends> <depends optional="true" config-file="de.marhali.easyi18n-vue.xml">org.jetbrains.plugins.vue</depends>
<depends optional="true" config-file="de.marhali.easyi18n-php.xml">com.jetbrains.php</depends>
<actions> <actions>
<action <action
@ -24,21 +25,18 @@
</actions> </actions>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<toolWindow id="Easy I18n" anchor="bottom" factoryClass="de.marhali.easyi18n.service.TranslatorToolWindowFactory" /> <toolWindow id="Easy I18n" anchor="bottom"
factoryClass="de.marhali.easyi18n.service.TranslatorToolWindowFactory"
icon="/icons/translate13.svg"/>
<projectService serviceImplementation="de.marhali.easyi18n.service.SettingsService" /> <projectService serviceImplementation="de.marhali.easyi18n.settings.ProjectSettingsService"/>
<completion.contributor language="any" <projectConfigurable parentId="tools" instance="de.marhali.easyi18n.settings.ProjectSettingsConfigurable"
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyCompletionContributor" /> id="de.marhali.easyi18n.service.AppSettingsConfigurable"
displayName="Easy I18n" nonDefaultProject="true"/>
<annotator language="" <notificationGroup displayType="BALLOON" id="Easy I18n Notification Group"/>
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyAnnotator" />
<psi.referenceContributor <errorHandler implementation="de.marhali.easyi18n.service.ErrorReportHandler"/>
implementation="de.marhali.easyi18n.editor.generic.GenericKeyReferenceContributor" />
<notificationGroup displayType="BALLOON" id="Easy I18n Notification Group" />
<errorHandler implementation="de.marhali.easyi18n.service.ErrorReportHandler" />
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View File

@ -1,9 +0,0 @@
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<g id="Layer_1">
<title>Layer 1</title>
<line stroke-linecap="null" stroke-linejoin="null" id="svg_16" y2="10.12481" x2="3.98555" y1="3.34971" x1="3.98555" opacity="undefined" fill-opacity="null" stroke-opacity="null" stroke-dasharray="null" stroke-width="null" stroke="#000000" fill="none"/>
<line stroke-linecap="null" stroke-linejoin="null" id="svg_17" y2="3.69653" x2="1.32659" y1="3.69653" x1="6.76012" opacity="undefined" fill-opacity="null" stroke-opacity="null" stroke-dasharray="null" stroke-width="null" stroke="#000000" fill="none"/>
<line stroke-linecap="null" stroke-linejoin="null" id="svg_21" y2="9.53468" x2="10.97977" y1="9.53468" x1="6.4711" opacity="undefined" fill-opacity="null" stroke-opacity="null" stroke-dasharray="null" stroke-width="null" stroke="#000000" fill="none"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 959 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13">
<path stroke="#6E6E6E" stroke-width="2" stroke-linecap="undefined" stroke-linejoin="undefined" fill="none"
d="M4.457 2.992v8.995M8.064 2.027H1.006M7.939 10.987h4.047"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13">
<path stroke="#AFB1B3" stroke-width="2" stroke-linecap="undefined" stroke-linejoin="undefined" fill="none"
d="M4.457 2.992v8.995M8.064 2.027H1.006M7.939 10.987h4.047"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

@ -1,7 +1,8 @@
view.tree.title=TreeView documentation=EasyI18n Translation
view.tree.title=Tree View
view.tree.collapse=Collapse Tree view.tree.collapse=Collapse Tree
view.tree.expand=Expand Tree view.tree.expand=Expand Tree
view.table.title=TableView view.table.title=Table View
view.empty=No translations found. Click on settings to specify a locales directory or reload view.empty=No translations found. Click on settings to specify a locales directory or reload
action.add=Add Translation action.add=Add Translation
action.edit=Edit Translation action.edit=Edit Translation
@ -10,21 +11,51 @@ action.reload=Reload From Disk
action.settings=Settings action.settings=Settings
action.search=Search... action.search=Search...
action.delete=Delete action.delete=Delete
action.extract=Extract translation
translation.key=Key translation.key=Key
translation.locales=Locales translation.locales=Locales
settings.path.title=Locales Directory # Settings
settings.path.text=Locales directory settings.hint.text=Project-specific configuration. For an easy start, you can use one of the existing presets.
settings.strategy.title=Translation file structure settings.hint.action=Fore more information, see the documentation
settings.strategy.folder=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale settings.preset.title=Preset
settings.strategy.folder.tooltip=What is the folder structure of your translation files? settings.preset.tooltip=Choose a configuration template that best fits your project. After that you can make further changes.
settings.strategy.parser=JSON;JSON5;YAML;YML;Properties;ARB # Resource Configuration
settings.strategy.parser.tooltip=Which file parser should be used to process your translation files? settings.resource.title=Resource Configuration
settings.strategy.file-pattern.tooltip=Defines a wildcard matcher to filter relevant translation files. For example *.json, *.???. settings.resource.path.window=Locales Directory
settings.preview=Preview locale settings.resource.path.title=Locales directory
settings.path.prefix=Path prefix settings.resource.path.tooltip=Define the folder which contains all translation files. For nested folders, use the top folder.
settings.keys.sort=Sort translation keys alphabetically settings.resource.strategy=File structure
settings.keys.nested=Escape delimiter character within a section layer. settings.resource.folder.items=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale
settings.editor.assistance=I18n key completion, annotation and reference inside editor settings.resource.folder.tooltip=What is the folder structure of your translation files?
settings.resource.parser.items=JSON;JSON5;YAML;YML;Properties;ARB
settings.resource.parser.tooltip=Which file parser should be used to process your translation files?
settings.resource.file-pattern.tooltip=Defines a wildcard matcher to filter relevant translation files. For example *.json, *.??? or *.*.
settings.resource.sorting.title=Sort translation keys alphabetically
settings.resource.sorting.tooltip=Sorts all translation keys alphabetically. If disabled, the original key-order in the files is kept.
# Editor Configuration
settings.editor.title=Editor Configuration
settings.editor.key.title=Key delimiters
settings.editor.key.namespace.title=[namespace]
settings.editor.key.namespace.tooltip=Sets the separator used between namespace and key path.
settings.editor.key.section.title=[section]
settings.editor.key.section.tooltip=Sets the separator used between section nodes of the key path.
settings.editor.key.leaf.title=[leaf
settings.editor.key.context.title=context
settings.editor.key.context.tooltip=Sets the separator used to define context-specific variants of a translation.
settings.editor.key.plural.title=pluralization]
settings.editor.key.plural.tooltip=Sets the separator used to define different pluralization's of a translation.
settings.editor.default-namespace.title=Default namespace
settings.editor.default-namespace.tooltip=Specifies the namespace to use by default if none is specified in a translation key. The field can be left blank to ignore this feature.
settings.editor.preview.title=Preview locale
settings.editor.preview.tooltip=Defines the language to be displayed in editor previews.
settings.editor.key.nesting.title=Escape section delimiter within a section layer
settings.editor.key.nesting.tooltip=Escapes the section delimiter within a section to properly reconstruct nested key sections. Disable this feature if nested key sections are the exception rather than the rule in your translation file.
settings.editor.assistance.title=Editor code assistance for translations
settings.editor.assistance.tooltip=Activates editor support to reference, auto-complete and folding of existing translations.
# Experimental configuration
settings.experimental.title=Experimental Configuration
settings.experimental.always-fold.title=Always fold translation keys
settings.experimental.always-fold.tooltip=Forces the editor to always display the value behind a translation key. The value cannot be unfolded when this function is active.
error.io=An error occurred while processing translation files. \n\ error.io=An error occurred while processing translation files. \n\
Config: {0} => {1} ({2}) \n\ Config: {0} => {1} ({2}) \n\
Path: {3} \n\ Path: {3} \n\

Some files were not shown because too many files have changed in this diff Show More