subKeys = ResourceKeys.extractChildKeys(keys, rootKey);
- object.add(rootKey, toJson(translations, subKey, subKeys));
- });
- return object;
- }
- if (key == null) {
- return new JsonObject();
- }
- return new JsonPrimitive(translations.get(key));
- }
-
- private static String es6ToJson(String content) {
- return content.replaceAll("export +default", "").replaceAll("} *;", "}");
- }
-
- private static String jsonToEs6(String content) {
- return "export default " + content + ";";
- }
-}
diff --git a/src/main/java/com/jvms/i18neditor/util/Resources.java b/src/main/java/com/jvms/i18neditor/util/Resources.java
new file mode 100644
index 0000000..2153872
--- /dev/null
+++ b/src/main/java/com/jvms/i18neditor/util/Resources.java
@@ -0,0 +1,241 @@
+package com.jvms.i18neditor.util;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.SortedMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.LocaleUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.jvms.i18neditor.Resource;
+import com.jvms.i18neditor.ResourceType;
+
+/**
+ * This class provides utility functions for a {@link Resource}.
+ *
+ * @author Jacob
+ */
+public final class Resources {
+ private final static Charset UTF8_ENCODING = Charset.forName("UTF-8");
+ private final static String LOCALE_REGEX = "[a-z]{2}(_[A-Z]{2})?";
+
+ /**
+ * Gets all resources from the given {@code rootDir} directory path.
+ *
+ * The {@code baseName} is the base of the filename of the resource files to look for.
+ * The base name is without extension and without any locale information.
+ * When a resource type is given, only resources of that type will be looked for.
+ *
+ * This function will not load the contents of the file, only its description.
+ * If you want to load the contents, use {@link #load(Resource)} afterwards.
+ *
+ * @param rootDir the root directory of the resources
+ * @param baseName the base name of the resource files to look for
+ * @param type the type of the resource files to look for
+ * @return list of found resources
+ * @throws IOException if an I/O error occurs reading the directory.
+ */
+ public static List get(Path rootDir, String baseName, Optional type) throws IOException {
+ List result = Lists.newLinkedList();
+ Files.walk(rootDir, 1).forEach(p -> {
+ ResourceType resourceType = null;
+ if (isResource(p, ResourceType.ES6, baseName)) {
+ resourceType = ResourceType.ES6;
+ } else if (isResource(p, ResourceType.JSON, baseName)) {
+ resourceType = ResourceType.JSON;
+ } else if (isResource(p, ResourceType.Properties, baseName)) {
+ resourceType = ResourceType.Properties;
+ }
+ if (resourceType != null && (!type.isPresent() || type.get() == resourceType)) {
+ String fileName = p.getFileName().toString();
+ String extension = resourceType.getExtension();
+ Locale locale = null;
+ Path path = null;
+ if (resourceType.isEmbedLocale()) {
+ String pattern = "^" + baseName + "_("+LOCALE_REGEX+")" + extension + "$";
+ Matcher match = Pattern.compile(pattern).matcher(fileName);
+ if (match.find()) {
+ locale = LocaleUtils.toLocale(match.group(1));
+ }
+ path = Paths.get(rootDir.toString(), baseName + (locale == null ? "" : "_" + locale.toString()) + extension);
+ } else {
+ locale = LocaleUtils.toLocale(fileName);
+ path = Paths.get(rootDir.toString(), locale.toString(), baseName + extension);
+ }
+ result.add(new Resource(resourceType, path, locale));
+ }
+ });
+ return result;
+ }
+
+ /**
+ * Loads the translations of a {@link Resource} from disk.
+ *
+ * @param resource the resource.
+ * @throws IOException if an I/O error occurs reading the file.
+ */
+ public static void load(Resource resource) throws IOException {
+ ResourceType type = resource.getType();
+ Path path = resource.getPath();
+ SortedMap translations;
+ if (type == ResourceType.Properties) {
+ ExtendedProperties content = new ExtendedProperties();
+ content.load(path);
+ translations = fromProperties(content);
+ } else {
+ String content = Files.lines(path, UTF8_ENCODING).collect(Collectors.joining());
+ if (type == ResourceType.ES6) {
+ content = es6ToJson(content);
+ }
+ translations = fromJson(content);
+ }
+ resource.setTranslations(translations);
+ }
+
+ /**
+ * Writes the translations of the given resource to disk.
+ *
+ * @param resource the resource to write.
+ * @param prettyPrinting whether to pretty print the contents
+ * @throws IOException if an I/O error occurs writing the file.
+ */
+ public static void write(Resource resource, boolean prettyPrinting) throws IOException {
+ ResourceType type = resource.getType();
+ if (type == ResourceType.Properties) {
+ ExtendedProperties content = toProperties(resource.getTranslations());
+ content.store(resource.getPath());
+ } else {
+ String content = toJson(resource.getTranslations(), prettyPrinting);
+ if (type == ResourceType.ES6) {
+ content = jsonToEs6(content);
+ }
+ if (!Files.exists(resource.getPath())) {
+ Files.createDirectories(resource.getPath().getParent());
+ Files.createFile(resource.getPath());
+ }
+ Files.write(resource.getPath(), Lists.newArrayList(content), UTF8_ENCODING);
+ }
+ }
+
+ /**
+ * Creates a new {@link Resource} with the given {@link ResourceType} in the given directory path.
+ * This function should be used to create new resources. For creating an instance of an
+ * existing resource on disk, see {@link #read(Path)}.
+ *
+ * @param type the type of the resource to create.
+ * @param root the root directory to write the resource to.
+ * @return The newly created resource.
+ * @throws IOException if an I/O error occurs writing the file.
+ */
+ public static Resource create(Path root, ResourceType type, Optional locale, String baseName) throws IOException {
+ String extension = type.getExtension();
+ Path path;
+ if (type.isEmbedLocale()) {
+ path = Paths.get(root.toString(), baseName + (locale.isPresent() ? "_" + locale.get().toString() : "") + extension);
+ } else {
+ path = Paths.get(root.toString(), locale.get().toString(), baseName + extension);
+ }
+ Resource resource = new Resource(type, path, locale.orElse(null));
+ write(resource, false);
+ return resource;
+ }
+
+ private static boolean isResource(Path path, ResourceType type, String baseName) {
+ String extension = type.getExtension();
+ if (type.isEmbedLocale()) {
+ return Files.isRegularFile(path) &&
+ Pattern.matches("^" + baseName + "(_"+LOCALE_REGEX+")?" + extension + "$", path.getFileName().toString());
+ } else {
+ return Files.isDirectory(path) &&
+ Pattern.matches("^" + LOCALE_REGEX + "$", path.getFileName().toString()) &&
+ Files.isRegularFile(Paths.get(path.toString(), baseName + extension));
+ }
+ }
+
+ private static SortedMap fromProperties(ExtendedProperties properties) {
+ SortedMap result = Maps.newTreeMap();
+ properties.forEach((key, value) -> {
+ result.put((String)key, StringEscapeUtils.unescapeJava((String)value));
+ });
+ return result;
+ }
+
+ private static ExtendedProperties toProperties(Map translations) {
+ ExtendedProperties result = new ExtendedProperties();
+ result.putAll(translations);
+ return result;
+ }
+
+ private static SortedMap fromJson(String json) {
+ SortedMap result = Maps.newTreeMap();
+ JsonElement elem = new JsonParser().parse(json);
+ fromJson(null, elem, result);
+ return result;
+ }
+
+ private static void fromJson(String key, JsonElement elem, Map content) {
+ if (elem.isJsonObject()) {
+ elem.getAsJsonObject().entrySet().forEach(entry -> {
+ String newKey = key == null ? entry.getKey() : ResourceKeys.create(key, entry.getKey());
+ fromJson(newKey, entry.getValue(), content);
+ });
+ } else if (elem.isJsonPrimitive()) {
+ content.put(key, StringEscapeUtils.unescapeJava(elem.getAsString()));
+ } else if (elem.isJsonNull()) {
+ content.put(key, "");
+ } else {
+ throw new IllegalArgumentException("Found invalid json element.");
+ }
+ }
+
+ private static String toJson(Map translations, boolean prettify) {
+ List keys = Lists.newArrayList(translations.keySet());
+ JsonElement elem = toJson(translations, null, keys);
+ GsonBuilder builder = new GsonBuilder().disableHtmlEscaping();
+ if (prettify) {
+ builder.setPrettyPrinting();
+ }
+ return builder.create().toJson(elem);
+ }
+
+ private static JsonElement toJson(Map translations, String key, List keys) {
+ if (keys.size() > 0) {
+ JsonObject object = new JsonObject();
+ ResourceKeys.uniqueRootKeys(keys).forEach(rootKey -> {
+ String subKey = ResourceKeys.create(key, rootKey);
+ List subKeys = ResourceKeys.extractChildKeys(keys, rootKey);
+ object.add(rootKey, toJson(translations, subKey, subKeys));
+ });
+ return object;
+ }
+ if (key == null) {
+ return new JsonObject();
+ }
+ return new JsonPrimitive(translations.get(key));
+ }
+
+ private static String es6ToJson(String content) {
+ return content.replaceAll("export +default", "").replaceAll("} *;", "}");
+ }
+
+ private static String jsonToEs6(String content) {
+ return "export default " + content + ";";
+ }
+}
diff --git a/src/main/resources/bundles/messages.properties b/src/main/resources/bundles/messages.properties
index 1c8e51f..08c8ca7 100644
--- a/src/main/resources/bundles/messages.properties
+++ b/src/main/resources/bundles/messages.properties
@@ -1,13 +1,16 @@
-core.intro.text = Drop a folder here or go to File\ufe65Open Folder... to start
+core.intro.text = Drop an existing project here or go to File\ufe65New Project to start
dialogs.about.title = About {0}
-dialogs.import.title = Open Folder
dialogs.error.title = Error
dialogs.locale.add.error.create = An error occurred while creating the new locale.
dialogs.locale.add.error.invalid = The locale you entered is invalid or does already exist.
dialogs.locale.add.text = Enter locale (i.e. en_US):
-dialogs.locale.add.title = Add Locale ({0})
+dialogs.locale.add.title = Add Locale
+dialogs.preferences.editor.title = Preferences
+dialogs.preferences.project.title = Project Preferences
+dialogs.project.import.title = Import Project
+dialogs.project.new.title = New Project
dialogs.save.text = You have unsaved changes, do you want to save them?
dialogs.save.title = Save Translations
dialogs.translation.add.error = The translation key you entered is invalid.
@@ -30,9 +33,7 @@ dialogs.version.new = A new version is available:
dialogs.version.title = Available Updates
dialogs.version.uptodate = You are using the latest version.
-menu.edit.add.locale.es6.title = ES6 Format...
-menu.edit.add.locale.json.title = JSON Format...
-menu.edit.add.locale.title = Add Locale
+menu.edit.add.locale.title = Add Locale...
menu.edit.add.translation.title = Add Translation...
menu.edit.delete.title = Delete Translation
menu.edit.duplicate.title = Duplicate Translation...
@@ -43,9 +44,15 @@ menu.edit.vk = E
menu.file.exit.title = Exit
menu.file.exit.vk = E
menu.file.folder.title = Open Containing Folder
-menu.file.open.title = Open Folder...
-menu.file.open.vk = O
-menu.file.recent.title = Open Recent Folder
+menu.file.project.import.title = Import Project...
+menu.file.project.import.vk = I
+menu.file.project.new.es6.title = ES6 Format...
+menu.file.project.new.json.title = JSON Format...
+menu.file.project.new.properties.title = Properties Format...
+menu.file.project.new.title = New Project
+menu.file.project.new.vk = N
+menu.file.recent.clear.title = Clear List
+menu.file.recent.title = Open Recent
menu.file.recent.vk = C
menu.file.reload.title = Reload from Disk
menu.file.reload.vk = R
@@ -57,7 +64,8 @@ menu.help.about.title = About {0}
menu.help.title = Help
menu.help.version.title = Check for Updates...
menu.help.vk = H
-menu.settings.minify.title = Minify Translations on Save
+menu.settings.preferences.editor.title = Preferences...
+menu.settings.preferences.project.title = Project Preferences...
menu.settings.title = Settings
menu.settings.vk = S
menu.view.collapse.title = Collapse All Translations
@@ -65,8 +73,15 @@ menu.view.expand.title = Expand All Translations
menu.view.title = View
menu.view.vk = V
-resources.open.error.multiple = An error occurred while opening translation files.
-resources.open.error.single = An error occurred while opening the translation file ''{0}''.
+resources.import.empty = No translation files found in ''{0}''.
+resources.import.error.multiple = An error occurred while opening translation files.
+resources.import.error.single = An error occurred while opening the translation file ''{0}''.
resources.write.error.single = An error occurred while writing the translation file ''{0}''.
-translations.model.name = Translations
+settings.fieldset.general = General
+settings.fieldset.newprojects = New Projects
+settings.minify.title = Minify translations on save
+settings.resourcename.title = Translations filename
+settings.version.title = Check for new version on startup
+
+tree.root.name = Translations
diff --git a/src/main/resources/bundles/messages_nl.properties b/src/main/resources/bundles/messages_nl.properties
index fa8bda2..2a17c57 100644
--- a/src/main/resources/bundles/messages_nl.properties
+++ b/src/main/resources/bundles/messages_nl.properties
@@ -1,13 +1,16 @@
-core.intro.text = Sleep een map hierheen of ga naar Bestand\ufe65Map Openen... om te starten
+core.intro.text = Sleep een bestaand project hierheen of ga naar Bestand\ufe65Nieuw Project om te starten
dialogs.about.title = Over {0}
-dialogs.import.title = Map Openen
dialogs.error.title = Fout
dialogs.locale.add.error.create = Er is iets fout gegaan bij het toevoegen van de nieuwe locale.
dialogs.locale.add.error.invalid = De opgegeven locale is niet geldig of bestaat al.
dialogs.locale.add.text = Locale (bijv. nl_NL):
-dialogs.locale.add.title = Locale Toevoegen ({0})
+dialogs.locale.add.title = Locale Toevoegen
+dialogs.preferences.editor.title = Voorkeuren
+dialogs.preferences.project.title = Projectvoorkeuren
+dialogs.project.import.title = Importeer Project
+dialogs.project.new.title = Nieuw Project
dialogs.save.text = U heeft nog onopgeslagen wijzigingen, wilt u deze opslaan?
dialogs.save.title = Vertalingen Opslaan
dialogs.translation.add.error = De opgegeven naam voor de vertaling is niet geldig.
@@ -30,9 +33,7 @@ dialogs.version.new = Een nieuwe versie is beschikbaar:
dialogs.version.title = Beschikbare Updates
dialogs.version.uptodate = Je gebruikt de nieuwste versie.
-menu.edit.add.locale.es6.title = ES6 Formaat...
-menu.edit.add.locale.json.title = JSON Formaat...
-menu.edit.add.locale.title = Locale Toevoegen
+menu.edit.add.locale.title = Locale Toevoegen...
menu.edit.add.translation.title = Vertaling Toevoegen...
menu.edit.delete.title = Vertaling Verwijderen
menu.edit.duplicate.title = Vertaling Dupliceren...
@@ -43,11 +44,17 @@ menu.edit.vk = W
menu.file.exit.title = Sluiten
menu.file.exit.vk = S
menu.file.folder.title = In Map Weergeven
-menu.file.open.title = Map Openen...
-menu.file.open.vk = O
-menu.file.recent.title = Recente Map Openen
+menu.file.project.import.title = Importeer Project...
+menu.file.project.import.vk = I
+menu.file.project.new.es6.title = ES6 Formaat...
+menu.file.project.new.json.title = JSON Formaat...
+menu.file.project.new.properties.title = Properties Formaat...
+menu.file.project.new.title = Nieuw Project
+menu.file.project.new.vk = N
+menu.file.recent.clear.title = Lijst Wissen
+menu.file.recent.title = Recent Geopend
menu.file.recent.vk = R
-menu.file.reload.title = Herladen van Schijf
+menu.file.reload.title = Herladen vanaf Schijf
menu.file.reload.vk = H
menu.file.save.title = Opslaan
menu.file.save.vk = P
@@ -57,7 +64,8 @@ menu.help.about.title = Over {0}
menu.help.title = Help
menu.help.version.title = Controleer nieuwe Versie...
menu.help.vk = H
-menu.settings.minify.title = Comprimeer Vertalingen bij Opslaan
+menu.settings.preferences.editor.title = Voorkeuren...
+menu.settings.preferences.project.title = Projectvoorkeuren...
menu.settings.title = Instellingen
menu.settings.vk = I
menu.view.collapse.title = Alle Vertalingen Invouwen
@@ -65,8 +73,15 @@ menu.view.expand.title = Alle Vertalingen Uitvouwen
menu.view.title = Beeld
menu.view.vk = L
-resources.open.error.multiple = Er is iets fout gegaan bij het openen van de vertaalbetanden.
-resources.open.error.single = Er is iets fout gegaan bij het openen van het vertaalbestand ''{0}''.
+resources.import.empty = Geen vertaalbestanden gevonden in ''{0}''.
+resources.import.error.multiple = Er is iets fout gegaan bij het openen van de vertaalbetanden.
+resources.import.error.single = Er is iets fout gegaan bij het openen van het vertaalbestand ''{0}''.
resources.write.error.single = Er is iets fout gegaan bij het opslaan van het vertaalbestand ''{0}''.
-translations.model.name = Vertalingen
+settings.fieldset.general = Algemeen
+settings.fieldset.newprojects = Nieuwe Projecten
+settings.minify.title = Comprimeer vertalingen bij opslaan
+settings.resourcename.title = Vertaalbestandsnaam
+settings.version.title = Controleer op nieuwe versie bij opstarten
+
+tree.root.name = Vertalingen
diff --git a/src/main/resources/bundles/messages_pt_BR.properties b/src/main/resources/bundles/messages_pt_BR.properties
index ce14991..96baa75 100644
--- a/src/main/resources/bundles/messages_pt_BR.properties
+++ b/src/main/resources/bundles/messages_pt_BR.properties
@@ -1,11 +1,16 @@
+core.intro.text = Solte um projeto existente aqui ou v\u00e1 para Arquivo\ufe65Novo Projeto para iniciar
+
dialogs.about.title = Sobre {0}
-dialogs.import.title = Abrir Pasta
dialogs.error.title = Erro
dialogs.locale.add.error.create = Ocorreu um erro ao criar o nova localidade.
dialogs.locale.add.error.invalid = A localidade digitada \u00e9 inv\u00e1lida ou n\u00e3o existe.
dialogs.locale.add.text = Informe uma localidade (Ex. pt_BR):
-dialogs.locale.add.title = Incluir localidade ({0})
+dialogs.locale.add.title = Incluir localidade
+dialogs.preferences.editor.title = Prefer\u00eancias...
+dialogs.preferences.project.title = Prefer\u00eancias do Projeto...
+dialogs.project.import.title = Projeto de Importa\u00E7\u00E3o
+dialogs.project.new.title = Novo Projeto
dialogs.save.text = Voc\u00ea tem modifica\u00e7\u00f5es n\u00e3o salvas, deseja salv\u00e1-las?
dialogs.save.title = Salvar Tradu\u00e7\u00f5es
dialogs.translation.add.error = A chave de tradu\u00e7\u00e3o inofrmada \u00e9 inv\u00e1lida.
@@ -28,9 +33,7 @@ dialogs.version.new = Uma nova vers\u00e3o est\u00e1 dispon\u00edvel:
dialogs.version.title = Atualiza\u00e7\u00f5es Dispon\u00edveis
dialogs.version.uptodate = Voc\u00ea est\u00e1 atualizado com a vers\u00e3o mais recente.
-menu.edit.add.locale.es6.title = Formato ES6...
-menu.edit.add.locale.json.title = Formato JSON...
-menu.edit.add.locale.title = Incluir Localiza\u00e7\u00e3o
+menu.edit.add.locale.title = Incluir Localiza\u00e7\u00e3o...
menu.edit.add.translation.title = Incluir Tradu\u00e7\u00e3o...
menu.edit.delete.title = Excluir Tradu\u00e7\u00e3o
menu.edit.duplicate.title = Duplicar Tradu\u00e7\u00e3o...
@@ -41,9 +44,15 @@ menu.edit.vk = E
menu.file.exit.title = Sair
menu.file.exit.vk = S
menu.file.folder.title = Abrir Pasta de Conte\u00fado
-menu.file.open.title = Abrir Pasta...
-menu.file.open.vk = A
-menu.file.recent.title = Abrir Pasta Recente
+menu.file.project.import.title = Projeto de Importa\u00e7\u00e3o...
+menu.file.project.import.vk = I
+menu.file.project.new.es6.title = Formato ES6...
+menu.file.project.new.json.title = Formato JSON...
+menu.file.project.new.properties.title = Formato Properties...
+menu.file.project.new.title = Novo Projeto
+menu.file.project.new.vk = N
+menu.file.recent.clear.title = Limpar Lista
+menu.file.recent.title = Abrir Recente
menu.file.recent.vk = E
menu.file.reload.title = Recarregar do Disco
menu.file.reload.vk = R
@@ -53,9 +62,10 @@ menu.file.title = Arquivo
menu.file.vk = A
menu.help.about.title = Sobre {0}
menu.help.title = Ajuda
-menu.help.version.title = Verificar novas vers\u00f5es...
+menu.help.version.title = Verificar a nova vers\u00e3o...
menu.help.vk = J
-menu.settings.minify.title = Minificar JSON ao salvar
+menu.settings.preferences.editor.title = Prefer\u00eancias...
+menu.settings.preferences.project.title = Prefer\u00eancias do Projeto...
menu.settings.title = Configura\u00e7\u00f5es
menu.settings.vk = C
menu.view.collapse.title = Recolher todas as tradu\u00e7\u00f5es
@@ -63,8 +73,15 @@ menu.view.expand.title = Expandir todas as tradu\u00e7\u00f5es
menu.view.title = Ver
menu.view.vk = V
-resources.open.error.multiple = Um erro ocorreu enquanto os arquivos de tradu\u00e7\u00f5es eram carregados.
-resources.open.error.single = Um erro ocorreu enquanto o arquivo de tradu\u00e7\u00e3o era carregado ''{0}''.
+resources.import.empty = Nenhum arquivo de tradu\u00e7\u00e3o encontrado em ''{0}''.
+resources.import.error.multiple = Um erro ocorreu enquanto os arquivos de tradu\u00e7\u00f5es eram carregados.
+resources.import.error.single = Um erro ocorreu enquanto o arquivo de tradu\u00e7\u00e3o era carregado ''{0}''.
resources.write.error.single = Um erro ocorreu enquanto o arquivo de tradu\u00e7\u00e3o era salvo ''{0}''.
-translations.model.name = Tradu\u00e7\u00f5es
+settings.fieldset.general = Geral
+settings.fieldset.newprojects = Novos Projetos
+settings.minify.title = Minificar tradu\u00e7\u00f5es ao salvar
+settings.resourcename.title = Nome do arquivo de tradu\u00e7\u00e3es
+settings.version.title = Verificar a nova vers\u00e3o na inicializa\u00e7\u00e3o
+
+tree.root.name = Tradu\u00e7\u00f5es
diff --git a/src/test/java/com/jvms/i18neditor/ResourceTest.java b/src/test/java/com/jvms/i18neditor/ResourceTest.java
index 6942421..04437de 100644
--- a/src/test/java/com/jvms/i18neditor/ResourceTest.java
+++ b/src/test/java/com/jvms/i18neditor/ResourceTest.java
@@ -8,7 +8,6 @@
import com.google.common.collect.Maps;
import com.jvms.i18neditor.Resource;
-import com.jvms.i18neditor.Resource.ResourceType;
import static org.junit.Assert.*;
@@ -20,7 +19,8 @@ public void setup() throws Exception {
SortedMap translations = Maps.newTreeMap();
translations.put("a.a", "aa");
translations.put("a.b", "ab");
- resource = new Resource(ResourceType.JSON, null, new Locale("en"), translations);
+ resource = new Resource(ResourceType.JSON, null, new Locale("en"));
+ resource.setTranslations(translations);
}
@Test