commit
e4410c264c
@ -3,6 +3,13 @@
|
||||
# easy-i18n Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Support for YAML locale files. Thanks to @sunarya-thito
|
||||
- Optional path-prefix for translations
|
||||
|
||||
### Changed
|
||||
- Optimized i18n key completion
|
||||
|
||||
## [1.4.1]
|
||||
### Added
|
||||
- Support for IntelliJ 2021.2
|
||||
|
17
example/yaml/locale-de.yml
Normal file
17
example/yaml/locale-de.yml
Normal file
@ -0,0 +1,17 @@
|
||||
alpha:
|
||||
spacing: ' führendes Leerzeichen'
|
||||
first: Beispiel Übersetzung
|
||||
beta:
|
||||
title: Titel
|
||||
nested:
|
||||
title: Ein verschachtelter Titel
|
||||
gamma:
|
||||
array:
|
||||
escaped:
|
||||
- Erstes;Element
|
||||
- Zweites Element
|
||||
- Drittes;Element
|
||||
simple:
|
||||
- Erstes Element
|
||||
- Zweites Element
|
||||
title: Gamma Titel
|
17
example/yaml/locale-en.yml
Normal file
17
example/yaml/locale-en.yml
Normal file
@ -0,0 +1,17 @@
|
||||
alpha:
|
||||
spacing: ' leading space'
|
||||
first: Example Translation
|
||||
beta:
|
||||
title: Title
|
||||
nested:
|
||||
title: some nested title
|
||||
gamma:
|
||||
array:
|
||||
escaped:
|
||||
- first;element
|
||||
- second element
|
||||
- third;element
|
||||
simple:
|
||||
- first element
|
||||
- second element
|
||||
title: gamma title
|
@ -26,7 +26,8 @@ public class SettingsDialog {
|
||||
|
||||
private TextFieldWithBrowseButton pathText;
|
||||
private JBTextField filePatternText;
|
||||
private JBTextField previewText;
|
||||
private JBTextField previewLocaleText;
|
||||
private JBTextField pathPrefixText;
|
||||
private JBCheckBox codeAssistanceCheckbox;
|
||||
|
||||
public SettingsDialog(Project project) {
|
||||
@ -37,22 +38,25 @@ public class SettingsDialog {
|
||||
String localesPath = SettingsService.getInstance(project).getState().getLocalesPath();
|
||||
String filePattern = SettingsService.getInstance(project).getState().getFilePattern();
|
||||
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
|
||||
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
|
||||
boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance();
|
||||
|
||||
if(prepare(localesPath, filePattern, previewLocale, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
|
||||
if(prepare(localesPath, filePattern, previewLocale, pathPrefix, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes
|
||||
SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText());
|
||||
SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText());
|
||||
SettingsService.getInstance(project).getState().setPreviewLocale(previewText.getText());
|
||||
SettingsService.getInstance(project).getState().setPreviewLocale(previewLocaleText.getText());
|
||||
SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected());
|
||||
SettingsService.getInstance(project).getState().setPathPrefix(pathPrefixText.getText());
|
||||
|
||||
// Reload instance
|
||||
DataStore.getInstance(project).reloadFromDisk();
|
||||
}
|
||||
}
|
||||
|
||||
private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, boolean codeAssistance) {
|
||||
private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, String pathPrefix, boolean codeAssistance) {
|
||||
JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2));
|
||||
|
||||
/* path */
|
||||
JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text"));
|
||||
pathText = new TextFieldWithBrowseButton(new JTextField(localesPath));
|
||||
|
||||
@ -63,20 +67,29 @@ public class SettingsDialog {
|
||||
rootPanel.add(pathLabel);
|
||||
rootPanel.add(pathText);
|
||||
|
||||
/* file pattern */
|
||||
JBLabel filePatternLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.file-pattern"));
|
||||
filePatternText = new JBTextField(filePattern);
|
||||
|
||||
rootPanel.add(filePatternLabel);
|
||||
rootPanel.add(filePatternText);
|
||||
|
||||
/* preview locale */
|
||||
JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview"));
|
||||
previewLocaleText = new JBTextField(previewLocale);
|
||||
previewLocaleLabel.setLabelFor(previewLocaleText);
|
||||
|
||||
JBLabel previewLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview"));
|
||||
previewText = new JBTextField(previewLocale);
|
||||
previewLabel.setLabelFor(previewText);
|
||||
rootPanel.add(previewLocaleLabel);
|
||||
rootPanel.add(previewLocaleText);
|
||||
|
||||
rootPanel.add(previewLabel);
|
||||
rootPanel.add(previewText);
|
||||
/* path prefix */
|
||||
JBLabel pathPrefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.prefix"));
|
||||
pathPrefixText = new JBTextField(pathPrefix);
|
||||
|
||||
rootPanel.add(pathPrefixLabel);
|
||||
rootPanel.add(pathPrefixText);
|
||||
|
||||
/* code assistance */
|
||||
codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance"));
|
||||
codeAssistanceCheckbox.setSelected(codeAssistance);
|
||||
|
||||
|
@ -29,7 +29,17 @@ public class KeyAnnotator {
|
||||
}
|
||||
|
||||
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
|
||||
LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(key);
|
||||
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
|
||||
|
||||
String searchKey = key.length() >= pathPrefix.length()
|
||||
? key.substring(pathPrefix.length())
|
||||
: key;
|
||||
|
||||
if(searchKey.startsWith(".")) {
|
||||
searchKey = searchKey.substring(1);
|
||||
}
|
||||
|
||||
LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(searchKey);
|
||||
|
||||
if(node == null) { // Unknown translation. Just ignore it
|
||||
return;
|
||||
|
@ -1,20 +1,16 @@
|
||||
package de.marhali.easyi18n.editor;
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionParameters;
|
||||
import com.intellij.codeInsight.completion.CompletionProvider;
|
||||
import com.intellij.codeInsight.completion.CompletionResultSet;
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.util.ProcessingContext;
|
||||
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
import de.marhali.easyi18n.service.DataStore;
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
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.model.*;
|
||||
import de.marhali.easyi18n.service.*;
|
||||
import de.marhali.easyi18n.util.TranslationsUtil;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* I18n translation key completion provider.
|
||||
@ -33,38 +29,58 @@ public class KeyCompletionProvider extends CompletionProvider<CompletionParamete
|
||||
return;
|
||||
}
|
||||
|
||||
DataStore store = DataStore.getInstance(project);
|
||||
String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale();
|
||||
String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix();
|
||||
|
||||
String query = result.getPrefixMatcher().getPrefix();
|
||||
List<String> sections = TranslationsUtil.getSections(query);
|
||||
String lastSection = null;
|
||||
String path = result.getPrefixMatcher().getPrefix();
|
||||
|
||||
if(!sections.isEmpty() && !query.endsWith(".")) {
|
||||
lastSection = sections.remove(sections.size() - 1);
|
||||
if(pathPrefix == null) {
|
||||
pathPrefix = "";
|
||||
}
|
||||
|
||||
String path = TranslationsUtil.sectionsToFullPath(sections);
|
||||
if(path.startsWith(pathPrefix)) {
|
||||
path = path.substring(pathPrefix.length());
|
||||
|
||||
LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes()
|
||||
: DataStore.getInstance(project).getTranslations().getNode(path);
|
||||
if(path.startsWith(".")) { // Remove leading dot
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
if(node == null) { // Unknown translation
|
||||
return;
|
||||
} else {
|
||||
path = ""; // Show suggestions for root view
|
||||
}
|
||||
|
||||
for(LocalizedNode children : node.getChildren()) {
|
||||
if(lastSection == null || children.getKey().startsWith(lastSection)) {
|
||||
// Construct full key path / Fore nested objects add '.' to indicate deeper level
|
||||
String fullKey = (path.isEmpty() ? children.getKey() : path + "." + children.getKey()) + (children.isLeaf() ? "" : ".");
|
||||
if(pathPrefix.length() > 0 && !pathPrefix.endsWith(".")) {
|
||||
pathPrefix += ".";
|
||||
}
|
||||
|
||||
result.addElement(LookupElementBuilder.create(fullKey)
|
||||
.appendTailText(getTailText(children, previewLocale), true));
|
||||
List<String> fullKeys = store.getTranslations().getFullKeys();
|
||||
|
||||
int sections = path.split("\\.").length;
|
||||
int maxSectionForwardLookup = 5;
|
||||
|
||||
for(String key : fullKeys) {
|
||||
// Path matches
|
||||
if(key.startsWith(path)) {
|
||||
String[] keySections = key.split("\\.");
|
||||
|
||||
if(keySections.length > sections + maxSectionForwardLookup) { // Key is too deep nested
|
||||
String shrinkKey = TranslationsUtil.sectionsToFullPath(Arrays.asList(
|
||||
Arrays.copyOf(keySections, sections + maxSectionForwardLookup)));
|
||||
|
||||
result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey)
|
||||
.appendTailText(" I18n([])", true));
|
||||
|
||||
} else {
|
||||
LocalizedNode node = store.getTranslations().getNode(key);
|
||||
String translation = node != null ? node.getValue().get(previewLocale) : null;
|
||||
|
||||
result.addElement(LookupElementBuilder.create(pathPrefix + key)
|
||||
.withIcon(AllIcons.Actions.PreserveCaseHover)
|
||||
.appendTailText(" I18n(" + previewLocale + ": " + translation + ")", true)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTailText(LocalizedNode node, String previewLocale) {
|
||||
return !node.isLeaf() ? " I18n([])"
|
||||
: " I18n(" + previewLocale + ": " + node.getValue().get(previewLocale) + ")";
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ 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.PsiLiteralValue;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.xml.*;
|
||||
import de.marhali.easyi18n.editor.KeyCompletionProvider;
|
||||
|
||||
/**
|
||||
@ -13,6 +14,10 @@ import de.marhali.easyi18n.editor.KeyCompletionProvider;
|
||||
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());
|
||||
}
|
||||
|
@ -0,0 +1,125 @@
|
||||
package de.marhali.easyi18n.io.implementation;
|
||||
|
||||
import com.intellij.openapi.application.*;
|
||||
import com.intellij.openapi.project.*;
|
||||
import com.intellij.openapi.vfs.*;
|
||||
|
||||
import de.marhali.easyi18n.io.*;
|
||||
import de.marhali.easyi18n.model.*;
|
||||
import de.marhali.easyi18n.util.*;
|
||||
import de.marhali.easyi18n.util.array.YamlArrayUtil;
|
||||
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import thito.nodeflow.config.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
|
||||
public class YamlTranslatorIO implements TranslatorIO {
|
||||
@Override
|
||||
public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer<Translations> callback) {
|
||||
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
|
||||
|
||||
ApplicationManager.getApplication().runReadAction(() -> {
|
||||
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath));
|
||||
|
||||
if(directory == null || directory.getChildren() == null) {
|
||||
throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")");
|
||||
}
|
||||
|
||||
VirtualFile[] files = directory.getChildren();
|
||||
|
||||
List<String> locales = new ArrayList<>();
|
||||
LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>());
|
||||
|
||||
try {
|
||||
for(VirtualFile file : files) {
|
||||
|
||||
if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern
|
||||
continue;
|
||||
}
|
||||
|
||||
locales.add(file.getNameWithoutExtension());
|
||||
|
||||
try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
Section section = Section.parseToMap(reader);
|
||||
load(file.getNameWithoutExtension(), nodes, section);
|
||||
}
|
||||
}
|
||||
|
||||
callback.accept(new Translations(locales, nodes));
|
||||
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
callback.accept(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void load(String locale, LocalizedNode node, Section section) {
|
||||
if (section instanceof MapSection) {
|
||||
for (String key : section.getKeys()) {
|
||||
LocalizedNode child = node.getChildren(key);
|
||||
if (child == null) {
|
||||
node.addChildren(child = new LocalizedNode(key, new ArrayList<>()));
|
||||
}
|
||||
LocalizedNode finalChild = child;
|
||||
MapSection map = section.getMap(key).orElse(null);
|
||||
if (map != null) {
|
||||
load(locale, finalChild, map);
|
||||
} else {
|
||||
|
||||
if(section.isList(key) && section.getList(key).isPresent()) {
|
||||
child.getValue().put(locale, YamlArrayUtil.read(section.getList(key).get()));
|
||||
} else {
|
||||
String value = section.getString(key).orElse(null);
|
||||
if (value != null) {
|
||||
child.getValue().put(locale, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void save(LocalizedNode node, String locale, Section section, String path) {
|
||||
if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) {
|
||||
String value = node.getValue().get(locale);
|
||||
if (value != null) {
|
||||
section.set(path, YamlArrayUtil.isArray(value) ? YamlArrayUtil.write(value) : value);
|
||||
}
|
||||
} else {
|
||||
for (LocalizedNode child : node.getChildren()) {
|
||||
save(child, locale, section, path == null ? child.getKey() : path + "." + child.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer<Boolean> callback) {
|
||||
ApplicationManager.getApplication().runWriteAction(() -> {
|
||||
try {
|
||||
for(String locale : translations.getLocales()) {
|
||||
Section section = new MapSection();
|
||||
|
||||
save(translations.getNodes(), locale, section, null);
|
||||
|
||||
String fullPath = directoryPath + "/" + locale + ".yml";
|
||||
VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath));
|
||||
|
||||
file.setBinaryContent(Section.toString(section).getBytes(file.getCharset()));
|
||||
}
|
||||
|
||||
// Successfully saved
|
||||
callback.accept(true);
|
||||
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
callback.accept(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ public class SettingsState {
|
||||
private String localesPath;
|
||||
private String filePattern;
|
||||
private String previewLocale;
|
||||
private String pathPrefix;
|
||||
private Boolean codeAssistance;
|
||||
|
||||
public SettingsState() {}
|
||||
@ -51,4 +52,12 @@ public class SettingsState {
|
||||
public void setCodeAssistance(boolean codeAssistance) {
|
||||
this.codeAssistance = codeAssistance;
|
||||
}
|
||||
|
||||
public void setPathPrefix(String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
public String getPathPrefix() {
|
||||
return pathPrefix;
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import com.intellij.openapi.components.PersistentStateComponent;
|
||||
import com.intellij.openapi.components.State;
|
||||
import com.intellij.openapi.project.Project;
|
||||
|
||||
import com.intellij.util.xmlb.XmlSerializerUtil;
|
||||
import de.marhali.easyi18n.model.SettingsState;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -3,9 +3,7 @@ package de.marhali.easyi18n.util;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import de.marhali.easyi18n.io.implementation.JsonTranslatorIO;
|
||||
import de.marhali.easyi18n.io.implementation.ModularizedJsonTranslatorIO;
|
||||
import de.marhali.easyi18n.io.implementation.PropertiesTranslatorIO;
|
||||
import de.marhali.easyi18n.io.implementation.*;
|
||||
import de.marhali.easyi18n.io.TranslatorIO;
|
||||
|
||||
import de.marhali.easyi18n.service.SettingsService;
|
||||
@ -50,7 +48,8 @@ public class IOUtil {
|
||||
|
||||
case "properties":
|
||||
return new PropertiesTranslatorIO();
|
||||
|
||||
case "yml":
|
||||
return new YamlTranslatorIO();
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported i18n locale file format: " +
|
||||
any.get().getFileType().getDefaultExtension());
|
||||
|
@ -1,51 +0,0 @@
|
||||
package de.marhali.easyi18n.util;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility methods to read and write json arrays.
|
||||
* @author marhali
|
||||
*/
|
||||
public class JsonArrayUtil {
|
||||
|
||||
public static String ARRAY_PREFIX = "!arr[";
|
||||
public static String ARRAY_SUFFIX = "]";
|
||||
public static char ARRAY_DELIMITER = ';';
|
||||
|
||||
public static String read(JsonArray array) {
|
||||
StringBuilder builder = new StringBuilder(ARRAY_PREFIX);
|
||||
|
||||
for(int i = 0; i < array.size(); i++) {
|
||||
if(i > 0) {
|
||||
builder.append(ARRAY_DELIMITER);
|
||||
}
|
||||
|
||||
String value = array.get(i).getAsString().replace(";", "\\;");
|
||||
builder.append(StringUtil.escapeControls(value, true));
|
||||
}
|
||||
|
||||
builder.append(ARRAY_SUFFIX);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static JsonArray write(String concat) {
|
||||
concat = concat.substring(ARRAY_PREFIX.length(), concat.length() - ARRAY_SUFFIX.length());
|
||||
String regex = "(?<!\\\\)" + Pattern.quote(String.valueOf(ARRAY_DELIMITER));
|
||||
|
||||
JsonArray array = new JsonArray();
|
||||
|
||||
for(String element : concat.split(regex)) {
|
||||
element = element.replace("\\" + ARRAY_DELIMITER, String.valueOf(ARRAY_DELIMITER));
|
||||
array.add(StringEscapeUtils.unescapeJava(element));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static boolean isArray(String concat) {
|
||||
return concat != null && concat.startsWith(ARRAY_PREFIX) && concat.endsWith(ARRAY_SUFFIX);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import com.google.gson.JsonPrimitive;
|
||||
|
||||
import de.marhali.easyi18n.model.LocalizedNode;
|
||||
|
||||
import de.marhali.easyi18n.util.array.JsonArrayUtil;
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
58
src/main/java/de/marhali/easyi18n/util/array/ArrayUtil.java
Normal file
58
src/main/java/de/marhali/easyi18n/util/array/ArrayUtil.java
Normal file
@ -0,0 +1,58 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
|
||||
import de.marhali.easyi18n.util.StringUtil;
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility methods for simple array support.
|
||||
* @author marhali
|
||||
*/
|
||||
public abstract class ArrayUtil {
|
||||
|
||||
static final String PREFIX = "!arr[";
|
||||
static final String SUFFIX = "]";
|
||||
static final char DELIMITER = ';';
|
||||
|
||||
static final String SPLITERATOR_REGEX =
|
||||
MessageFormat.format("(?<!\\\\){0}", Pattern.quote(String.valueOf(DELIMITER)));
|
||||
|
||||
static <T> String read(Iterator<T> elements, Function<T, String> stringFactory) {
|
||||
StringBuilder builder = new StringBuilder(PREFIX);
|
||||
|
||||
int i = 0;
|
||||
while(elements.hasNext()) {
|
||||
if(i > 0) {
|
||||
builder.append(DELIMITER);
|
||||
}
|
||||
|
||||
String value = stringFactory.apply(elements.next());
|
||||
|
||||
builder.append(StringUtil.escapeControls(
|
||||
value.replace(String.valueOf(DELIMITER), "\\" + DELIMITER), true));
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
builder.append(SUFFIX);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static void write(String concat, Consumer<String> writeElement) {
|
||||
concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length());
|
||||
|
||||
for(String element : concat.split(SPLITERATOR_REGEX)) {
|
||||
element = element.replace("\\" + DELIMITER, String.valueOf(DELIMITER));
|
||||
writeElement.accept(StringEscapeUtils.unescapeJava(element));
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isArray(String concat) {
|
||||
return concat != null && concat.startsWith(PREFIX) && concat.endsWith(SUFFIX);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
/**
|
||||
* Utility methods to read and write json arrays.
|
||||
* @author marhali
|
||||
*/
|
||||
public class JsonArrayUtil extends ArrayUtil {
|
||||
public static String read(JsonArray array) {
|
||||
return read(array.iterator(), JsonElement::getAsString);
|
||||
}
|
||||
|
||||
public static JsonArray write(String concat) {
|
||||
JsonArray array = new JsonArray();
|
||||
write(concat, array::add);
|
||||
return array;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package de.marhali.easyi18n.util.array;
|
||||
|
||||
import thito.nodeflow.config.ListSection;
|
||||
|
||||
/**
|
||||
* Utility methods to read and write yaml lists.
|
||||
* @author marhali
|
||||
*/
|
||||
public class YamlArrayUtil extends ArrayUtil {
|
||||
|
||||
public static String read(ListSection list) {
|
||||
return read(list.iterator(), Object::toString);
|
||||
}
|
||||
|
||||
public static ListSection write(String concat) {
|
||||
ListSection list = new ListSection();
|
||||
write(concat, list::add);
|
||||
return list;
|
||||
}
|
||||
}
|
227
src/main/java/thito/nodeflow/config/ListSection.java
Normal file
227
src/main/java/thito/nodeflow/config/ListSection.java
Normal file
@ -0,0 +1,227 @@
|
||||
package thito.nodeflow.config;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
public class ListSection extends ArrayList<Object> implements Section {
|
||||
private Section parent;
|
||||
private String name;
|
||||
|
||||
public ListSection() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ListSection(Collection<?> c) {
|
||||
super();
|
||||
addAll(c);
|
||||
}
|
||||
|
||||
protected void setParent(Section parent, String name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
if (name == null && parent != null) {
|
||||
if (parent instanceof ListSection) {
|
||||
return String.valueOf(((ListSection) parent).indexOf(this));
|
||||
}
|
||||
if (parent instanceof MapSection) {
|
||||
return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this))
|
||||
.findAny().map(Map.Entry::getKey).orElse(null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public Optional<Object> getObject(int index) {
|
||||
return index >= 0 && index < size() ? Optional.ofNullable(get(index)) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Section getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeys() {
|
||||
return IntStream.range(0, size()).mapToObj(String::valueOf).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInScope(String key) {
|
||||
try {
|
||||
return Optional.ofNullable(get(Integer.parseInt(key)));
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInScope(String key, Object value) {
|
||||
try {
|
||||
set(Integer.parseInt(key), value);
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object set(int index, Object element) {
|
||||
element = Section.wrap(element);
|
||||
if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element);
|
||||
return super.set(index, element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Object o) {
|
||||
o = Section.wrap(o);
|
||||
if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o);
|
||||
return super.add(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, Object element) {
|
||||
element = Section.wrap(element);
|
||||
if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element);
|
||||
super.add(index, element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<?> c) {
|
||||
c.forEach(o -> add(o));
|
||||
return !c.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, Collection<?> c) {
|
||||
List<Object> wrapped = new ArrayList<>();
|
||||
c.forEach(obj -> {
|
||||
Object o = Section.wrap(obj);
|
||||
if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o);
|
||||
wrapped.add(o);
|
||||
});
|
||||
return super.addAll(index, wrapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Section.toString(this);
|
||||
}
|
||||
|
||||
public <T extends Enum<T>> Optional<T> getEnum(int index, Class<T> clz) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Enum.valueOf(clz, String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<String> getString(int index) {
|
||||
return getObject(index).map(String::valueOf);
|
||||
}
|
||||
|
||||
public Optional<Integer> getInteger(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Double> getDouble(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Double.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Long> getLong(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Float> getFloat(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Float.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Short> getShort(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Short.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Byte> getByte(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
try {
|
||||
return Byte.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Character> getCharacter(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
String text = String.valueOf(o);
|
||||
return text.isEmpty() ? null : text.charAt(0);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Boolean> getBoolean(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
String text = String.valueOf(o);
|
||||
return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<MapSection> getMap(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
if (o instanceof Map) {
|
||||
if (o instanceof MapSection) return (MapSection) o;
|
||||
MapSection mapSection = new MapSection((Map<?, ?>) o);
|
||||
mapSection.setParent(this, null);
|
||||
return mapSection;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<ListSection> getList(int index) {
|
||||
return getObject(index).map(o -> {
|
||||
if (o instanceof List) {
|
||||
if (o instanceof ListSection) {
|
||||
return (ListSection) o;
|
||||
}
|
||||
ListSection list = new ListSection((List<?>) o);
|
||||
list.setParent(this, null);
|
||||
return list;
|
||||
}
|
||||
ListSection list = new ListSection(Collections.singleton(o));
|
||||
list.setParent(this, null);
|
||||
return list;
|
||||
});
|
||||
}
|
||||
}
|
73
src/main/java/thito/nodeflow/config/MapSection.java
Normal file
73
src/main/java/thito/nodeflow/config/MapSection.java
Normal file
@ -0,0 +1,73 @@
|
||||
package thito.nodeflow.config;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MapSection extends HashMap<String, Object> implements Section {
|
||||
private Section parent;
|
||||
private String name;
|
||||
|
||||
public MapSection() {
|
||||
super();
|
||||
}
|
||||
|
||||
public MapSection(Map<?, ?> m) {
|
||||
super();
|
||||
m.forEach((key, value) -> put(String.valueOf(key), value));
|
||||
}
|
||||
|
||||
protected void setParent(Section parent, String name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
if (name == null && parent != null) {
|
||||
if (parent instanceof ListSection) {
|
||||
return String.valueOf(((ListSection) parent).indexOf(this));
|
||||
}
|
||||
if (parent instanceof MapSection) {
|
||||
return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this))
|
||||
.findAny().map(Entry::getKey).orElse(null);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Section getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInScope(String key, Object value) {
|
||||
put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
value = Section.wrap(value);
|
||||
if (value instanceof Section) value = Section.wrapParent(this, key, (Section) value);
|
||||
return super.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> m) {
|
||||
m.forEach(this::put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeys() {
|
||||
return keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<?> getInScope(String key) {
|
||||
return Optional.ofNullable(get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Section.toString(this);
|
||||
}
|
||||
}
|
221
src/main/java/thito/nodeflow/config/Section.java
Normal file
221
src/main/java/thito/nodeflow/config/Section.java
Normal file
@ -0,0 +1,221 @@
|
||||
package thito.nodeflow.config;
|
||||
|
||||
import org.yaml.snakeyaml.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
|
||||
public interface Section {
|
||||
String SEPARATOR = ".";
|
||||
static Object wrap(Object o) {
|
||||
if (o instanceof Section) return o;
|
||||
if (o instanceof List) {
|
||||
return new ListSection((List<?>) o);
|
||||
}
|
||||
if (o instanceof Map) {
|
||||
return new MapSection((Map<?, ?>) o);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static String getName(String path) {
|
||||
String[] split = path.split(Pattern.quote(SEPARATOR));
|
||||
return split[split.length - 1];
|
||||
}
|
||||
static Section wrapParent(Section parent, String name, Section current) {
|
||||
if (current.getParent() != null && current.getParent() != parent) {
|
||||
if (current instanceof MapSection) {
|
||||
MapSection mapSection = new MapSection((MapSection) current);
|
||||
mapSection.setParent(parent, name);
|
||||
return mapSection;
|
||||
}
|
||||
if (current instanceof ListSection) {
|
||||
ListSection objects = new ListSection((ListSection) current);
|
||||
objects.setParent(parent, name);
|
||||
return objects;
|
||||
}
|
||||
} else {
|
||||
if (current instanceof MapSection) {
|
||||
((MapSection) current).setParent(parent, name);
|
||||
}
|
||||
if (current instanceof ListSection) {
|
||||
((ListSection) current).setParent(parent, name);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
static String toString(Section section) {
|
||||
DumperOptions options = new DumperOptions();
|
||||
options.setIndent(4);
|
||||
options.setAllowUnicode(true);
|
||||
options.setPrettyFlow(true);
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
Yaml yaml = new Yaml(options);
|
||||
return yaml.dumpAsMap(section);
|
||||
}
|
||||
static MapSection parseToMap(Reader reader) {
|
||||
Yaml yaml = new Yaml();
|
||||
return new MapSection(yaml.loadAs(reader, Map.class));
|
||||
}
|
||||
Set<String> getKeys();
|
||||
default Set<String> getPaths() {
|
||||
Set<String> paths = new HashSet<>();
|
||||
for (String k : getKeys()) {
|
||||
Object lookup = getInScope(k).orElse(null);
|
||||
if (lookup instanceof Section) {
|
||||
for (String p : ((Section) lookup).getPaths()) {
|
||||
paths.add(k + "." + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
String getName();
|
||||
default String getPath() {
|
||||
StringBuilder path = new StringBuilder(getName());
|
||||
Section parent;
|
||||
while ((parent = getParent()) != null) {
|
||||
path.insert(0, parent.getName() + SEPARATOR);
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
Section getParent();
|
||||
Optional<?> getInScope(String key);
|
||||
void setInScope(String key, Object value);
|
||||
default void set(String path, Object value) {
|
||||
String[] paths = path.split(Pattern.quote(SEPARATOR));
|
||||
Object lookup = this;
|
||||
for (int i = 0; i < paths.length - 1; i++) {
|
||||
Section oldLookup = (Section) lookup;
|
||||
lookup = oldLookup.getInScope(paths[i]).orElse(null);
|
||||
if (!(lookup instanceof Section)) {
|
||||
oldLookup.setInScope(paths[i], lookup = new MapSection());
|
||||
}
|
||||
}
|
||||
if (paths.length > 0) {
|
||||
((Section) lookup).setInScope(paths[paths.length - 1], value);
|
||||
}
|
||||
}
|
||||
default Optional<?> getObject(String path) {
|
||||
String[] paths = path.split(Pattern.quote(SEPARATOR));
|
||||
Object lookup = this;
|
||||
for (String s : paths) {
|
||||
if (lookup instanceof Section) {
|
||||
lookup = ((Section) lookup).getInScope(s).orElse(null);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(lookup);
|
||||
}
|
||||
default <T extends Enum<T>> Optional<T> getEnum(String path, Class<T> clz) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Enum.valueOf(clz, String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<String> getString(String path) {
|
||||
return getObject(path).map(String::valueOf);
|
||||
}
|
||||
default Optional<Integer> getInteger(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Double> getDouble(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Double.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Long> getLong(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Float> getFloat(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Float.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Short> getShort(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Short.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Byte> getByte(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
try {
|
||||
return Byte.valueOf(String.valueOf(o));
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
default Optional<Character> getCharacter(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
String text = String.valueOf(o);
|
||||
return text.isEmpty() ? null : text.charAt(0);
|
||||
});
|
||||
}
|
||||
default Optional<Boolean> getBoolean(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
String text = String.valueOf(o);
|
||||
return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null;
|
||||
});
|
||||
}
|
||||
default Optional<MapSection> getMap(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
if (o instanceof Map) {
|
||||
if (o instanceof MapSection) return (MapSection) o;
|
||||
MapSection mapSection = new MapSection((Map<?, ?>) o);
|
||||
mapSection.setParent(this, Section.getName(path));
|
||||
return mapSection;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
default boolean isList(String path) {
|
||||
Optional<?> o = getObject(path);
|
||||
return o.isPresent() && o.get() instanceof List;
|
||||
}
|
||||
|
||||
default Optional<ListSection> getList(String path) {
|
||||
return getObject(path).map(o -> {
|
||||
if (o instanceof List) {
|
||||
if (o instanceof ListSection) {
|
||||
return (ListSection) o;
|
||||
}
|
||||
ListSection list = new ListSection((List<?>) o);
|
||||
list.setParent(this, Section.getName(path));
|
||||
return list;
|
||||
}
|
||||
ListSection list = new ListSection(Collections.singleton(o));
|
||||
list.setParent(this, Section.getName(path));
|
||||
return list;
|
||||
});
|
||||
}
|
||||
}
|
@ -14,5 +14,6 @@ translation.locales=Locales
|
||||
settings.path.title=Locales Directory
|
||||
settings.path.text=Locales directory
|
||||
settings.path.file-pattern=Translation file pattern
|
||||
settings.path.prefix=Path prefix
|
||||
settings.preview=Preview locale
|
||||
settings.editor.assistance=I18n key completion and annotation inside editor
|
Loading…
x
Reference in New Issue
Block a user