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,6 +3,24 @@
# easy-i18n Changelog
## [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]
### Added

View File

@ -91,6 +91,9 @@ _For more examples, please refer to the [Examples Directory](https://github.com/
## Roadmap
- [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
- [ ] Mark duplicate translation values

View File

@ -4,7 +4,7 @@
pluginGroup = de.marhali.easyi18n
pluginName = easy-i18n
# 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
# 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
# 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
javaVersion = 11

View File

@ -1,9 +1,9 @@
package de.marhali.easyi18n;
import de.marhali.easyi18n.model.KeyPath;
import de.marhali.easyi18n.model.bus.BusListener;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.KeyPath;
import org.jetbrains.annotations.NotNull;
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.io.IOHandler;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
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 org.jetbrains.annotations.NotNull;
@ -35,7 +35,7 @@ public class DataStore {
this.changeListener = new FileChangeListener(project);
VirtualFileManager.getInstance().addAsyncFileListener(
this.changeListener, Disposer.newDisposable("EasyI18n"));
this.changeListener, Disposer.newDisposable(project, "EasyI18n"));
}
public @NotNull TranslationData getData() {
@ -48,18 +48,18 @@ public class DataStore {
* @param successResult Consumer will inform if operation was successful
*/
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().runReadAction(() -> {
try {
this.data = new IOHandler(settings).read();
this.changeListener.updateLocalesPath(settings.getLocalesPath());
this.changeListener.updateLocalesPath(settings.getLocalesDirectory());
successResult.accept(true);
} catch (Exception ex) {
this.data = new TranslationData(settings.isSortKeys());
this.data = new TranslationData(settings.isSorting());
successResult.accept(false);
if(ex instanceof EmptyLocalesDirException) {
@ -76,7 +76,7 @@ public class DataStore {
* @param successResult Consumer will inform if operation was successful
*/
public void saveToPersistenceLayer(@NotNull Consumer<Boolean> successResult) {
SettingsState settings = SettingsService.getInstance(this.project).getState();
ProjectSettings settings = ProjectSettingsService.get(project).getState();
ApplicationManager.getApplication().runWriteAction(() -> {
try {

View File

@ -3,7 +3,7 @@ package de.marhali.easyi18n;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import de.marhali.easyi18n.model.TranslationUpdate;
import de.marhali.easyi18n.model.action.TranslationUpdate;
import org.jetbrains.annotations.NotNull;
@ -52,13 +52,22 @@ public class InstanceManager {
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) {
if(update.isDeletion() || update.isKeyChange()) { // Remove origin translation
this.store.getData().setTranslation(update.getOrigin().getKey(), null);
}
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 -> {

View File

@ -6,10 +6,10 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
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.model.KeyPath;
import de.marhali.easyi18n.service.WindowManager;
import de.marhali.easyi18n.util.KeyPathConverter;
import de.marhali.easyi18n.util.TreeUtil;
import org.jetbrains.annotations.NotNull;
@ -32,7 +32,7 @@ public class AddAction extends AnAction {
@Override
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) {
@ -58,7 +58,7 @@ public class AddAction extends AnAction {
if(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 java.util.Objects;
import java.util.ResourceBundle;
/**
@ -23,9 +24,6 @@ public class ReloadAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
InstanceManager manager = InstanceManager.get(e.getProject());
manager.store().loadFromPersistenceLayer((success) -> {
manager.bus().propagate().onUpdateData(manager.store().getData());
});
InstanceManager.get(Objects.requireNonNull(e.getProject())).reload();
}
}

View File

@ -3,7 +3,10 @@ package de.marhali.easyi18n.action;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
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 java.util.ResourceBundle;
@ -21,6 +24,6 @@ public class SettingsAction extends AnAction {
@Override
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.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import de.marhali.easyi18n.editor.KeyCompletionProvider;
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry;
/**
* Kotlin specific translation key completion contributor.
* Kotlin specific completion contributor.
* @author marhali
*/
public class KotlinKeyCompletionContributor extends CompletionContributor {
public KotlinKeyCompletionContributor() {
public class KtCompletionContributor extends CompletionContributor {
public KtCompletionContributor() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(KtLiteralStringTemplateEntry.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.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.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.model.action.TranslationCreate;
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.Nullable;
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
*/
public class AddDialog {
public class AddDialog extends TranslationDialog {
private final @NotNull Project project;
private final @NotNull KeyPathConverter converter;
private @NotNull KeyPath preKey;
private JBTextField keyTextField;
private Map<String, JBTextField> valueTextFields;
public AddDialog(@NotNull Project project, @Nullable KeyPath preKey) {
this(project);
this.preKey = preKey == null ? new KeyPath() : preKey;
/**
* Constructs a new create dialog with prefilled fields
* @param project Opened project
* @param prefillKey Prefill translation key
* @param prefillLocale Prefill preview locale value
*/
public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale) {
super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(),
prefillLocale != null
? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale)
: null)
);
}
/**
* Constructs a new create dialog without prefilled fields.
* @param project Opened project
*/
public AddDialog(@NotNull Project project) {
this.project = project;
this.converter = new KeyPathConverter(project);
this.preKey = new KeyPath();
this(project, 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();
builder.setTitle(ResourceBundle.getBundle("messages").getString("action.add"));
builder.setTitle(bundle.getString("action.add"));
builder.removeAllActions();
builder.addOkAction();
builder.addCancelAction();
builder.setCenterPanel(rootPanel);
builder.setCenterPanel(centerPanel);
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.ui.DialogBuilder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.JBTextField;
import de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.model.*;
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.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
*/
public class EditDialog {
public class EditDialog extends TranslationDialog {
private final Project project;
private final KeyPathConverter converter;
private final KeyedTranslation origin;
private JBTextField keyTextField;
private Map<String, JBTextField> valueTextFields;
public EditDialog(Project project, KeyedTranslation origin) {
this.project = project;
this.converter = new KeyPathConverter(project);
this.origin = origin;
/**
* Constructs a new edit dialog with the provided translation
* @param project Opened project
* @param origin Translation to edit
*/
public EditDialog(@NotNull Project project, @NotNull Translation origin) {
super(project, origin);
}
public void showAndHandle() {
int code = prepare().show();
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);
@Override
protected @NotNull DialogBuilder configure(@NotNull JComponent centerPanel) {
DialogBuilder builder = new DialogBuilder();
builder.setTitle(ResourceBundle.getBundle("messages").getString("action.edit"));
builder.setTitle(bundle.getString("action.edit"));
builder.removeAllActions();
builder.addCancelAction();
builder.addActionDescriptor(new DeleteActionDescriptor());
builder.addOkAction();
builder.setCenterPanel(rootPanel);
builder.setCenterPanel(centerPanel);
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.model.*;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import java.io.File;
@ -21,23 +22,23 @@ import java.util.List;
*/
public class IOHandler {
private final @NotNull SettingsState settings;
private final @NotNull ProjectSettings settings;
private final @NotNull FolderStrategy folderStrategy;
private final @NotNull ParserStrategyType parserStrategyType;
private final @NotNull ParserStrategy parserStrategy;
public IOHandler(@NotNull SettingsState settings) throws Exception {
public IOHandler(@NotNull ProjectSettings settings) throws Exception {
this.settings = settings;
this.folderStrategy = settings.getFolderStrategy().getStrategy()
.getDeclaredConstructor(SettingsState.class).newInstance(settings);
.getDeclaredConstructor(ProjectSettings.class).newInstance(settings);
this.parserStrategyType = settings.getParserStrategy();
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
*/
public @NotNull TranslationData read() throws IOException {
String localesPath = this.settings.getLocalesPath();
String localesPath = this.settings.getLocalesDirectory();
if(localesPath == null || localesPath.isEmpty()) {
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 + ")");
}
TranslationData data = new TranslationData(this.settings.isSortKeys());
TranslationData data = new TranslationData(this.settings.isSorting());
List<TranslationFile> translationFiles = this.folderStrategy.analyzeFolderStructure(localesDirectory);
for(TranslationFile file : translationFiles) {
@ -80,7 +81,7 @@ public class IOHandler {
* @throws IOException Write action failed
*/
public void write(@NotNull TranslationData data) throws IOException {
String localesPath = this.settings.getLocalesPath();
String localesPath = this.settings.getLocalesDirectory();
if(localesPath == null || localesPath.isEmpty()) {
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 de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.NotNull;
@ -22,9 +22,9 @@ import java.util.Objects;
*/
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;
}

View File

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

View File

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

View File

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

View File

@ -3,9 +3,9 @@ package de.marhali.easyi18n.io.folder;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategyType;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationFile;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
@ -22,7 +22,7 @@ import java.util.List;
*/
public class SingleFolderStrategy extends FolderStrategy {
public SingleFolderStrategy(@NotNull SettingsState settings) {
public SingleFolderStrategy(@NotNull ProjectSettings 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.KeyPath;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@ -12,9 +14,9 @@ import java.util.Objects;
*/
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;
}
@ -45,10 +47,10 @@ public abstract class ParserStrategy {
if(file.getNamespace() != null) {
String moduleName = file.getNamespace();
TranslationNode moduleNode = data.getNode(KeyPath.of(moduleName));
TranslationNode moduleNode = data.getNode(new KeyPath(moduleName));
if(moduleNode == null) {
moduleNode = new TranslationNode(this.settings.isSortKeys());
moduleNode = new TranslationNode(this.settings.isSorting());
data.getRootNode().setChildren(moduleName, moduleNode);
}
@ -68,7 +70,7 @@ public abstract class ParserStrategy {
TranslationNode targetNode = data.getRootNode();
if(file.getNamespace() != null) {
targetNode = data.getNode(KeyPath.of(file.getNamespace()));
targetNode = data.getNode(new KeyPath(file.getNamespace()));
}
return Objects.requireNonNull(targetNode);

View File

@ -4,8 +4,8 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils;
@ -30,7 +30,7 @@ public class JsonMapper {
// Nested element - run recursively
read(locale, value.getAsJsonObject(), childNode);
} else {
Translation translation = childNode.getValue();
TranslationValue translation = childNode.getValue();
String content = entry.getValue().isJsonArray()
? JsonArrayMapper.read(value.getAsJsonArray())
@ -55,7 +55,7 @@ public class JsonMapper {
json.add(key, childJson);
}
} else {
Translation translation = childNode.getValue();
TranslationValue translation = childNode.getValue();
String content = translation.get(locale);
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.GsonBuilder;
import com.google.gson.JsonObject;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.parser.ParserStrategy;
import de.marhali.easyi18n.model.*;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Objects;
@ -23,7 +24,7 @@ public class JsonParserStrategy extends ParserStrategy {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
public JsonParserStrategy(@NotNull SettingsState settings) {
public JsonParserStrategy(@NotNull ProjectSettings settings) {
super(settings);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +1,37 @@
package de.marhali.easyi18n.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
/**
* Represents a full translation key with all sections.
* Implementations can use single section or variable section length variants.
* The respective layer (io, presentation) is responsible for using the correct mapping mechanism.
* Represents the absolute key path for a desired translation.
* The key could be based one or many sections.
* Classes implementing this structure need to take care on how to layer translations paths.
* @author marhali
*/
public class KeyPath extends ArrayList<String> {
public static final String DELIMITER = ".";
public KeyPath() {}
public static KeyPath of(@NotNull String... path) {
return new KeyPath(List.of(path));
public KeyPath(@Nullable String... path) {
super.addAll(List.of(path));
}
public KeyPath() {
super();
public KeyPath(@NotNull List<String> path) {
super(path);
}
public KeyPath(@NotNull KeyPath path, String... pathToAppend) {
public KeyPath(@NotNull KeyPath path, @Nullable String... pathToAppend) {
this(path);
this.addAll(List.of(pathToAppend));
super.addAll(List.of(pathToAppend));
}
public KeyPath(@NotNull Collection<? extends String> c) {
super(c);
}
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();
@Override
public String toString() {
// Just a simple array view (e.g. [first, second]) - use KeyPathConverter to properly convert a key path
return super.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;
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.
* This class contains only the translations for this unspecific element.
* Represents a translation with defined key and locale values.
*
* @author marhali
*/
public class Translation extends HashMap<String, String> {
public Translation() {
super();
public class Translation {
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();
super.put(locale, content);
/**
* @return Absolute key path
*/
public @NotNull KeyPath getKey() {
return key;
}
public Translation add(String locale, String content) {
super.put(locale, content);
return this;
/**
* @return values - nullable to indicate removal
*/
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
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
* @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);
if(node == null || !node.isLeaf()) {
@ -109,7 +109,7 @@ public class TranslationData {
* @param fullPath Absolute translation key path
* @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()) {
throw new IllegalArgumentException("Key path cannot be empty");
}

View File

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

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;
@ -7,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
* @author marhali
*/
public class TranslationCreate extends TranslationUpdate {
public TranslationCreate(@NotNull KeyedTranslation translation) {
public TranslationCreate(@NotNull Translation 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;
@ -7,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
* @author marhali
*/
public class TranslationDelete extends TranslationUpdate {
public TranslationDelete(@NotNull KeyedTranslation translation) {
public TranslationDelete(@NotNull Translation translation) {
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;
/**
@ -10,19 +11,19 @@ import org.jetbrains.annotations.Nullable;
*/
public class TranslationUpdate {
private final @Nullable KeyedTranslation origin;
private final @Nullable KeyedTranslation change;
private final @Nullable Translation origin;
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.change = change;
}
public @Nullable KeyedTranslation getOrigin() {
public @Nullable Translation getOrigin() {
return origin;
}
public @Nullable KeyedTranslation getChange() {
public @Nullable Translation getChange() {
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 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.listener.ReturnKeyListener;
import de.marhali.easyi18n.listener.DeleteKeyListener;
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.KeyPath;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationValue;
import de.marhali.easyi18n.renderer.TableRenderer;
import de.marhali.easyi18n.tabs.mapper.TableModelMapper;
import de.marhali.easyi18n.util.KeyPathConverter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -56,20 +61,20 @@ public class TableView implements BusListener {
return;
}
KeyPath fullPath = this.converter.split(String.valueOf(this.table.getValueAt(row, 0)));
Translation translation = InstanceManager.get(project).store().getData().getTranslation(fullPath);
KeyPath fullPath = this.converter.fromString(String.valueOf(this.table.getValueAt(row, 0)));
TranslationValue value = InstanceManager.get(project).store().getData().getTranslation(fullPath);
if (translation != null) {
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
if (value != null) {
new EditDialog(project, new Translation(fullPath, value)).showAndHandle();
}
}
private void deleteSelectedRows() {
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(
new TranslationDelete(new KeyedTranslation(fullPath, null))
new TranslationDelete(new Translation(fullPath, null))
);
}
}
@ -84,7 +89,7 @@ public class TableView implements BusListener {
@Override
public void onFocusKey(@NotNull KeyPath key) {
String concatKey = this.converter.concat(key);
String concatKey = this.converter.toString(key);
int row = -1;
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 de.marhali.easyi18n.InstanceManager;
import de.marhali.easyi18n.dialog.EditDialog;
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.action.treeview.CollapseTreeViewAction;
import de.marhali.easyi18n.action.treeview.ExpandTreeViewAction;
import de.marhali.easyi18n.dialog.EditDialog;
import de.marhali.easyi18n.listener.DeleteKeyListener;
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.service.SettingsService;
import de.marhali.easyi18n.settings.ProjectSettingsService;
import de.marhali.easyi18n.tabs.mapper.TreeModelMapper;
import de.marhali.easyi18n.util.TreeUtil;
@ -28,6 +32,8 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
/**
@ -80,7 +86,21 @@ public class TreeView implements BusListener {
@Override
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
@ -127,13 +147,13 @@ public class TreeView implements BusListener {
}
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;
}
new EditDialog(project, new KeyedTranslation(fullPath, translation)).showAndHandle();
new EditDialog(project, new Translation(fullPath, value)).showAndHandle();
}
private void deleteSelectedNodes() {
@ -147,7 +167,7 @@ public class TreeView implements BusListener {
KeyPath fullPath = TreeUtil.getFullPath(path);
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;
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.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.NotNull;
@ -50,10 +55,10 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
List<KeyPath> matches = new ArrayList<>();
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);
} else {
for(String content : this.data.getTranslation(key).values()) {
for(String content : this.data.getTranslation(key).getLocaleContents()) {
if(content.toLowerCase().contains(query)) {
matches.add(key);
}
@ -74,7 +79,7 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
List<KeyPath> matches = new ArrayList<>();
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);
}
}
@ -117,25 +122,25 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
KeyPath key = this.fullKeys.get(rowIndex);
if(columnIndex == 0) { // Keys
return this.converter.concat(key);
return this.converter.toString(key);
}
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
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
KeyPath key = this.fullKeys.get(rowIndex);
Translation translation = this.data.getTranslation(key);
TranslationValue translation = this.data.getTranslation(key);
if(translation == null) { // Unknown cell
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
if(columnIndex > 0) {
@ -146,8 +151,8 @@ public class TableModelMapper implements TableModel, SearchQueryListener, Filter
}
}
TranslationUpdate update = new TranslationUpdate(new KeyedTranslation(key, translation),
new KeyedTranslation(newKey, translation));
TranslationUpdate update = new TranslationUpdate(new Translation(key, translation),
new Translation(newKey, translation));
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.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.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 org.jetbrains.annotations.NotNull;
@ -24,13 +29,13 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
private final TranslationData data;
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);
this.data = data;
this.converter = new KeyPathConverter(state.isNestedKeys());
this.converter = new KeyPathConverter(state);
this.state = state;
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
@ -41,7 +46,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
@Override
public void onSearchQuery(@Nullable String query) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
TranslationData shadow = new TranslationData(this.state.isSortKeys());
TranslationData shadow = new TranslationData(this.state.isSorting());
if(query == null) { // Reset
this.generateNodes(rootNode, this.data.getRootNode());
@ -52,15 +57,15 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
query = query.toLowerCase();
for(KeyPath currentKey : this.data.getFullKeys()) {
Translation translation = this.data.getTranslation(currentKey);
String loweredKey = this.converter.concat(currentKey).toLowerCase();
TranslationValue translation = this.data.getTranslation(currentKey);
String loweredKey = this.converter.toString(currentKey).toLowerCase();
if(query.contains(loweredKey) || loweredKey.contains(query)) {
shadow.setTranslation(currentKey, translation);
continue;
}
for(String currentContent : translation.values()) {
for(String currentContent : translation.getLocaleContents()) {
if(currentContent.toLowerCase().contains(query)) {
shadow.setTranslation(currentKey, translation);
break;
@ -75,7 +80,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
@Override
public void onFilterMissingTranslations(boolean filter) {
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
TranslationData shadow = new TranslationData(this.state.isSortKeys());
TranslationData shadow = new TranslationData(this.state.isSorting());
if(!filter) { // Reset
this.generateNodes(rootNode, this.data.getRootNode());
@ -84,9 +89,9 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
}
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);
}
}
@ -124,7 +129,7 @@ public class TreeModelMapper extends DefaultTreeModel implements SearchQueryList
} else {
String previewLocale = this.state.getPreviewLocale();
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);
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 de.marhali.easyi18n.action.SettingsAction;
import de.marhali.easyi18n.io.IOHandler;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.settings.ProjectSettings;
import org.jetbrains.annotations.NotNull;
import java.text.MessageFormat;
@ -17,11 +17,11 @@ import java.util.ResourceBundle;
*/
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");
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);
}

View File

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

View File

@ -1,5 +1,27 @@
<idea-plugin>
<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>
</idea-plugin>

View File

@ -1,6 +1,45 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="JavaScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<lang.foldingBuilder language="TypeScript" implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<psi.referenceContributor
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>
</idea-plugin>

View File

@ -1,11 +1,27 @@
<idea-plugin>
<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"
implementationClass="de.marhali.easyi18n.editor.kotlin.KotlinKeyCompletionContributor" />
<psi.referenceContributor
language="kotlin"
implementation="de.marhali.easyi18n.assistance.reference.KtKeyReferenceContributor"
/>
<psi.referenceContributor language="kotlin"
implementation="de.marhali.easyi18n.editor.kotlin.KotlinKeyReferenceContributor" />
<lang.documentationProvider
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>
</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>
<extensions defaultExtensionNs="com.intellij">
<lang.foldingBuilder language="HTML"
implementationClass="de.marhali.easyi18n.editor.generic.GenericFoldingBuilder" />
<lang.foldingBuilder language="VueJS" implementationClass="de.marhali.easyi18n.assistance.folding.JsFoldingBuilder" />
<lang.documentationProvider
language="VueJS"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
<lang.documentationProvider
language="Vue"
implementationClass="de.marhali.easyi18n.assistance.documentation.CommonDocumentationProvider"
/>
</extensions>
</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-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-php.xml">com.jetbrains.php</depends>
<actions>
<action
@ -24,18 +25,15 @@
</actions>
<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"
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyCompletionContributor" />
<annotator language=""
implementationClass="de.marhali.easyi18n.editor.generic.GenericKeyAnnotator" />
<psi.referenceContributor
implementation="de.marhali.easyi18n.editor.generic.GenericKeyReferenceContributor" />
<projectConfigurable parentId="tools" instance="de.marhali.easyi18n.settings.ProjectSettingsConfigurable"
id="de.marhali.easyi18n.service.AppSettingsConfigurable"
displayName="Easy I18n" nonDefaultProject="true"/>
<notificationGroup displayType="BALLOON" id="Easy I18n Notification Group"/>

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,3 +1,4 @@
documentation=EasyI18n Translation
view.tree.title=Tree View
view.tree.collapse=Collapse Tree
view.tree.expand=Expand Tree
@ -10,21 +11,51 @@ action.reload=Reload From Disk
action.settings=Settings
action.search=Search...
action.delete=Delete
action.extract=Extract translation
translation.key=Key
translation.locales=Locales
settings.path.title=Locales Directory
settings.path.text=Locales directory
settings.strategy.title=Translation file structure
settings.strategy.folder=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale
settings.strategy.folder.tooltip=What is the folder structure of your translation files?
settings.strategy.parser=JSON;JSON5;YAML;YML;Properties;ARB
settings.strategy.parser.tooltip=Which file parser should be used to process your translation files?
settings.strategy.file-pattern.tooltip=Defines a wildcard matcher to filter relevant translation files. For example *.json, *.???.
settings.preview=Preview locale
settings.path.prefix=Path prefix
settings.keys.sort=Sort translation keys alphabetically
settings.keys.nested=Escape delimiter character within a section layer.
settings.editor.assistance=I18n key completion, annotation and reference inside editor
# Settings
settings.hint.text=Project-specific configuration. For an easy start, you can use one of the existing presets.
settings.hint.action=Fore more information, see the documentation
settings.preset.title=Preset
settings.preset.tooltip=Choose a configuration template that best fits your project. After that you can make further changes.
# Resource Configuration
settings.resource.title=Resource Configuration
settings.resource.path.window=Locales Directory
settings.resource.path.title=Locales directory
settings.resource.path.tooltip=Define the folder which contains all translation files. For nested folders, use the top folder.
settings.resource.strategy=File structure
settings.resource.folder.items=Single Directory;Modularized: Locale / Namespace;Modularized: Namespace / Locale
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\
Config: {0} => {1} ({2}) \n\
Path: {3} \n\

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