diff --git a/src/main/java/de/marhali/easyi18n/DataStore.java b/src/main/java/de/marhali/easyi18n/DataStore.java index 593b9c2..d48444e 100644 --- a/src/main/java/de/marhali/easyi18n/DataStore.java +++ b/src/main/java/de/marhali/easyi18n/DataStore.java @@ -5,6 +5,8 @@ import com.intellij.openapi.project.Project; import de.marhali.easyi18n.io.IOStrategy; import de.marhali.easyi18n.io.json.JsonIOStrategy; import de.marhali.easyi18n.io.json.ModularizedJsonIOStrategy; +import de.marhali.easyi18n.io.properties.PropertiesIOStrategy; +import de.marhali.easyi18n.io.yaml.YamlIOStrategy; import de.marhali.easyi18n.model.SettingsState; import de.marhali.easyi18n.model.TranslationData; import de.marhali.easyi18n.service.SettingsService; @@ -24,7 +26,9 @@ import java.util.function.Consumer; public class DataStore { private static final Set STRATEGIES = new LinkedHashSet<>(Arrays.asList( - new JsonIOStrategy(), new ModularizedJsonIOStrategy() + new JsonIOStrategy(), new ModularizedJsonIOStrategy(), + new YamlIOStrategy("yaml"), new YamlIOStrategy("yml"), + new PropertiesIOStrategy() )); private final Project project; diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java index d4ab551..40d57af 100644 --- a/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java +++ b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java @@ -66,7 +66,7 @@ public class JsonIOStrategy implements IOStrategy { try { for(VirtualFile file : directory.getChildren()) { - if(!isFileRelevant(state, file)) { + if(file.isDirectory() || !isFileRelevant(state, file)) { continue; } diff --git a/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java index 6cfb7db..391b73a 100644 --- a/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java +++ b/src/main/java/de/marhali/easyi18n/io/json/ModularizedJsonIOStrategy.java @@ -84,7 +84,7 @@ public class ModularizedJsonIOStrategy implements IOStrategy { // Read all underlying module files for(VirtualFile module : localeDir.getChildren()) { - if(module.isDirectory() || isFileRelevant(state, module)) { + if(module.isDirectory() || !isFileRelevant(state, module)) { continue; } diff --git a/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java new file mode 100644 index 0000000..a0063f8 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesIOStrategy.java @@ -0,0 +1,116 @@ +package de.marhali.easyi18n.io.properties; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; + +import de.marhali.easyi18n.io.IOStrategy; +import de.marhali.easyi18n.model.SettingsState; +import de.marhali.easyi18n.model.TranslationData; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.function.Consumer; + +/** + * Strategy for simple 'properties' locale files. Each locale has its own file. + * For example localesPath/en.properties, localesPath/de.properties. + * @author marhali + */ +public class PropertiesIOStrategy implements IOStrategy { + + private static final String FILE_EXTENSION = "properties"; + + @Override + public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) { + VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath)); + + if(directory == null || directory.getChildren() == null) { + return false; + } + + for(VirtualFile children : directory.getChildren()) { + if(!children.isDirectory() && isFileRelevant(state, children)) { + if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) { + return true; + } + } + } + + return false; + } + + @Override + public void read(@NotNull Project project, @NotNull String localesPath, + @NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) { + ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added) + + ApplicationManager.getApplication().runReadAction(() -> { + VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath)); + + if(directory == null || directory.getChildren() == null) { + throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")"); + } + + TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys()); + + try { + for(VirtualFile file : directory.getChildren()) { + if(file.isDirectory() || !isFileRelevant(state, file)) { + continue; + } + + String locale = file.getNameWithoutExtension(); + data.addLocale(locale); + + SortableProperties properties = new SortableProperties(state.isSortKeys()); + properties.load(new InputStreamReader(file.getInputStream())); + PropertiesMapper.read(locale, properties, data); + } + + result.accept(data); + + } catch(IOException e) { + e.printStackTrace(); + result.accept(null); + } + }); + } + + @Override + public void write(@NotNull Project project, @NotNull String localesPath, + @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer result) { + ApplicationManager.getApplication().runWriteAction(() -> { + try { + for(String locale : data.getLocales()) { + SortableProperties properties = new SortableProperties(state.isSortKeys()); + PropertiesMapper.write(locale, properties, data); + + File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION); + boolean exists = file.createNewFile(); + + VirtualFile vf = exists + ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) + : LocalFileSystem.getInstance().findFileByIoFile(file); + + StringWriter writer = new StringWriter(); + properties.store(writer, null); + + vf.setBinaryContent(writer.toString().getBytes(vf.getCharset())); + } + + result.accept(true); + + } catch(IOException e) { + e.printStackTrace(); + result.accept(false); + } + }); + } +} diff --git a/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java new file mode 100644 index 0000000..472416e --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/properties/PropertiesMapper.java @@ -0,0 +1,44 @@ +package de.marhali.easyi18n.io.properties; + +import de.marhali.easyi18n.model.Translation; +import de.marhali.easyi18n.model.TranslationData; +import de.marhali.easyi18n.util.StringUtil; + +import org.apache.commons.lang.StringEscapeUtils; + +import java.util.Map; + +/** + * Mapper for mapping properties files into translation nodes and backwards. + * @author marhali + */ +public class PropertiesMapper { + + // TODO: support array values + + public static void read(String locale, SortableProperties properties, TranslationData data) { + for(Map.Entry entry : properties.entrySet()) { + String key = String.valueOf(entry.getKey()); + String content = StringUtil.escapeControls(String.valueOf(entry.getValue()), true); + + Translation translation = data.getTranslation(key); + + if(translation == null) { + translation = new Translation(); + } + + translation.put(locale, content); + } + } + + public static void write(String locale, SortableProperties properties, TranslationData data) { + for(String key : data.getFullKeys()) { + Translation translation = data.getTranslation(key); + + if(translation != null && translation.containsKey(locale)) { + String content = StringEscapeUtils.unescapeJava(translation.get(locale)); + properties.put(key, content); + } + } + } +} diff --git a/src/main/java/de/marhali/easyi18n/io/properties/SortableProperties.java b/src/main/java/de/marhali/easyi18n/io/properties/SortableProperties.java new file mode 100644 index 0000000..c2d08ef --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/properties/SortableProperties.java @@ -0,0 +1,35 @@ +package de.marhali.easyi18n.io.properties; + +import java.util.*; + +/** + * Extends {@link Properties} class to support sorted or non-sorted keys. + * @author marhali + */ +public class SortableProperties extends Properties { + + private final transient Map properties; + + public SortableProperties(boolean sort) { + this.properties = sort ? new TreeMap<>() : new LinkedHashMap<>(); + } + + public Map getProperties() { + return this.properties; + } + + @Override + public Set keySet() { + return Collections.unmodifiableSet(new TreeSet<>(super.keySet())); + } + + @Override + public Set> entrySet() { + return this.properties.entrySet(); + } + + @Override + public synchronized Object put(Object key, Object value) { + return this.properties.put(key, value); + } +} \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/io/yaml/YamlArrayMapper.java b/src/main/java/de/marhali/easyi18n/io/yaml/YamlArrayMapper.java new file mode 100644 index 0000000..fb2b340 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/yaml/YamlArrayMapper.java @@ -0,0 +1,21 @@ +package de.marhali.easyi18n.io.yaml; + +import de.marhali.easyi18n.io.ArrayMapper; + +import thito.nodeflow.config.ListSection; + +/** + * Map for yaml array values. + * @author marhali + */ +public class YamlArrayMapper extends ArrayMapper { + 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; + } +} \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/io/yaml/YamlIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/yaml/YamlIOStrategy.java new file mode 100644 index 0000000..f5e7557 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/yaml/YamlIOStrategy.java @@ -0,0 +1,121 @@ +package de.marhali.easyi18n.io.yaml; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; + +import de.marhali.easyi18n.io.IOStrategy; +import de.marhali.easyi18n.model.SettingsState; +import de.marhali.easyi18n.model.TranslationData; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import thito.nodeflow.config.MapSection; +import thito.nodeflow.config.Section; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.function.Consumer; + +/** + * Strategy for simple yaml locale files. Each locale has its own file. + * For example localesPath/en.y(a)ml, localesPath/de.y(a)ml + * @author marhali + */ +public class YamlIOStrategy implements IOStrategy { + + private final String FILE_EXTENSION; + + public YamlIOStrategy(@NotNull String fileExtension) { + this.FILE_EXTENSION = fileExtension; + } + + @Override + public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) { + VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath)); + + if(directory == null || directory.getChildren() == null) { + return false; + } + + for(VirtualFile children : directory.getChildren()) { + if(!children.isDirectory() && isFileRelevant(state, children)) { + if(children.getFileType().getDefaultExtension().equalsIgnoreCase(FILE_EXTENSION)) { + return true; + } + } + } + + return false; + } + + @Override + public void read(@NotNull Project project, @NotNull String localesPath, + @NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) { + ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added) + + ApplicationManager.getApplication().runReadAction(() -> { + VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath)); + + if(directory == null || directory.getChildren() == null) { + throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")"); + } + + TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys()); + + try { + for(VirtualFile file : directory.getChildren()) { + if(file.isDirectory() || !isFileRelevant(state, file)) { + continue; + } + + String locale = file.getNameWithoutExtension(); + data.addLocale(locale); + + try(Reader reader = new InputStreamReader(file.getInputStream())) { + Section section = Section.parseToMap(reader); + YamlMapper.read(locale, section, data.getRootNode()); + } + } + + result.accept(data); + + } catch(IOException e) { + e.printStackTrace(); + result.accept(null); + } + }); + } + + @Override + public void write(@NotNull Project project, @NotNull String localesPath, + @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer result) { + ApplicationManager.getApplication().runWriteAction(() -> { + try { + for(String locale : data.getLocales()) { + Section section = new MapSection(); + YamlMapper.write(locale, section, data.getRootNode()); + + File file = new File(localesPath + "/" + locale + "." + FILE_EXTENSION); + boolean exists = file.createNewFile(); + + VirtualFile vf = exists + ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) + : LocalFileSystem.getInstance().findFileByIoFile(file); + + vf.setBinaryContent(Section.toString(section).getBytes(vf.getCharset())); + } + + result.accept(true); + + } catch(IOException e) { + e.printStackTrace(); + result.accept(false); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/io/yaml/YamlMapper.java b/src/main/java/de/marhali/easyi18n/io/yaml/YamlMapper.java new file mode 100644 index 0000000..82618ca --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/yaml/YamlMapper.java @@ -0,0 +1,71 @@ +package de.marhali.easyi18n.io.yaml; + +import de.marhali.easyi18n.model.Translation; +import de.marhali.easyi18n.model.TranslationNode; +import de.marhali.easyi18n.util.StringUtil; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.math.NumberUtils; + +import thito.nodeflow.config.MapSection; +import thito.nodeflow.config.Section; + +import java.util.Map; + +/** + * Mapper for mapping yaml files into translation nodes and backwards. + * @author marhali + */ +public class YamlMapper { + + public static void read(String locale, Section section, TranslationNode node) { + for(String key : section.getKeys()) { + TranslationNode childNode = node.getOrCreateChildren(key); + + if(section.getMap(key).isPresent()) { + // Nested element - run recursively + read(locale, section.getMap(key).get(), childNode); + } else { + Translation translation = childNode.getValue(); + + if(section.getList(key).isPresent() || section.getString(key).isPresent()) { + String content = section.isList(key) && section.getList(key).isPresent() + ? YamlArrayMapper.read(section.getList(key).get()) + : StringUtil.escapeControls(section.getString(key).get(), true); + + translation.put(locale, content); + childNode.setValue(translation); + } + } + } + } + + public static void write(String locale, Section section, TranslationNode node) { + for(Map.Entry entry : node.getChildren().entrySet()) { + String key = entry.getKey(); + TranslationNode childNode = entry.getValue(); + + if(!childNode.isLeaf()) { + // Nested node - run recursively + MapSection childSection = new MapSection(); + write(locale, childSection, childNode); + if(childSection.size() > 0) { + section.set(key, childSection); + } + } else { + Translation translation = childNode.getValue(); + String content = translation.get(locale); + + if(content != null) { + if(YamlArrayMapper.isArray(content)) { + section.set(key, YamlArrayMapper.write(content)); + } else if(NumberUtils.isNumber(content)) { + section.set(key, NumberUtils.createNumber(content)); + } else { + section.set(key, StringEscapeUtils.unescapeJava(content)); + } + } + } + } + } +} diff --git a/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java b/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java index 86f4db6..6c4cac1 100644 --- a/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java +++ b/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java @@ -6,6 +6,7 @@ import thito.nodeflow.config.ListSection; * Utility methods to read and write yaml lists. * @author marhali */ +@Deprecated public class YamlArrayUtil extends ArrayUtil { public static String read(ListSection list) {