From 35ea5da01fa605a9bd64f40459adb90fe6c9c7df Mon Sep 17 00:00:00 2001 From: Jacob van Mourik Date: Wed, 20 Jun 2018 12:24:47 +0200 Subject: [PATCH] Added new way projects are loaded/saved to support saving without directories for JSON/ES6 files and to support custom file naming. #28 #39 --- .../java/com/jvms/i18neditor/Resource.java | 27 +++- .../com/jvms/i18neditor/ResourceType.java | 19 +-- .../editor/AbstractSettingsPane.java | 8 +- .../com/jvms/i18neditor/editor/Editor.java | 58 ++++--- .../jvms/i18neditor/editor/EditorProject.java | 22 ++- .../editor/EditorProjectSettingsPane.java | 30 ++-- .../i18neditor/editor/EditorSettings.java | 19 ++- .../i18neditor/editor/EditorSettingsPane.java | 27 ++-- .../com/jvms/i18neditor/swing/JHelpLabel.java | 26 +++ .../i18neditor/util/ChecksumException.java | 26 +++ .../com/jvms/i18neditor/util/Locales.java | 34 ++++ .../jvms/i18neditor/util/MessageBundle.java | 3 +- .../com/jvms/i18neditor/util/Resources.java | 148 +++++++++++------- .../resources/bundles/messages.properties | 4 +- .../resources/bundles/messages_nl.properties | 4 +- .../bundles/messages_pt_BR.properties | 4 +- 16 files changed, 334 insertions(+), 125 deletions(-) create mode 100644 src/main/java/com/jvms/i18neditor/swing/JHelpLabel.java create mode 100644 src/main/java/com/jvms/i18neditor/util/ChecksumException.java create mode 100644 src/main/java/com/jvms/i18neditor/util/Locales.java diff --git a/src/main/java/com/jvms/i18neditor/Resource.java b/src/main/java/com/jvms/i18neditor/Resource.java index 68c0e61..7c15e2a 100644 --- a/src/main/java/com/jvms/i18neditor/Resource.java +++ b/src/main/java/com/jvms/i18neditor/Resource.java @@ -34,7 +34,8 @@ public class Resource { private final ResourceType type; private final List listeners = Lists.newLinkedList(); private SortedMap translations = Maps.newTreeMap(); - + private String checksum; + /** * See {@link #Resource(ResourceType, Path, Locale)}. */ @@ -95,6 +96,11 @@ public SortedMap getTranslations() { return ImmutableSortedMap.copyOf(translations); } + /** + * Sets the translations of the resource. + * + * @param translations the translations + */ public void setTranslations(SortedMap translations) { this.translations = translations; } @@ -199,6 +205,25 @@ public void removeListener(ResourceListener listener) { listeners.remove(listener); } + /** + * Gets the checksum of the resource's file. + * This method only returns the checksum set via {@link #setChecksum(checksum)}. + * + * @return the checksum. + */ + public String getChecksum() { + return checksum; + } + + /** + * Sets the checksum of the resource's file. + * + * @param checksum the checksum to set. + */ + public void setChecksum(String checksum) { + this.checksum = checksum; + } + private void duplicateTranslation(String key, String newKey, boolean keepOld) { Map newTranslations = Maps.newTreeMap(); translations.keySet().forEach(k -> { diff --git a/src/main/java/com/jvms/i18neditor/ResourceType.java b/src/main/java/com/jvms/i18neditor/ResourceType.java index c5cd37d..a2e2577 100644 --- a/src/main/java/com/jvms/i18neditor/ResourceType.java +++ b/src/main/java/com/jvms/i18neditor/ResourceType.java @@ -8,12 +8,11 @@ * @author Jacob van Mourik */ public enum ResourceType { - JSON(".json", false), - ES6(".js", false), - Properties(".properties", true); + JSON(".json"), + ES6(".js"), + Properties(".properties"); private final String extension; - private final boolean embedLocale; /** * Gets the file extension of the resource type. @@ -24,17 +23,7 @@ public String getExtension() { return extension; } - /** - * Whether the locale should be embedded in the filename for this resource type. - * - * @return whether the locale should be embedded in the filename. - */ - public boolean isEmbedLocale() { - return embedLocale; - } - - private ResourceType(String extension, boolean embedLocale) { + private ResourceType(String extension) { this.extension = extension; - this.embedLocale = embedLocale; } } \ No newline at end of file diff --git a/src/main/java/com/jvms/i18neditor/editor/AbstractSettingsPane.java b/src/main/java/com/jvms/i18neditor/editor/AbstractSettingsPane.java index 56a53d9..6afee06 100644 --- a/src/main/java/com/jvms/i18neditor/editor/AbstractSettingsPane.java +++ b/src/main/java/com/jvms/i18neditor/editor/AbstractSettingsPane.java @@ -13,6 +13,7 @@ * @author Jacob van Mourik */ public abstract class AbstractSettingsPane extends JPanel { + private final static long serialVersionUID = -8953194193840198893L; private GridBagConstraints vGridBagConstraints; protected AbstractSettingsPane() { @@ -24,11 +25,16 @@ protected AbstractSettingsPane() { vGridBagConstraints.weightx = 1; } - protected GridBagConstraints createVerticalGridBagConstraints() { + protected GridBagConstraints createVerticalGridBagConstraints(int weigthy) { vGridBagConstraints.gridy = (vGridBagConstraints.gridy + 1) % Integer.MAX_VALUE; + vGridBagConstraints.weighty = weigthy; return vGridBagConstraints; } + protected GridBagConstraints createVerticalGridBagConstraints() { + return createVerticalGridBagConstraints(1); + } + protected JPanel createFieldset(String title) { JPanel fieldset = new JPanel(new GridBagLayout()); fieldset.setBorder(BorderFactory.createCompoundBorder( diff --git a/src/main/java/com/jvms/i18neditor/editor/Editor.java b/src/main/java/com/jvms/i18neditor/editor/Editor.java index c426969..a32373e 100644 --- a/src/main/java/com/jvms/i18neditor/editor/Editor.java +++ b/src/main/java/com/jvms/i18neditor/editor/Editor.java @@ -52,12 +52,12 @@ import javax.swing.plaf.basic.BasicSplitPaneUI; import javax.swing.tree.TreePath; -import org.apache.commons.lang3.LocaleUtils; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.jvms.i18neditor.Resource; @@ -70,6 +70,7 @@ import com.jvms.i18neditor.util.GithubRepoUtil; import com.jvms.i18neditor.util.GithubRepoUtil.GithubRepoReleaseData; import com.jvms.i18neditor.util.Images; +import com.jvms.i18neditor.util.Locales; import com.jvms.i18neditor.util.MessageBundle; import com.jvms.i18neditor.util.ResourceKeys; import com.jvms.i18neditor.util.Resources; @@ -86,10 +87,10 @@ public class Editor extends JFrame { public final static String TITLE = "i18n-editor"; public final static String VERSION = "1.0.0"; public final static String GITHUB_REPO = "jcbvm/i18n-editor"; - public final static String DEFAULT_RESOURCE_NAME = "translations"; public final static String PROJECT_FILE = ".i18n-editor-metadata"; public final static String SETTINGS_FILE = ".i18n-editor"; public final static String SETTINGS_DIR = System.getProperty("user.home"); + public final static String DEFAULT_RESOURCE_DEFINITION = "translations{_LOCALE}"; private EditorProject project; private EditorSettings settings = new EditorSettings(); @@ -121,7 +122,8 @@ public void createProject(Path dir, ResourceType type) { return; } - List resourceList = Resources.get(dir, settings.getResourceName(), Optional.empty()); + List resourceList = Resources.get(dir, + settings.getResourceFileDefinition(), settings.isUseResourceDirectories(), Optional.of(type)); if (!resourceList.isEmpty()) { boolean importProject = Dialogs.showConfirmDialog(this, MessageBundle.get("dialogs.project.new.conflict.title"), @@ -138,8 +140,9 @@ public void createProject(Path dir, ResourceType type) { restoreProjectState(project); project.setResourceType(type); - if (type == ResourceType.Properties) { - Resource resource = Resources.create(dir, type, Optional.empty(), project.getResourceName()); + if (!project.isUseResourceDirectories()) { + Resource resource = Resources.create(type, dir, + project.getResourceFileDefinition(), false, Optional.empty()); setupResource(resource); project.addResource(resource); } else { @@ -169,7 +172,8 @@ public void importProject(Path dir, boolean showEmptyProjectError) { restoreProjectState(project); Optional type = Optional.ofNullable(project.getResourceType()); - List resourceList = Resources.get(dir, project.getResourceName(), type); + List resourceList = Resources.get(dir, + project.getResourceFileDefinition(), project.isUseResourceDirectories(), type); Map keys = Maps.newTreeMap(); if (resourceList.isEmpty()) { @@ -371,13 +375,13 @@ public void showAddLocaleDialog() { MessageBundle.get("dialogs.locale.add.text"), JOptionPane.QUESTION_MESSAGE); if (localeString != null) { - localeString = localeString.trim(); - if (localeString.isEmpty()) { + Locale locale = Locales.parseLocale(localeString.trim()); + if (locale == null) { showError(MessageBundle.get("dialogs.locale.add.error.invalid")); } else { try { - Locale locale = LocaleUtils.toLocale(localeString); - Resource resource = Resources.create(path, type, Optional.of(locale), project.getResourceName()); + Resource resource = Resources.create(type, path, + project.getResourceFileDefinition(), project.isUseResourceDirectories(), Optional.of(locale)); addResource(resource); } catch (IOException e) { log.error("Error creating new locale", e); @@ -829,6 +833,7 @@ private void showError(String message) { } private void updateTreeNodeStatuses() { + if (project == null) return; Set keys = project.getResources().stream() .flatMap(r -> r.getTranslations().keySet().stream()) .filter(key -> project.getResources().stream().anyMatch(r -> !r.hasTranslation(key))) @@ -837,6 +842,7 @@ private void updateTreeNodeStatuses() { } private void updateTreeNodeStatus(String key) { + if (project == null) return; boolean hasError = project.getResources().stream().anyMatch(r -> !r.hasTranslation(key)); translationTree.updateNode(key, hasError); } @@ -845,8 +851,9 @@ private void storeProjectState() { ExtendedProperties props = new ExtendedProperties(); props.setProperty("minify_resources", project.isMinifyResources()); props.setProperty("plain_json", project.isPlainJSON()); - props.setProperty("resource_name", project.getResourceName()); props.setProperty("resource_type", project.getResourceType().toString()); + props.setProperty("resource_definition", project.getResourceFileDefinition()); + props.setProperty("resource_directories", project.isUseResourceDirectories()); props.store(Paths.get(project.getPath().toString(), PROJECT_FILE)); } @@ -857,11 +864,20 @@ private void restoreProjectState(EditorProject project) { props.load(Paths.get(project.getPath().toString(), PROJECT_FILE)); project.setMinifyResources(props.getBooleanProperty("minify_resources", settings.isMinifyResources())); project.setPlainJSON(props.getBooleanProperty("plain_json", settings.isPlainJSON())); - project.setResourceName(props.getProperty("resource_name", settings.getResourceName())); project.setResourceType(props.getEnumProperty("resource_type", ResourceType.class)); + String resourceName = props.getProperty("resource_name"); // for backwards compatibility + if (Strings.isNullOrEmpty(resourceName)) { + project.setResourceFileDefinition(props.getProperty("resource_definition", settings.getResourceFileDefinition())); + project.setUseResourceDirectories(props.getBooleanProperty("resource_directories", settings.isUseResourceDirectories())); + } else { + project.setResourceFileDefinition(resourceName); + project.setUseResourceDirectories(true); + } } else { - project.setResourceName(settings.getResourceName()); project.setMinifyResources(settings.isMinifyResources()); + project.setPlainJSON(settings.isPlainJSON()); + project.setResourceFileDefinition(settings.getResourceFileDefinition()); + project.setUseResourceDirectories(settings.isUseResourceDirectories()); } } @@ -874,7 +890,8 @@ private void storeEditorState() { props.setProperty("window_div_pos", contentPane.getDividerLocation()); props.setProperty("minify_resources", settings.isMinifyResources()); props.setProperty("plain_json", settings.isPlainJSON()); - props.setProperty("resource_name", settings.getResourceName()); + props.setProperty("resource_definition", settings.getResourceFileDefinition()); + props.setProperty("resource_directories", settings.isUseResourceDirectories()); props.setProperty("check_version", settings.isCheckVersionOnStartup()); props.setProperty("default_input_height", settings.getDefaultInputHeight()); props.setProperty("key_field_enabled", settings.isKeyFieldEnabled()); @@ -903,16 +920,17 @@ private void restoreEditorState() { settings.setWindowPositionX(props.getIntegerProperty("window_pos_x", 0)); settings.setWindowPositionY(props.getIntegerProperty("window_pos_y", 0)); settings.setWindowDeviderPosition(props.getIntegerProperty("window_div_pos", 250)); - settings.setHistory(props.getListProperty("history")); - settings.setLastExpandedNodes(props.getListProperty("last_expanded")); - settings.setLastSelectedNode(props.getProperty("last_selected")); - settings.setMinifyResources(props.getBooleanProperty("minify_resources", false)); - settings.setPlainJSON(props.getBooleanProperty("plain_json", false)); - settings.setResourceName(props.getProperty("resource_name", DEFAULT_RESOURCE_NAME)); settings.setCheckVersionOnStartup(props.getBooleanProperty("check_version", true)); settings.setDefaultInputHeight(props.getIntegerProperty("default_input_height", 5)); settings.setKeyFieldEnabled(props.getBooleanProperty("key_field_enabled", true)); settings.setDoubleClickTreeToggling(props.getBooleanProperty("double_click_tree_toggling", false)); + settings.setMinifyResources(props.getBooleanProperty("minify_resources", false)); + settings.setPlainJSON(props.getBooleanProperty("plain_json", false)); + settings.setHistory(props.getListProperty("history")); + settings.setLastExpandedNodes(props.getListProperty("last_expanded")); + settings.setLastSelectedNode(props.getProperty("last_selected")); + settings.setResourceFileDefinition(props.getProperty("resource_definition", DEFAULT_RESOURCE_DEFINITION)); + settings.setUseResourceDirectories(props.getBooleanProperty("resource_directories", false)); } private class TranslationTreeMouseListener extends MouseAdapter { diff --git a/src/main/java/com/jvms/i18neditor/editor/EditorProject.java b/src/main/java/com/jvms/i18neditor/editor/EditorProject.java index 0f6a9e0..bb4e711 100644 --- a/src/main/java/com/jvms/i18neditor/editor/EditorProject.java +++ b/src/main/java/com/jvms/i18neditor/editor/EditorProject.java @@ -15,14 +15,16 @@ */ public class EditorProject { private Path path; - private String resourceName; + private String resourceFileDefinition; private ResourceType resourceType; - private List resources = Lists.newLinkedList(); + private List resources; private boolean minifyResources; private boolean plainJSON; + private boolean resourceDirectories; public EditorProject(Path path) { this.path = path; + this.resources = Lists.newLinkedList(); } public Path getPath() { @@ -56,13 +58,21 @@ public void addResource(Resource resource) { public boolean hasResources() { return !resources.isEmpty(); } + + public boolean isUseResourceDirectories() { + return resourceDirectories; + } - public String getResourceName() { - return resourceName; + public void setUseResourceDirectories(boolean resourceDirectories) { + this.resourceDirectories = resourceDirectories; + } + + public String getResourceFileDefinition() { + return resourceFileDefinition; } - public void setResourceName(String resourceFilename) { - this.resourceName = resourceFilename; + public void setResourceFileDefinition(String resourceFileDefinition) { + this.resourceFileDefinition = resourceFileDefinition; } public boolean isMinifyResources() { diff --git a/src/main/java/com/jvms/i18neditor/editor/EditorProjectSettingsPane.java b/src/main/java/com/jvms/i18neditor/editor/EditorProjectSettingsPane.java index d0940b2..b8feb59 100644 --- a/src/main/java/com/jvms/i18neditor/editor/EditorProjectSettingsPane.java +++ b/src/main/java/com/jvms/i18neditor/editor/EditorProjectSettingsPane.java @@ -10,6 +10,7 @@ import javax.swing.JPanel; import com.jvms.i18neditor.ResourceType; +import com.jvms.i18neditor.swing.JHelpLabel; import com.jvms.i18neditor.swing.JTextField; import com.jvms.i18neditor.util.MessageBundle; @@ -29,7 +30,6 @@ public EditorProjectSettingsPane(Editor editor) { } private void setupUI() { - EditorSettings settings = editor.getSettings(); EditorProject project = editor.getProject(); // General settings @@ -41,25 +41,35 @@ private void setupUI() { minifyBox.addChangeListener(e -> project.setMinifyResources(minifyBox.isSelected())); fieldset1.add(minifyBox, createVerticalGridBagConstraints()); } + if (project.getResourceType().equals(ResourceType.JSON)) { JCheckBox plainJSONBox = new JCheckBox(MessageBundle.get("settings.plainJSON.title")); plainJSONBox.setSelected(project.isPlainJSON()); plainJSONBox.addChangeListener(e -> project.setPlainJSON(plainJSONBox.isSelected())); fieldset1.add(plainJSONBox, createVerticalGridBagConstraints()); } - JPanel resourcePanel = new JPanel(new GridLayout(0, 1)); - JLabel resourceNameLabel = new JLabel(MessageBundle.get("settings.resourcename.title")); - JTextField resourceNameField = new JTextField(project.getResourceName()); - resourceNameField.addKeyListener(new KeyAdapter() { + + JCheckBox useResourceDirsBox = new JCheckBox(MessageBundle.get("settings.resourcedirs.title")); + useResourceDirsBox.setSelected(project.isUseResourceDirectories()); + useResourceDirsBox.addChangeListener(e -> project.setUseResourceDirectories(useResourceDirsBox.isSelected())); + fieldset1.add(useResourceDirsBox, createVerticalGridBagConstraints()); + + JPanel resourceDefinitionPanel = new JPanel(new GridLayout(0, 1)); + JLabel resourceDefinitionLabel = new JLabel(MessageBundle.get("settings.resourcedef.title")); + JTextField resourceDefinitionField = new JTextField(project.getResourceFileDefinition()); + resourceDefinitionField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { - String value = resourceNameField.getText().trim(); - project.setResourceName(value.isEmpty() ? settings.getResourceName() : value); + String value = resourceDefinitionField.getText().trim(); + project.setResourceFileDefinition(value.isEmpty() ? Editor.DEFAULT_RESOURCE_DEFINITION : value); } }); - resourcePanel.add(resourceNameLabel); - resourcePanel.add(resourceNameField); - fieldset1.add(resourcePanel, createVerticalGridBagConstraints()); + resourceDefinitionPanel.add(resourceDefinitionLabel); + resourceDefinitionPanel.add(resourceDefinitionField); + fieldset1.add(resourceDefinitionPanel, createVerticalGridBagConstraints()); + + JHelpLabel resourceDefinitionHelpLabel = new JHelpLabel(MessageBundle.get("settings.resourcedef.help")); + fieldset1.add(resourceDefinitionHelpLabel, createVerticalGridBagConstraints(3)); setLayout(new GridBagLayout()); add(fieldset1, createVerticalGridBagConstraints()); diff --git a/src/main/java/com/jvms/i18neditor/editor/EditorSettings.java b/src/main/java/com/jvms/i18neditor/editor/EditorSettings.java index 381049f..67fc74e 100644 --- a/src/main/java/com/jvms/i18neditor/editor/EditorSettings.java +++ b/src/main/java/com/jvms/i18neditor/editor/EditorSettings.java @@ -13,7 +13,6 @@ public class EditorSettings { private int windowDeviderPosition; private int windowWidth; private int windowHeight; - private String resourceName; private boolean minifyResources; private boolean plainJSON; private List history; @@ -23,6 +22,8 @@ public class EditorSettings { private int defaultInputHeight; private boolean keyFieldEnabled; private boolean doubleClickTreeToggling; + private String resourceFileDifinition; + private boolean resourceDirectories; public int getWindowPositionX() { return windowPositionX; @@ -88,12 +89,12 @@ public void setLastSelectedNode(String lastSelectedNode) { this.lastSelectedNode = lastSelectedNode; } - public String getResourceName() { - return resourceName; + public String getResourceFileDefinition() { + return resourceFileDifinition; } - public void setResourceName(String resourceName) { - this.resourceName = resourceName; + public void setResourceFileDefinition(String resourceFileDifinition) { + this.resourceFileDifinition = resourceFileDifinition; } public boolean isMinifyResources() { @@ -143,4 +144,12 @@ public boolean isPlainJSON() { public void setPlainJSON(boolean plainJSON) { this.plainJSON = plainJSON; } + + public boolean isUseResourceDirectories() { + return resourceDirectories; + } + + public void setUseResourceDirectories(boolean resourceDirectories) { + this.resourceDirectories = resourceDirectories; + } } diff --git a/src/main/java/com/jvms/i18neditor/editor/EditorSettingsPane.java b/src/main/java/com/jvms/i18neditor/editor/EditorSettingsPane.java index b3927a3..7298a1e 100644 --- a/src/main/java/com/jvms/i18neditor/editor/EditorSettingsPane.java +++ b/src/main/java/com/jvms/i18neditor/editor/EditorSettingsPane.java @@ -10,6 +10,7 @@ import javax.swing.JPanel; import javax.swing.JSlider; +import com.jvms.i18neditor.swing.JHelpLabel; import com.jvms.i18neditor.swing.JTextField; import com.jvms.i18neditor.util.MessageBundle; @@ -47,19 +48,27 @@ private void setupUI() { minifyBox.addChangeListener(e -> settings.setMinifyResources(minifyBox.isSelected())); fieldset2.add(minifyBox, createVerticalGridBagConstraints()); - JPanel resourceNamePanel = new JPanel(new GridLayout(0, 1)); - JLabel resourceNameLabel = new JLabel(MessageBundle.get("settings.resourcename.title")); - JTextField resourceNameField = new JTextField(settings.getResourceName()); - resourceNameField.addKeyListener(new KeyAdapter() { + JCheckBox useResourceDirsBox = new JCheckBox(MessageBundle.get("settings.resourcedirs.title")); + useResourceDirsBox.setSelected(settings.isUseResourceDirectories()); + useResourceDirsBox.addChangeListener(e -> settings.setUseResourceDirectories(useResourceDirsBox.isSelected())); + fieldset2.add(useResourceDirsBox, createVerticalGridBagConstraints()); + + JPanel resourceDefinitionPanel = new JPanel(new GridLayout(0, 1)); + JLabel resourceDefinitionLabel = new JLabel(MessageBundle.get("settings.resourcedef.title")); + JTextField resourceDefinitionField = new JTextField(settings.getResourceFileDefinition()); + resourceDefinitionField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { - String value = resourceNameField.getText().trim(); - settings.setResourceName(value.isEmpty() ? Editor.DEFAULT_RESOURCE_NAME : value); + String value = resourceDefinitionField.getText().trim(); + settings.setResourceFileDefinition(value.isEmpty() ? Editor.DEFAULT_RESOURCE_DEFINITION : value); } }); - resourceNamePanel.add(resourceNameLabel); - resourceNamePanel.add(resourceNameField); - fieldset2.add(resourceNamePanel, createVerticalGridBagConstraints()); + resourceDefinitionPanel.add(resourceDefinitionLabel); + resourceDefinitionPanel.add(resourceDefinitionField); + fieldset2.add(resourceDefinitionPanel, createVerticalGridBagConstraints()); + + JHelpLabel resourceDefinitionHelpLabel = new JHelpLabel(MessageBundle.get("settings.resourcedef.help")); + fieldset2.add(resourceDefinitionHelpLabel, createVerticalGridBagConstraints(3)); // Editing settings JPanel fieldset3 = createFieldset(MessageBundle.get("settings.fieldset.editing")); diff --git a/src/main/java/com/jvms/i18neditor/swing/JHelpLabel.java b/src/main/java/com/jvms/i18neditor/swing/JHelpLabel.java new file mode 100644 index 0000000..4c1076f --- /dev/null +++ b/src/main/java/com/jvms/i18neditor/swing/JHelpLabel.java @@ -0,0 +1,26 @@ +package com.jvms.i18neditor.swing; + +import java.awt.Font; + +import javax.swing.JLabel; +import javax.swing.UIManager; + +/** + * This class extends a default {@link javax.swing.JLabel} with a custom look and feel + * for help messages. + * + * @author Jacob van Mourik + */ +public class JHelpLabel extends JLabel { + private final static long serialVersionUID = -6879887592161450052L; + + /** + * Constructs a {@link JHelpLabel}. + */ + public JHelpLabel(String text) { + super(text); + + setFont(getFont().deriveFont(Font.PLAIN, getFont().getSize()-1)); + setForeground(UIManager.getColor("Label.disabledForeground")); + } +} diff --git a/src/main/java/com/jvms/i18neditor/util/ChecksumException.java b/src/main/java/com/jvms/i18neditor/util/ChecksumException.java new file mode 100644 index 0000000..de99a93 --- /dev/null +++ b/src/main/java/com/jvms/i18neditor/util/ChecksumException.java @@ -0,0 +1,26 @@ +package com.jvms.i18neditor.util; + +import java.io.IOException; + +public class ChecksumException extends IOException { + private final static long serialVersionUID = -5164866588227844439L; + + /** + * Constructs an {@code ChecksumException} with {@code null} + * as its error detail message. + */ + public ChecksumException() { + super(); + } + + /** + * Constructs an {@code ChecksumException} with the specified detail message. + * + * @param message + * The detail message (which is saved for later retrieval + * by the {@link #getMessage()} method) + */ + public ChecksumException(String message) { + super(message); + } +} diff --git a/src/main/java/com/jvms/i18neditor/util/Locales.java b/src/main/java/com/jvms/i18neditor/util/Locales.java new file mode 100644 index 0000000..126e7d7 --- /dev/null +++ b/src/main/java/com/jvms/i18neditor/util/Locales.java @@ -0,0 +1,34 @@ +package com.jvms.i18neditor.util; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Strings; + +/** + * This class provides utility functions for locales. + * + * @author Jacob van Mourik + */ +public final class Locales { + public final static String LOCALE_REGEX = "([^_-]*)(?:[_-]([^_-]*)(?:[_-]([^_-]*))?)?"; + public final static Pattern LOCALE_PATTERN = Pattern.compile(LOCALE_REGEX); + + public static Locale parseLocale(String localeString) { + if (Strings.isNullOrEmpty(localeString)) { + return null; + } + Matcher matcher = LOCALE_PATTERN.matcher(localeString); + if (matcher.matches()) { + String language = matcher.group(1); + language = (language == null) ? "" : language; + String country = matcher.group(2); + country = (country == null) ? "" : country; + String variant = matcher.group(3); + variant = (variant == null) ? "" : variant; + return new Locale(language, country, variant); + } + return null; + } +} diff --git a/src/main/java/com/jvms/i18neditor/util/MessageBundle.java b/src/main/java/com/jvms/i18neditor/util/MessageBundle.java index c20562c..2098f89 100644 --- a/src/main/java/com/jvms/i18neditor/util/MessageBundle.java +++ b/src/main/java/com/jvms/i18neditor/util/MessageBundle.java @@ -1,7 +1,6 @@ package com.jvms.i18neditor.util; import java.text.MessageFormat; -import java.util.Locale; import java.util.ResourceBundle; /** @@ -17,7 +16,7 @@ public final class MessageBundle { private final static ResourceBundle RESOURCES; static { - RESOURCES = ResourceBundle.getBundle(RESOURCES_PATH, Locale.getDefault()); + RESOURCES = ResourceBundle.getBundle(RESOURCES_PATH); } /** diff --git a/src/main/java/com/jvms/i18neditor/util/Resources.java b/src/main/java/com/jvms/i18neditor/util/Resources.java index 82ce310..9226018 100644 --- a/src/main/java/com/jvms/i18neditor/util/Resources.java +++ b/src/main/java/com/jvms/i18neditor/util/Resources.java @@ -1,10 +1,13 @@ package com.jvms.i18neditor.util; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Locale; import java.util.Map; @@ -14,7 +17,6 @@ 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; @@ -33,55 +35,72 @@ * @author Jacob van Mourik */ 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})?"; + private final static Charset UTF8_ENCODING; + private final static String FILENAME_LOCALE_REGEX; + + static { + UTF8_ENCODING = Charset.forName("UTF-8"); + FILENAME_LOCALE_REGEX = Pattern.quote("{") + "(.*)" + Pattern.quote("LOCALE") + "(.*)" + Pattern.quote("}"); + } /** - * Gets all resources from the given {@code rootDir} directory path. + * Gets all resources from the given rootDir directory path. + * + *

The fileDefinition is the filename definition of resource files to look for. + * The definition consists of a filename including optional locale part (see useLocaleDirs). + * The locale part should be in the format: {LOCALE}, where { and } tags + * defines the start and end of the locale part and LOCALE the location of the locale itself.

* - *

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 returned.

+ *

When a resource type is given, only resources of that type will returned.

* *

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 root the root directory of the resources + * @param fileDefinition the resource's file definition for lookup (using locale interpolation) + * @param directories whether to look for resources in (locale) directories only * @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 { + public static List get(Path root, String fileDefinition, boolean directories, Optional type) + throws IOException { List result = Lists.newLinkedList(); - List files = Files.walk(rootDir, 1).collect(Collectors.toList()); - for (Path p : files) { - ResourceType resourceType = null; - for (ResourceType t : ResourceType.values()) { - if (isResourceType(type, t) && isResource(rootDir, p, t, baseName)) { - resourceType = t; - break; - } + List files = Files.walk(root, 1).collect(Collectors.toList()); + String defaultFileName = getFilename(fileDefinition, Optional.empty()); + Pattern fileDefinitionPattern = Pattern.compile("^" + getFilenameRegex(fileDefinition) + "$"); + + for (Path file : files) { + Path parent = file.getParent(); + if (parent == null || Files.isSameFile(root, file) || !Files.isSameFile(root, parent)) { + continue; } - if (resourceType != null) { - 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)); + String filename = com.google.common.io.Files.getNameWithoutExtension(file.getFileName().toString()); + for (ResourceType rt : ResourceType.values()) { + if (!type.orElse(rt).equals(rt)) { + continue; + } + if (directories && Files.isDirectory(file)) { + Locale locale = Locales.parseLocale(filename); + if (locale == null) { + continue; } - 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); + Path rf = Paths.get(root.toString(), locale.toString(), getFilename(fileDefinition, Optional.of(locale)) + rt.getExtension()); + if (Files.isRegularFile(rf)) { + result.add(new Resource(rt, rf, locale)); + } + } + if (!directories && Files.isRegularFile(file)) { + Matcher matcher = fileDefinitionPattern.matcher(filename); + if (!matcher.matches() && !filename.equals(defaultFileName)) { + continue; + } + Locale locale = matcher.matches() ? Locales.parseLocale(matcher.group(1)) : null; + result.add(new Resource(rt, file, locale)); } - result.add(new Resource(resourceType, path, locale)); } }; + return result; } @@ -107,6 +126,7 @@ public static void load(Resource resource) throws IOException { translations = fromJson(content); } resource.setTranslations(translations); + resource.setChecksum(createChecksum(resource)); } /** @@ -114,10 +134,16 @@ public static void load(Resource resource) throws IOException { * * @param resource the resource to write. * @param prettyPrinting whether to pretty print the contents - * @param plainKeys + * @param plainKeys * @throws IOException if an I/O error occurs writing the file. */ public static void write(Resource resource, boolean prettyPrinting, boolean plainKeys) throws IOException { + if (resource.getChecksum() != null) { + String checksum = createChecksum(resource); + if (!checksum.equals(resource.getChecksum())) { + throw new ChecksumException("File on disk has been changed."); + } + } ResourceType type = resource.getType(); if (type == ResourceType.Properties) { ExtendedProperties content = toProperties(resource.getTranslations()); @@ -133,6 +159,7 @@ public static void write(Resource resource, boolean prettyPrinting, boolean plai } Files.write(resource.getPath(), Lists.newArrayList(content), UTF8_ENCODING); } + resource.setChecksum(createChecksum(resource)); } /** @@ -142,39 +169,32 @@ public static void write(Resource resource, boolean prettyPrinting, boolean plai * * @param type the type of the resource to create. * @param root the root directory to write the resource to. + * @param filenameDefinition the filename definition of the resource. + * @param directory whether to store the translation into a (locale) directory + * @param locale the locale of the resource (optional). * @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 { + public static Resource create(ResourceType type, Path root, String fileDefinition, boolean directory, Optional locale) + throws IOException { String extension = type.getExtension(); Path path; - if (type.isEmbedLocale()) { - path = Paths.get(root.toString(), baseName + (locale.isPresent() ? "_" + locale.get().toString() : "") + extension); + if (directory) { + path = Paths.get(root.toString(), locale.get().toString(), getFilename(fileDefinition, locale) + extension); } else { - path = Paths.get(root.toString(), locale.get().toString(), baseName + extension); + path = Paths.get(root.toString(), getFilename(fileDefinition, locale) + extension); } Resource resource = new Resource(type, path, locale.orElse(null)); write(resource, false, false); return resource; } - private static boolean isResource(Path root, Path path, ResourceType type, String baseName) throws IOException { - String extension = type.getExtension(); - Path parent = path.getParent(); - if (parent == null || Files.isSameFile(root, path) || !Files.isSameFile(root, parent)) { - return false; - } else 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 String getFilenameRegex(String fileDefinition) { + return fileDefinition.replaceAll(FILENAME_LOCALE_REGEX, "$1(" + Locales.LOCALE_REGEX + ")$2"); } - private static boolean isResourceType(Optional a, ResourceType b) { - return !a.isPresent() || a.get() == b; + private static String getFilename(String fileDefinition, Optional locale) { + return fileDefinition.replaceAll(FILENAME_LOCALE_REGEX, locale.isPresent() ? ("$1" + locale.get().toString() + "$2") : ""); } private static SortedMap fromProperties(ExtendedProperties properties) { @@ -258,4 +278,26 @@ private static String es6ToJson(String content) { private static String jsonToEs6(String content) { return "export default " + content + ";"; } + + private static String createChecksum(Resource resource) throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + byte[] buffer = new byte[1024]; + int bytesRead = 0; + try (InputStream is = Files.newInputStream(resource.getPath())) { + while ((bytesRead = is.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + } + String result = ""; + byte[] bytes = digest.digest(); + for (int i = 0; i < bytes.length; i++) { + result += Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1); + } + return result; + } } diff --git a/src/main/resources/bundles/messages.properties b/src/main/resources/bundles/messages.properties index 6d26699..5c638d2 100644 --- a/src/main/resources/bundles/messages.properties +++ b/src/main/resources/bundles/messages.properties @@ -89,7 +89,9 @@ settings.fieldset.newprojects = New Projects settings.inputheight.title = Default height of input fields settings.keyfield.title = Show translation key field settings.minify.title = Minify translations on save -settings.resourcename.title = Translations filename +settings.resourcedef.help = You can use '{' '}' tags to specify the locale part within the filename.
The text LOCALE within this tags will be replaced by the actual locale.
Example: translations'{'_LOCALE'}' will become translations_en_US. +settings.resourcedef.title = Translations filename +settings.resourcedirs.title = Save translation files in separate folders settings.treetogglemode.title = Expand and collapse translations using double click settings.checkversion.title = Check for new version on startup settings.plainJSON.title= Plain JSON keys diff --git a/src/main/resources/bundles/messages_nl.properties b/src/main/resources/bundles/messages_nl.properties index fabea37..05926c6 100644 --- a/src/main/resources/bundles/messages_nl.properties +++ b/src/main/resources/bundles/messages_nl.properties @@ -89,7 +89,9 @@ settings.fieldset.newprojects = Nieuwe Projecten settings.inputheight.title = Standaardhoogte van invoervelden settings.keyfield.title = Toon vertalingskey veld settings.minify.title = Comprimeer vertalingen bij opslaan -settings.resourcename.title = Bestandsnaam vertalingen +settings.resourcedef.help = U kunt met '{' '}' tags het locale gedeelte aangeven binnen de bestandsnaam.
De tekst LOCALE binnen deze tags zal vervangen worden door de werkelijke locale.
Voorbeeld: translations'{'_LOCALE'}' wordt translations_nl_NL. +settings.resourcedef.title = Bestandsnaam vertalingen +settings.resourcedirs.title = Vertalingsbestanden opslaan in afzonderlijke mappen settings.treetogglemode.title = Vertalingen in- en uitvouwen met dubbelklik settings.checkversion.title = Controleer op nieuwe versie bij opstarten settings.plainJSON.title= Plain JSON keys diff --git a/src/main/resources/bundles/messages_pt_BR.properties b/src/main/resources/bundles/messages_pt_BR.properties index 086004d..e203ab5 100644 --- a/src/main/resources/bundles/messages_pt_BR.properties +++ b/src/main/resources/bundles/messages_pt_BR.properties @@ -89,7 +89,9 @@ settings.fieldset.newprojects = Novos Projetos settings.keyfield.title = Mostrar campo da chave de tradu\u00e7\u00e3o settings.inputheight.title = Altura padr\u00e3o dos campos de entrada settings.minify.title = Minificar tradu\u00e7\u00f5es ao salvar -settings.resourcename.title = Nome do arquivo de tradu\u00e7\u00e3es +settings.resourcedef.help = You can use '{' '}' tags to specify the locale part within the filename.
The text LOCALE within this tags will be replaced by the actual locale.
Example: translations'{'_LOCALE'}' will become translations_en_US. +settings.resourcedef.title = Nome do arquivo de tradu\u00e7\u00e3es +settings.resourcedirs.title = Save translation files in locale folders settings.treetogglemode.title = Expandir e contrair tradu\u00e7\u00f5es usando duplo clique settings.checkversion.title = Verificar a nova vers\u00e3o na inicializa\u00e7\u00e3o settings.plainJSON.title= Plain JSON keys