diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java b/src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java new file mode 100644 index 0000000..aac41ed --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/json/JsonArrayMapper.java @@ -0,0 +1,21 @@ +package de.marhali.easyi18n.io.json; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import de.marhali.easyi18n.io.ArrayMapper; + +/** + * Map json array values. + * @author marhali + */ +public class JsonArrayMapper extends ArrayMapper { + 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; + } +} \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java new file mode 100644 index 0000000..26f7930 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/json/JsonIOStrategy.java @@ -0,0 +1,92 @@ +package de.marhali.easyi18n.io.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +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 de.marhali.easyi18n.model.TranslationNode; +import net.minidev.json.JSONObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.TreeMap; +import java.util.function.Consumer; + +/** + * Strategy for simple json locale files. Each locale has its own file. + * For example localesPath/en.json, localesPath/de.json. + * @author marhali + */ +public class JsonIOStrategy implements IOStrategy { + + private static final String FILE_EXTENSION = "json"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + @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().toLowerCase().equals(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(!isFileRelevant(state, file)) { + continue; + } + + data.addLocale(file.getNameWithoutExtension()); + + JSONObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(), file.getCharset()), JSONObject.class); + } + } 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) { + + } +} diff --git a/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java b/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java new file mode 100644 index 0000000..55f5c43 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/json/JsonMapper.java @@ -0,0 +1,73 @@ +package de.marhali.easyi18n.io.json; + +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.util.StringUtil; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.math.NumberUtils; + +import java.util.Map; + +/** + * Mapper for mapping json objects into translation nodes and backwards. + * @author marhali + */ +public class JsonMapper { + + public static void read(String locale, JsonObject json, TranslationNode node) { + for(Map.Entry entry : json.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + + TranslationNode childNode = node.getOrCreateChildren(key); + + if(value.isJsonObject()) { + // Nested element - run recursively + read(locale, value.getAsJsonObject(), childNode); + } else { + Translation translation = childNode.getValue(); + + String content = entry.getValue().isJsonArray() + ? JsonArrayMapper.read(value.getAsJsonArray()) + : StringUtil.escapeControls(value.getAsString(), true); + + translation.put(locale, content); + childNode.setValue(translation); + } + } + } + + public static void write(String locale, JsonObject json, TranslationNode node) { + for(Map.Entry entry : node.getChildren().entrySet()) { + String key = entry.getKey(); + TranslationNode childNode = entry.getValue(); + + if(!childNode.isLeaf()) { + // Nested node - run recursively + JsonObject childJson = new JsonObject(); + write(locale, childJson, childNode); + if(childJson.size() > 0) { + json.add(key, childJson); + } + } else { + Translation translation = childNode.getValue(); + String content = translation.get(locale); + + if(content != null) { + if(JsonArrayMapper.isArray(content)) { + json.add(key, JsonArrayMapper.write(content)); + } else if(NumberUtils.isNumber(content)) { + json.add(key, new JsonPrimitive(NumberUtils.createNumber(content))); + } else { + json.add(key, new JsonPrimitive(StringEscapeUtils.unescapeJava(content))); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/de/marhali/easyi18n/mapper/AbstractMapperTest.java b/src/test/java/de/marhali/easyi18n/mapper/AbstractMapperTest.java new file mode 100644 index 0000000..9771660 --- /dev/null +++ b/src/test/java/de/marhali/easyi18n/mapper/AbstractMapperTest.java @@ -0,0 +1,45 @@ +package de.marhali.easyi18n.mapper; + +import de.marhali.easyi18n.model.Translation; + +import org.junit.Test; + +/** + * Defines test cases for {@link de.marhali.easyi18n.model.TranslationNode} mapping. + * @author marhali + */ +public abstract class AbstractMapperTest { + + protected final String specialCharacters = "Special characters: äü@Öä€/$§;.-?+~#```'' end"; + protected final String arraySimple = "!arr[first;second]"; + protected final String arrayEscaped = "!arr[first\\;element;second element;third\\;element]"; + protected final String leadingSpace = " leading space"; + + @Test + public abstract void testNonSorting(); + + @Test + public abstract void testSorting(); + + @Test + public abstract void testArrays(); + + @Test + public abstract void testSpecialCharacters(); + + @Test + public abstract void testNestedKeys(); + + @Test + public abstract void testNonNestedKeys(); + + @Test + public abstract void testLeadingSpace(); + + @Test + public abstract void testNumbers(); + + protected Translation create(String content) { + return new Translation("en", content); + } +} \ No newline at end of file diff --git a/src/test/java/de/marhali/easyi18n/mapper/JsonMapperTest.java b/src/test/java/de/marhali/easyi18n/mapper/JsonMapperTest.java new file mode 100644 index 0000000..37ae662 --- /dev/null +++ b/src/test/java/de/marhali/easyi18n/mapper/JsonMapperTest.java @@ -0,0 +1,153 @@ +package de.marhali.easyi18n.mapper; + +import com.google.gson.JsonObject; + +import com.google.gson.JsonPrimitive; + +import de.marhali.easyi18n.io.json.JsonArrayMapper; +import de.marhali.easyi18n.io.json.JsonMapper; +import de.marhali.easyi18n.model.TranslationData; + +import org.apache.commons.lang.StringEscapeUtils; +import org.junit.Assert; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Unit tests for {@link de.marhali.easyi18n.io.json.JsonMapper} + * @author marhali + */ +public class JsonMapperTest extends AbstractMapperTest { + + @Override + public void testNonSorting() { + JsonObject input = new JsonObject(); + input.add("zulu", new JsonPrimitive("test")); + input.add("alpha", new JsonPrimitive("test")); + input.add("bravo", new JsonPrimitive("test")); + + TranslationData data = new TranslationData(false, true); + JsonMapper.read("en", input, data.getRootNode()); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Set expect = new LinkedHashSet<>(Arrays.asList("zulu", "alpha", "bravo")); + Assert.assertEquals(expect, output.keySet()); + } + + @Override + public void testSorting() { + JsonObject input = new JsonObject(); + input.add("zulu", new JsonPrimitive("test")); + input.add("alpha", new JsonPrimitive("test")); + input.add("bravo", new JsonPrimitive("test")); + + TranslationData data = new TranslationData(true, true); + JsonMapper.read("en", input, data.getRootNode()); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Set expect = new LinkedHashSet<>(Arrays.asList("alpha", "bravo", "zulu")); + Assert.assertEquals(expect, output.keySet()); + } + + @Override + public void testArrays() { + TranslationData data = new TranslationData(true, true); + data.setTranslation("simple", create(arraySimple)); + data.setTranslation("escaped", create(arrayEscaped)); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertTrue(output.get("simple").isJsonArray()); + Assert.assertEquals(arraySimple, JsonArrayMapper.read(output.get("simple").getAsJsonArray())); + Assert.assertTrue(output.get("escaped").isJsonArray()); + Assert.assertEquals(arrayEscaped, StringEscapeUtils.unescapeJava(JsonArrayMapper.read(output.get("escaped").getAsJsonArray()))); + } + + @Override + public void testSpecialCharacters() { + TranslationData data = new TranslationData(true, true); + data.setTranslation("chars", create(specialCharacters)); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertEquals(specialCharacters, output.get("chars").getAsString()); + + TranslationData input = new TranslationData(true, true); + JsonMapper.read("en", output, input.getRootNode()); + + Assert.assertEquals(specialCharacters, StringEscapeUtils.unescapeJava(input.getTranslation("chars").get("en"))); + } + + @Override + public void testNestedKeys() { + TranslationData data = new TranslationData(true, true); + data.setTranslation("nested.key.section", create("test")); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertEquals("test", output.getAsJsonObject("nested").getAsJsonObject("key").get("section").getAsString()); + + TranslationData input = new TranslationData(true, true); + JsonMapper.read("en", output, input.getRootNode()); + + Assert.assertEquals("test", input.getTranslation("nested.key.section").get("en")); + } + + @Override + public void testNonNestedKeys() { + TranslationData data = new TranslationData(true, false); + data.setTranslation("long.key.with.many.sections", create("test")); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertTrue(output.has("long.key.with.many.sections")); + + TranslationData input = new TranslationData(true, false); + JsonMapper.read("en", output, input.getRootNode()); + + Assert.assertEquals("test", input.getTranslation("long.key.with.many.sections").get("en")); + } + + @Override + public void testLeadingSpace() { + TranslationData data = new TranslationData(true, true); + data.setTranslation("space", create(leadingSpace)); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertEquals(leadingSpace, output.get("space").getAsString()); + + TranslationData input = new TranslationData(true, true); + JsonMapper.read("en", output, input.getRootNode()); + + Assert.assertEquals(leadingSpace, input.getTranslation("space").get("en")); + } + + @Override + public void testNumbers() { + TranslationData data = new TranslationData(true, true); + data.setTranslation("numbered", create("15000")); + + JsonObject output = new JsonObject(); + JsonMapper.write("en", output, data.getRootNode()); + + Assert.assertEquals(15000, output.get("numbered").getAsNumber()); + + JsonObject input = new JsonObject(); + input.addProperty("numbered", 143.23); + JsonMapper.read("en", input, data.getRootNode()); + + Assert.assertEquals("143.23", data.getTranslation("numbered").get("en")); + } +} \ No newline at end of file