├── .gitignore ├── .travis.yml ├── screenshot-1.jpg ├── screenshot-2.jpg ├── src ├── main │ ├── resources │ │ ├── images │ │ │ ├── icon.ico │ │ │ ├── icon-16.png │ │ │ ├── icon-20.png │ │ │ ├── icon-24.png │ │ │ ├── icon-32.png │ │ │ ├── icon-48.png │ │ │ ├── icon-64.png │ │ │ ├── icons.icns │ │ │ ├── icon-128.png │ │ │ ├── icon-256.png │ │ │ ├── icon-512.png │ │ │ ├── icon-intro.png │ │ │ └── icon-folder.png │ │ └── bundles │ │ │ ├── .i18n-editor-metadata │ │ │ ├── messages.properties │ │ │ ├── messages_pt_BR.properties │ │ │ ├── messages_nl.properties │ │ │ └── messages_es_ES.properties │ └── java │ │ └── com │ │ └── jvms │ │ └── i18neditor │ │ ├── FileStructure.java │ │ ├── ResourceListener.java │ │ ├── editor │ │ ├── ResourcesPaneMenu.java │ │ ├── menu │ │ │ ├── ExpandTranslationsMenuItem.java │ │ │ ├── CollapseTranslationsMenuItem.java │ │ │ ├── RemoveTranslationMenuItem.java │ │ │ ├── AddLocaleMenuItem.java │ │ │ ├── RenameTranslationMenuItem.java │ │ │ ├── FindTranslationMenuItem.java │ │ │ ├── DuplicateTranslationMenuItem.java │ │ │ ├── CopyTranslationKeyToClipboardMenuItem.java │ │ │ └── AddTranslationMenuItem.java │ │ ├── TranslationKeyField.java │ │ ├── TranslationTreeMenu.java │ │ ├── TranslationTreeNodeMenu.java │ │ ├── TranslationTreeToggleIcon.java │ │ ├── TranslationTreeStatusIcon.java │ │ ├── TranslationKeyCaret.java │ │ ├── ResourceField.java │ │ ├── TranslationTreeCellRenderer.java │ │ ├── TranslationTreeUI.java │ │ ├── TranslationTreeNode.java │ │ ├── EditorProject.java │ │ ├── AbstractSettingsPane.java │ │ ├── TranslationTreeModel.java │ │ ├── EditorProjectSettingsPane.java │ │ ├── EditorSettings.java │ │ ├── EditorSettingsPane.java │ │ ├── TranslationTree.java │ │ └── EditorMenuBar.java │ │ ├── ResourceEvent.java │ │ ├── swing │ │ ├── text │ │ │ ├── BlinkCaret.java │ │ │ ├── DeleteAction.java │ │ │ ├── SelectAllAction.java │ │ │ ├── JTextComponentMenuListener.java │ │ │ └── JTextComponentMenu.java │ │ ├── RedoAction.java │ │ ├── UndoAction.java │ │ ├── JHelpLabel.java │ │ ├── JHtmlPane.java │ │ ├── event │ │ │ └── RequestInitialFocusListener.java │ │ ├── JScrollablePanel.java │ │ ├── JTextArea.java │ │ ├── JTextField.java │ │ └── util │ │ │ └── Dialogs.java │ │ ├── ResourceType.java │ │ ├── io │ │ └── ChecksumException.java │ │ ├── util │ │ ├── Images.java │ │ ├── Locales.java │ │ ├── Colors.java │ │ ├── MessageBundle.java │ │ ├── GithubRepoUtil.java │ │ ├── ResourceKeys.java │ │ ├── ExtendedProperties.java │ │ └── Resources.java │ │ ├── Main.java │ │ └── Resource.java └── test │ └── java │ └── com │ └── jvms │ └── i18neditor │ ├── util │ └── ResourceKeysTest.java │ └── ResourceTest.java ├── .settings ├── org.eclipse.m2e.core.prefs └── org.eclipse.jdt.core.prefs ├── .project ├── LICENSE ├── .classpath ├── .inno.iss ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | dist/ 3 | .idea 4 | i18n-editor.iml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: mvn compile 3 | jdk: 4 | - oraclejdk8 -------------------------------------------------------------------------------- /screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/screenshot-1.jpg -------------------------------------------------------------------------------- /screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/screenshot-2.jpg -------------------------------------------------------------------------------- /src/main/resources/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon.ico -------------------------------------------------------------------------------- /src/main/resources/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-16.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-20.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-24.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-32.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-48.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-64.png -------------------------------------------------------------------------------- /src/main/resources/images/icons.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icons.icns -------------------------------------------------------------------------------- /src/main/resources/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-128.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-256.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-512.png -------------------------------------------------------------------------------- /src/main/resources/images/icon-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-intro.png -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /src/main/resources/images/icon-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcbvm/i18n-editor/HEAD/src/main/resources/images/icon-folder.png -------------------------------------------------------------------------------- /src/main/resources/bundles/.i18n-editor-metadata: -------------------------------------------------------------------------------- 1 | flatten_json=0 2 | minify_resources=0 3 | resource_definition=messages{_LOCALE} 4 | resource_structure=Flat 5 | resource_type=Properties 6 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/FileStructure.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | /** 4 | * An enum describing file structures. 5 | * 6 | * @author Jacob van Mourik 7 | */ 8 | public enum FileStructure { 9 | Flat, 10 | Nested 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/ResourceListener.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | import java.util.EventListener; 4 | 5 | /** 6 | * Defines an object which listens for {@link ResourceEvent}s. 7 | * 8 | * @author Jacob van Mourik 9 | */ 10 | public interface ResourceListener extends EventListener { 11 | 12 | /** 13 | * Invoked when the target {@link Resource} of the listener has changed its data. 14 | * 15 | * @param e the resource event. 16 | */ 17 | void resourceChanged(ResourceEvent e); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/ResourcesPaneMenu.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import javax.swing.JPopupMenu; 4 | 5 | import com.jvms.i18neditor.editor.menu.AddLocaleMenuItem; 6 | 7 | /** 8 | * This class represents a right click menu for the resource pane. 9 | * 10 | * @author Jacob van Mourik 11 | */ 12 | public class ResourcesPaneMenu extends JPopupMenu { 13 | private final static long serialVersionUID = 2259323824622576156L; 14 | 15 | public ResourcesPaneMenu(Editor editor) { 16 | super(); 17 | add(new AddLocaleMenuItem(editor, true)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | editor 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/ResourceEvent.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | /** 4 | * An event wrapper for a {@link Resource}. 5 | * 6 | * @author Jacob van Mourik 7 | */ 8 | public class ResourceEvent { 9 | private final Resource resource; 10 | 11 | /** 12 | * Creates an event object for a {@link Resource}. 13 | * 14 | * @param resource the resource. 15 | */ 16 | public ResourceEvent(Resource resource) { 17 | this.resource = resource; 18 | } 19 | 20 | /** 21 | * Gets the resource wrapped by this event object. 22 | * 23 | * @return the resource. 24 | */ 25 | public Resource getResource() { 26 | return resource; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/text/BlinkCaret.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.text; 2 | 3 | import javax.swing.UIManager; 4 | import javax.swing.text.DefaultCaret; 5 | 6 | /** 7 | * This class extends the {@link DefaultCaret} with a default blink rate set. 8 | * 9 | * @author Jacob van Mourik 10 | */ 11 | public class BlinkCaret extends DefaultCaret { 12 | private final static long serialVersionUID = -3365578081904749196L; 13 | 14 | public BlinkCaret() { 15 | int blinkRate = 0; 16 | Object o = UIManager.get("TextArea.caretBlinkRate"); 17 | if (o != null && o instanceof Integer) { 18 | blinkRate = ((Integer) o).intValue(); 19 | } 20 | setBlinkRate(blinkRate); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/ResourceType.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | /** 4 | * An enum describing the type of a {@link Resource}. 5 | * 6 | *

A resource type additionally holds information about the filename representation.

7 | * 8 | * @author Jacob van Mourik 9 | */ 10 | public enum ResourceType { 11 | JSON(".json"), 12 | ES6(".js"), 13 | Properties(".properties"); 14 | 15 | private final String extension; 16 | 17 | /** 18 | * Gets the file extension of the resource type. 19 | * 20 | * @return the file extension. 21 | */ 22 | public String getExtension() { 23 | return extension; 24 | } 25 | 26 | private ResourceType(String extension) { 27 | this.extension = extension; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/ExpandTranslationsMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import javax.swing.JMenuItem; 4 | 5 | import com.jvms.i18neditor.editor.TranslationTree; 6 | import com.jvms.i18neditor.util.MessageBundle; 7 | 8 | /** 9 | * This class represents a menu item for expanding all keys in of the translation tree. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class ExpandTranslationsMenuItem extends JMenuItem { 14 | private final static long serialVersionUID = 7316102121075733726L; 15 | 16 | public ExpandTranslationsMenuItem(TranslationTree tree) { 17 | super(MessageBundle.get("menu.view.expand.title")); 18 | addActionListener(e -> tree.expandAll()); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/CollapseTranslationsMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import javax.swing.JMenuItem; 4 | 5 | import com.jvms.i18neditor.editor.TranslationTree; 6 | import com.jvms.i18neditor.util.MessageBundle; 7 | 8 | /** 9 | * This class represents a menu item for collapsing all keys of the translation tree. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class CollapseTranslationsMenuItem extends JMenuItem { 14 | private final static long serialVersionUID = 7885728865417192564L; 15 | 16 | public CollapseTranslationsMenuItem(TranslationTree tree) { 17 | super(MessageBundle.get("menu.view.collapse.title")); 18 | addActionListener(e -> tree.collapseAll()); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/text/DeleteAction.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.text; 2 | 3 | import java.awt.event.ActionEvent; 4 | 5 | import javax.swing.text.JTextComponent; 6 | import javax.swing.text.TextAction; 7 | 8 | /** 9 | * An action implementation useful for deleting text. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class DeleteAction extends TextAction { 14 | private final static long serialVersionUID = -7933405670677160997L; 15 | 16 | public DeleteAction(String name) { 17 | super(name); 18 | } 19 | 20 | @Override 21 | public void actionPerformed(ActionEvent e) { 22 | JTextComponent component = getFocusedComponent(); 23 | component.replaceSelection(""); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/text/SelectAllAction.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.text; 2 | 3 | import java.awt.event.ActionEvent; 4 | 5 | import javax.swing.text.JTextComponent; 6 | import javax.swing.text.TextAction; 7 | 8 | /** 9 | * An action implementation useful for selecting all text. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class SelectAllAction extends TextAction { 14 | private final static long serialVersionUID = -4913270947629733919L; 15 | 16 | public SelectAllAction(String name) { 17 | super(name); 18 | } 19 | 20 | @Override 21 | public void actionPerformed(ActionEvent e) { 22 | JTextComponent component = getFocusedComponent(); 23 | component.selectAll(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/RedoAction.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.event.ActionEvent; 4 | 5 | import javax.swing.AbstractAction; 6 | import javax.swing.undo.UndoManager; 7 | 8 | /** 9 | * An action implementation useful for redoing an edit. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class RedoAction extends AbstractAction { 14 | private final static long serialVersionUID = -3051499148079684354L; 15 | private final UndoManager undoManager; 16 | 17 | public RedoAction(UndoManager undoManager) { 18 | super(); 19 | this.undoManager = undoManager; 20 | } 21 | 22 | @Override 23 | public void actionPerformed(ActionEvent e) { 24 | if (undoManager.canRedo()) { 25 | undoManager.redo(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/UndoAction.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.event.ActionEvent; 4 | 5 | import javax.swing.AbstractAction; 6 | import javax.swing.undo.UndoManager; 7 | 8 | /** 9 | * An action implementation useful for undoing an edit. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class UndoAction extends AbstractAction { 14 | private final static long serialVersionUID = -3051499148079684354L; 15 | private final UndoManager undoManager; 16 | 17 | public UndoAction(UndoManager undoManager) { 18 | super(); 19 | this.undoManager = undoManager; 20 | } 21 | 22 | @Override 23 | public void actionPerformed(ActionEvent e) { 24 | if (undoManager.canUndo()) { 25 | undoManager.undo(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate 4 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 5 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 6 | org.eclipse.jdt.core.compiler.compliance=1.8 7 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 8 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 9 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 10 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 11 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 12 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 13 | org.eclipse.jdt.core.compiler.source=1.8 14 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/io/ChecksumException.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.io; 2 | 3 | import java.io.IOException; 4 | 5 | public class ChecksumException extends IOException { 6 | private final static long serialVersionUID = -5164866588227844439L; 7 | 8 | /** 9 | * Constructs an {@code ChecksumException} with {@code null} 10 | * as its error detail message. 11 | */ 12 | public ChecksumException() { 13 | super(); 14 | } 15 | 16 | /** 17 | * Constructs an {@code ChecksumException} with the specified detail message. 18 | * 19 | * @param message 20 | * The detail message (which is saved for later retrieval 21 | * by the {@link #getMessage()} method) 22 | */ 23 | public ChecksumException(String message) { 24 | super(message); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/JHelpLabel.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.Dimension; 4 | import java.awt.Font; 5 | 6 | import javax.swing.JLabel; 7 | import javax.swing.UIManager; 8 | 9 | /** 10 | * This class extends a default {@link javax.swing.JLabel} with a custom look and feel 11 | * for help messages. 12 | * 13 | * @author Jacob van Mourik 14 | */ 15 | public class JHelpLabel extends JLabel { 16 | private final static long serialVersionUID = -6879887592161450052L; 17 | 18 | /** 19 | * Constructs a {@link JHelpLabel}. 20 | */ 21 | public JHelpLabel(String text) { 22 | super(text); 23 | 24 | Dimension size = getSize(); 25 | size.height -= 10; 26 | setSize(size); 27 | setFont(getFont().deriveFont(Font.PLAIN, getFont().getSize()-1)); 28 | setForeground(UIManager.getColor("Label.disabledForeground")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationKeyField.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import com.jvms.i18neditor.swing.JTextField; 4 | 5 | /** 6 | * This class represents a text field for displaying, finding and adding a translation key. 7 | * 8 | * @author Jacob van Mourik 9 | */ 10 | public class TranslationKeyField extends JTextField { 11 | private final static long serialVersionUID = -3951187528785224704L; 12 | 13 | public TranslationKeyField() { 14 | super(); 15 | setupUI(); 16 | } 17 | 18 | public void clear() { 19 | setValue(null); 20 | } 21 | 22 | public String getValue() { 23 | return getText().trim(); 24 | } 25 | 26 | public void setValue(String value) { 27 | setText(value); 28 | undoManager.discardAllEdits(); 29 | } 30 | 31 | private void setupUI() { 32 | setEditable(false); 33 | setCaret(new TranslationKeyCaret()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/Images.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.net.URL; 4 | 5 | import javax.swing.ImageIcon; 6 | 7 | /** 8 | * This class provides utility functions for loading images. 9 | * 10 | * @author Jacob van Mourik 11 | */ 12 | public final class Images { 13 | 14 | /** 15 | * Loads an image icon from the current classpath. 16 | * 17 | * @param path the path of the image to load 18 | * @return the image icon 19 | */ 20 | public static ImageIcon loadFromClasspath(String path) { 21 | return new ImageIcon(getClasspathURL(path)); 22 | } 23 | 24 | /** 25 | * Gets the URL of an image from the current classpath. 26 | * 27 | * @param path the path of the image to load 28 | * @return the image icon 29 | */ 30 | public static URL getClasspathURL(String path) { 31 | return Images.class.getClassLoader().getResource(path); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/RemoveTranslationMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.event.KeyEvent; 4 | 5 | import javax.swing.JMenuItem; 6 | import javax.swing.KeyStroke; 7 | 8 | import com.jvms.i18neditor.editor.Editor; 9 | import com.jvms.i18neditor.util.MessageBundle; 10 | 11 | /** 12 | * This class represents a menu item for removing a translation key. 13 | * 14 | * @author Jacob van Mourik 15 | */ 16 | public class RemoveTranslationMenuItem extends JMenuItem { 17 | private final static long serialVersionUID = 5207946396515235714L; 18 | 19 | public RemoveTranslationMenuItem(Editor editor, boolean enabled) { 20 | super(MessageBundle.get("menu.edit.delete.title")); 21 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); 22 | addActionListener(e -> editor.removeSelectedTranslation()); 23 | setEnabled(enabled); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/AddLocaleMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.JMenuItem; 7 | import javax.swing.KeyStroke; 8 | 9 | import com.jvms.i18neditor.editor.Editor; 10 | import com.jvms.i18neditor.util.MessageBundle; 11 | 12 | /** 13 | * This class represents a menu item for adding a new locale. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class AddLocaleMenuItem extends JMenuItem { 18 | private final static long serialVersionUID = -5108677891532028898L; 19 | 20 | public AddLocaleMenuItem(Editor editor, boolean enabled) { 21 | super(MessageBundle.get("menu.edit.add.locale.title")); 22 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 23 | addActionListener(e -> editor.showAddLocaleDialog()); 24 | setEnabled(enabled); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/RenameTranslationMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.JMenuItem; 7 | import javax.swing.KeyStroke; 8 | 9 | import com.jvms.i18neditor.editor.Editor; 10 | import com.jvms.i18neditor.util.MessageBundle; 11 | 12 | /** 13 | * This class represents a menu item for renaming a translation key. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class RenameTranslationMenuItem extends JMenuItem { 18 | private final static long serialVersionUID = 907122077814626286L; 19 | 20 | public RenameTranslationMenuItem(Editor editor, boolean enabled) { 21 | super(MessageBundle.get("menu.edit.rename.title")); 22 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 23 | addActionListener(e -> editor.renameSelectedTranslation()); 24 | setEnabled(enabled); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/FindTranslationMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.JMenuItem; 7 | import javax.swing.KeyStroke; 8 | 9 | import com.jvms.i18neditor.editor.Editor; 10 | import com.jvms.i18neditor.util.MessageBundle; 11 | 12 | /** 13 | * This class represents a menu item for searching a translation key. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class FindTranslationMenuItem extends JMenuItem { 18 | private final static long serialVersionUID = -1298283182450978961L; 19 | 20 | public FindTranslationMenuItem(Editor editor, boolean enabled) { 21 | super(MessageBundle.get("menu.edit.find.translation.title")); 22 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 23 | addActionListener(e -> editor.showFindTranslationDialog()); 24 | setEnabled(enabled); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/DuplicateTranslationMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.JMenuItem; 7 | import javax.swing.KeyStroke; 8 | 9 | import com.jvms.i18neditor.editor.Editor; 10 | import com.jvms.i18neditor.util.MessageBundle; 11 | 12 | /** 13 | * This class represents a menu item for duplicating a translation key. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class DuplicateTranslationMenuItem extends JMenuItem { 18 | private final static long serialVersionUID = 5207946396515235714L; 19 | 20 | public DuplicateTranslationMenuItem(Editor editor, boolean enabled) { 21 | super(MessageBundle.get("menu.edit.duplicate.title")); 22 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 23 | addActionListener(e -> editor.duplicateSelectedTranslation()); 24 | setEnabled(enabled); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeMenu.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import javax.swing.JPopupMenu; 4 | 5 | import com.jvms.i18neditor.editor.menu.AddTranslationMenuItem; 6 | import com.jvms.i18neditor.editor.menu.CollapseTranslationsMenuItem; 7 | import com.jvms.i18neditor.editor.menu.ExpandTranslationsMenuItem; 8 | import com.jvms.i18neditor.editor.menu.FindTranslationMenuItem; 9 | 10 | /** 11 | * This class represents a right click menu for the translation tree. 12 | * 13 | * @author Jacob van Mourik 14 | */ 15 | public class TranslationTreeMenu extends JPopupMenu { 16 | private final static long serialVersionUID = -8450484152294368841L; 17 | 18 | public TranslationTreeMenu(Editor editor, TranslationTree tree) { 19 | super(); 20 | add(new AddTranslationMenuItem(editor, tree, true)); 21 | add(new FindTranslationMenuItem(editor, true)); 22 | addSeparator(); 23 | add(new ExpandTranslationsMenuItem(tree)); 24 | add(new CollapseTranslationsMenuItem(tree)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/JHtmlPane.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.Component; 4 | import java.awt.Desktop; 5 | 6 | import javax.swing.JEditorPane; 7 | import javax.swing.event.HyperlinkEvent; 8 | 9 | /** 10 | * This class extends a default {@link JEditorPane} with default settings for HTML content. 11 | * 12 | * @author Jacob van Mourik 13 | */ 14 | public class JHtmlPane extends JEditorPane { 15 | private final static long serialVersionUID = 2873290055720408299L; 16 | 17 | public JHtmlPane(Component parent, String content) { 18 | super("text/html", content); 19 | setEditable(false); 20 | setBackground(parent.getBackground()); 21 | addHyperlinkListener(e -> { 22 | if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) { 23 | try { 24 | Desktop.getDesktop().browse(e.getURL().toURI()); 25 | } catch (Exception e1) { 26 | // 27 | } 28 | } 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/event/RequestInitialFocusListener.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.event; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.*; 5 | 6 | /** 7 | * This class implements a {@link AncestorListener} to request initial focus on a component. 8 | * 9 | *

When the component is added to an active ancestor the component will request focus immediately.
10 | * When the component is added to a non active ancestor, the focus request will be made once the ancestor is active.

11 | * 12 | * @author Jacob van Mourik 13 | */ 14 | public class RequestInitialFocusListener implements AncestorListener { 15 | 16 | @Override 17 | public void ancestorAdded(AncestorEvent e) { 18 | JComponent component = e.getComponent(); 19 | component.requestFocusInWindow(); 20 | component.removeAncestorListener(this); 21 | } 22 | 23 | @Override 24 | public void ancestorMoved(AncestorEvent e) { 25 | // 26 | } 27 | 28 | @Override 29 | public void ancestorRemoved(AncestorEvent e) { 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/CopyTranslationKeyToClipboardMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.JMenuItem; 7 | import javax.swing.KeyStroke; 8 | 9 | import com.jvms.i18neditor.editor.Editor; 10 | import com.jvms.i18neditor.util.MessageBundle; 11 | 12 | /** 13 | * This class represents a menu item for copying a translations key to the system clipboard. 14 | * 15 | * @author Fabian Terstegen 16 | * 17 | */ 18 | public class CopyTranslationKeyToClipboardMenuItem extends JMenuItem { 19 | private static final long serialVersionUID = 6032182493888769724L; 20 | 21 | public CopyTranslationKeyToClipboardMenuItem(Editor editor, boolean enabled) { 22 | super(MessageBundle.get("menu.edit.copy.key.title")); 23 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 24 | addActionListener(e -> editor.copySelectedTranslationKey()); 25 | setEnabled(enabled); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Jacob van Mourik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/Locales.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.util.Locale; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | import com.google.common.base.Strings; 8 | 9 | /** 10 | * This class provides utility functions for locales. 11 | * 12 | * @author Jacob van Mourik 13 | */ 14 | public final class Locales { 15 | public final static String LOCALE_REGEX = "([^_-]*)(?:[_-]([^_-]*)(?:[_-]([^_-]*))?)?"; 16 | public final static Pattern LOCALE_PATTERN = Pattern.compile(LOCALE_REGEX); 17 | 18 | public static Locale parseLocale(String localeString) { 19 | if (Strings.isNullOrEmpty(localeString)) { 20 | return null; 21 | } 22 | Matcher matcher = LOCALE_PATTERN.matcher(localeString); 23 | if (matcher.matches()) { 24 | String language = matcher.group(1); 25 | language = (language == null) ? "" : language; 26 | String country = matcher.group(2); 27 | country = (country == null) ? "" : country; 28 | String variant = matcher.group(3); 29 | variant = (variant == null) ? "" : variant; 30 | return new Locale(language, country, variant); 31 | } 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/text/JTextComponentMenuListener.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.text; 2 | 3 | import java.awt.event.MouseAdapter; 4 | import java.awt.event.MouseEvent; 5 | 6 | import javax.swing.text.JTextComponent; 7 | import javax.swing.undo.UndoManager; 8 | 9 | /** 10 | * Mouse listener for showing a {@link JTextComponentMenu}. 11 | * 12 | * @author Jacob van Mourik 13 | */ 14 | public class JTextComponentMenuListener extends MouseAdapter { 15 | private final JTextComponent parent; 16 | private final JTextComponentMenu menu; 17 | 18 | public JTextComponentMenuListener(JTextComponent parent, UndoManager undoManager) { 19 | super(); 20 | this.parent = parent; 21 | this.menu = new JTextComponentMenu(parent, undoManager); 22 | } 23 | 24 | @Override 25 | public void mouseReleased(MouseEvent e) { 26 | showPopupMenu(e); 27 | } 28 | 29 | @Override 30 | public void mousePressed(MouseEvent e) { 31 | showPopupMenu(e); 32 | } 33 | 34 | private void showPopupMenu(MouseEvent e) { 35 | if (!e.isPopupTrigger() || !parent.isEditable()) { 36 | return; 37 | } 38 | parent.requestFocusInWindow(); 39 | menu.show(parent, e.getX(), e.getY()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/Colors.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.awt.Color; 4 | 5 | /** 6 | * This class provides utility functions for colors. 7 | * 8 | * @author Jacob van Mourik 9 | */ 10 | public final class Colors { 11 | 12 | /** 13 | * Multiplies the RGB values of a color with the given factor. 14 | * 15 | * @param c the original color 16 | * @param factor the factor to scale with 17 | * @return the scaled color 18 | */ 19 | public static Color scale(Color c, float factor) { 20 | return new Color( 21 | roundColorValue(c.getRed()*factor), 22 | roundColorValue(c.getGreen()*factor), 23 | roundColorValue(c.getBlue()*factor), 24 | c.getAlpha()); 25 | } 26 | 27 | /** 28 | * Gets the hex representation of the RGB values of the given color. 29 | * 30 | * @param color the color to get the hex value from 31 | * @return the hex representation of the color 32 | */ 33 | public static String hexValue(Color color) { 34 | return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); 35 | } 36 | 37 | private static int roundColorValue(float value) { 38 | return Math.round(Math.max(0, Math.min(255, value))); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeNodeMenu.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import javax.swing.JPopupMenu; 4 | 5 | import com.jvms.i18neditor.editor.menu.AddTranslationMenuItem; 6 | import com.jvms.i18neditor.editor.menu.CopyTranslationKeyToClipboardMenuItem; 7 | import com.jvms.i18neditor.editor.menu.DuplicateTranslationMenuItem; 8 | import com.jvms.i18neditor.editor.menu.RemoveTranslationMenuItem; 9 | import com.jvms.i18neditor.editor.menu.RenameTranslationMenuItem; 10 | 11 | /** 12 | * This class represents a right click menu for a single node of the translation tree. 13 | * 14 | * @author Jacob van Mourik 15 | */ 16 | public class TranslationTreeNodeMenu extends JPopupMenu { 17 | private final static long serialVersionUID = -8450484152294368841L; 18 | 19 | public TranslationTreeNodeMenu(Editor editor, TranslationTreeNode node) { 20 | super(); 21 | add(new AddTranslationMenuItem(editor, node, true)); 22 | if (!node.isRoot()) { 23 | addSeparator(); 24 | add(new RenameTranslationMenuItem(editor, true)); 25 | add(new DuplicateTranslationMenuItem(editor, true)); 26 | add(new RemoveTranslationMenuItem(editor, true)); 27 | add(new CopyTranslationKeyToClipboardMenuItem(editor, true)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/menu/AddTranslationMenuItem.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor.menu; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.ActionListener; 5 | import java.awt.event.KeyEvent; 6 | 7 | import javax.swing.JMenuItem; 8 | import javax.swing.KeyStroke; 9 | 10 | import com.jvms.i18neditor.editor.Editor; 11 | import com.jvms.i18neditor.editor.TranslationTree; 12 | import com.jvms.i18neditor.editor.TranslationTreeNode; 13 | import com.jvms.i18neditor.util.MessageBundle; 14 | 15 | /** 16 | * This class represents a menu item for adding a new translation. 17 | * 18 | * @author Jacob van Mourik 19 | */ 20 | public class AddTranslationMenuItem extends JMenuItem { 21 | private final static long serialVersionUID = -2673278052970076105L; 22 | 23 | public AddTranslationMenuItem(Editor editor, TranslationTreeNode node, boolean enabled) { 24 | this(editor, enabled, e -> editor.showAddTranslationDialog(node)); 25 | } 26 | 27 | public AddTranslationMenuItem(Editor editor, TranslationTree tree, boolean enabled) { 28 | this(editor, enabled, e -> editor.showAddTranslationDialog(tree.getSelectionNode())); 29 | } 30 | 31 | private AddTranslationMenuItem(Editor editor, boolean enabled, ActionListener action) { 32 | super(MessageBundle.get("menu.edit.add.translation.title")); 33 | setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 34 | addActionListener(action); 35 | setEnabled(enabled); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeToggleIcon.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Component; 4 | import java.awt.Graphics; 5 | 6 | import javax.swing.Icon; 7 | import javax.swing.UIManager; 8 | 9 | import com.jvms.i18neditor.util.Colors; 10 | 11 | /** 12 | * This class represents a toggle icon for a translation tree cell. 13 | * 14 | * @author Jacob van Mourik 15 | */ 16 | public class TranslationTreeToggleIcon implements Icon { 17 | private final static int SIZE = 10; 18 | private final ToggleIconType type; 19 | 20 | public enum ToggleIconType { 21 | Collapsed, Expanded 22 | } 23 | 24 | public TranslationTreeToggleIcon(ToggleIconType type) { 25 | this.type = type; 26 | } 27 | 28 | @Override 29 | public void paintIcon(Component c, Graphics g, int x, int y) { 30 | g.setColor(UIManager.getColor("Tree.background")); 31 | g.fillRect(x, y, SIZE, SIZE); 32 | g.setColor(Colors.scale(UIManager.getColor("Panel.background"), .8f)); 33 | g.drawRect(x, y, SIZE, SIZE); 34 | g.setColor(UIManager.getColor("Tree.foreground")); 35 | g.drawLine(x + 2, y + SIZE/2, x + SIZE - 2, y + SIZE/2); 36 | if (type == ToggleIconType.Collapsed) { 37 | g.drawLine(x + SIZE/2, y + 2, x + SIZE/2, y + SIZE - 2); 38 | } 39 | } 40 | 41 | @Override 42 | public int getIconWidth() { 43 | return SIZE; 44 | } 45 | 46 | @Override 47 | public int getIconHeight() { 48 | return SIZE; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeStatusIcon.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Color; 4 | import java.awt.Component; 5 | import java.awt.Graphics; 6 | import java.awt.Graphics2D; 7 | import java.awt.RenderingHints; 8 | 9 | import javax.swing.Icon; 10 | 11 | /** 12 | * This class represents a status icon for a translation tree cell. 13 | * 14 | * @author Jacob van Mourik 15 | */ 16 | public class TranslationTreeStatusIcon implements Icon { 17 | private final static int SIZE = 7; 18 | private final StatusIconType type; 19 | 20 | public enum StatusIconType { 21 | Warning(new Color(220,160,0)); 22 | 23 | private Color color; 24 | 25 | public Color getColor() { 26 | return color; 27 | } 28 | 29 | private StatusIconType(Color color) { 30 | this.color = color; 31 | } 32 | } 33 | 34 | public TranslationTreeStatusIcon(StatusIconType type) { 35 | this.type = type; 36 | } 37 | 38 | @Override 39 | public void paintIcon(Component c, Graphics g, int x, int y) { 40 | Graphics2D g2 = (Graphics2D) g.create(); 41 | g2.setRenderingHints(new RenderingHints( 42 | RenderingHints.KEY_ANTIALIASING, 43 | RenderingHints.VALUE_ANTIALIAS_ON)); 44 | g2.setColor(type.getColor()); 45 | g2.fillOval(x, y, SIZE, SIZE); 46 | g2.dispose(); 47 | } 48 | 49 | @Override 50 | public int getIconWidth() { 51 | return SIZE; 52 | } 53 | 54 | @Override 55 | public int getIconHeight() { 56 | return SIZE; 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationKeyCaret.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.event.MouseEvent; 4 | 5 | import javax.swing.SwingUtilities; 6 | 7 | import com.jvms.i18neditor.swing.JTextField; 8 | import com.jvms.i18neditor.swing.text.BlinkCaret; 9 | 10 | public class TranslationKeyCaret extends BlinkCaret { 11 | private final static long serialVersionUID = -4481421558690248419L; 12 | 13 | @Override 14 | public void mouseClicked(MouseEvent e) { 15 | if (!e.isConsumed() && SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { 16 | handleDoubleClick(e); 17 | return; 18 | } 19 | super.mouseClicked(e); 20 | } 21 | 22 | @Override 23 | public void mousePressed(MouseEvent e) { 24 | if (!e.isConsumed() && SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { 25 | handleDoubleClick(e); 26 | return; 27 | } 28 | super.mousePressed(e); 29 | } 30 | 31 | private void handleDoubleClick(MouseEvent e) { 32 | JTextField field = (JTextField)e.getComponent(); 33 | 34 | int caretPos = field.getCaretPosition(); 35 | int start = caretPos; 36 | int end = caretPos; 37 | String text = field.getText(); 38 | 39 | while (start > 0) { 40 | if (text.charAt(start-1) == '.') { 41 | break; 42 | } 43 | start--; 44 | } 45 | while (end < text.length()) { 46 | if (text.charAt(end) == '.') { 47 | break; 48 | } 49 | end++; 50 | } 51 | 52 | field.setSelectionStart(start); 53 | field.setSelectionEnd(end); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/MessageBundle.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.Locale; 5 | import java.util.ResourceBundle; 6 | 7 | /** 8 | * This class provides utility functions for retrieving translations from a resource bundle.
9 | * By default it loads translations from {@value #RESOURCES_PATH}. 10 | * 11 | *

The locale used is the current value of the default locale for this instance of the Java Virtual Machine.

12 | * 13 | * @author Jacob van Mourik 14 | */ 15 | public final class MessageBundle { 16 | private final static String RESOURCES_PATH = "bundles/messages"; 17 | private static ResourceBundle RESOURCES; 18 | 19 | /** 20 | * Sets the preferred locale to use. 21 | * When calling this function resources will be reloaded from disk. 22 | * 23 | * @param locale the preferred locale to use 24 | */ 25 | public static void setLocale(Locale locale) { 26 | RESOURCES = ResourceBundle.getBundle(RESOURCES_PATH, locale); 27 | } 28 | 29 | /** 30 | * Gets a value from this bundle for the given {@code key}. Any second arguments will 31 | * be used to format the value. 32 | * 33 | * @param key the bundle key 34 | * @param args objects used to format the value. 35 | * @return the formatted value for the given key. 36 | */ 37 | public static String get(String key, Object... args) { 38 | String value = RESOURCES.getString(key); 39 | return MessageFormat.format(value, args); 40 | } 41 | 42 | /** 43 | * Gets a mnemonic value from this bundle. 44 | * 45 | * @param key the bundle key. 46 | * @return the mnemonic value for the given key. 47 | */ 48 | public static Character getMnemonic(String key) { 49 | String value = RESOURCES.getString(key); 50 | return value.charAt(0); 51 | } 52 | 53 | /** 54 | * Loads the resources. 55 | */ 56 | public static void loadResources() { 57 | RESOURCES = ResourceBundle.getBundle(RESOURCES_PATH); 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/JScrollablePanel.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.Dimension; 4 | import java.awt.Rectangle; 5 | 6 | import javax.swing.JPanel; 7 | import javax.swing.Scrollable; 8 | 9 | /** 10 | * This class extends a default {@link JPanel} with {@link Scrollable} capabilities. 11 | * 12 | * @author Jacob van Mourik 13 | */ 14 | public class JScrollablePanel extends JPanel implements Scrollable { 15 | private final static long serialVersionUID = -7947570506111556197L; 16 | private final boolean scrollableTracksViewportWidth; 17 | private final boolean scrollableTracksViewportHeight; 18 | 19 | /** 20 | * Constructs a {@link JScrollablePanel}. 21 | * 22 | * @param scrollableTracksViewportWidth whether to force the width of this panel to match the width of the viewport. 23 | * @param scrollableTracksViewportHeight whether to force the height of this panel to match the height of the viewport. 24 | */ 25 | public JScrollablePanel(boolean scrollableTracksViewportWidth, boolean scrollableTracksViewportHeight) { 26 | super(); 27 | this.scrollableTracksViewportWidth = scrollableTracksViewportWidth; 28 | this.scrollableTracksViewportHeight = scrollableTracksViewportHeight; 29 | } 30 | 31 | @Override 32 | public Dimension getPreferredScrollableViewportSize() { 33 | return getPreferredSize(); 34 | } 35 | 36 | @Override 37 | public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { 38 | return 10; 39 | } 40 | 41 | @Override 42 | public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { 43 | return 100; 44 | } 45 | 46 | @Override 47 | public boolean getScrollableTracksViewportWidth() { 48 | return scrollableTracksViewportWidth; 49 | } 50 | 51 | @Override 52 | public boolean getScrollableTracksViewportHeight() { 53 | return scrollableTracksViewportHeight; 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/JTextArea.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.BorderFactory; 7 | import javax.swing.KeyStroke; 8 | import javax.swing.UIManager; 9 | import javax.swing.border.Border; 10 | import javax.swing.undo.UndoManager; 11 | 12 | import com.jvms.i18neditor.swing.text.JTextComponentMenuListener; 13 | import com.jvms.i18neditor.util.Colors; 14 | 15 | /** 16 | * This class extends a default {@link javax.swing.JTextArea} with a {@link UndoManager}, 17 | * a right click menu and a custom look and feel. 18 | * 19 | * @author Jacob van Mourik 20 | */ 21 | public class JTextArea extends javax.swing.JTextArea { 22 | private final static long serialVersionUID = -5043046809426384893L; 23 | protected final UndoManager undoManager = new UndoManager(); 24 | 25 | @Override 26 | public void setEnabled(boolean enabled) { 27 | super.setEnabled(enabled); 28 | setOpaque(enabled); 29 | } 30 | 31 | /** 32 | * Constructs a {@link JTextArea}. 33 | */ 34 | public JTextArea() { 35 | super(); 36 | 37 | Border border = BorderFactory.createLineBorder(Colors.scale(UIManager.getColor("Panel.background"), .8f)); 38 | setBorder(BorderFactory.createCompoundBorder(border, BorderFactory.createEmptyBorder(5,8,5,8))); 39 | getDocument().addUndoableEditListener(e -> undoManager.addEdit(e.getEdit())); 40 | 41 | setAlignmentX(LEFT_ALIGNMENT); 42 | setLineWrap(true); 43 | setWrapStyleWord(true); 44 | 45 | // Add undo support 46 | getActionMap().put("undo", new UndoAction(undoManager)); 47 | getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "undo"); 48 | 49 | // Add redo support 50 | getActionMap().put("redo", new RedoAction(undoManager)); 51 | getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "redo"); 52 | 53 | // Add popup menu support 54 | addMouseListener(new JTextComponentMenuListener(this, undoManager)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/Main.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | import java.awt.Font; 4 | 5 | import javax.swing.SwingUtilities; 6 | import javax.swing.UIDefaults; 7 | import javax.swing.UIManager; 8 | 9 | import org.apache.commons.lang3.SystemUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.google.common.collect.Sets; 14 | import com.jvms.i18neditor.editor.Editor; 15 | 16 | /** 17 | * 18 | * @author Jacob van Mourik 19 | */ 20 | public class Main { 21 | private final static Logger log = LoggerFactory.getLogger(Main.class); 22 | 23 | public static void main(String[] args) { 24 | SwingUtilities.invokeLater(() -> { 25 | // Enable global menu on MAC OS 26 | if (SystemUtils.IS_OS_MAC) { 27 | System.setProperty("apple.laf.useScreenMenuBar", "true"); 28 | } 29 | try { 30 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 31 | // For windows use menu font for entire UI 32 | if (SystemUtils.IS_OS_WINDOWS) { 33 | setUIFont(UIManager.getFont("Menu.font")); 34 | } 35 | } catch (Exception e) { 36 | log.warn("Unable to use native look and feel"); 37 | } 38 | new Editor().launch(); 39 | }); 40 | } 41 | 42 | private static void setUIFont(Font font) { 43 | UIDefaults defaults = UIManager.getDefaults(); 44 | Sets.newHashSet( 45 | "List.font", 46 | "TableHeader.font", 47 | "Panel.font", 48 | "TextArea.font", 49 | "ToggleButton.font", 50 | "ComboBox.font", 51 | "ScrollPane.font", 52 | "Spinner.font", 53 | "Slider.font", 54 | "EditorPane.font", 55 | "OptionPane.font", 56 | "ToolBar.font", 57 | "Tree.font", 58 | "TitledBorder.font", 59 | "Table.font", 60 | "Label.font", 61 | "TextField.font", 62 | "TextPane.font", 63 | "CheckBox.font", 64 | "ProgressBar.font", 65 | "FormattedTextField.font", 66 | "ColorChooser.font", 67 | "PasswordField.font", 68 | "Viewport.font", 69 | "TabbedPane.font", 70 | "RadioButton.font", 71 | "ToolTip.font", 72 | "Button.font" 73 | ).forEach(key -> defaults.put(key, font)); 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/JTextField.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing; 2 | 3 | import java.awt.Toolkit; 4 | import java.awt.event.KeyEvent; 5 | 6 | import javax.swing.BorderFactory; 7 | import javax.swing.KeyStroke; 8 | import javax.swing.UIManager; 9 | import javax.swing.border.Border; 10 | import javax.swing.undo.UndoManager; 11 | 12 | import com.jvms.i18neditor.swing.text.JTextComponentMenuListener; 13 | import com.jvms.i18neditor.util.Colors; 14 | 15 | /** 16 | * This class extends a default {@link javax.swing.JTextField} with a {@link UndoManager}, 17 | * a right click menu and a custom look and feel. 18 | * 19 | * @author Jacob van Mourik 20 | */ 21 | public class JTextField extends javax.swing.JTextField { 22 | private final static long serialVersionUID = 5296894112638304738L; 23 | protected final UndoManager undoManager = new UndoManager(); 24 | 25 | /** 26 | * Constructs a {@link JTextField}. 27 | */ 28 | public JTextField() { 29 | this(null); 30 | } 31 | 32 | @Override 33 | public void setEnabled(boolean enabled) { 34 | super.setEnabled(enabled); 35 | setOpaque(enabled); 36 | } 37 | 38 | /** 39 | * Constructs a {@link JTextField} with an initial text. 40 | */ 41 | public JTextField(String text) { 42 | super(text, 25); 43 | 44 | Border border = BorderFactory.createLineBorder(Colors.scale(UIManager.getColor("Panel.background"), .8f)); 45 | setBorder(BorderFactory.createCompoundBorder(border, BorderFactory.createEmptyBorder(5,8,5,8))); 46 | getDocument().addUndoableEditListener(e -> undoManager.addEdit(e.getEdit())); 47 | 48 | // Add undo support 49 | getActionMap().put("undo", new UndoAction(undoManager)); 50 | getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "undo"); 51 | 52 | // Add redo support 53 | getActionMap().put("redo", new RedoAction(undoManager)); 54 | getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "redo"); 55 | 56 | // Add popup menu support 57 | addMouseListener(new JTextComponentMenuListener(this, undoManager)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.inno.iss: -------------------------------------------------------------------------------- 1 | #define AppName "i18n-editor" 2 | #define AppVersion "2.0.0-beta.1" 3 | #define AppPublisher "JvMs Software" 4 | #define AppURL "https://github.com/jcbvm/i18n-editor" 5 | #define AppExeName "i18n-editor.exe" 6 | 7 | [Setup] 8 | AppId={{16A49296-8A8D-4BDA-A743-5F1BF02953D5} 9 | AppName={#AppName} 10 | AppVersion={#AppVersion} 11 | AppPublisher={#AppPublisher} 12 | AppPublisherURL={#AppURL} 13 | AppSupportURL={#AppURL} 14 | AppUpdatesURL={#AppURL} 15 | DefaultDirName={pf}\{#AppPublisher}\{#AppName} 16 | DisableProgramGroupPage=auto 17 | DisableDirPage=auto 18 | AlwaysShowDirOnReadyPage=yes 19 | LicenseFile=LICENSE 20 | OutputBaseFilename={#AppName}-{#AppVersion}-setup 21 | OutputDir=target\{#AppName}-{#AppVersion} 22 | SetupIconFile=src\main\resources\images\icon.ico 23 | UninstallDisplayIcon={uninstallexe} 24 | Compression=lzma 25 | SolidCompression=yes 26 | ArchitecturesAllowed=x86 x64 ia64 27 | ArchitecturesInstallIn64BitMode=x64 ia64 28 | 29 | [Languages] 30 | Name: "english"; MessagesFile: "compiler:Default.isl" 31 | Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl" 32 | Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" 33 | 34 | [Tasks] 35 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 36 | Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 37 | 38 | [Files] 39 | Source: "target\{#AppName}-{#AppVersion}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion 40 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 41 | 42 | [Icons] 43 | Name: "{commonprograms}\{#AppPublisher}\{#AppName}"; Filename: "{app}\{#AppExeName}" 44 | Name: "{commondesktop}\{#AppPublisher}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon 45 | Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#AppPublisher}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: quicklaunchicon 46 | 47 | [Run] 48 | Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 49 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/ResourceField.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.KeyboardFocusManager; 4 | import java.awt.Rectangle; 5 | import java.awt.event.FocusAdapter; 6 | import java.awt.event.FocusEvent; 7 | import java.util.Locale; 8 | 9 | import javax.swing.JComponent; 10 | 11 | import com.jvms.i18neditor.Resource; 12 | import com.jvms.i18neditor.swing.JTextArea; 13 | 14 | /** 15 | * This class represents a text area to edit the value of a translation. 16 | * 17 | * @author Jacob van Mourik 18 | */ 19 | public class ResourceField extends JTextArea implements Comparable { 20 | private final static long serialVersionUID = 2034814490878477055L; 21 | private final Resource resource; 22 | 23 | public ResourceField(Resource resource) { 24 | super(); 25 | this.resource = resource; 26 | setupUI(); 27 | } 28 | 29 | public String getValue() { 30 | return getText().trim(); 31 | } 32 | 33 | public void setValue(String key) { 34 | setText(resource.getTranslation(key)); 35 | undoManager.discardAllEdits(); 36 | } 37 | 38 | public Resource getResource() { 39 | return resource; 40 | } 41 | 42 | @Override 43 | public int compareTo(ResourceField o) { 44 | Locale a = getResource().getLocale(); 45 | Locale b = o.getResource().getLocale(); 46 | if (a == null) { 47 | return -1; 48 | } 49 | if (b == null) { 50 | return 1; 51 | } 52 | return a.getDisplayName().compareTo(b.getDisplayName()); 53 | } 54 | 55 | private void setupUI() { 56 | // Add focus traversal support 57 | setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null); 58 | setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null); 59 | addFocusListener(new ResourceFieldFocusListener()); 60 | } 61 | 62 | private class ResourceFieldFocusListener extends FocusAdapter { 63 | @Override 64 | public void focusGained(FocusEvent e) { 65 | JComponent parent = (JComponent)getParent(); 66 | Rectangle bounds = new Rectangle(getBounds()); 67 | bounds.y -= 35; // add fixed space at the top 68 | bounds.height += 70; // add fixed space at the bottom 69 | parent.scrollRectToVisible(bounds); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeCellRenderer.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Color; 4 | import java.awt.Component; 5 | import java.awt.event.MouseListener; 6 | 7 | import javax.swing.ImageIcon; 8 | import javax.swing.JLabel; 9 | import javax.swing.JTree; 10 | import javax.swing.UIManager; 11 | import javax.swing.tree.DefaultTreeCellRenderer; 12 | 13 | import com.jvms.i18neditor.editor.TranslationTreeStatusIcon.StatusIconType; 14 | import com.jvms.i18neditor.util.Images; 15 | 16 | /** 17 | * This class represents a default cell renderer for the translation tree. 18 | * 19 | * @author Jacob van Mourik 20 | */ 21 | public class TranslationTreeCellRenderer extends DefaultTreeCellRenderer { 22 | private final static long serialVersionUID = 3511394180407171920L; 23 | private final static ImageIcon ROOT_ICON = Images.loadFromClasspath("images/icon-folder.png"); 24 | private final Color selectionBackground; 25 | 26 | public TranslationTreeCellRenderer() { 27 | super(); 28 | Color bg = UIManager.getColor("Panel.background"); 29 | selectionBackground = new Color(bg.getRed(), bg.getGreen(), bg.getBlue()); 30 | setLeafIcon(null); 31 | setClosedIcon(null); 32 | setOpenIcon(null); 33 | for (MouseListener l : getMouseListeners()) { 34 | removeMouseListener(l); 35 | } 36 | } 37 | 38 | public Color getSelectionBackground() { 39 | return selectionBackground; 40 | } 41 | 42 | @Override 43 | public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, 44 | boolean leaf, int row, boolean hasFocus) { 45 | TranslationTreeNode node = (TranslationTreeNode) value; 46 | TranslationTreeModel model = (TranslationTreeModel) tree.getModel(); 47 | JLabel l = (JLabel) super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); 48 | l.setOpaque(true); 49 | l.setForeground(tree.getForeground()); 50 | l.setBackground(tree.getBackground()); 51 | if (!node.isRoot() && (node.hasError() || model.hasErrorChildNode(node))) { 52 | l.setIcon(new TranslationTreeStatusIcon(StatusIconType.Warning)); 53 | } 54 | if (node.isRoot()) { 55 | l.setIcon(ROOT_ICON); 56 | } 57 | if (selected) { 58 | l.setBackground(selectionBackground); 59 | } 60 | return l; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeUI.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Graphics; 4 | import java.awt.Insets; 5 | import java.awt.Rectangle; 6 | 7 | import javax.swing.Icon; 8 | import javax.swing.JComponent; 9 | import javax.swing.JTree; 10 | import javax.swing.plaf.basic.BasicTreeUI; 11 | import javax.swing.tree.TreePath; 12 | 13 | import com.jvms.i18neditor.editor.TranslationTreeToggleIcon.ToggleIconType; 14 | 15 | /** 16 | * This class represents a default UI for the translation tree. 17 | * 18 | * @author Jacob van Mourik 19 | */ 20 | public class TranslationTreeUI extends BasicTreeUI { 21 | private TranslationTreeToggleIcon expandedIcon = new TranslationTreeToggleIcon(ToggleIconType.Expanded); 22 | private TranslationTreeToggleIcon collapsedIcon = new TranslationTreeToggleIcon(ToggleIconType.Collapsed); 23 | 24 | @Override 25 | protected void toggleExpandState(TreePath path) { 26 | // do nothing 27 | } 28 | 29 | @Override 30 | protected void paintVerticalLine(Graphics g, JComponent c, int y, int left, int right) {} 31 | 32 | @Override 33 | protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {} 34 | 35 | @Override 36 | protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, TreePath path) {} 37 | 38 | @Override 39 | protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, 40 | TreePath path, int row, boolean expanded, boolean hasBeenExpanded, boolean leaf) {} 41 | 42 | @Override 43 | public Rectangle getPathBounds(JTree tree, TreePath path) { 44 | if (tree != null && treeState != null) { 45 | return getPathBounds(path, tree.getInsets(), new Rectangle()); 46 | } 47 | return null; 48 | } 49 | 50 | @Override 51 | public Icon getCollapsedIcon() { 52 | return collapsedIcon; 53 | } 54 | 55 | @Override 56 | public Icon getExpandedIcon() { 57 | return expandedIcon; 58 | } 59 | 60 | private Rectangle getPathBounds(TreePath path, Insets insets, Rectangle bounds) { 61 | bounds = treeState.getBounds(path, bounds); 62 | if (bounds != null) { 63 | bounds.x = 0; 64 | bounds.y += insets.top; 65 | bounds.width = tree.getWidth(); 66 | } 67 | return bounds; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import java.util.stream.Collectors; 8 | 9 | import javax.swing.tree.DefaultMutableTreeNode; 10 | import javax.swing.tree.TreeNode; 11 | 12 | import com.jvms.i18neditor.util.ResourceKeys; 13 | 14 | /** 15 | * This class represents a single node of the translation tree. 16 | * 17 | * @author Jacob van Mourik 18 | */ 19 | public class TranslationTreeNode extends DefaultMutableTreeNode { 20 | private final static long serialVersionUID = -7372403592538358822L; 21 | private String name; 22 | private boolean error; 23 | 24 | public TranslationTreeNode(String name, List keys) { 25 | super(); 26 | this.name = name; 27 | ResourceKeys.uniqueRootKeys(keys).forEach(rootKey -> { 28 | List subKeys = ResourceKeys.extractChildKeys(keys, rootKey); 29 | add(new TranslationTreeNode(rootKey, subKeys)); 30 | }); 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public void setError(boolean error) { 42 | this.error = error; 43 | } 44 | 45 | public boolean hasError() { 46 | return isEditable() && error; 47 | } 48 | 49 | public boolean isEditable() { 50 | return !isRoot(); 51 | } 52 | 53 | public String getKey() { 54 | List path = Arrays.asList(getPath()); 55 | List parts = path.stream() 56 | .filter(p -> !((TranslationTreeNode)p).isRoot()) 57 | .map(p -> p.toString()) 58 | .collect(Collectors.toList()); 59 | return ResourceKeys.create(parts); 60 | } 61 | 62 | @SuppressWarnings("unchecked") 63 | public List getChildren() { 64 | return Collections.list(children()); 65 | } 66 | 67 | public TranslationTreeNode getChild(String name) { 68 | Optional child = getChildren().stream() 69 | .filter(i -> i.getName().equals(name)) 70 | .findFirst(); 71 | return child.isPresent() ? child.get() : null; 72 | } 73 | 74 | public TranslationTreeNode cloneWithChildren() { 75 | return cloneWithChildren(this); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return name; 81 | } 82 | 83 | private TranslationTreeNode cloneWithChildren(TranslationTreeNode parent) { 84 | TranslationTreeNode newParent = (TranslationTreeNode) parent.clone(); 85 | for (TranslationTreeNode n : parent.getChildren()) { 86 | newParent.add(cloneWithChildren(n)); 87 | } 88 | return newParent; 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/EditorProject.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.common.collect.Lists; 8 | import com.jvms.i18neditor.FileStructure; 9 | import com.jvms.i18neditor.Resource; 10 | import com.jvms.i18neditor.ResourceType; 11 | 12 | /** 13 | * This class represents an editor project. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class EditorProject { 18 | private Path path; 19 | private String resourceFileDefinition; 20 | private ResourceType resourceType; 21 | private List resources; 22 | private boolean minifyResources; 23 | private boolean flattenJSON; 24 | private FileStructure resourceFileStructure; 25 | 26 | public EditorProject(Path path) { 27 | this.path = path; 28 | this.resources = Lists.newLinkedList(); 29 | } 30 | 31 | public Path getPath() { 32 | return path; 33 | } 34 | 35 | public void setPath(Path path) { 36 | this.path = path; 37 | } 38 | 39 | public ResourceType getResourceType() { 40 | return resourceType; 41 | } 42 | 43 | public void setResourceType(ResourceType resourceType) { 44 | this.resourceType = resourceType; 45 | } 46 | 47 | public List getResources() { 48 | return ImmutableList.copyOf(resources); 49 | } 50 | 51 | public void setResources(List resources) { 52 | this.resources = resources; 53 | } 54 | 55 | public void addResource(Resource resource) { 56 | resources.add(resource); 57 | } 58 | 59 | public boolean hasResources() { 60 | return !resources.isEmpty(); 61 | } 62 | 63 | public String getResourceFileDefinition() { 64 | return resourceFileDefinition; 65 | } 66 | 67 | public void setResourceFileDefinition(String resourceFileDefinition) { 68 | this.resourceFileDefinition = resourceFileDefinition; 69 | } 70 | 71 | public boolean isMinifyResources() { 72 | return minifyResources; 73 | } 74 | 75 | public void setMinifyResources(boolean minifyResources) { 76 | this.minifyResources = minifyResources; 77 | } 78 | 79 | public boolean isFlattenJSON() { 80 | return flattenJSON; 81 | } 82 | 83 | public void setFlattenJSON(boolean flattenJSON) { 84 | this.flattenJSON = flattenJSON; 85 | } 86 | 87 | public boolean supportsResourceParentValues() { 88 | return resourceType == ResourceType.Properties; 89 | } 90 | 91 | public FileStructure getResourceFileStructure() { 92 | return resourceFileStructure; 93 | } 94 | 95 | public void setResourceFileStructure(FileStructure resourceFileStructure) { 96 | this.resourceFileStructure = resourceFileStructure; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/GithubRepoUtil.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStreamReader; 5 | import java.net.HttpURLConnection; 6 | import java.net.URL; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.Future; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import com.google.common.base.Charsets; 15 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 16 | import com.google.gson.Gson; 17 | import com.google.gson.annotations.SerializedName; 18 | 19 | /** 20 | * This class provides utility functions for retrieving Github Repository data. 21 | * 22 | * @author Jacob van Mourik 23 | */ 24 | public final class GithubRepoUtil { 25 | private final static Logger log = LoggerFactory.getLogger(GithubRepoUtil.class); 26 | private final static Gson gson = new Gson(); 27 | private final static ExecutorService executor; 28 | 29 | static { 30 | executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder() 31 | .setNameFormat("github-repo-util-pool-%d") 32 | .build()); 33 | } 34 | 35 | /** 36 | * Gets the full Github Repository URL. 37 | * 38 | * @param username the Github Repository user name. 39 | * @param project the Github Repository project name. 40 | * @return the full Github Repository URL. 41 | */ 42 | public static String getURL(String username, String project) { 43 | return "https://github.com/" + username + "/" + project; 44 | } 45 | 46 | /** 47 | * Gets the latest release data of a Github Repository. 48 | * 49 | * @param username the Github Repository user name. 50 | * @param project the Github Repository project name. 51 | * @return the latest Github Repository release data. 52 | */ 53 | public static Future getLatestRelease(String username, String project) { 54 | return executor.submit(() -> { 55 | HttpURLConnection connection = null; 56 | URL url = new URL("https://api.github.com/repos/" + username + "/" + project + "/releases/latest"); 57 | try { 58 | connection = (HttpURLConnection)url.openConnection(); 59 | try (InputStreamReader reader = new InputStreamReader(connection.getInputStream(), Charsets.UTF_8)) { 60 | return gson.fromJson(reader, GithubRepoReleaseData.class); 61 | } 62 | } catch (IOException e) { 63 | log.error("Unable to retrieve latest github release data", e); 64 | return null; 65 | } finally { 66 | if (connection != null) { 67 | connection.disconnect(); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | /** 74 | * This class represents Github Repository release data. 75 | */ 76 | public final class GithubRepoReleaseData { 77 | @SerializedName("tag_name") 78 | private String tagName; 79 | @SerializedName("html_url") 80 | private String htmlUrl; 81 | 82 | public String getTagName() { 83 | return tagName; 84 | } 85 | 86 | public String getHtmlUrl() { 87 | return htmlUrl; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/text/JTextComponentMenu.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.text; 2 | 3 | import java.awt.Component; 4 | import java.awt.Toolkit; 5 | import java.awt.event.KeyEvent; 6 | 7 | import javax.swing.Action; 8 | import javax.swing.JPopupMenu; 9 | import javax.swing.KeyStroke; 10 | import javax.swing.text.DefaultEditorKit; 11 | import javax.swing.text.JTextComponent; 12 | import javax.swing.undo.UndoManager; 13 | 14 | import com.google.common.base.Strings; 15 | import com.jvms.i18neditor.swing.UndoAction; 16 | import com.jvms.i18neditor.util.MessageBundle; 17 | 18 | /** 19 | * A popup menu implementation useful as a edit menu for a {@link JTextComponent}. 20 | * 21 | * @author Jacob van Mourik 22 | */ 23 | public class JTextComponentMenu extends JPopupMenu { 24 | private final static long serialVersionUID = 5967213965940023534L; 25 | private JTextComponent parent; 26 | private UndoManager undoManager; 27 | private Action cutAction; 28 | private Action copyAction; 29 | private Action deleteAction; 30 | private Action undoAction; 31 | 32 | public JTextComponentMenu(JTextComponent parent, UndoManager undoManager) { 33 | super(); 34 | 35 | this.parent = parent; 36 | this.undoManager = undoManager; 37 | int keyMask =Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); 38 | 39 | undoAction = new UndoAction(undoManager); 40 | undoAction.putValue(Action.NAME, MessageBundle.get("swing.action.undo")); 41 | undoAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, keyMask)); 42 | add(undoAction); 43 | 44 | addSeparator(); 45 | 46 | cutAction = new DefaultEditorKit.CutAction(); 47 | cutAction.putValue(Action.NAME, MessageBundle.get("swing.action.cut")); 48 | cutAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X, keyMask)); 49 | add(cutAction); 50 | 51 | copyAction = new DefaultEditorKit.CopyAction(); 52 | copyAction.putValue(Action.NAME, MessageBundle.get("swing.action.copy")); 53 | copyAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, keyMask)); 54 | add(copyAction); 55 | 56 | Action pasteAction = new DefaultEditorKit.PasteAction(); 57 | pasteAction.putValue(Action.NAME, MessageBundle.get("swing.action.paste")); 58 | pasteAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V, keyMask)); 59 | add(pasteAction); 60 | 61 | deleteAction = new DeleteAction(MessageBundle.get("swing.action.delete")); 62 | add(deleteAction); 63 | 64 | addSeparator(); 65 | 66 | Action selectAllAction = new SelectAllAction(MessageBundle.get("swing.action.selectall")); 67 | selectAllAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, keyMask)); 68 | add(selectAllAction); 69 | } 70 | 71 | @Override 72 | public void show(Component invoker, int x, int y) { 73 | super.show(invoker, x, y); 74 | boolean hasSelection = !Strings.isNullOrEmpty(parent.getSelectedText()); 75 | undoAction.setEnabled(undoManager.canUndo()); 76 | cutAction.setEnabled(hasSelection); 77 | copyAction.setEnabled(hasSelection); 78 | deleteAction.setEnabled(hasSelection); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/AbstractSettingsPane.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.GridBagConstraints; 4 | import java.awt.GridBagLayout; 5 | import java.awt.Insets; 6 | import java.util.List; 7 | import java.util.Locale; 8 | import java.util.stream.Collectors; 9 | 10 | import javax.swing.BorderFactory; 11 | import javax.swing.JPanel; 12 | 13 | import com.google.common.collect.Lists; 14 | import com.jvms.i18neditor.FileStructure; 15 | import com.jvms.i18neditor.util.MessageBundle; 16 | 17 | /** 18 | * This class represents an abstract base class for all setting panes. 19 | * 20 | * @author Jacob van Mourik 21 | */ 22 | public abstract class AbstractSettingsPane extends JPanel { 23 | private final static long serialVersionUID = -8953194193840198893L; 24 | private GridBagConstraints vGridBagConstraints; 25 | 26 | protected final List fileStructureComboBoxItems = Lists.newArrayList(FileStructure.values()).stream() 27 | .map(val -> new ComboBoxFileStructure(val, MessageBundle.get("settings.filestructure." + val.name().toLowerCase()))) 28 | .sorted() 29 | .collect(Collectors.toList()); 30 | 31 | protected final List localeComboBoxItems = Editor.SUPPORTED_LANGUAGES.stream() 32 | .map(val -> new ComboBoxLocale(val)) 33 | .sorted() 34 | .collect(Collectors.toList()); 35 | 36 | protected AbstractSettingsPane() { 37 | super(); 38 | vGridBagConstraints = new GridBagConstraints(); 39 | vGridBagConstraints.insets = new Insets(4,4,4,4); 40 | vGridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; 41 | vGridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; 42 | vGridBagConstraints.weightx = 1; 43 | } 44 | 45 | protected GridBagConstraints createVerticalGridBagConstraints() { 46 | vGridBagConstraints.gridy = (vGridBagConstraints.gridy + 1) % Integer.MAX_VALUE; 47 | return vGridBagConstraints; 48 | } 49 | 50 | protected JPanel createFieldset(String title) { 51 | JPanel fieldset = new JPanel(new GridBagLayout()); 52 | fieldset.setBorder(BorderFactory.createCompoundBorder( 53 | BorderFactory.createTitledBorder(null, title), 54 | BorderFactory.createEmptyBorder(5,5,5,5))); 55 | return fieldset; 56 | } 57 | 58 | protected class ComboBoxFileStructure implements Comparable { 59 | private FileStructure structure; 60 | private String label; 61 | 62 | public ComboBoxFileStructure(FileStructure structure, String label) { 63 | this.structure = structure; 64 | this.label = label; 65 | } 66 | 67 | public FileStructure getStructure() { 68 | return structure; 69 | } 70 | 71 | public String toString() { 72 | return label; 73 | } 74 | 75 | @Override 76 | public int compareTo(ComboBoxFileStructure o) { 77 | return toString().compareTo(o.toString()); 78 | } 79 | } 80 | 81 | protected class ComboBoxLocale implements Comparable { 82 | private Locale locale; 83 | 84 | public ComboBoxLocale(Locale locale) { 85 | this.locale = locale; 86 | } 87 | 88 | public Locale getLocale() { 89 | return locale; 90 | } 91 | 92 | public String toString() { 93 | return locale.getDisplayName(); 94 | } 95 | 96 | @Override 97 | public int compareTo(ComboBoxLocale o) { 98 | return toString().compareTo(o.toString()); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTreeModel.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.util.Enumeration; 4 | import java.util.List; 5 | 6 | import javax.swing.tree.DefaultTreeModel; 7 | 8 | import com.google.common.collect.Lists; 9 | import com.jvms.i18neditor.util.MessageBundle; 10 | import com.jvms.i18neditor.util.ResourceKeys; 11 | 12 | /** 13 | * This class represents a model for the translation tree. 14 | * 15 | * @author Jacob van Mourik 16 | */ 17 | public class TranslationTreeModel extends DefaultTreeModel { 18 | private final static long serialVersionUID = 3261808274177599488L; 19 | 20 | public TranslationTreeModel() { 21 | super(new TranslationTreeNode(MessageBundle.get("tree.root.name"), Lists.newArrayList())); 22 | } 23 | 24 | public TranslationTreeModel(List keys) { 25 | super(new TranslationTreeNode(MessageBundle.get("tree.root.name"), keys)); 26 | } 27 | 28 | public Enumeration getEnumeration() { 29 | return getEnumeration((TranslationTreeNode) getRoot()); 30 | } 31 | 32 | @SuppressWarnings("unchecked") 33 | public Enumeration getEnumeration(TranslationTreeNode node) { 34 | return node.depthFirstEnumeration(); 35 | } 36 | 37 | public TranslationTreeNode getNodeByKey(String key) { 38 | Enumeration e = getEnumeration(); 39 | while (e.hasMoreElements()) { 40 | TranslationTreeNode n = e.nextElement(); 41 | if (n.getKey().equals(key)) { 42 | return n; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | public boolean hasErrorChildNode(TranslationTreeNode node) { 49 | Enumeration e = getEnumeration(node); 50 | while (e.hasMoreElements()) { 51 | TranslationTreeNode n = e.nextElement(); 52 | if (n.hasError()) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | public TranslationTreeNode getClosestParentNodeByKey(String key) { 60 | TranslationTreeNode node = null; 61 | int count = ResourceKeys.size(key); 62 | while (node == null && count > 0) { 63 | key = ResourceKeys.withoutLastPart(key); 64 | node = getNodeByKey(key); 65 | count--; 66 | } 67 | if (node != null) { 68 | return node; 69 | } else { 70 | return (TranslationTreeNode) getRoot(); 71 | } 72 | } 73 | 74 | public void insertNodeInto(TranslationTreeNode newChild, TranslationTreeNode parent) { 75 | insertNodeInto(newChild, parent, getNewChildIndex(newChild, parent)); 76 | } 77 | 78 | public void insertDescendantsInto(TranslationTreeNode source, TranslationTreeNode target) { 79 | source.getChildren().forEach(child -> { 80 | TranslationTreeNode existing = target.getChild(child.getName()); 81 | if (existing != null) { 82 | if (existing.isLeaf()) { 83 | removeNodeFromParent(existing); 84 | insertNodeInto(child, target); 85 | } else { 86 | insertDescendantsInto(child, existing); 87 | } 88 | } else { 89 | insertNodeInto(child, target); 90 | } 91 | }); 92 | } 93 | 94 | public void nodeWithParentsChanged(TranslationTreeNode node) { 95 | while (node != null) { 96 | nodeChanged(node); 97 | node = (TranslationTreeNode) node.getParent(); 98 | } 99 | } 100 | 101 | private int getNewChildIndex(TranslationTreeNode newChild, TranslationTreeNode parent) { 102 | int result = 0; 103 | for (TranslationTreeNode n : parent.getChildren()) { 104 | if (n.getName().compareTo(newChild.getName()) < 0) { 105 | result++; 106 | } 107 | } 108 | return result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/swing/util/Dialogs.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.swing.util; 2 | 3 | import java.awt.Component; 4 | import java.awt.Font; 5 | import java.awt.GridBagLayout; 6 | import java.awt.GridLayout; 7 | 8 | import javax.swing.BorderFactory; 9 | import javax.swing.JLabel; 10 | import javax.swing.JOptionPane; 11 | import javax.swing.JPanel; 12 | import javax.swing.text.Caret; 13 | 14 | import com.google.common.base.Strings; 15 | import com.jvms.i18neditor.swing.JHelpLabel; 16 | import com.jvms.i18neditor.swing.JHtmlPane; 17 | import com.jvms.i18neditor.swing.JTextField; 18 | import com.jvms.i18neditor.swing.event.RequestInitialFocusListener; 19 | import com.jvms.i18neditor.swing.text.BlinkCaret; 20 | 21 | /** 22 | * This class provides utility functions for dialogs using {@link JOptionPane}. 23 | * 24 | * @author Jacob van Mourik 25 | */ 26 | public final class Dialogs { 27 | 28 | public static void showErrorDialog(Component parent, String title, String message) { 29 | JOptionPane.showMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE); 30 | } 31 | 32 | public static void showWarningDialog(Component parent, String title, String message) { 33 | JOptionPane.showMessageDialog(parent, message, title, JOptionPane.WARNING_MESSAGE); 34 | } 35 | 36 | public static void showInfoDialog(Component parent, String title, String message) { 37 | JOptionPane.showMessageDialog(parent, message, title, JOptionPane.INFORMATION_MESSAGE); 38 | } 39 | 40 | public static void showMessageDialog(Component parent, String title, String message) { 41 | JOptionPane.showMessageDialog(parent, message, title, JOptionPane.PLAIN_MESSAGE); 42 | } 43 | 44 | public static void showComponentDialog(Component parent, String title, Component component) { 45 | JOptionPane.showMessageDialog(parent, component, title, JOptionPane.PLAIN_MESSAGE); 46 | } 47 | 48 | public static void showHtmlDialog(Component parent, String title, String body) { 49 | Font font = parent.getFont(); 50 | JHtmlPane pane = new JHtmlPane(parent, "" + body + ""); 52 | pane.setBorder(BorderFactory.createEmptyBorder(15,15,15,15)); 53 | showComponentDialog(parent, title, pane); 54 | } 55 | 56 | public static boolean showConfirmDialog(Component parent, String title, String message, int type) { 57 | return JOptionPane.showConfirmDialog(parent, message, title, JOptionPane.YES_NO_OPTION, type) == JOptionPane.YES_OPTION; 58 | } 59 | 60 | public static String showInputDialog(Component parent, String title, String label, String help, int type) { 61 | return showInputDialog(parent, title, label, help, type, null, new BlinkCaret()); 62 | } 63 | 64 | public static String showInputDialog(Component parent, String title, String label, String help, int type, String initialText, Caret caret) { 65 | JPanel content = new JPanel(new GridLayout(0, 1)); 66 | 67 | if (!Strings.isNullOrEmpty(label)) { 68 | content.add(new JLabel(label)); 69 | } 70 | 71 | JTextField field = new JTextField(initialText); 72 | field.addAncestorListener(new RequestInitialFocusListener()); 73 | field.setCaret(caret); 74 | if (initialText != null) { 75 | field.setCaretPosition(initialText.length()); 76 | } 77 | content.add(field); 78 | 79 | if (!Strings.isNullOrEmpty(help)) { 80 | content.add(new JHelpLabel(help)); 81 | } 82 | 83 | JPanel container = new JPanel(new GridBagLayout()); 84 | container.add(content); 85 | 86 | int result = JOptionPane.showConfirmDialog(parent, container, title, JOptionPane.OK_CANCEL_OPTION, type); 87 | return result == JOptionPane.OK_OPTION ? field.getText() : null; 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/EditorProjectSettingsPane.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.GridBagLayout; 4 | import java.awt.GridLayout; 5 | import java.awt.event.KeyAdapter; 6 | import java.awt.event.KeyEvent; 7 | 8 | import javax.swing.JCheckBox; 9 | import javax.swing.JComboBox; 10 | import javax.swing.JLabel; 11 | import javax.swing.JPanel; 12 | 13 | import com.jvms.i18neditor.ResourceType; 14 | import com.jvms.i18neditor.swing.JHelpLabel; 15 | import com.jvms.i18neditor.swing.JTextField; 16 | import com.jvms.i18neditor.util.MessageBundle; 17 | 18 | /** 19 | * This class represents the project settings pane. 20 | * 21 | * @author Jacob van Mourik 22 | */ 23 | public class EditorProjectSettingsPane extends AbstractSettingsPane { 24 | private final static long serialVersionUID = 5665963334924596315L; 25 | private Editor editor; 26 | 27 | public EditorProjectSettingsPane(Editor editor) { 28 | super(); 29 | this.editor = editor; 30 | this.setupUI(); 31 | } 32 | 33 | private void setupUI() { 34 | EditorProject project = editor.getProject(); 35 | 36 | // General settings 37 | JPanel fieldset1 = createFieldset(MessageBundle.get("settings.fieldset.general")); 38 | 39 | ComboBoxFileStructure currentFileStructureItem = null; 40 | for (ComboBoxFileStructure item : fileStructureComboBoxItems) { 41 | if (item.getStructure().equals(project.getResourceFileStructure())) { 42 | currentFileStructureItem = item; 43 | break; 44 | } 45 | } 46 | JPanel fileStructurePanel = new JPanel(new GridLayout(0, 1)); 47 | JLabel fileStructureLabel = new JLabel(MessageBundle.get("settings.filestructure.title")); 48 | JComboBox fileStructureField = new JComboBox(fileStructureComboBoxItems.toArray()); 49 | fileStructureField.setSelectedItem(currentFileStructureItem); 50 | fileStructureField.addActionListener(e -> { 51 | project.setResourceFileStructure(((ComboBoxFileStructure)fileStructureField.getSelectedItem()).getStructure()); 52 | }); 53 | fileStructurePanel.add(fileStructureLabel); 54 | fileStructurePanel.add(fileStructureField); 55 | fieldset1.add(fileStructurePanel, createVerticalGridBagConstraints()); 56 | 57 | JPanel resourceDefinitionPanel = new JPanel(new GridLayout(0, 1)); 58 | JLabel resourceDefinitionLabel = new JLabel(MessageBundle.get("settings.resourcedef.title")); 59 | JHelpLabel resourceDefinitionHelpLabel = new JHelpLabel(MessageBundle.get("settings.resourcedef.help")); 60 | JTextField resourceDefinitionField = new JTextField(project.getResourceFileDefinition()); 61 | resourceDefinitionField.addKeyListener(new KeyAdapter() { 62 | @Override 63 | public void keyReleased(KeyEvent e) { 64 | String value = resourceDefinitionField.getText().trim(); 65 | project.setResourceFileDefinition(value.isEmpty() ? EditorSettings.DEFAULT_RESOURCE_FILE_DEFINITION : value); 66 | } 67 | }); 68 | resourceDefinitionPanel.add(resourceDefinitionLabel); 69 | resourceDefinitionPanel.add(resourceDefinitionField); 70 | fieldset1.add(resourceDefinitionPanel, createVerticalGridBagConstraints()); 71 | fieldset1.add(resourceDefinitionHelpLabel, createVerticalGridBagConstraints()); 72 | 73 | ResourceType type = project.getResourceType(); 74 | if (type == ResourceType.JSON || type == ResourceType.ES6) { 75 | JCheckBox minifyBox = new JCheckBox(MessageBundle.get("settings.minify.title")); 76 | minifyBox.setSelected(project.isMinifyResources()); 77 | minifyBox.addChangeListener(e -> project.setMinifyResources(minifyBox.isSelected())); 78 | fieldset1.add(minifyBox, createVerticalGridBagConstraints()); 79 | 80 | JCheckBox flattenJSONBox = new JCheckBox(MessageBundle.get("settings.flattenjson.title")); 81 | flattenJSONBox.setSelected(project.isFlattenJSON()); 82 | flattenJSONBox.addChangeListener(e -> project.setFlattenJSON(flattenJSONBox.isSelected())); 83 | fieldset1.add(flattenJSONBox, createVerticalGridBagConstraints()); 84 | } 85 | 86 | setLayout(new GridBagLayout()); 87 | add(fieldset1, createVerticalGridBagConstraints()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | THIS PROJECT IS NO LONGER MAINTAINED 2 | 3 | # i18n-editor [![Build Status](https://travis-ci.org/jcbvm/i18n-editor.svg?branch=master)](https://travis-ci.org/jcbvm/i18n-editor) 4 | 5 | This application lets you manage multiple translation files at once.
6 | The editor supports translation files with the following format: 7 | - `JSON` 8 | - `ES6` (JSON wrapped in a javascript ES6 module) 9 | - `Properties` (java properties files, for example to be used for a ResourceBundle). 10 | 11 | ### 12 | 13 | 14 | 15 | ## Features 16 | 17 | - Editing multiple translation files at once. 18 | - Creating new translations/locales or editing existing ones. 19 | - Renaming, duplicating, creating or deleting individual translations. 20 | - Detecting missing translations. 21 | - Supports multiple project layouts. 22 | - Supports custom file naming. 23 | - Supports file minification. 24 | - Supports both nested and flat JSON/ES6 structure. 25 | 26 | ## Requirements 27 | 28 | The application requires java 8 to be installed on your system.
29 | http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html 30 | 31 | ## Download 32 | 33 | You can download the latest release by going to [this page](https://github.com/jcbvm/i18n-editor/releases/latest) and downloading the `.zip` file.
If you're on Windows you can install the application by running the `.exe` file. If you're on Mac you can use the application by running the `.app` file. If you're on Linux you can use the application by running the `.jar` file. 34 | 35 | ## Usage 36 | 37 | #### Getting started 38 | To start, open the application and go to `File > New Project` to create a new project. After choosing the desired file format for your translations, select the root folder where you want to store your translation files. After selecting the root folder you'll be asked to add your first locale. From here you can add more locales by going to `Edit > Add Locale...` or start adding translations either via `Edit > Add Translation...`, via the right click menu in the left side panel or via the key field at the bottom of the left side panel. Each time you start the editor it will open the last project you was working on. You can always import an existing project by going to `File > Import Project...` and selecting the root folder of your existing project or by simply dragging your project folder into the application. 39 | 40 | #### Project layout 41 | The translations files can be stored in two different ways. Either using flat or folder structure. 42 | This setting can be changed in the settings menu, the default setting is flat structure. 43 | 44 | ##### Flat structure 45 | When using the flat structure the translation files are all in the same folder and follow the name pattern `translations_en_US`, `translations_nl_NL` etc. (the name and place of the locale within the name can be changed in the settings menu). When you create a project with a flat structure there will also a file be created called `translations`, this is a file you can use as default/fallback translation file (if you don't need a default/fallback file, you can simply remove it from your project folder). 46 | 47 | ##### Folder structure 48 | When using the folder structure folders will be named after the locales and the files within these folders will be of the form `translations` (the name can be changed in the settings menu). 49 | 50 | #### Changing settings 51 | You can access the settings of the editor by going to `Settings > Preferences...`. Here you can change the project file structure, the filename of the translation files you want to use (by default they are named `translations`), select whether you want to minify and or flatten the translations on save and change interface related properties. Some of this settings can also be applied on each project individually via `Settings > Project Preferences...`. 52 | 53 | ## Help translating 54 | 55 | Do you want this editor to be in your native language? You are free to create a pull request or issue with a new translation file of your desired language. Take a look at `src/main/resources/bundles` on how to create a translation file. 56 | 57 | ## License 58 | 59 | This project is released under the MIT license. 60 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.jvms 5 | i18n-editor 6 | 2.0.0-beta.1 7 | jar 8 | 9 | 10 | org.apache.commons 11 | commons-lang3 12 | 3.5 13 | 14 | 15 | com.google.code.gson 16 | gson 17 | 2.8.0 18 | 19 | 20 | com.google.guava 21 | guava 22 | 21.0 23 | 24 | 25 | org.slf4j 26 | slf4j-simple 27 | 1.7.22 28 | 29 | 30 | junit 31 | junit 32 | 4.12 33 | test 34 | 35 | 36 | 37 | 38 | 39 | maven-compiler-plugin 40 | 3.3 41 | 42 | 1.8 43 | 1.8 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-assembly-plugin 49 | 2.6 50 | 51 | 52 | package 53 | 54 | single 55 | 56 | 57 | 58 | 59 | com.jvms.i18neditor.Main 60 | 61 | 62 | 63 | jar-with-dependencies 64 | 65 | 66 | 67 | 68 | 69 | 70 | sh.tak.appbundler 71 | appbundle-maven-plugin 72 | 1.2.0 73 | 74 | 75 | package 76 | 77 | bundle 78 | 79 | 80 | com.jvms.i18neditor.Main 81 | images/icons.icns 82 | 83 | 84 | 85 | 86 | 87 | com.akathist.maven.plugins.launch4j 88 | launch4j-maven-plugin 89 | 1.7.7 90 | 91 | 92 | package 93 | 94 | launch4j 95 | 96 | 97 | gui 98 | ${project.build.directory}/${project.artifactId}-${project.version}/${project.artifactId}.exe 99 | ${project.build.directory}/${project.artifactId}-${project.version}-jar-with-dependencies.jar 100 | i18n Editor 101 | 102 | com.jvms.i18neditor.Main 103 | 104 | 105 | 1.8.0 106 | 107 | ${project.build.outputDirectory}/images/icon.ico 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-antrun-plugin 115 | 1.8 116 | 117 | 118 | install 119 | 120 | run 121 | 122 | 123 | 124 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/EditorSettings.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.util.List; 4 | import java.util.Locale; 5 | 6 | import com.jvms.i18neditor.FileStructure; 7 | 8 | /** 9 | * This class represents the editor settings. 10 | * 11 | * @author Jacob van Mourik 12 | */ 13 | public class EditorSettings { 14 | public final static String DEFAULT_RESOURCE_FILE_DEFINITION = "translations{_LOCALE}"; 15 | 16 | private int windowPositionX; 17 | private int windowPositionY; 18 | private int windowDeviderPosition; 19 | private int windowWidth; 20 | private int windowHeight; 21 | private boolean minifyResources; 22 | private boolean flattenJSON; 23 | private List history; 24 | private List lastExpandedNodes; 25 | private String lastSelectedNode; 26 | private boolean checkVersionOnStartup; 27 | private int defaultInputHeight; 28 | private boolean keyFieldEnabled; 29 | private boolean doubleClickTreeToggling; 30 | private String resourceFileDifinition; 31 | private Locale editorLanguage; 32 | private FileStructure resourceFileStructure; 33 | 34 | public int getWindowPositionX() { 35 | return windowPositionX; 36 | } 37 | 38 | public void setWindowPositionX(int windowPositionX) { 39 | this.windowPositionX = windowPositionX; 40 | } 41 | 42 | public int getWindowPositionY() { 43 | return windowPositionY; 44 | } 45 | 46 | public void setWindowPositionY(int windowPositionY) { 47 | this.windowPositionY = windowPositionY; 48 | } 49 | 50 | public int getWindowDeviderPosition() { 51 | return windowDeviderPosition; 52 | } 53 | 54 | public void setWindowDeviderPosition(int deviderPosition) { 55 | this.windowDeviderPosition = deviderPosition; 56 | } 57 | 58 | public int getWindowWidth() { 59 | return windowWidth; 60 | } 61 | 62 | public void setWindowWidth(int width) { 63 | this.windowWidth = width; 64 | } 65 | 66 | public int getWindowHeight() { 67 | return windowHeight; 68 | } 69 | 70 | public void setWindowHeight(int height) { 71 | this.windowHeight = height; 72 | } 73 | 74 | public List getHistory() { 75 | return history; 76 | } 77 | 78 | public void setHistory(List history) { 79 | this.history = history; 80 | } 81 | 82 | public List getLastExpandedNodes() { 83 | return lastExpandedNodes; 84 | } 85 | 86 | public void setLastExpandedNodes(List lastExpandedNodes) { 87 | this.lastExpandedNodes = lastExpandedNodes; 88 | } 89 | 90 | public String getLastSelectedNode() { 91 | return lastSelectedNode; 92 | } 93 | 94 | public void setLastSelectedNode(String lastSelectedNode) { 95 | this.lastSelectedNode = lastSelectedNode; 96 | } 97 | 98 | public String getResourceFileDefinition() { 99 | return resourceFileDifinition; 100 | } 101 | 102 | public void setResourceFileDefinition(String resourceFileDifinition) { 103 | this.resourceFileDifinition = resourceFileDifinition; 104 | } 105 | 106 | public boolean isMinifyResources() { 107 | return minifyResources; 108 | } 109 | 110 | public void setMinifyResources(boolean minifyResources) { 111 | this.minifyResources = minifyResources; 112 | } 113 | 114 | public boolean isCheckVersionOnStartup() { 115 | return checkVersionOnStartup; 116 | } 117 | 118 | public void setCheckVersionOnStartup(boolean checkVersionOnStartup) { 119 | this.checkVersionOnStartup = checkVersionOnStartup; 120 | } 121 | 122 | public int getDefaultInputHeight() { 123 | return defaultInputHeight; 124 | } 125 | 126 | public void setDefaultInputHeight(int rows) { 127 | this.defaultInputHeight = rows; 128 | } 129 | 130 | public boolean isKeyFieldEnabled() { 131 | return keyFieldEnabled; 132 | } 133 | 134 | public void setKeyFieldEnabled(boolean keyFieldEnabled) { 135 | this.keyFieldEnabled = keyFieldEnabled; 136 | } 137 | 138 | public boolean isDoubleClickTreeToggling() { 139 | return doubleClickTreeToggling; 140 | } 141 | 142 | public void setDoubleClickTreeToggling(boolean doubleClickTreeToggling) { 143 | this.doubleClickTreeToggling = doubleClickTreeToggling; 144 | } 145 | 146 | public boolean isFlattenJSON() { 147 | return flattenJSON; 148 | } 149 | 150 | public void setFlattenJSON(boolean flattenJSON) { 151 | this.flattenJSON = flattenJSON; 152 | } 153 | 154 | public Locale getEditorLanguage() { 155 | return editorLanguage; 156 | } 157 | 158 | public void setEditorLanguage(Locale editorLanguage) { 159 | this.editorLanguage = editorLanguage; 160 | } 161 | 162 | public FileStructure getResourceFileStructure() { 163 | return resourceFileStructure; 164 | } 165 | 166 | public void setResourceFileStructure(FileStructure resourceFileStructure) { 167 | this.resourceFileStructure = resourceFileStructure; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/com/jvms/i18neditor/util/ResourceKeysTest.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import org.junit.Test; 9 | 10 | import com.google.common.collect.Lists; 11 | 12 | /** 13 | * 14 | * @author Jacob 15 | */ 16 | public class ResourceKeysTest { 17 | 18 | @Test 19 | public void createTest() { 20 | assertEquals("a", ResourceKeys.create("a")); 21 | assertEquals("a.b", ResourceKeys.create("a","b")); 22 | assertEquals("a.b.c", ResourceKeys.create("a","b.c")); 23 | assertEquals("a.b.c", ResourceKeys.create(Arrays.asList("a","b","c"))); 24 | assertEquals("a.b.c", ResourceKeys.create(new String[]{"a","b","c"})); 25 | } 26 | 27 | @Test 28 | public void sizeTest() { 29 | assertEquals(1, ResourceKeys.size("")); 30 | assertEquals(1, ResourceKeys.size("a")); 31 | assertEquals(2, ResourceKeys.size("a.b")); 32 | assertEquals(3, ResourceKeys.size("a.b.c")); 33 | } 34 | 35 | @Test 36 | public void partsTest() { 37 | assertArrayEquals(new String[]{""}, ResourceKeys.parts("")); 38 | assertArrayEquals(new String[]{"a"}, ResourceKeys.parts("a")); 39 | assertArrayEquals(new String[]{"a","b"}, ResourceKeys.parts("a.b")); 40 | assertArrayEquals(new String[]{"a","b","c"}, ResourceKeys.parts("a.b.c")); 41 | } 42 | 43 | @Test 44 | public void subPartsTest() { 45 | assertArrayEquals(new String[]{""}, ResourceKeys.subParts("",0)); 46 | assertArrayEquals(new String[]{}, ResourceKeys.subParts("",1)); 47 | assertArrayEquals(new String[]{"a"}, ResourceKeys.subParts("a",0)); 48 | assertArrayEquals(new String[]{}, ResourceKeys.subParts("a",1)); 49 | assertArrayEquals(new String[]{"a","b"}, ResourceKeys.subParts("a.b",0)); 50 | assertArrayEquals(new String[]{"b"}, ResourceKeys.subParts("a.b",1)); 51 | assertArrayEquals(new String[]{}, ResourceKeys.subParts("a.b",2)); 52 | assertArrayEquals(new String[]{"a","b","c"}, ResourceKeys.subParts("a.b.c",0)); 53 | assertArrayEquals(new String[]{"b","c"}, ResourceKeys.subParts("a.b.c",1)); 54 | assertArrayEquals(new String[]{"c"}, ResourceKeys.subParts("a.b.c",2)); 55 | assertArrayEquals(new String[]{}, ResourceKeys.subParts("a.b.c",3)); 56 | } 57 | 58 | @Test 59 | public void firstPartTest() { 60 | assertEquals("", ResourceKeys.firstPart("")); 61 | assertEquals("a", ResourceKeys.firstPart("a")); 62 | assertEquals("a", ResourceKeys.firstPart("a.b")); 63 | assertEquals("a", ResourceKeys.firstPart("a.b.c")); 64 | } 65 | 66 | @Test 67 | public void lastPartTest() { 68 | assertEquals("", ResourceKeys.lastPart("")); 69 | assertEquals("a", ResourceKeys.lastPart("a")); 70 | assertEquals("b", ResourceKeys.lastPart("a.b")); 71 | assertEquals("c", ResourceKeys.lastPart("a.b.c")); 72 | } 73 | 74 | @Test 75 | public void withoutFirstPartTest() { 76 | assertEquals("", ResourceKeys.withoutFirstPart("")); 77 | assertEquals("", ResourceKeys.withoutFirstPart("a")); 78 | assertEquals("b", ResourceKeys.withoutFirstPart("a.b")); 79 | assertEquals("b.c", ResourceKeys.withoutFirstPart("a.b.c")); 80 | } 81 | 82 | @Test 83 | public void withoutLastPartTest() { 84 | assertEquals("", ResourceKeys.withoutLastPart("")); 85 | assertEquals("", ResourceKeys.withoutLastPart("a")); 86 | assertEquals("a", ResourceKeys.withoutLastPart("a.b")); 87 | assertEquals("a.b", ResourceKeys.withoutLastPart("a.b.c")); 88 | } 89 | 90 | @Test 91 | public void isChildKeyOfTest() { 92 | assertFalse(ResourceKeys.isChildKeyOf("", "")); 93 | assertFalse(ResourceKeys.isChildKeyOf("a", "a")); 94 | assertTrue(ResourceKeys.isChildKeyOf("a.b.c", "a")); 95 | assertTrue(ResourceKeys.isChildKeyOf("a.b.c", "a.b")); 96 | } 97 | 98 | @Test 99 | public void childKeyTest() { 100 | assertEquals("", ResourceKeys.childKey("", "")); 101 | assertEquals("", ResourceKeys.childKey("a", "a")); 102 | assertEquals("", ResourceKeys.childKey("a", "a.b.c")); 103 | assertEquals("d", ResourceKeys.childKey("a.b.c.d", "a.b.c")); 104 | assertEquals("d.e.f", ResourceKeys.childKey("a.b.c.d.e.f", "a.b.c")); 105 | assertEquals("", ResourceKeys.childKey("b.c.d", "a.b.c")); 106 | } 107 | 108 | @Test 109 | public void uniqueRootKeysTest() { 110 | List keys = Lists.newArrayList("a.b.c", "a.b", "b.c.d", "b.c", "c.d.e", "c.d"); 111 | List expected = Lists.newArrayList("a", "b", "c"); 112 | assertEquals(expected, ResourceKeys.uniqueRootKeys(keys)); 113 | } 114 | 115 | @Test 116 | public void extractChildKeysTest() { 117 | List keys = Lists.newArrayList("a.b.c", "b.c.d", "a.b.c.d.e", "b.c", "b.c.d.e"); 118 | List expected = Lists.newArrayList("d", "d.e"); 119 | assertEquals(expected, ResourceKeys.extractChildKeys(keys, "b.c")); 120 | } 121 | 122 | @Test 123 | public void isValidTest() { 124 | assertTrue(ResourceKeys.isValid("a")); 125 | assertTrue(ResourceKeys.isValid("a.b")); 126 | assertTrue(ResourceKeys.isValid("a.b.c")); 127 | assertFalse(ResourceKeys.isValid(".")); 128 | assertFalse(ResourceKeys.isValid(".a")); 129 | assertFalse(ResourceKeys.isValid(".a.b")); 130 | assertFalse(ResourceKeys.isValid("a.")); 131 | assertFalse(ResourceKeys.isValid("a.b.")); 132 | assertFalse(ResourceKeys.isValid(" ")); 133 | assertFalse(ResourceKeys.isValid("a.\tb")); 134 | assertFalse(ResourceKeys.isValid("a.\nb")); 135 | assertFalse(ResourceKeys.isValid("a .b")); 136 | assertFalse(ResourceKeys.isValid("a. b")); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/resources/bundles/messages.properties: -------------------------------------------------------------------------------- 1 | core.intro.text=Drop an existing project here or go to File\uFE65New Project to start 2 | dialogs.about.title=About {0} 3 | dialogs.error.title=Error 4 | dialogs.locale.add.error.create=An error occurred while creating the new locale. 5 | dialogs.locale.add.error.invalid=The locale you entered is invalid or does already exist. 6 | dialogs.locale.add.text=Enter locale (i.e. en_US)\: 7 | dialogs.locale.add.title=Add Locale 8 | dialogs.preferences.editor.title=Preferences 9 | dialogs.preferences.project.title=Project Preferences 10 | dialogs.project.import.title=Import Project 11 | dialogs.project.new.conflict.text=There are existing translations found at this location, do you want to import them instead? 12 | dialogs.project.new.conflict.title=Conflict 13 | dialogs.project.new.title=New Project 14 | dialogs.save.checksum.text=The following translation file has been modified by another program\:
"{0}"

Do you want to overwrite the file? 15 | dialogs.save.checksum.title=Save Translation 16 | dialogs.save.text=You have unsaved changes, do you want to save them? 17 | dialogs.save.title=Save Translations 18 | dialogs.translation.add.help=HINT\: use dots (.) to create a nested key (i.e. a.b) 19 | dialogs.translation.add.text=Enter translation key\: 20 | dialogs.translation.add.title=Add Translation 21 | dialogs.translation.conflict.text.merge=There already exists a translation with this key, do you want to merge it? 22 | dialogs.translation.conflict.text.replace=There already exists a translation with this key, do you want to replace it? 23 | dialogs.translation.conflict.title=Conflict 24 | dialogs.translation.duplicate.text=Enter new translation key\: 25 | dialogs.translation.duplicate.title=Duplicate Translation 26 | dialogs.translation.find.error=No translation found. 27 | dialogs.translation.find.text=Enter translation key\: 28 | dialogs.translation.find.title=Find Translation 29 | dialogs.translation.key.error=The translation key you entered is invalid. 30 | dialogs.translation.overwrite.text=Performing this action will erase the value for translation\:
"{0}"

Do you want to continue? 31 | dialogs.translation.overwrite.title=Confirm 32 | dialogs.translation.rename.text=Enter new translation key\: 33 | dialogs.translation.rename.title=Rename Translation 34 | dialogs.version.link=Click here to download 35 | dialogs.version.new=A new version is available\: 36 | dialogs.version.title=Available Updates 37 | dialogs.version.uptodate=You are using the latest version. 38 | menu.edit.add.locale.title=Add Locale... 39 | menu.edit.add.translation.title=Add Translation... 40 | menu.edit.copy.key.title=Copy Translation Key 41 | menu.edit.delete.title=Delete Translation 42 | menu.edit.duplicate.title=Duplicate Translation... 43 | menu.edit.find.translation.title=Find Translation... 44 | menu.edit.rename.title=Rename Translation... 45 | menu.edit.title=Edit 46 | menu.edit.vk=E 47 | menu.file.exit.title=Exit 48 | menu.file.exit.vk=E 49 | menu.file.folder.title=Open Containing Folder 50 | menu.file.project.import.title=Import Project... 51 | menu.file.project.import.vk=I 52 | menu.file.project.new.es6.title=ES6 Format... 53 | menu.file.project.new.json.title=JSON Format... 54 | menu.file.project.new.properties.title=Properties Format... 55 | menu.file.project.new.title=New Project 56 | menu.file.project.new.vk=N 57 | menu.file.recent.clear.title=Clear List 58 | menu.file.recent.title=Open Recent 59 | menu.file.recent.vk=C 60 | menu.file.reload.title=Reload from Disk 61 | menu.file.reload.vk=R 62 | menu.file.save.title=Save 63 | menu.file.save.vk=S 64 | menu.file.title=File 65 | menu.file.vk=F 66 | menu.help.about.title=About {0} 67 | menu.help.home.title={0} Website 68 | menu.help.title=Help 69 | menu.help.version.title=Check for Updates... 70 | menu.help.vk=H 71 | menu.settings.preferences.editor.title=Preferences... 72 | menu.settings.preferences.project.title=Project Preferences... 73 | menu.settings.title=Settings 74 | menu.settings.vk=S 75 | menu.view.collapse.title=Collapse All Translation Keys 76 | menu.view.expand.title=Expand All Translation Keys 77 | menu.view.title=View 78 | menu.view.vk=V 79 | resources.create.error=An error occurred while creating translation files. 80 | resources.import.empty=No translation files found in\:
"{0}" 81 | resources.import.error.multiple=An error occurred while opening translation files. 82 | resources.import.error.single=An error occurred while opening the translation file\:
"{0}" 83 | resources.locale.default=Default 84 | resources.write.error.single=An error occurred while writing the translation file\:
"{0}" 85 | settings.checkversion.title=Check for new version on startup 86 | settings.fieldset.editing=Editing 87 | settings.fieldset.general=General 88 | settings.fieldset.newprojects=New Projects 89 | settings.filestructure.flat=Flat structure 90 | settings.filestructure.nested=Folder structure 91 | settings.filestructure.title=Project layout 92 | settings.flattenjson.title=Flatten translation keys on save 93 | settings.inputheight.title=Default height of input fields 94 | settings.keyfield.title=Show translation key field 95 | settings.language.title=Interface language (restart required) 96 | settings.minify.title=Minify JSON/ES6 translation files on save 97 | settings.resource.jsones6=(JSON/ES6) 98 | 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. 99 | settings.resourcedef.title=Translations filename 100 | settings.treetogglemode.title=Expand and collapse translation keys using double click 101 | swing.action.copy=Copy 102 | swing.action.cut=Cut 103 | swing.action.delete=Delete 104 | swing.action.paste=Paste 105 | swing.action.selectall=Select All 106 | swing.action.undo=Undo 107 | tree.root.name=Translations 108 | -------------------------------------------------------------------------------- /src/main/resources/bundles/messages_pt_BR.properties: -------------------------------------------------------------------------------- 1 | core.intro.text=Solte um projeto existente aqui ou v\u00E1 para Arquivo\uFE65Novo Projeto para iniciar 2 | dialogs.about.title=Sobre {0} 3 | dialogs.error.title=Erro 4 | dialogs.locale.add.error.create=Ocorreu um erro ao criar o nova localidade. 5 | dialogs.locale.add.error.invalid=A localidade digitada \u00E9 inv\u00E1lida ou n\u00E3o existe. 6 | dialogs.locale.add.text=Informe uma localidade (Ex. pt_BR)\: 7 | dialogs.locale.add.title=Incluir localidade 8 | dialogs.preferences.editor.title=Prefer\u00EAncias 9 | dialogs.preferences.project.title=Prefer\u00EAncias do Projeto 10 | dialogs.project.import.title=Projeto de Importa\u00E7\u00E3o 11 | dialogs.project.new.conflict.text=Existem tradu\u00E7\u00F5es existentes neste local, voc\u00EA deseja import\u00E1-las? 12 | dialogs.project.new.conflict.title=Conflito 13 | dialogs.project.new.title=Novo Projeto 14 | dialogs.save.text=Voc\u00EA tem modifica\u00E7\u00F5es n\u00E3o salvas, deseja salv\u00E1-las? 15 | dialogs.save.title=Salvar Tradu\u00E7\u00F5es 16 | dialogs.translation.add.help=HINT\: use dots (.) to create a nested key (i.e. a.b) 17 | dialogs.translation.add.text=Informe a chave da tradu\u00E7\u00E3o\: 18 | dialogs.translation.add.title=Incluir Tradu\u00E7\u00E3o 19 | dialogs.translation.conflict.text.merge=J\u00E1 existe uma tradu\u00E7\u00E3o com esta chave, deseja mescl\u00E1-la? 20 | dialogs.translation.conflict.text.replace=J\u00E1 existe uma tradu\u00E7\u00E3o com esta chave, deseja substitu\u00ED-la? 21 | dialogs.translation.conflict.title=Conflito na Tradu\u00E7\u00E3o 22 | dialogs.translation.duplicate.text=Informe uma nova chave\: 23 | dialogs.translation.duplicate.title=Duplicar Tradu\u00E7\u00E3o 24 | dialogs.translation.find.error=Nenhumma tradu\u00E7\u00E3o encontrada. 25 | dialogs.translation.find.text=Informe uma chave de tradu\u00E7\u00E3o\: 26 | dialogs.translation.find.title=Localizar Tradu\u00E7\u00E3o 27 | dialogs.translation.key.error=A chave de tradu\u00E7\u00E3o inofrmada \u00E9 inv\u00E1lida. 28 | dialogs.translation.overwrite.text=Performing this action will erase the value for translation\:
"{0}"

Do you want to continue? 29 | dialogs.translation.overwrite.title=Confirm 30 | dialogs.translation.rename.text=Informe a nova chave\: 31 | dialogs.translation.rename.title=Renomear Tradu\u00E7\u00E3o 32 | dialogs.version.link=Clique para baixar 33 | dialogs.version.new=Uma nova vers\u00E3o est\u00E1 dispon\u00EDvel\: 34 | dialogs.version.title=Atualiza\u00E7\u00F5es Dispon\u00EDveis 35 | dialogs.version.uptodate=Voc\u00EA est\u00E1 atualizado com a vers\u00E3o mais recente. 36 | menu.edit.add.locale.title=Incluir Localiza\u00E7\u00E3o... 37 | menu.edit.add.translation.title=Incluir Tradu\u00E7\u00E3o... 38 | menu.edit.copy.key.title=Copy Translation Key 39 | menu.edit.delete.title=Excluir Tradu\u00E7\u00E3o 40 | menu.edit.duplicate.title=Duplicar Tradu\u00E7\u00E3o... 41 | menu.edit.find.translation.title=Localizar Tradu\u00E7\u00E3o... 42 | menu.edit.rename.title=Renomear Tradu\u00E7\u00E3o... 43 | menu.edit.title=Editar 44 | menu.edit.vk=E 45 | menu.file.exit.title=Sair 46 | menu.file.exit.vk=S 47 | menu.file.folder.title=Abrir Pasta de Conte\u00FAdo 48 | menu.file.project.import.title=Projeto de Importa\u00E7\u00E3o... 49 | menu.file.project.import.vk=I 50 | menu.file.project.new.es6.title=Formato ES6... 51 | menu.file.project.new.json.title=Formato JSON... 52 | menu.file.project.new.properties.title=Formato Properties... 53 | menu.file.project.new.title=Novo Projeto 54 | menu.file.project.new.vk=N 55 | menu.file.recent.clear.title=Limpar Lista 56 | menu.file.recent.title=Abrir Recente 57 | menu.file.recent.vk=E 58 | menu.file.reload.title=Recarregar do Disco 59 | menu.file.reload.vk=R 60 | menu.file.save.title=Salvar 61 | menu.file.save.vk=L 62 | menu.file.title=Arquivo 63 | menu.file.vk=A 64 | menu.help.about.title=Sobre {0} 65 | menu.help.title=Ajuda 66 | menu.help.version.title=Verificar a nova vers\u00E3o... 67 | menu.help.vk=J 68 | menu.settings.preferences.editor.title=Prefer\u00EAncias... 69 | menu.settings.preferences.project.title=Prefer\u00EAncias do Projeto... 70 | menu.settings.title=Configura\u00E7\u00F5es 71 | menu.settings.vk=C 72 | menu.view.collapse.title=Recolher todas as tradu\u00E7\u00F5es 73 | menu.view.expand.title=Expandir todas as tradu\u00E7\u00F5es 74 | menu.view.title=Ver 75 | menu.view.vk=V 76 | resources.create.error=Um erro ocorreu ao criar arquivos de tradu\u00E7\u00F5es. 77 | resources.import.empty=Nenhum arquivo de tradu\u00E7\u00E3o encontrado em\:
"{0}" 78 | resources.import.error.multiple=Um erro ocorreu enquanto os arquivos de tradu\u00E7\u00F5es eram carregados. 79 | resources.import.error.single=Um erro ocorreu enquanto o arquivo de tradu\u00E7\u00E3o era carregado\:
"{0}" 80 | resources.locale.default=padr\u00E3o 81 | resources.write.error.single=Um erro ocorreu enquanto o arquivo de tradu\u00E7\u00E3o era salvo\:
"{0}" 82 | settings.checkversion.title=Verificar a nova vers\u00E3o na inicializa\u00E7\u00E3o 83 | settings.fieldset.editing=Edi\u00E7\u00E3o 84 | settings.fieldset.general=Geral 85 | settings.fieldset.newprojects=Novos Projetos 86 | settings.filestructure.flat=Estrutura plana 87 | settings.filestructure.nested=Estrutura de pastas 88 | settings.filestructure.title=Layout do projeto 89 | settings.flattenjson.title=Achatar as chaves de tradu\u00E7\u00E3o ao salvar 90 | settings.inputheight.title=Altura padr\u00E3o dos campos de entrada 91 | settings.keyfield.title=Mostrar campo da chave de tradu\u00E7\u00E3o 92 | settings.language.title=Interface de linguagem (\u00E9 necess\u00E1rio reiniciar) 93 | settings.minify.title=Minificar tradu\u00E7\u00F5es ao salvar 94 | settings.resource.jsones6=(JSON/ES6) 95 | settings.resourcedef.title=Nome do arquivo de tradu\u00E7\u00E3es 96 | settings.treetogglemode.title=Expandir e contrair tradu\u00E7\u00F5es usando duplo clique 97 | swing.action.copy=Copiar 98 | swing.action.cut=Cortar 99 | swing.action.delete=Apagar 100 | swing.action.paste=Colar 101 | swing.action.selectall=Selecionar Tudo 102 | swing.action.undo=Desfazer 103 | tree.root.name=Tradu\u00E7\u00F5es 104 | -------------------------------------------------------------------------------- /src/main/resources/bundles/messages_nl.properties: -------------------------------------------------------------------------------- 1 | core.intro.text=Sleep een bestaand project hierheen of ga naar Bestand\uFE65Nieuw Project om te starten 2 | dialogs.about.title=Over {0} 3 | dialogs.error.title=Fout 4 | dialogs.locale.add.error.create=Er is iets fout gegaan bij het toevoegen van de nieuwe taal. 5 | dialogs.locale.add.error.invalid=De opgegeven taal is niet geldig of bestaat al. 6 | dialogs.locale.add.text=Taal (bijv. nl_NL)\: 7 | dialogs.locale.add.title=Taal Toevoegen 8 | dialogs.preferences.editor.title=Voorkeuren 9 | dialogs.preferences.project.title=Projectvoorkeuren 10 | dialogs.project.import.title=Importeer Project 11 | dialogs.project.new.conflict.text=Er zijn bestaande vertaalbestanden gevonden op deze locatie, wilt u deze importeren? 12 | dialogs.project.new.conflict.title=Conflict 13 | dialogs.project.new.title=Nieuw Project 14 | dialogs.save.checksum.text=Het volgende vertaalbestand is gewijzigd door een ander programma\:
"{0}"

Wilt u het bestand overschrijven? 15 | dialogs.save.checksum.title=Vertaling Opslaan 16 | dialogs.save.text=U heeft nog onopgeslagen wijzigingen, wilt u deze opslaan? 17 | dialogs.save.title=Vertalingen Opslaan 18 | dialogs.translation.add.help=TIP\: gebruik punten (.) om een geneste sleutel te cree\u00EBren (bijv. a.b) 19 | dialogs.translation.add.text=Vertaalsleutel\: 20 | dialogs.translation.add.title=Vertaling Toevoegen 21 | dialogs.translation.conflict.text.merge=Er bestaat al een vertaling met deze vertaalsleutel, wilt u deze samenvoegen? 22 | dialogs.translation.conflict.text.replace=Er bestaat al een vertaling met deze vertaalsleutel, wilt u deze vervangen? 23 | dialogs.translation.conflict.title=Conflict 24 | dialogs.translation.duplicate.text=Nieuwe vertaalsleutel\: 25 | dialogs.translation.duplicate.title=Vertaling Dupliceren 26 | dialogs.translation.find.error=Geen vertaling gevonden. 27 | dialogs.translation.find.text=Vertaalsleutel\: 28 | dialogs.translation.find.title=Vertaling Zoeken 29 | dialogs.translation.key.error=De opgegeven vertaalsleutel is niet geldig. 30 | dialogs.translation.overwrite.text=Deze actie zal de volgende vertaling verwijderen\:
"{0}"

Wilt u doorgaan? 31 | dialogs.translation.overwrite.title=Bevestigen 32 | dialogs.translation.rename.text=Nieuwe vertaalsleutel\: 33 | dialogs.translation.rename.title=Vertaling Hernoemen 34 | dialogs.version.link=Klik hier om te downloaden 35 | dialogs.version.new=Een nieuwe versie is beschikbaar\: 36 | dialogs.version.title=Beschikbare Updates 37 | dialogs.version.uptodate=U gebruikt de nieuwste versie. 38 | menu.edit.add.locale.title=Taal Toevoegen... 39 | menu.edit.add.translation.title=Vertaling Toevoegen... 40 | menu.edit.copy.key.title=Vertaalsleutel Kopi\u00EBren 41 | menu.edit.delete.title=Vertaling Verwijderen 42 | menu.edit.duplicate.title=Vertaling Dupliceren... 43 | menu.edit.find.translation.title=Vertaling Zoeken... 44 | menu.edit.rename.title=Vertaling Hernoemen... 45 | menu.edit.title=Bewerken 46 | menu.edit.vk=W 47 | menu.file.exit.title=Sluiten 48 | menu.file.exit.vk=S 49 | menu.file.folder.title=In Map Weergeven 50 | menu.file.project.import.title=Importeer Project... 51 | menu.file.project.import.vk=I 52 | menu.file.project.new.es6.title=ES6 Formaat... 53 | menu.file.project.new.json.title=JSON Formaat... 54 | menu.file.project.new.properties.title=Properties Formaat... 55 | menu.file.project.new.title=Nieuw Project 56 | menu.file.project.new.vk=N 57 | menu.file.recent.clear.title=Lijst Wissen 58 | menu.file.recent.title=Recent Geopend 59 | menu.file.recent.vk=R 60 | menu.file.reload.title=Herladen vanaf Schijf 61 | menu.file.reload.vk=H 62 | menu.file.save.title=Opslaan 63 | menu.file.save.vk=P 64 | menu.file.title=Bestand 65 | menu.file.vk=B 66 | menu.help.about.title=Over {0} 67 | menu.help.home.title={0} Website 68 | menu.help.title=Help 69 | menu.help.version.title=Controleer nieuwe Versie... 70 | menu.help.vk=H 71 | menu.settings.preferences.editor.title=Voorkeuren... 72 | menu.settings.preferences.project.title=Projectvoorkeuren... 73 | menu.settings.title=Instellingen 74 | menu.settings.vk=I 75 | menu.view.collapse.title=Alle Vertaalsleutels Invouwen 76 | menu.view.expand.title=Alle Vertaalsleutels Uitvouwen 77 | menu.view.title=Beeld 78 | menu.view.vk=L 79 | resources.create.error=Er is iets fout gegaan bij het aanmaken van de vertaalbestanden. 80 | resources.import.empty=Geen vertaalbestanden gevonden in\:
"{0}" 81 | resources.import.error.multiple=Er is iets fout gegaan bij het openen van de vertaalbetanden. 82 | resources.import.error.single=Er is iets fout gegaan bij het openen van het vertaalbestand\:
"{0}" 83 | resources.locale.default=Standaard 84 | resources.write.error.single=Er is iets fout gegaan bij het opslaan van het vertaalbestand\:
"{0}" 85 | settings.checkversion.title=Controleer op nieuwe versie bij opstarten 86 | settings.fieldset.editing=Weergave 87 | settings.fieldset.general=Algemeen 88 | settings.fieldset.newprojects=Nieuwe Projecten 89 | settings.filestructure.flat=Platte bestandsstructuur 90 | settings.filestructure.nested=Mappenstructuur 91 | settings.filestructure.title=Project lay-out 92 | settings.flattenjson.title=Sla vertaalsleutels in platte structuur op 93 | settings.inputheight.title=Standaardhoogte van invoervelden 94 | settings.keyfield.title=Toon veld met vertaalsleutel 95 | settings.language.title=Interfacetaal (herstart vereist) 96 | settings.minify.title=Sla vertaalbestanden gecomprimeerd op 97 | settings.resource.jsones6=(JSON/ES6) 98 | settings.resourcedef.help=U kunt met '{' '}' tags het gedeelte van de taal aangeven binnen de bestandsnaam.
De tekst LOCALE binnen deze tags zal vervangen worden door de werkelijke taal.
Voorbeeld\: translations'{'_LOCALE'}' wordt translations_nl_NL. 99 | settings.resourcedef.title=Bestandsnaam vertalingen 100 | settings.treetogglemode.title=Vertaalsleutels in- en uitvouwen met dubbelklik 101 | swing.action.copy=Kopi\u00EBren 102 | swing.action.cut=Knippen 103 | swing.action.delete=Verwijderen 104 | swing.action.paste=Plakken 105 | swing.action.selectall=Alles Selecteren 106 | swing.action.undo=Ongedaan Maken 107 | tree.root.name=Vertalingen 108 | -------------------------------------------------------------------------------- /src/main/resources/bundles/messages_es_ES.properties: -------------------------------------------------------------------------------- 1 | core.intro.text=Arrastre aqu\u00ED un proyecto existente o vaya a Archivo->Nuevo proyecto para empezar 2 | dialogs.about.title=Sobre {0} 3 | dialogs.error.title=Error 4 | dialogs.locale.add.error.create=Ocurri\u00F3 alg\u00FAn error al crear la nueva locale 5 | dialogs.locale.add.error.invalid=La locale que ha introducido es inv\u00E1lida o ya existe. 6 | dialogs.locale.add.text=Introduzca una locale (p. ej. es_ES)\: 7 | dialogs.locale.add.title=A\u00F1adir locale 8 | dialogs.preferences.editor.title=Preferencias 9 | dialogs.preferences.project.title=Preferencias del proyecto 10 | dialogs.project.import.title=Importar Proyecto 11 | dialogs.project.new.conflict.text=Se han encontrado traducciones en esta ubicaci\u00F3n, \u00BFdesea importarlas? 12 | dialogs.project.new.conflict.title=Conflicto 13 | dialogs.project.new.title=Nuevo Proyecto 14 | dialogs.save.checksum.text=Esta traducci\u00F3n ha sido modificada por otro programa\:
"{0}"

\u00BFDesea sobreescribir el fichero? 15 | dialogs.save.checksum.title=Guardar traducci\u00F3n 16 | dialogs.save.text=Hay cambios sin guardar, \u00BFdesea guardar los cambios? 17 | dialogs.save.title=Guardar traducciones 18 | dialogs.translation.add.help=SUGERENCIA\: utilice puntos (.) para crear claves anidadas (i.e.a.b) 19 | dialogs.translation.add.text=Introducir clave de traducci\u00F3n\: 20 | dialogs.translation.add.title=A\u00F1adir Traducci\u00F3n 21 | dialogs.translation.conflict.text.merge=Ya existe una traducci\u00F3n con esta clave, \u00BFdesea mezclarla? 22 | dialogs.translation.conflict.text.replace=Ya existe una traducci\u00F3n con esta clave, \u00BFdesea reemplazarla? 23 | dialogs.translation.conflict.title=Conflicto 24 | dialogs.translation.duplicate.text=Introducir nueva clave de traducci\u00F3n\: 25 | dialogs.translation.duplicate.title=Duplicar Traducci\u00F3n 26 | dialogs.translation.find.error=No se ha encontrado traducci\u00F3n. 27 | dialogs.translation.find.text=Introducir clave de traducci\u00F3n\: 28 | dialogs.translation.find.title=Buscar Traducci\u00F3n 29 | dialogs.translation.key.error=La clave de traducci\u00F3n que ha introducido es incorrecta. 30 | dialogs.translation.overwrite.text=Esta acci\u00F3n borrar\u00E1 el valor de la traducci\u00F3n\:
"{0}"

\u00BFDesea continuar? 31 | dialogs.translation.overwrite.title=Confirmar 32 | dialogs.translation.rename.text=Introducir nueva clave de traducci\u00F3n\: 33 | dialogs.translation.rename.title=Renombrar Traducci\u00F3n 34 | dialogs.version.link=Click para descargar 35 | dialogs.version.new=Hay una nueva versi\u00F3n disponible\: 36 | dialogs.version.title=Actualizaciones disponibles 37 | dialogs.version.uptodate=Est\u00E1 utilizando la \u00FAltima versi\u00F3n. 38 | menu.edit.add.locale.title=A\u00F1adir Locale... 39 | menu.edit.add.translation.title=A\u00F1adir Traducci\u00F3n... 40 | menu.edit.copy.key.title=Copiar clave de Traducci\u00F3n 41 | menu.edit.delete.title=Borrar Traducci\u00F3n 42 | menu.edit.duplicate.title=Duplicar Traducci\u00F3n... 43 | menu.edit.find.translation.title=Buscar Traducci\u00F3n... 44 | menu.edit.rename.title=Renombrar Traducci\u00F3n... 45 | menu.edit.title=Editar 46 | menu.edit.vk=E 47 | menu.file.exit.title=Salir 48 | menu.file.exit.vk=S 49 | menu.file.folder.title=Abrir desde directorio 50 | menu.file.project.import.title=Importar Proyecto 51 | menu.file.project.import.vk=I 52 | menu.file.project.new.es6.title=Formato ES6... 53 | menu.file.project.new.json.title=Formato JSON... 54 | menu.file.project.new.properties.title=Formato Properties... 55 | menu.file.project.new.title=Nuevo Proyecto 56 | menu.file.project.new.vk=N 57 | menu.file.recent.clear.title=Limpiar Lista 58 | menu.file.recent.title=Abrir Reciente 59 | menu.file.recent.vk=A 60 | menu.file.reload.title=Recargar desde Disco 61 | menu.file.reload.vk=R 62 | menu.file.save.title=Guardar 63 | menu.file.save.vk=G 64 | menu.file.title=Archivo 65 | menu.file.vk=A 66 | menu.help.about.title=Sobre {0} 67 | menu.help.home.title=Web {0} 68 | menu.help.title=Ayuda 69 | menu.help.version.title=Comprobar actualizaciones... 70 | menu.help.vk=y 71 | menu.settings.preferences.editor.title=Preferencias... 72 | menu.settings.preferences.project.title=Preferencias del proyecto... 73 | menu.settings.title=Configuraci\u00F3n 74 | menu.settings.vk=C 75 | menu.view.collapse.title=Agrupar todas las claves de traducci\u00F3n 76 | menu.view.expand.title=Expandir todas las claves de traducci\u00F3n 77 | menu.view.title=Ver 78 | menu.view.vk=V 79 | resources.create.error=Ocurri\u00F3 un error al crear los ficheros de traducci\u00F3n. 80 | resources.import.empty=No se han encontrado ficheros de traducci\u00F3n en\:
"{0}" 81 | resources.import.error.multiple=Ocurri\u00F3 un error al abrir los ficheros de traducci\u00F3n. 82 | resources.import.error.single=Ocurri\u00F3 un error al abrir el fichero de traducci\u00F3n\:
"{0}" 83 | resources.locale.default=Por defecto 84 | resources.write.error.single=Ocurri\u00F3 un error al escribir el fichero de traducci\u00F3n\:
"{0}" 85 | settings.checkversion.title=Comprobar actualizaciones en el inicio 86 | settings.fieldset.editing=Edici\u00F3n 87 | settings.fieldset.general=General 88 | settings.fieldset.newprojects=Nuevos proyectos 89 | settings.filestructure.flat=Estructura plana 90 | settings.filestructure.nested=Estructura de arbol 91 | settings.filestructure.title=Estructura del proyecto 92 | settings.flattenjson.title=Aplanar las claves de traducci\u00F3n al guardar 93 | settings.inputheight.title=Altura por defecto de los campos de entrada 94 | settings.keyfield.title=Mostrar el campo clave de traducci\u00F3n 95 | settings.language.title=Idioma de la interfaz (requiere reiniciar) 96 | settings.minify.title=Minimizar los ficheros JSON/ES6 al guardar 97 | settings.resource.jsones6=(JSON/ES6) 98 | settings.resourcedef.help=Puede utilizar los tags '{' '}' para especificar la locale como parte del nombre del fichero.
El texto LOCALE ser\u00E1 reemplazado por la locale en curso.
Ejemplo\: translations'{'_LOCALE'}' se converir\u00EDa en translations_es_ES. 99 | settings.resourcedef.title=Nombre del fichero de traducciones 100 | settings.treetogglemode.title=Expandir y contraer las claves de traducci\u00F3n utilizando doble click 101 | swing.action.copy=Copiar 102 | swing.action.cut=Cortar 103 | swing.action.delete=Borrar 104 | swing.action.paste=Pegar 105 | swing.action.selectall=Seleccionar todo 106 | swing.action.undo=Deshacer 107 | tree.root.name=Traducciones 108 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/ResourceKeys.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import com.google.common.collect.Lists; 8 | import com.jvms.i18neditor.Resource; 9 | 10 | /** 11 | * This class provides translation key utility functions for a {@link Resource}. 12 | * 13 | *

A translation key is a {@code String} consisting of one or more parts separated by a dot.
14 | * A key starting or ending with a dot or a key containing white spaces is considered to be invalid.

15 | * 16 | * @author Jacob van Mourik 17 | */ 18 | public final class ResourceKeys { 19 | 20 | /** 21 | * See {@link #create(List)}. 22 | */ 23 | public static String create(String... parts) { 24 | return create(Arrays.asList(parts)); 25 | } 26 | 27 | /** 28 | * Creates a key by joining the given parts. 29 | * 30 | * @param parts the parts of the key. 31 | * @return the created key. 32 | */ 33 | public static String create(List parts) { 34 | return parts.stream().filter(p -> p != null && !p.isEmpty()).collect(Collectors.joining(".")); 35 | } 36 | 37 | /** 38 | * Checks whether the given key is a valid key. 39 | * A key starting or ending with a dot or a key containing white spaces is considered to be invalid. 40 | * 41 | * @param key the key to validate. 42 | * @return whether the key is valid or not. 43 | */ 44 | public static boolean isValid(String key) { 45 | return !key.isEmpty() && !key.startsWith(".") && !key.endsWith(".") && key.matches("[^\\s]+"); 46 | } 47 | 48 | /** 49 | * Returns the size of a key. 50 | * The size is the number of parts the key consists of. 51 | * 52 | * @param key the key. 53 | * @return the number of parts the key consists of. 54 | */ 55 | public static int size(String key) { 56 | return parts(key).length; 57 | } 58 | 59 | /** 60 | * Returns the parts of a key. 61 | * 62 | * @param key the key. 63 | * @return the parts of the key. 64 | */ 65 | public static String[] parts(String key) { 66 | return key.split("\\."); 67 | } 68 | 69 | /** 70 | * Returns the last parts of a key with a given offset. 71 | * The offset must lie between zero and the total number of parts, inclusive. 72 | * 73 | * @param key the key. 74 | * @param offset the number of parts to skip from the beginning. 75 | * @return the last sub parts. 76 | */ 77 | public static String[] subParts(String key, int offset) { 78 | String[] parts = parts(key); 79 | return Arrays.copyOfRange(parts, offset, parts.length); 80 | } 81 | 82 | /** 83 | * Returns the first part of a key. 84 | * 85 | * @param key the key. 86 | * @return the first part. 87 | */ 88 | public static String firstPart(String key) { 89 | return parts(key)[0]; 90 | } 91 | 92 | /** 93 | * Returns the last part of a key. 94 | * 95 | * @param key the key. 96 | * @return the last part. 97 | */ 98 | public static String lastPart(String key) { 99 | String[] parts = parts(key); 100 | return parts[parts.length-1]; 101 | } 102 | 103 | /** 104 | * Creates a new key consisting of all but the first part. 105 | * If the key has only one part, an empty key will be returned. 106 | * 107 | * @param key the key. 108 | * @return the key without the first part. 109 | */ 110 | public static String withoutFirstPart(String key) { 111 | if (size(key) == 1) return ""; 112 | return key.replaceAll("^[^.]+\\.", ""); 113 | } 114 | 115 | /** 116 | * Creates a new key consisting of all but the last part. 117 | * If the key has only one part, an empty key will be returned. 118 | * 119 | * @param key the key. 120 | * @return the key without the last part. 121 | */ 122 | public static String withoutLastPart(String key) { 123 | if (size(key) == 1) return ""; 124 | return key.replaceAll("\\.[^.]+$", ""); 125 | } 126 | 127 | /** 128 | * Retrieve the part of the given key which is a child part of the given parent key. 129 | * A key is a child of another key if it has the same parts at the beginning as the other key. 130 | * This function will only return the child parts, so without the beginning parent parts. 131 | * 132 | *

If the resulting key is the same as the given key, the key is considered not to be a child 133 | * of the given parent key, so an empty key will be returned.

134 | * 135 | * @param key the original key. 136 | * @param parentKey a possible parent key of the original key. 137 | * @return the part of the given key which is a child of the given parent key. 138 | */ 139 | public static String childKey(String key, String parentKey) { 140 | if (key == null || key.isEmpty()) return ""; 141 | if (parentKey == null || parentKey.isEmpty()) return key; 142 | String result = key.replaceFirst(parentKey + "\\.", ""); 143 | if (result.equals(key)) return ""; 144 | return result; 145 | } 146 | 147 | /** 148 | * Checks whether the given key is a child key of the given parent key. 149 | * A key is a child of another key if it has the same parts at the beginning as the other key. 150 | * 151 | * @param key the original key. 152 | * @param parentKey the possible parent key of the original key. 153 | * @return whether the given key is a child of the given parent key. 154 | */ 155 | public static boolean isChildKeyOf(String key, String parentKey) { 156 | return key.startsWith(parentKey + "."); 157 | } 158 | 159 | /** 160 | * Gets the unique root keys of a list of keys. A root key is the first part of a key. 161 | * 162 | * @param keys the keys to retrieve the unique root keys from. 163 | * @return the unique root keys. 164 | */ 165 | public static List uniqueRootKeys(List keys) { 166 | List result = Lists.newLinkedList(); 167 | keys.forEach(key -> { 168 | String rootKey = firstPart(key); 169 | if (!result.contains(rootKey)) { 170 | result.add(rootKey); 171 | } 172 | }); 173 | return result; 174 | } 175 | 176 | /** 177 | * Gets all keys from the given key list which are a child of the given parent key. 178 | * A key is a child of another key if it has the same parts at the beginning as the other key. 179 | * 180 | * @param keys the keys to retrieve the child keys from. 181 | * @param parentKey the parent key. 182 | * @return the keys of the given key list which are a child of the given parent key. 183 | */ 184 | public static List extractChildKeys(List keys, String parentKey) { 185 | List result = Lists.newLinkedList(); 186 | keys.forEach(key -> { 187 | if (isChildKeyOf(key, parentKey)) { 188 | result.add(childKey(key, parentKey)); 189 | } 190 | }); 191 | return result; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/EditorSettingsPane.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.GridBagLayout; 4 | import java.awt.GridLayout; 5 | import java.awt.event.KeyAdapter; 6 | import java.awt.event.KeyEvent; 7 | import java.util.Locale; 8 | 9 | import javax.swing.JCheckBox; 10 | import javax.swing.JComboBox; 11 | import javax.swing.JLabel; 12 | import javax.swing.JPanel; 13 | import javax.swing.JSlider; 14 | 15 | import org.apache.commons.lang3.LocaleUtils; 16 | 17 | import com.jvms.i18neditor.swing.JHelpLabel; 18 | import com.jvms.i18neditor.swing.JTextField; 19 | import com.jvms.i18neditor.util.MessageBundle; 20 | 21 | /** 22 | * This class represents the editor settings pane. 23 | * 24 | * @author Jacob van Mourik 25 | */ 26 | public class EditorSettingsPane extends AbstractSettingsPane { 27 | private final static long serialVersionUID = 4488173853564278813L; 28 | private Editor editor; 29 | 30 | public EditorSettingsPane(Editor editor) { 31 | super(); 32 | this.editor = editor; 33 | this.setupUI(); 34 | } 35 | 36 | private void setupUI() { 37 | EditorSettings settings = editor.getSettings(); 38 | 39 | // General settings 40 | JPanel fieldset1 = createFieldset(MessageBundle.get("settings.fieldset.general")); 41 | 42 | JCheckBox versionBox = new JCheckBox(MessageBundle.get("settings.checkversion.title")); 43 | versionBox.setSelected(settings.isCheckVersionOnStartup()); 44 | versionBox.addChangeListener(e -> settings.setCheckVersionOnStartup(versionBox.isSelected())); 45 | fieldset1.add(versionBox, createVerticalGridBagConstraints()); 46 | 47 | ComboBoxLocale currentLocaleItem = null; 48 | for (Locale locale : LocaleUtils.localeLookupList(editor.getCurrentLocale(), Locale.ENGLISH)) { 49 | for (ComboBoxLocale item : localeComboBoxItems) { 50 | if (item.getLocale().equals(locale)) { 51 | currentLocaleItem = item; 52 | break; 53 | } 54 | } 55 | if (currentLocaleItem != null) { 56 | break; 57 | } 58 | } 59 | JPanel languageListPanel = new JPanel(new GridLayout(0, 1)); 60 | JLabel languageListLabel = new JLabel(MessageBundle.get("settings.language.title")); 61 | JComboBox languageListField = new JComboBox(localeComboBoxItems.toArray()); 62 | languageListField.setSelectedItem(currentLocaleItem); 63 | languageListField.addActionListener(e -> { 64 | settings.setEditorLanguage(((ComboBoxLocale)languageListField.getSelectedItem()).getLocale()); 65 | }); 66 | languageListPanel.add(languageListLabel); 67 | languageListPanel.add(languageListField); 68 | fieldset1.add(languageListPanel, createVerticalGridBagConstraints()); 69 | 70 | // New project settings 71 | JPanel fieldset2 = createFieldset(MessageBundle.get("settings.fieldset.newprojects")); 72 | 73 | ComboBoxFileStructure currentFileStructureItem = null; 74 | for (ComboBoxFileStructure item : fileStructureComboBoxItems) { 75 | if (item.getStructure().equals(settings.getResourceFileStructure())) { 76 | currentFileStructureItem = item; 77 | break; 78 | } 79 | } 80 | JPanel fileStructurePanel = new JPanel(new GridLayout(0, 1)); 81 | JLabel fileStructureLabel = new JLabel(MessageBundle.get("settings.filestructure.title")); 82 | JComboBox fileStructureField = new JComboBox(fileStructureComboBoxItems.toArray()); 83 | fileStructureField.setSelectedItem(currentFileStructureItem); 84 | fileStructureField.addActionListener(e -> { 85 | settings.setResourceFileStructure(((ComboBoxFileStructure)fileStructureField.getSelectedItem()).getStructure()); 86 | }); 87 | fileStructurePanel.add(fileStructureLabel); 88 | fileStructurePanel.add(fileStructureField); 89 | fieldset2.add(fileStructurePanel, createVerticalGridBagConstraints()); 90 | 91 | JPanel resourceDefinitionPanel = new JPanel(new GridLayout(0, 1)); 92 | JLabel resourceDefinitionLabel = new JLabel(MessageBundle.get("settings.resourcedef.title")); 93 | JHelpLabel resourceDefinitionHelpLabel = new JHelpLabel(MessageBundle.get("settings.resourcedef.help")); 94 | JTextField resourceDefinitionField = new JTextField(settings.getResourceFileDefinition()); 95 | resourceDefinitionField.addKeyListener(new KeyAdapter() { 96 | @Override 97 | public void keyReleased(KeyEvent e) { 98 | String value = resourceDefinitionField.getText().trim(); 99 | settings.setResourceFileDefinition(value.isEmpty() ? EditorSettings.DEFAULT_RESOURCE_FILE_DEFINITION : value); 100 | } 101 | }); 102 | resourceDefinitionPanel.add(resourceDefinitionLabel); 103 | resourceDefinitionPanel.add(resourceDefinitionField); 104 | fieldset2.add(resourceDefinitionPanel, createVerticalGridBagConstraints()); 105 | fieldset2.add(resourceDefinitionHelpLabel, createVerticalGridBagConstraints()); 106 | 107 | JCheckBox minifyBox = new JCheckBox(MessageBundle.get("settings.minify.title") + " " + 108 | MessageBundle.get("settings.resource.jsones6")); 109 | minifyBox.setSelected(settings.isMinifyResources()); 110 | minifyBox.addChangeListener(e -> settings.setMinifyResources(minifyBox.isSelected())); 111 | fieldset2.add(minifyBox, createVerticalGridBagConstraints()); 112 | 113 | JCheckBox flattenJSONBox = new JCheckBox(MessageBundle.get("settings.flattenjson.title") + " " + 114 | MessageBundle.get("settings.resource.jsones6")); 115 | flattenJSONBox.setSelected(settings.isFlattenJSON()); 116 | flattenJSONBox.addChangeListener(e -> settings.setFlattenJSON(flattenJSONBox.isSelected())); 117 | fieldset2.add(flattenJSONBox, createVerticalGridBagConstraints()); 118 | 119 | // Editing settings 120 | JPanel fieldset3 = createFieldset(MessageBundle.get("settings.fieldset.editing")); 121 | 122 | JCheckBox keyFieldBox = new JCheckBox(MessageBundle.get("settings.keyfield.title")); 123 | keyFieldBox.setSelected(settings.isKeyFieldEnabled()); 124 | keyFieldBox.addChangeListener(e -> { 125 | settings.setKeyFieldEnabled(keyFieldBox.isSelected()); 126 | editor.updateUI(); 127 | }); 128 | fieldset3.add(keyFieldBox, createVerticalGridBagConstraints()); 129 | 130 | JCheckBox keyNodeClickBox = new JCheckBox(MessageBundle.get("settings.treetogglemode.title")); 131 | keyNodeClickBox.setSelected(settings.isDoubleClickTreeToggling()); 132 | keyNodeClickBox.addChangeListener(e -> { 133 | settings.setDoubleClickTreeToggling(keyNodeClickBox.isSelected()); 134 | editor.updateUI(); 135 | }); 136 | fieldset3.add(keyNodeClickBox, createVerticalGridBagConstraints()); 137 | 138 | JPanel resourceHeightPanel = new JPanel(new GridLayout(0, 1)); 139 | JLabel resourceHeightLabel = new JLabel(MessageBundle.get("settings.inputheight.title")); 140 | JSlider resourceHeightSlider = new JSlider(JSlider.HORIZONTAL, 1, 15, settings.getDefaultInputHeight()); 141 | resourceHeightSlider.addChangeListener(e -> { 142 | settings.setDefaultInputHeight(resourceHeightSlider.getValue()); 143 | editor.updateUI(); 144 | }); 145 | resourceHeightPanel.add(resourceHeightLabel); 146 | resourceHeightPanel.add(resourceHeightSlider); 147 | fieldset3.add(resourceHeightPanel, createVerticalGridBagConstraints()); 148 | 149 | setLayout(new GridBagLayout()); 150 | add(fieldset1, createVerticalGridBagConstraints()); 151 | add(fieldset2, createVerticalGridBagConstraints()); 152 | add(fieldset3, createVerticalGridBagConstraints()); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/jvms/i18neditor/ResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | import java.util.Locale; 4 | import java.util.SortedMap; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import com.google.common.collect.Maps; 10 | import com.jvms.i18neditor.Resource; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | /** 15 | * 16 | * @author Jacob 17 | */ 18 | public class ResourceTest { 19 | private Resource resource; 20 | 21 | @Before 22 | public void setup() throws Exception { 23 | SortedMap translations = Maps.newTreeMap(); 24 | translations.put("a.a", "aa"); 25 | translations.put("a.b", "ab"); 26 | resource = new Resource(ResourceType.JSON, null, new Locale("en")); 27 | resource.setTranslations(translations); 28 | } 29 | 30 | @Test 31 | public void addTranslationTest() { 32 | SortedMap translations; 33 | 34 | resource.storeTranslation("a.a", "b"); 35 | 36 | translations = resource.getTranslations(); 37 | assertEquals(2, translations.size()); 38 | assertEquals("b", translations.get("a.a")); 39 | assertEquals("ab", translations.get("a.b")); 40 | 41 | resource.storeTranslation("a.a.a", "b"); 42 | 43 | translations = resource.getTranslations(); 44 | assertEquals(2, translations.size()); 45 | assertEquals("b", translations.get("a.a.a")); 46 | assertNull(translations.get("a.a")); 47 | assertEquals("ab", translations.get("a.b")); 48 | 49 | resource.storeTranslation("a", "b"); 50 | 51 | translations = resource.getTranslations(); 52 | assertEquals(1, translations.size()); 53 | assertEquals("b", translations.get("a")); 54 | assertNull(translations.get("a.a.a")); 55 | assertNull(translations.get("a.a")); 56 | assertNull(translations.get("a.b")); 57 | } 58 | 59 | @Test(expected=IllegalArgumentException.class) 60 | public void addTranslationWithInvalidKeyTest() { 61 | resource.storeTranslation("a a", "b"); 62 | } 63 | 64 | @Test 65 | public void removeTranslationTest() { 66 | SortedMap translations; 67 | 68 | resource.storeTranslation("b", "b"); 69 | resource.removeTranslation("a"); 70 | 71 | translations = resource.getTranslations(); 72 | assertEquals(1, translations.size()); 73 | assertEquals("b", translations.get("b")); 74 | assertNull(translations.get("a.a")); 75 | assertNull(translations.get("a.b")); 76 | } 77 | 78 | @Test 79 | public void renameTranslationToUniqueKeyTest() { 80 | SortedMap translations; 81 | 82 | resource.storeTranslation("b.a", "ba"); 83 | resource.storeTranslation("b.b", "bb"); 84 | resource.renameTranslation("b", "c"); 85 | 86 | translations = resource.getTranslations(); 87 | assertEquals(4, translations.size()); 88 | assertEquals("aa", translations.get("a.a")); 89 | assertEquals("ab", translations.get("a.b")); 90 | assertNull(translations.get("b.a")); 91 | assertNull(translations.get("b.b")); 92 | assertEquals("ba", translations.get("c.a")); 93 | assertEquals("bb", translations.get("c.b")); 94 | 95 | resource.renameTranslation("c.a", "d"); 96 | 97 | translations = resource.getTranslations(); 98 | assertEquals(4, translations.size()); 99 | assertEquals("ba", translations.get("d")); 100 | assertEquals("aa", translations.get("a.a")); 101 | assertEquals("ab", translations.get("a.b")); 102 | assertNull(translations.get("b.a")); 103 | assertNull(translations.get("b.b")); 104 | assertNull(translations.get("c.a")); 105 | assertEquals("bb", translations.get("c.b")); 106 | } 107 | 108 | @Test 109 | public void renameTranslationToExistingKeyTest() { 110 | SortedMap translations; 111 | 112 | resource.storeTranslation("a.c", "ac"); 113 | resource.storeTranslation("b.a", "ba"); 114 | resource.storeTranslation("b.b.a", "bba"); 115 | resource.storeTranslation("b.b.b", "bbb"); 116 | resource.renameTranslation("b", "a"); 117 | 118 | translations = resource.getTranslations(); 119 | assertEquals(4, translations.size()); 120 | assertEquals("ba", translations.get("a.a")); 121 | assertNull(translations.get("a.b")); 122 | assertEquals("ac", translations.get("a.c")); 123 | assertEquals("bba", translations.get("a.b.a")); 124 | assertEquals("bbb", translations.get("a.b.b")); 125 | assertNull(translations.get("b.a")); 126 | assertNull(translations.get("b.b.a")); 127 | assertNull(translations.get("b.b.b")); 128 | 129 | resource.renameTranslation("a.b", "a"); 130 | 131 | translations = resource.getTranslations(); 132 | assertEquals(3, translations.size()); 133 | assertEquals("bba", translations.get("a.a")); 134 | assertEquals("bbb", translations.get("a.b")); 135 | assertEquals("ac", translations.get("a.c")); 136 | assertNull(translations.get("a.b.a")); 137 | assertNull(translations.get("a.b.b")); 138 | } 139 | 140 | @Test(expected=IllegalArgumentException.class) 141 | public void renameTranslationToInvalidKeyTest() { 142 | resource.storeTranslation("a.b", "ab"); 143 | resource.renameTranslation("a", "b c"); 144 | } 145 | 146 | @Test 147 | public void duplicateTranslationToUniqueKeyTest() { 148 | SortedMap translations; 149 | 150 | resource.storeTranslation("b.a", "ba"); 151 | resource.storeTranslation("b.b", "bb"); 152 | resource.duplicateTranslation("b", "c"); 153 | 154 | translations = resource.getTranslations(); 155 | assertEquals(6, translations.size()); 156 | assertEquals("aa", translations.get("a.a")); 157 | assertEquals("ab", translations.get("a.b")); 158 | assertEquals("ba", translations.get("b.a")); 159 | assertEquals("bb", translations.get("b.b")); 160 | assertEquals("ba", translations.get("c.a")); 161 | assertEquals("bb", translations.get("c.b")); 162 | 163 | resource.duplicateTranslation("c.a", "d"); 164 | 165 | translations = resource.getTranslations(); 166 | assertEquals(7, translations.size()); 167 | assertEquals("ba", translations.get("d")); 168 | assertEquals("aa", translations.get("a.a")); 169 | assertEquals("ab", translations.get("a.b")); 170 | assertEquals("ba", translations.get("b.a")); 171 | assertEquals("bb", translations.get("b.b")); 172 | assertEquals("ba", translations.get("c.a")); 173 | assertEquals("bb", translations.get("c.b")); 174 | } 175 | 176 | @Test 177 | public void duplicateTranslationToExistingKeyTest() { 178 | SortedMap translations; 179 | 180 | resource.storeTranslation("a.c", "ac"); 181 | resource.storeTranslation("b.a", "ba"); 182 | resource.storeTranslation("b.b.a", "bba"); 183 | resource.storeTranslation("b.b.b", "bbb"); 184 | resource.duplicateTranslation("b", "a"); 185 | 186 | translations = resource.getTranslations(); 187 | assertEquals(7, translations.size()); 188 | assertEquals("ba", translations.get("a.a")); 189 | assertNull(translations.get("a.b")); 190 | assertEquals("ac", translations.get("a.c")); 191 | assertEquals("bba", translations.get("a.b.a")); 192 | assertEquals("bbb", translations.get("a.b.b")); 193 | assertEquals("ba", translations.get("b.a")); 194 | assertEquals("bba", translations.get("b.b.a")); 195 | assertEquals("bbb", translations.get("b.b.b")); 196 | 197 | resource.duplicateTranslation("a.b", "a"); 198 | 199 | translations = resource.getTranslations(); 200 | assertEquals(6, translations.size()); 201 | assertEquals("bba", translations.get("a.a")); 202 | assertEquals("bbb", translations.get("a.b")); 203 | assertEquals("ac", translations.get("a.c")); 204 | assertNull(translations.get("a.b.a")); 205 | assertNull(translations.get("a.b.b")); 206 | assertEquals("ba", translations.get("b.a")); 207 | assertEquals("bba", translations.get("b.b.a")); 208 | assertEquals("bbb", translations.get("b.b.b")); 209 | } 210 | 211 | @Test(expected=IllegalArgumentException.class) 212 | public void duplicateTranslationToInvalidKeyTest() { 213 | resource.storeTranslation("a.b", "ab"); 214 | resource.duplicateTranslation("a", "b c"); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/TranslationTree.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Color; 4 | import java.awt.FontMetrics; 5 | import java.awt.Graphics; 6 | import java.awt.Rectangle; 7 | import java.awt.event.MouseAdapter; 8 | import java.awt.event.MouseEvent; 9 | import java.util.Enumeration; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import javax.swing.InputMap; 14 | import javax.swing.JTree; 15 | import javax.swing.KeyStroke; 16 | import javax.swing.UIManager; 17 | import javax.swing.event.TreeExpansionEvent; 18 | import javax.swing.event.TreeWillExpandListener; 19 | import javax.swing.tree.ExpandVetoException; 20 | import javax.swing.tree.TreePath; 21 | 22 | import com.google.common.collect.Lists; 23 | import com.jvms.i18neditor.util.ResourceKeys; 24 | 25 | /** 26 | * This class represents a tree view for translation keys. 27 | * 28 | * @author Jacob van Mourik 29 | */ 30 | public class TranslationTree extends JTree { 31 | private final static long serialVersionUID = -2888673305196385241L; 32 | 33 | public TranslationTree() { 34 | super(new TranslationTreeModel()); 35 | setupUI(); 36 | } 37 | 38 | public void collapseAll() { 39 | for (int i = getRowCount(); i >= 0; i--) { 40 | collapseRow(i); 41 | } 42 | } 43 | 44 | public void expandAll() { 45 | for (int i = 0; i < getRowCount(); i++) { 46 | expandRow(i); 47 | } 48 | } 49 | 50 | public void expand(List nodes) { 51 | nodes.forEach(n -> expandPath(new TreePath(n.getPath()))); 52 | } 53 | 54 | public void collapse(List nodes) { 55 | nodes.forEach(n -> collapsePath(new TreePath(n.getPath()))); 56 | } 57 | 58 | public void updateNodes(Set errorKeys) { 59 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 60 | Enumeration e = model.getEnumeration(); 61 | while (e.hasMoreElements()) { 62 | TranslationTreeNode n = e.nextElement(); 63 | n.setError(errorKeys.contains(n.getKey())); 64 | model.nodeChanged(n); 65 | } 66 | } 67 | 68 | public void updateNode(String key, boolean error) { 69 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 70 | TranslationTreeNode node = model.getNodeByKey(key); 71 | if (node != null && node.isLeaf()) { 72 | node.setError(error); 73 | model.nodeWithParentsChanged(node); 74 | } 75 | } 76 | 77 | public TranslationTreeNode addNodeByKey(String key) { 78 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 79 | TranslationTreeNode node = model.getNodeByKey(key); 80 | if (node == null) { 81 | TranslationTreeNode parent = (TranslationTreeNode) model.getClosestParentNodeByKey(key); 82 | String newKey = ResourceKeys.childKey(key, parent.getKey()); 83 | String firstPart = ResourceKeys.firstPart(newKey); 84 | String lastPart = ResourceKeys.withoutFirstPart(newKey); 85 | model.insertNodeInto(new TranslationTreeNode(firstPart, 86 | lastPart.isEmpty() ? Lists.newArrayList() : Lists.newArrayList(lastPart)), parent); 87 | node = model.getNodeByKey(key); 88 | } 89 | setSelectionNode(node); 90 | return node; 91 | } 92 | 93 | public void removeNodeByKey(String key) { 94 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 95 | TranslationTreeNode node = model.getNodeByKey(key); 96 | if (node != null) { 97 | model.removeNodeFromParent(node); 98 | } 99 | } 100 | 101 | public TranslationTreeNode getNodeByKey(String key) { 102 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 103 | return model.getNodeByKey(key); 104 | } 105 | 106 | public List getExpandedNodes() { 107 | TranslationTreeNode node = (TranslationTreeNode) getModel().getRoot(); 108 | return getExpandedNodes(node); 109 | } 110 | 111 | public List getExpandedNodes(TranslationTreeNode node) { 112 | List expandedNodes = Lists.newLinkedList(); 113 | Enumeration expandedChilds = getExpandedDescendants(new TreePath(node.getPath())); 114 | if (expandedChilds != null) { 115 | while (expandedChilds.hasMoreElements()) { 116 | TreePath path = expandedChilds.nextElement(); 117 | TranslationTreeNode expandedNode = (TranslationTreeNode) path.getLastPathComponent(); 118 | if (!expandedNode.isRoot()) { // do not return the root node 119 | expandedNodes.add(expandedNode); 120 | } 121 | } 122 | } 123 | return expandedNodes; 124 | } 125 | 126 | public void renameNodeByKey(String key, String newKey) { 127 | duplicateNodeByKey(key, newKey, false); 128 | } 129 | 130 | public void duplicateNodeByKey(String key, String newKey) { 131 | duplicateNodeByKey(key, newKey, true); 132 | } 133 | 134 | public TranslationTreeNode getSelectionNode() { 135 | return (TranslationTreeNode) getLastSelectedPathComponent(); 136 | } 137 | 138 | @Override 139 | public void setSelectionPath(TreePath path) { 140 | super.setSelectionPath(path); 141 | scrollPathToVisible(path); 142 | } 143 | 144 | @Override 145 | public void setSelectionRow(int row) { 146 | TreePath path = getPathForRow(row); 147 | setSelectionPath(path); 148 | } 149 | 150 | public void setSelectionNode(TranslationTreeNode node) { 151 | TreePath path = new TreePath(node.getPath()); 152 | setSelectionPath(path); 153 | } 154 | 155 | public void clear() { 156 | setModel(new TranslationTreeModel()); 157 | } 158 | 159 | @Override 160 | protected void paintComponent(Graphics g) { 161 | TranslationTreeCellRenderer renderer = (TranslationTreeCellRenderer) getCellRenderer(); 162 | Color c1 = renderer.getSelectionBackground(); 163 | 164 | FontMetrics metrics = g.getFontMetrics(getFont()); 165 | setRowHeight(metrics.getHeight() + 8); 166 | 167 | g.setColor(getBackground()); 168 | g.fillRect(0, 0, getWidth(), getHeight()); 169 | 170 | for (int i : getSelectionRows()) { 171 | Rectangle r = getRowBounds(i); 172 | g.setColor(c1); 173 | g.fillRect(0, r.y, getWidth(), r.height); 174 | } 175 | 176 | super.paintComponent(g); 177 | } 178 | 179 | private void setupUI() { 180 | UIManager.put("Tree.repaintWholeRow", Boolean.TRUE); 181 | 182 | // Remove all key strokes 183 | InputMap inputMap = getInputMap().getParent(); 184 | for (KeyStroke k : getRegisteredKeyStrokes()) { 185 | inputMap.remove(k); 186 | } 187 | 188 | setUI(new TranslationTreeUI()); 189 | setCellRenderer(new TranslationTreeCellRenderer()); 190 | addTreeWillExpandListener(new TranslationTreeExpandListener()); 191 | addMouseListener(new TranslationTreeMouseListener()); 192 | setEditable(false); 193 | setOpaque(false); 194 | } 195 | 196 | private void duplicateNodeByKey(String key, String newKey, boolean keepOld) { 197 | TranslationTreeModel model = (TranslationTreeModel) getModel(); 198 | TranslationTreeNode node = model.getNodeByKey(key); 199 | TranslationTreeNode newNode = model.getNodeByKey(newKey); 200 | List expandedNodes = null; 201 | 202 | if (keepOld) { 203 | node = node.cloneWithChildren(); 204 | } else { 205 | expandedNodes = getExpandedNodes(node); 206 | model.removeNodeFromParent(node); 207 | } 208 | 209 | if (node.isLeaf() && newNode != null) { 210 | model.removeNodeFromParent(newNode); 211 | newNode = null; 212 | } 213 | if (newNode != null) { 214 | model.insertDescendantsInto(node, newNode); 215 | node = newNode; 216 | } else { 217 | TranslationTreeNode parent = addNodeByKey(ResourceKeys.withoutLastPart(newKey)); 218 | node.setName(ResourceKeys.lastPart(newKey)); 219 | model.insertNodeInto(node, parent); 220 | } 221 | 222 | if (expandedNodes != null) { 223 | expand(expandedNodes); 224 | } 225 | 226 | setSelectionNode(node); 227 | } 228 | 229 | private class TranslationTreeMouseListener extends MouseAdapter { 230 | private boolean isPopupTrigger; 231 | 232 | @Override 233 | public void mousePressed(MouseEvent e) { 234 | isPopupTrigger = e.isPopupTrigger(); 235 | } 236 | 237 | @Override 238 | public void mouseReleased(MouseEvent e) { 239 | if (!isPopupTrigger && !e.isPopupTrigger() && e.getClickCount() == getToggleClickCount()) { 240 | int row = getRowForLocation(e.getX(), e.getY()); 241 | if (isCollapsed(row)) { 242 | expandRow(row); 243 | } else { 244 | collapseRow(row); 245 | } 246 | } 247 | } 248 | } 249 | 250 | private class TranslationTreeExpandListener implements TreeWillExpandListener { 251 | @Override 252 | public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException {} 253 | 254 | @Override 255 | public void treeWillCollapse(TreeExpansionEvent e) throws ExpandVetoException { 256 | // Prevent root key from being collapsed 257 | if (e.getPath().getPathCount() == 1) { 258 | throw new ExpandVetoException(e); 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/Resource.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import java.util.Locale; 6 | import java.util.Map; 7 | import java.util.SortedMap; 8 | 9 | import com.google.common.base.Preconditions; 10 | import com.google.common.base.Strings; 11 | import com.google.common.collect.ImmutableSortedMap; 12 | import com.google.common.collect.Lists; 13 | import com.google.common.collect.Maps; 14 | import com.jvms.i18neditor.util.ResourceKeys; 15 | 16 | /** 17 | * A resource is a container for storing i18n data and is defined by the following properties: 18 | * 19 | *
    20 | *
  • {@code type} the type of the resource, either {@code JSON} or {@code ES6}.
  • 21 | *
  • {@code path} the path to the resource file on disk.
  • 22 | *
  • {@code locale} the locale of the resource.
  • 23 | *
  • {@code translations} a sorted map containing the translations of the resource by key value pair.
  • 24 | *
25 | * 26 | *

Objects can listen to a resource by adding a {@link ResourceListener} which 27 | * will be called when any change is made to the {@code translations}.

28 | * 29 | * @author Jacob van Mourik 30 | */ 31 | public class Resource { 32 | private final Path path; 33 | private final Locale locale; 34 | private final ResourceType type; 35 | private final List listeners = Lists.newLinkedList(); 36 | private SortedMap translations = Maps.newTreeMap(); 37 | private String checksum; 38 | 39 | /** 40 | * See {@link #Resource(ResourceType, Path, Locale)}. 41 | */ 42 | public Resource(ResourceType type, Path path) { 43 | this(type, path, null); 44 | } 45 | 46 | /** 47 | * Creates a new instance of a resource. 48 | * 49 | * @param type the type of the resource. 50 | * @param path the path to the file on disk. 51 | * @param locale the locale of the translations. 52 | */ 53 | public Resource(ResourceType type, Path path, Locale locale) { 54 | this.path = path; 55 | this.locale = locale; 56 | this.type = type; 57 | } 58 | 59 | /** 60 | * Gets the type of the resource. 61 | * 62 | * @return the type. 63 | */ 64 | public ResourceType getType() { 65 | return type; 66 | } 67 | 68 | /** 69 | * Gets the path to the resource file on disk. 70 | * 71 | * @return the path. 72 | */ 73 | public Path getPath() { 74 | return path; 75 | } 76 | 77 | /** 78 | * Gets the locale of the translations of the resource. 79 | * 80 | * @return the locale of the resource, may be {@code null}. 81 | */ 82 | public Locale getLocale() { 83 | return locale; 84 | } 85 | 86 | /** 87 | * Gets a map of the translations of the resource. 88 | * 89 | *

The returned map is an immutable sorted map. Modifications to the translations should be done via 90 | * {@link #storeTranslation(String, String)}, {@link #removeTranslation(String)} or 91 | * {@link #renameTranslation(String, String)}.

92 | * 93 | * @return the translations of the resource. 94 | */ 95 | public SortedMap getTranslations() { 96 | return ImmutableSortedMap.copyOf(translations); 97 | } 98 | 99 | /** 100 | * Sets the translations of the resource. 101 | * 102 | * @param translations the translations 103 | */ 104 | public void setTranslations(SortedMap translations) { 105 | this.translations = translations; 106 | } 107 | 108 | /** 109 | * Checks whether the resource has a translation with the given key. 110 | * 111 | * @param key the key of the translation to look for. 112 | * @return whether a translation with the given key exists. 113 | */ 114 | public boolean hasTranslation(String key) { 115 | return !Strings.isNullOrEmpty(translations.get(key)); 116 | } 117 | 118 | /** 119 | * Gets a translation from the resource's translations. 120 | * 121 | * @param key the key of the translation to get. 122 | * @return value of the translation or {@code null} if there is no translation for the given key. 123 | */ 124 | public String getTranslation(String key) { 125 | return translations.get(key); 126 | } 127 | 128 | /** 129 | * Stores a translation to the resource's translations. 130 | * 131 | *
    132 | *
  • If the given key does not exists yet and is not empty, a new translation will be added to the map.
  • 133 | *
  • If the given key already exists, the existing value will be overwritten with the given value.
  • 134 | *
  • If the given key is a parent key of any existing keys, the existing child keys will be removed (when parent values are not supported).
  • 135 | *
  • If the given key is a child key of any existing keys, the existing parent keys will be removed (when parent values are not supported).
  • 136 | *
137 | * 138 | * @param key the key of the translation to add. 139 | * @param value the value of the translation to add corresponding the given key. 140 | */ 141 | public void storeTranslation(String key, String value) { 142 | checkKey(key); 143 | String existing = translations.get(key); 144 | if (value == null || existing != null && existing.equals(value)) { 145 | return; 146 | } 147 | if (!supportsParentValues()) { 148 | removeParents(key); 149 | removeChildren(key); 150 | } 151 | translations.put(key, value); 152 | notifyListeners(); 153 | } 154 | 155 | /** 156 | * Removes a translation from the resource's translations. 157 | * Any child keys of the given key will also be removed. 158 | * 159 | * @param key the key of the translation to remove. 160 | */ 161 | public void removeTranslation(String key) { 162 | removeChildren(key); 163 | translations.remove(key); 164 | notifyListeners(); 165 | } 166 | 167 | /** 168 | * Renames a translation key in the resource's translations. 169 | * Any existing child keys of the translation key will also be renamed. 170 | * 171 | * @param key the old key of the translation to rename. 172 | * @param newKey the new key. 173 | */ 174 | public void renameTranslation(String key, String newKey) { 175 | checkKey(newKey); 176 | duplicateTranslation(key, newKey, false); 177 | notifyListeners(); 178 | } 179 | 180 | /** 181 | * Duplicates a translation key, and any child keys, in the resource's translations. 182 | * 183 | * @param key the key of the translation to duplicate. 184 | * @param newKey the new key. 185 | */ 186 | public void duplicateTranslation(String key, String newKey) { 187 | checkKey(newKey); 188 | duplicateTranslation(key, newKey, true); 189 | notifyListeners(); 190 | } 191 | 192 | /** 193 | * Adds a listener to the resource. The listener will be called whenever there is made 194 | * a change to the translations of the resource. 195 | * 196 | * @param listener the listener to add. 197 | */ 198 | public void addListener(ResourceListener listener) { 199 | listeners.add(listener); 200 | } 201 | 202 | /** 203 | * Removes a listener from the resource previously added by {@link #addListener(ResourceListener)}. 204 | * 205 | * @param listener the listener to remove. 206 | */ 207 | public void removeListener(ResourceListener listener) { 208 | listeners.remove(listener); 209 | } 210 | 211 | /** 212 | * Gets the checksum of the resource's file. 213 | * This method only returns the checksum set via {@link #setChecksum(checksum)}. 214 | * 215 | * @return the checksum. 216 | */ 217 | public String getChecksum() { 218 | return checksum; 219 | } 220 | 221 | /** 222 | * Returns whether the resource has support for parent values. 223 | * 224 | *

For example if we have a value set for the key a.b we 225 | * might also set a value for a.

226 | * 227 | * @return whether parent values are supported. 228 | */ 229 | public boolean supportsParentValues() { 230 | return type == ResourceType.Properties; 231 | } 232 | 233 | /** 234 | * Sets the checksum of the resource's file. 235 | * 236 | * @param checksum the checksum to set. 237 | */ 238 | public void setChecksum(String checksum) { 239 | this.checksum = checksum; 240 | } 241 | 242 | private void duplicateTranslation(String key, String newKey, boolean keepOld) { 243 | Map newTranslations = Maps.newTreeMap(); 244 | translations.keySet().forEach(k -> { 245 | if (ResourceKeys.isChildKeyOf(k, key)) { 246 | String nk = ResourceKeys.create(newKey, ResourceKeys.childKey(k, key)); 247 | newTranslations.put(nk, translations.get(k)); 248 | } 249 | }); 250 | if (translations.containsKey(key)) { 251 | newTranslations.put(newKey, translations.get(key)); 252 | } 253 | if (!keepOld) { 254 | removeChildren(key); 255 | translations.remove(key); 256 | } 257 | newTranslations.forEach(this::storeTranslation); 258 | } 259 | 260 | private void removeChildren(String key) { 261 | Lists.newLinkedList(translations.keySet()).forEach(k -> { 262 | if (ResourceKeys.isChildKeyOf(k, key)) { 263 | translations.remove(k); 264 | } 265 | }); 266 | } 267 | 268 | private void removeParents(String key) { 269 | Lists.newLinkedList(translations.keySet()).forEach(k -> { 270 | if (ResourceKeys.isChildKeyOf(key, k)) { 271 | translations.remove(k); 272 | } 273 | }); 274 | } 275 | 276 | private void notifyListeners() { 277 | listeners.forEach(l -> l.resourceChanged(new ResourceEvent(this))); 278 | } 279 | 280 | private void checkKey(String key) { 281 | Preconditions.checkArgument(ResourceKeys.isValid(key), "Key is not valid."); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/editor/EditorMenuBar.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.editor; 2 | 3 | import java.awt.Desktop; 4 | import java.awt.Toolkit; 5 | import java.awt.event.KeyEvent; 6 | import java.awt.event.WindowEvent; 7 | import java.io.IOException; 8 | import java.net.URI; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.List; 12 | 13 | import javax.swing.JMenu; 14 | import javax.swing.JMenuBar; 15 | import javax.swing.JMenuItem; 16 | import javax.swing.KeyStroke; 17 | import javax.swing.SwingUtilities; 18 | 19 | import com.jvms.i18neditor.ResourceType; 20 | import com.jvms.i18neditor.editor.menu.AddLocaleMenuItem; 21 | import com.jvms.i18neditor.editor.menu.AddTranslationMenuItem; 22 | import com.jvms.i18neditor.editor.menu.CollapseTranslationsMenuItem; 23 | import com.jvms.i18neditor.editor.menu.CopyTranslationKeyToClipboardMenuItem; 24 | import com.jvms.i18neditor.editor.menu.DuplicateTranslationMenuItem; 25 | import com.jvms.i18neditor.editor.menu.ExpandTranslationsMenuItem; 26 | import com.jvms.i18neditor.editor.menu.FindTranslationMenuItem; 27 | import com.jvms.i18neditor.editor.menu.RemoveTranslationMenuItem; 28 | import com.jvms.i18neditor.editor.menu.RenameTranslationMenuItem; 29 | import com.jvms.i18neditor.swing.util.Dialogs; 30 | import com.jvms.i18neditor.util.GithubRepoUtil; 31 | import com.jvms.i18neditor.util.MessageBundle; 32 | 33 | /** 34 | * This class represents the top bar menu of the editor. 35 | * 36 | * @author Jacob van Mourik 37 | */ 38 | public class EditorMenuBar extends JMenuBar { 39 | private final static long serialVersionUID = -101788804096708514L; 40 | 41 | private final Editor editor; 42 | private final TranslationTree tree; 43 | private JMenuItem saveMenuItem; 44 | private JMenuItem reloadMenuItem; 45 | private JMenuItem addTranslationMenuItem; 46 | private JMenuItem findTranslationMenuItem; 47 | private JMenuItem renameTranslationMenuItem; 48 | private JMenuItem copyTranslationKeyMenuItem; 49 | private JMenuItem duplicateTranslationMenuItem; 50 | private JMenuItem removeTranslationMenuItem; 51 | private JMenuItem openContainingFolderMenuItem; 52 | private JMenuItem projectSettingsMenuItem; 53 | private JMenuItem editorSettingsMenuItem; 54 | private JMenu openRecentMenuItem; 55 | private JMenu editMenu; 56 | private JMenu viewMenu; 57 | private JMenu settingsMenu; 58 | 59 | public EditorMenuBar(Editor editor, TranslationTree tree) { 60 | super(); 61 | this.editor = editor; 62 | this.tree = tree; 63 | setupUI(); 64 | setEnabled(false); 65 | setSaveable(false); 66 | setEditable(false); 67 | } 68 | 69 | @Override 70 | public void setEnabled(boolean enabled) { 71 | reloadMenuItem.setEnabled(enabled); 72 | openContainingFolderMenuItem.setEnabled(enabled); 73 | editMenu.setEnabled(enabled); 74 | viewMenu.setEnabled(enabled); 75 | settingsMenu.removeAll(); 76 | if (enabled) { 77 | settingsMenu.add(projectSettingsMenuItem); 78 | settingsMenu.addSeparator(); 79 | settingsMenu.add(editorSettingsMenuItem); 80 | } else { 81 | settingsMenu.add(editorSettingsMenuItem); 82 | } 83 | SwingUtilities.updateComponentTreeUI(this); 84 | } 85 | 86 | public void setSaveable(boolean saveable) { 87 | saveMenuItem.setEnabled(saveable); 88 | } 89 | 90 | public void setEditable(boolean editable) { 91 | addTranslationMenuItem.setEnabled(editable); 92 | findTranslationMenuItem.setEnabled(editable); 93 | } 94 | 95 | public void setRecentItems(List items) { 96 | openRecentMenuItem.removeAll(); 97 | if (items.isEmpty()) { 98 | openRecentMenuItem.setEnabled(false); 99 | } else { 100 | openRecentMenuItem.setEnabled(true); 101 | for (int i = 0; i < items.size(); i++) { 102 | Integer n = i + 1; 103 | JMenuItem menuItem = new JMenuItem(n + ": " + items.get(i), Character.forDigit(i, 10)); 104 | Path path = Paths.get(menuItem.getText().replaceFirst("[0-9]+: ","")); 105 | menuItem.addActionListener(e -> editor.importProject(path, true)); 106 | openRecentMenuItem.add(menuItem); 107 | } 108 | JMenuItem clearMenuItem = new JMenuItem(MessageBundle.get("menu.file.recent.clear.title")); 109 | clearMenuItem.addActionListener(e -> editor.clearHistory()); 110 | openRecentMenuItem.addSeparator(); 111 | openRecentMenuItem.add(clearMenuItem); 112 | } 113 | } 114 | 115 | private void setupUI() { 116 | int keyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); 117 | 118 | // File menu 119 | JMenu fileMenu = new JMenu(MessageBundle.get("menu.file.title")); 120 | fileMenu.setMnemonic(MessageBundle.getMnemonic("menu.file.vk")); 121 | 122 | JMenuItem createJsonMenuItem = new JMenuItem(MessageBundle.get("menu.file.project.new.json.title")); 123 | createJsonMenuItem.addActionListener(e -> editor.showCreateProjectDialog(ResourceType.JSON)); 124 | createJsonMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_J, keyMask+KeyEvent.SHIFT_DOWN_MASK)); 125 | 126 | JMenuItem createEs6MenuItem = new JMenuItem(MessageBundle.get("menu.file.project.new.es6.title")); 127 | createEs6MenuItem.addActionListener(e -> editor.showCreateProjectDialog(ResourceType.ES6)); 128 | createEs6MenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, keyMask+KeyEvent.SHIFT_DOWN_MASK)); 129 | 130 | JMenuItem createPropertiesMenuItem = new JMenuItem(MessageBundle.get("menu.file.project.new.properties.title")); 131 | createPropertiesMenuItem.addActionListener(e -> editor.showCreateProjectDialog(ResourceType.Properties)); 132 | createPropertiesMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, keyMask+KeyEvent.SHIFT_DOWN_MASK)); 133 | 134 | JMenu createMenuItem = new JMenu(MessageBundle.get("menu.file.project.new.title")); 135 | createMenuItem.setMnemonic(MessageBundle.getMnemonic("menu.file.project.new.vk")); 136 | createMenuItem.add(createJsonMenuItem); 137 | createMenuItem.add(createEs6MenuItem); 138 | createMenuItem.add(createPropertiesMenuItem); 139 | 140 | JMenuItem importMenuItem = new JMenuItem(MessageBundle.get("menu.file.project.import.title")); 141 | importMenuItem.setMnemonic(MessageBundle.getMnemonic("menu.file.project.import.vk")); 142 | importMenuItem.addActionListener(e -> editor.showImportProjectDialog()); 143 | 144 | openContainingFolderMenuItem = new JMenuItem(MessageBundle.get("menu.file.folder.title")); 145 | openContainingFolderMenuItem.addActionListener(e -> editor.openProjectDirectory()); 146 | 147 | openRecentMenuItem = new JMenu(MessageBundle.get("menu.file.recent.title")); 148 | openRecentMenuItem.setMnemonic(MessageBundle.getMnemonic("menu.file.recent.vk")); 149 | 150 | saveMenuItem = new JMenuItem(MessageBundle.get("menu.file.save.title"), MessageBundle.getMnemonic("menu.file.save.vk")); 151 | saveMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, keyMask)); 152 | saveMenuItem.addActionListener(e -> editor.saveProject()); 153 | 154 | reloadMenuItem = new JMenuItem(MessageBundle.get("menu.file.reload.title"), MessageBundle.getMnemonic("menu.file.reload.vk")); 155 | reloadMenuItem.addActionListener(e -> editor.reloadProject()); 156 | 157 | JMenuItem exitMenuItem = new JMenuItem(MessageBundle.get("menu.file.exit.title"), MessageBundle.getMnemonic("menu.file.exit.vk")); 158 | exitMenuItem.addActionListener(e -> editor.dispatchEvent(new WindowEvent(editor, WindowEvent.WINDOW_CLOSING))); 159 | 160 | fileMenu.add(createMenuItem); 161 | fileMenu.add(importMenuItem); 162 | if (Desktop.isDesktopSupported()) { 163 | fileMenu.add(openContainingFolderMenuItem); 164 | } 165 | fileMenu.add(openRecentMenuItem); 166 | fileMenu.addSeparator(); 167 | fileMenu.add(saveMenuItem); 168 | fileMenu.add(reloadMenuItem); 169 | fileMenu.addSeparator(); 170 | fileMenu.add(exitMenuItem); 171 | 172 | // Edit menu 173 | editMenu = new JMenu(MessageBundle.get("menu.edit.title")); 174 | editMenu.setMnemonic(MessageBundle.getMnemonic("menu.edit.vk")); 175 | 176 | addTranslationMenuItem = new AddTranslationMenuItem(editor, tree, false); 177 | findTranslationMenuItem = new FindTranslationMenuItem(editor, false); 178 | removeTranslationMenuItem = new RemoveTranslationMenuItem(editor, false); 179 | duplicateTranslationMenuItem = new DuplicateTranslationMenuItem(editor, true); 180 | renameTranslationMenuItem = new RenameTranslationMenuItem(editor, false); 181 | copyTranslationKeyMenuItem = new CopyTranslationKeyToClipboardMenuItem(editor, false); 182 | 183 | editMenu.add(new AddLocaleMenuItem(editor, true)); 184 | editMenu.addSeparator(); 185 | editMenu.add(addTranslationMenuItem); 186 | editMenu.add(findTranslationMenuItem); 187 | editMenu.addSeparator(); 188 | editMenu.add(renameTranslationMenuItem); 189 | editMenu.add(duplicateTranslationMenuItem); 190 | editMenu.add(removeTranslationMenuItem); 191 | editMenu.add(copyTranslationKeyMenuItem); 192 | 193 | // View menu 194 | viewMenu = new JMenu(MessageBundle.get("menu.view.title")); 195 | viewMenu.setMnemonic(MessageBundle.getMnemonic("menu.view.vk")); 196 | viewMenu.add(new ExpandTranslationsMenuItem(tree)); 197 | viewMenu.add(new CollapseTranslationsMenuItem(tree)); 198 | 199 | // Settings menu 200 | settingsMenu = new JMenu(MessageBundle.get("menu.settings.title")); 201 | settingsMenu.setMnemonic(MessageBundle.getMnemonic("menu.settings.vk")); 202 | 203 | editorSettingsMenuItem = new JMenuItem(MessageBundle.get("menu.settings.preferences.editor.title")); 204 | editorSettingsMenuItem.addActionListener(e -> { 205 | Dialogs.showComponentDialog(editor, 206 | MessageBundle.get("dialogs.preferences.editor.title"), 207 | new EditorSettingsPane(editor)); 208 | }); 209 | 210 | projectSettingsMenuItem = new JMenuItem(MessageBundle.get("menu.settings.preferences.project.title")); 211 | projectSettingsMenuItem.addActionListener(e -> { 212 | Dialogs.showComponentDialog(editor, 213 | MessageBundle.get("dialogs.preferences.project.title"), 214 | new EditorProjectSettingsPane(editor)); 215 | }); 216 | 217 | settingsMenu.add(editorSettingsMenuItem); 218 | 219 | // Help menu 220 | JMenu helpMenu = new JMenu(MessageBundle.get("menu.help.title")); 221 | helpMenu.setMnemonic(MessageBundle.getMnemonic("menu.help.vk")); 222 | 223 | JMenuItem versionMenuItem = new JMenuItem(MessageBundle.get("menu.help.version.title")); 224 | versionMenuItem.addActionListener(e -> editor.showVersionDialog(false)); 225 | 226 | JMenuItem homeMenuItem = new JMenuItem(MessageBundle.get("menu.help.home.title", Editor.TITLE)); 227 | homeMenuItem.addActionListener(e -> { 228 | try { 229 | Desktop.getDesktop().browse(URI.create(GithubRepoUtil.getURL(Editor.GITHUB_USER, Editor.GITHUB_PROJECT))); 230 | } catch (IOException e1) { 231 | // 232 | } 233 | }); 234 | 235 | JMenuItem aboutMenuItem = new JMenuItem(MessageBundle.get("menu.help.about.title", Editor.TITLE)); 236 | aboutMenuItem.addActionListener(e -> editor.showAboutDialog()); 237 | 238 | helpMenu.add(versionMenuItem); 239 | helpMenu.addSeparator(); 240 | helpMenu.add(homeMenuItem); 241 | helpMenu.add(aboutMenuItem); 242 | 243 | add(fileMenu); 244 | add(editMenu); 245 | add(viewMenu); 246 | add(settingsMenu); 247 | add(helpMenu); 248 | 249 | tree.addTreeSelectionListener(e -> { 250 | TranslationTreeNode node = tree.getSelectionNode(); 251 | boolean enabled = node != null && !node.isRoot(); 252 | renameTranslationMenuItem.setEnabled(enabled); 253 | copyTranslationKeyMenuItem.setEnabled(enabled); 254 | duplicateTranslationMenuItem.setEnabled(enabled); 255 | removeTranslationMenuItem.setEnabled(enabled); 256 | }); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/ExtendedProperties.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.Enumeration; 12 | import java.util.List; 13 | import java.util.Locale; 14 | import java.util.Properties; 15 | import java.util.TreeSet; 16 | import java.util.stream.Collectors; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import com.google.common.base.Strings; 22 | import com.google.common.collect.Lists; 23 | 24 | /** 25 | * This class extends {@link Properties}. 26 | * 27 | *

This implementation adds the ability to load and store properties from a given file path 28 | * and adds support to retrieve and store {@code Integer} and {@code List} values.

29 | * 30 | *

This implementation also adds extended functionality like {@link #containsKeys(String...)}.

31 | * 32 | * @author Jacob van Mourik 33 | */ 34 | public class ExtendedProperties extends Properties { 35 | private final static long serialVersionUID = 6042931434040718478L; 36 | private final static Logger log = LoggerFactory.getLogger(ExtendedProperties.class); 37 | private final String listSeparator; 38 | 39 | /** 40 | * Creates an empty property list with no default values and "," as list separator. 41 | */ 42 | public ExtendedProperties() { 43 | this(null, ","); 44 | } 45 | 46 | /** 47 | * Creates an empty property list with no default values and the specified list separator. 48 | * 49 | * @param separator the separator used for storing list values. 50 | */ 51 | public ExtendedProperties(String separator) { 52 | this(null, separator); 53 | } 54 | 55 | /** 56 | * Creates an empty property list with the specified defaults and "," as list separator. 57 | * 58 | * @param defaults the defaults. 59 | */ 60 | public ExtendedProperties(Properties defaults) { 61 | this(defaults, ","); 62 | } 63 | 64 | /** 65 | * Creates an empty property list with the specified defaults and list separator. 66 | * 67 | * @param defaults the defaults. 68 | * @param separator the separator used for storing list values. 69 | */ 70 | public ExtendedProperties(Properties defaults, String separator) { 71 | super(defaults); 72 | this.listSeparator = separator; 73 | } 74 | 75 | /** 76 | * Reads a property list from the given file path. 77 | * 78 | *

Any {@code IOException} will be ignored.

79 | * 80 | * @param path the path to the property file. 81 | */ 82 | public void load(Path path) { 83 | try (InputStream in = Files.newInputStream(path)) { 84 | load(in); 85 | } catch (IOException e) { 86 | log.error("Unable to load properties from " + path, e); 87 | } 88 | } 89 | 90 | /** 91 | * Writes the property list to the given file path. 92 | * 93 | *

Any {@code IOException} will be ignored.

94 | * 95 | * @param path the path to the property file. 96 | */ 97 | public void store(Path path) { 98 | try (OutputStream out = new OutputStreamWrapper(Files.newOutputStream(path))) { 99 | store(out, null); 100 | } catch (IOException e) { 101 | log.error("Unable to store properties to " + path, e); 102 | } 103 | } 104 | 105 | /** 106 | * Sets a value in the property list. The list of values will be converted 107 | * to a single string separated by {@value #listSeparator}. 108 | * 109 | * @param key the key to be placed in this property list. 110 | * @param values the value corresponding to {@code key}. 111 | */ 112 | public void setProperty(String key, List values) { 113 | setProperty(key, values.stream().collect(Collectors.joining(listSeparator))); 114 | } 115 | 116 | /** 117 | * Sets a value in the property list. The {@code Integer} value will be 118 | * stored as a {@code String} value. 119 | * 120 | * @param key the key to be placed in this property list. 121 | * @param value the value corresponding to {@code key}. 122 | */ 123 | public void setProperty(String key, Integer value) { 124 | setProperty(key, value == null ? null : value.toString()); 125 | } 126 | 127 | /** 128 | * Sets a value in the property list. The {@code boolean} value will be 129 | * stored as a {@code String} value; "1" for {@code true}, "0" for {@code false}. 130 | * 131 | * @param key the key to be placed in this property list. 132 | * @param value the value corresponding to {@code key}. 133 | */ 134 | public void setProperty(String key, Boolean value) { 135 | setProperty(key, value == null ? null : (value ? 1 : 0)); 136 | } 137 | 138 | /** 139 | * Sets a value in the property list. The {@code Enum} value will be 140 | * stored as a {@code String} value. 141 | * 142 | * @param key the key to be placed in this property list. 143 | * @param value the value corresponding to {@code key}. 144 | */ 145 | public void setProperty(String key, Enum value) { 146 | setProperty(key, value.toString()); 147 | } 148 | 149 | /** 150 | * Sets a value in the property list. The {@code Locale} value will be 151 | * stored as a {@code String} value. 152 | * 153 | * @param key the key to be placed in this property list. 154 | * @param value the value corresponding to {@code key}. 155 | */ 156 | public void setProperty(String key, Locale locale) { 157 | setProperty(key, locale.toString()); 158 | } 159 | 160 | /** 161 | * Gets a value from the property list as a {@code List}. This method should be used 162 | * to retrieve a value previously stored by {@link #setProperty(String, List)}. 163 | * 164 | * @param key the property key. 165 | * @return the value in this property list with the specified key value or an empty list 166 | * if no such key exists. 167 | */ 168 | public List getListProperty(String key) { 169 | String value = getProperty(key); 170 | return value == null ? Lists.newLinkedList() : Lists.newLinkedList(Arrays.asList(value.split(listSeparator))); 171 | } 172 | 173 | /** 174 | * Gets a value from the property list as an {@code Integer}. This method should be used 175 | * to retrieve a value previously stored by {@link #setProperty(String, Integer)}. 176 | * 177 | * @param key the property key. 178 | * @return the value in this property list with the specified key value or {@code null} 179 | * if no such key exists or the value is not a valid {@code Integer}. 180 | */ 181 | public Integer getIntegerProperty(String key) { 182 | String value = getProperty(key); 183 | if (!Strings.isNullOrEmpty(value)) { 184 | try { 185 | return Integer.parseInt(value); 186 | } catch (Exception e) { 187 | log.warn("Unable to parse integer property value " + value); 188 | } 189 | } 190 | return null; 191 | } 192 | 193 | /** 194 | * See {@link #getIntegerProperty(String)}. This method returns {@code defaultValue} when 195 | * there is no value in the property list with the specified {@code key} or when 196 | * the value is not a valid {@code Integer}. 197 | * 198 | * @param key the property key. 199 | * @param defaultValue the default value to return when there is no value for the specified key 200 | * @return the value in this property list with the specified key value or {@code defaultValue} 201 | * if no such key exists or the value is not a valid {@code Integer}. 202 | */ 203 | public Integer getIntegerProperty(String key, Integer defaultValue) { 204 | Integer value = getIntegerProperty(key); 205 | return value != null ? value : defaultValue; 206 | } 207 | 208 | /** 209 | * Gets a value from the property list as an {@code Boolean}. This method should be used 210 | * to retrieve a value previously stored by {@link #setProperty(String, Boolean)}. 211 | * 212 | * @param key the property key. 213 | * @return the value in this property list with the specified key value or {@code null} 214 | * if no such key exists. 215 | */ 216 | public Boolean getBooleanProperty(String key) { 217 | Integer value = getIntegerProperty(key); 218 | return value != null ? (value != 0) : null; 219 | } 220 | 221 | /** 222 | * See {@link #getBooleanProperty(String)}. This method returns {@code defaultValue} when 223 | * there is no value in the property list with the specified {@code key}. 224 | * 225 | * @param key the property key. 226 | * @param defaultValue the default value to return when there is no value for the specified key 227 | * @return the value in this property list with the specified key value or {@code defaultValue} 228 | * if no such key exists. 229 | */ 230 | public Boolean getBooleanProperty(String key, boolean defaultValue) { 231 | Boolean value = getBooleanProperty(key); 232 | return value != null ? value : defaultValue; 233 | } 234 | 235 | /** 236 | * Gets a value from the property list as an {@code Enum}. This method should be used 237 | * to retrieve a value previously stored by {@link #setProperty(String, Enum)}. 238 | * 239 | * @param key the property key. 240 | * @param defaultValue the default value to return when there is no value for the specified key 241 | * @return the value in this property list with the specified key value or {@code null} 242 | * if no such key exists or the value is not a valid enum value. 243 | */ 244 | public > T getEnumProperty(String key, Class enumType) { 245 | String value = getProperty(key); 246 | if (!Strings.isNullOrEmpty(value)) { 247 | try { 248 | return T.valueOf(enumType, value); 249 | } catch (Exception e) { 250 | log.warn("Unable to parse enum property value " + value); 251 | } 252 | } 253 | return null; 254 | } 255 | 256 | /** 257 | * See {@link #getEnumProperty(String, Class)}. This method returns {@code defaultValue} when 258 | * there is no value in the property list with the specified {@code key} or when 259 | * the value is not a valid enum value. 260 | * 261 | * @param key the property key. 262 | * @param defaultValue the default value to return when there is no value for the specified key 263 | * @return the value in this property list with the specified key value or {@code defaultValue} 264 | * if no such key exists or the value is not a valid enum value. 265 | */ 266 | public > T getEnumProperty(String key, Class enumType, T defaultValue) { 267 | T value = getEnumProperty(key, enumType); 268 | return value != null ? value : defaultValue; 269 | } 270 | 271 | /** 272 | * Gets a value from the property list as an {@code Locale}. This method should be used 273 | * to retrieve a value previously stored by {@link #setProperty(String, Locale)}. 274 | * 275 | * @param key the property key. 276 | * @return the value in this property list with the specified key value or {@code null} 277 | * if no such key exists. 278 | */ 279 | public Locale getLocaleProperty(String key) { 280 | String value = getProperty(key); 281 | return Locales.parseLocale(value); 282 | } 283 | 284 | /** 285 | * See {@link #getLocaleProperty(Locale)}. This method returns {@code defaultValue} when 286 | * there is no value in the property list with the specified {@code key}. 287 | * 288 | * @param key the property key. 289 | * @param defaultValue the default value to return when there is no value for the specified key 290 | * @return the value in this property list with the specified key value or {@code defaultValue} 291 | * if no such key exists. 292 | */ 293 | public Locale getLocaleProperty(String key, Locale defaultValue) { 294 | Locale value = getLocaleProperty(key); 295 | return value != null ? value : defaultValue; 296 | } 297 | 298 | /** 299 | * This function does the same as {@link #containsKey(Object)}, only for multiple keys. 300 | * 301 | * @param keys possible keys. 302 | * @return {@code true} if and only if the specified objects are keys in this hashtable, 303 | * as determined by the equals method; false otherwise. 304 | */ 305 | public boolean containsKeys(String... keys) { 306 | return Arrays.asList(keys).stream().allMatch(k -> containsKey(k)); 307 | } 308 | 309 | @Override 310 | public synchronized Enumeration keys() { 311 | return Collections.enumeration(new TreeSet(super.keySet())); 312 | } 313 | 314 | private class OutputStreamWrapper extends FilterOutputStream { 315 | private boolean firstlineseen = false; 316 | 317 | public OutputStreamWrapper(OutputStream out) { 318 | super(out); 319 | } 320 | 321 | @Override 322 | public void write(int b) throws IOException { 323 | if (firstlineseen) { 324 | super.write(b); 325 | } else if (b == '\n') { 326 | firstlineseen = true; 327 | } 328 | } 329 | } 330 | } -------------------------------------------------------------------------------- /src/main/java/com/jvms/i18neditor/util/Resources.java: -------------------------------------------------------------------------------- 1 | package com.jvms.i18neditor.util; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.charset.Charset; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.security.MessageDigest; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.util.List; 12 | import java.util.Locale; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.Properties; 16 | import java.util.SortedMap; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | import java.util.stream.Collectors; 20 | 21 | import org.apache.commons.lang3.StringEscapeUtils; 22 | 23 | import com.google.common.base.Strings; 24 | import com.google.common.collect.Lists; 25 | import com.google.common.collect.Maps; 26 | import com.google.gson.GsonBuilder; 27 | import com.google.gson.JsonElement; 28 | import com.google.gson.JsonNull; 29 | import com.google.gson.JsonObject; 30 | import com.google.gson.JsonParser; 31 | import com.google.gson.JsonPrimitive; 32 | import com.jvms.i18neditor.FileStructure; 33 | import com.jvms.i18neditor.Resource; 34 | import com.jvms.i18neditor.ResourceType; 35 | import com.jvms.i18neditor.io.ChecksumException; 36 | 37 | /** 38 | * This class provides utility functions for a {@link Resource}. 39 | * 40 | * @author Jacob van Mourik 41 | */ 42 | public final class Resources { 43 | private final static Charset UTF8_ENCODING; 44 | private final static String FILENAME_LOCALE_REGEX; 45 | 46 | static { 47 | UTF8_ENCODING = Charset.forName("UTF-8"); 48 | FILENAME_LOCALE_REGEX = Pattern.quote("{") + "(.*)" + Pattern.quote("LOCALE") + "(.*)" + Pattern.quote("}"); 49 | } 50 | 51 | /** 52 | * Gets all resources from the given rootDir directory path. 53 | * 54 | *

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

58 | * 59 | *

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

60 | * 61 | *

This function will not load the contents of the file, only its description.
62 | * If you want to load the contents, use {@link #load(Resource)} afterwards.

63 | * 64 | * @param root the root directory of the resources 65 | * @param fileDefinition the resource's file definition for lookup (using locale interpolation) 66 | * @param structure the file structure used for the lookup 67 | * @param type the type of the resource files to look for 68 | * @return list of found resources 69 | * @throws IOException if an I/O error occurs reading the directory. 70 | */ 71 | public static List get(Path root, String fileDefinition, FileStructure structure, Optional type) 72 | throws IOException { 73 | List result = Lists.newLinkedList(); 74 | List files = Files.walk(root, 1).collect(Collectors.toList()); 75 | String defaultFileName = getFilename(fileDefinition, Optional.empty()); 76 | Pattern fileDefinitionPattern = Pattern.compile("^" + getFilenameRegex(fileDefinition) + "$"); 77 | 78 | for (Path file : files) { 79 | Path parent = file.getParent(); 80 | if (parent == null || Files.isSameFile(root, file) || !Files.isSameFile(root, parent)) { 81 | continue; 82 | } 83 | String filename = com.google.common.io.Files.getNameWithoutExtension(file.getFileName().toString()); 84 | for (ResourceType rt : ResourceType.values()) { 85 | if (!type.orElse(rt).equals(rt)) { 86 | continue; 87 | } 88 | if (structure == FileStructure.Nested && Files.isDirectory(file)) { 89 | Locale locale = Locales.parseLocale(filename); 90 | if (locale == null) { 91 | continue; 92 | } 93 | Path rf = Paths.get(root.toString(), locale.toString(), getFilename(fileDefinition, Optional.of(locale)) + rt.getExtension()); 94 | if (Files.isRegularFile(rf)) { 95 | result.add(new Resource(rt, rf, locale)); 96 | } 97 | } 98 | if (structure == FileStructure.Flat && Files.isRegularFile(file)) { 99 | Matcher matcher = fileDefinitionPattern.matcher(filename); 100 | if (!matcher.matches() && !filename.equals(defaultFileName)) { 101 | continue; 102 | } 103 | if (!matchesResourceType(file, rt)) { 104 | continue; 105 | } 106 | Locale locale = null; 107 | if (matcher.matches() && matcher.groupCount() > 0) { 108 | locale = Locales.parseLocale(matcher.group(1)); 109 | } 110 | result.add(new Resource(rt, file, locale)); 111 | } 112 | } 113 | }; 114 | 115 | return result; 116 | } 117 | 118 | /** 119 | * Loads the translations of a {@link Resource} from disk. 120 | * 121 | *

This function will store a checksum to the resource.

122 | * 123 | * @param resource the resource. 124 | * @throws IOException if an I/O error occurs reading the file. 125 | */ 126 | public static void load(Resource resource) throws IOException { 127 | ResourceType type = resource.getType(); 128 | Path path = resource.getPath(); 129 | SortedMap translations; 130 | if (type == ResourceType.Properties) { 131 | ExtendedProperties content = new ExtendedProperties(); 132 | content.load(path); 133 | translations = fromProperties(content); 134 | } else { 135 | String content = Files.lines(path, UTF8_ENCODING).collect(Collectors.joining()); 136 | if (type == ResourceType.ES6) { 137 | content = es6ToJson(content); 138 | } 139 | translations = fromJson(content); 140 | } 141 | resource.setTranslations(translations); 142 | resource.setChecksum(createChecksum(resource)); 143 | } 144 | 145 | /** 146 | * Writes the translations of the given {@link Resource} to disk. 147 | * Empty translation values will be skipped. 148 | * 149 | *

This function will perform a checksum check before saving 150 | * to see if the file on disk has been changed in the meantime.

151 | * 152 | *

This function will store a checksum to the resource.

153 | * 154 | * @param resource the resource to write. 155 | * @param prettyPrinting whether to pretty print the contents 156 | * @param plainKeys 157 | * @throws IOException if an I/O error occurs writing the file. 158 | */ 159 | public static void write(Resource resource, boolean prettyPrinting, boolean flattenKeys) throws IOException { 160 | if (resource.getChecksum() != null) { 161 | String checksum = createChecksum(resource); 162 | if (!checksum.equals(resource.getChecksum())) { 163 | throw new ChecksumException("File on disk has been changed."); 164 | } 165 | } 166 | ResourceType type = resource.getType(); 167 | if (type == ResourceType.Properties) { 168 | ExtendedProperties content = toProperties(resource.getTranslations()); 169 | content.store(resource.getPath()); 170 | } else { 171 | String content = toJson(resource.getTranslations(), prettyPrinting, flattenKeys); 172 | if (type == ResourceType.ES6) { 173 | content = jsonToEs6(content); 174 | } 175 | if (!Files.exists(resource.getPath())) { 176 | Files.createDirectories(resource.getPath().getParent()); 177 | Files.createFile(resource.getPath()); 178 | } 179 | Files.write(resource.getPath(), Lists.newArrayList(content), UTF8_ENCODING); 180 | } 181 | resource.setChecksum(createChecksum(resource)); 182 | } 183 | 184 | /** 185 | * Creates a new {@link Resource} with the given {@link ResourceType} in the given directory path. 186 | * This function should be used to create new resources. For creating an instance of an 187 | * existing resource on disk, see {@link #read(Path)}. 188 | * 189 | *

This function will store a checksum to the resource.

190 | * 191 | * @param type the type of the resource to create. 192 | * @param root the root directory to write the resource to. 193 | * @param filenameDefinition the filename definition of the resource. 194 | * @param structure the file structure to use 195 | * @param locale the locale of the resource (optional). 196 | * @return The newly created resource. 197 | * @throws IOException if an I/O error occurs writing the file. 198 | */ 199 | public static Resource create(ResourceType type, Path root, String fileDefinition, FileStructure structure, Optional locale) 200 | throws IOException { 201 | String extension = type.getExtension(); 202 | Path path; 203 | if (structure == FileStructure.Nested) { 204 | path = Paths.get(root.toString(), locale.get().toString(), getFilename(fileDefinition, locale) + extension); 205 | } else { 206 | path = Paths.get(root.toString(), getFilename(fileDefinition, locale) + extension); 207 | } 208 | Resource resource = new Resource(type, path, locale.orElse(null)); 209 | write(resource, false, false); 210 | return resource; 211 | } 212 | 213 | private static String getFilenameRegex(String fileDefinition) { 214 | return fileDefinition.replaceAll(FILENAME_LOCALE_REGEX, "$1(" + Locales.LOCALE_REGEX + ")$2"); 215 | } 216 | 217 | private static String getFilename(String fileDefinition, Optional locale) { 218 | return fileDefinition.replaceAll(FILENAME_LOCALE_REGEX, locale.isPresent() ? ("$1" + locale.get().toString() + "$2") : ""); 219 | } 220 | 221 | private static SortedMap fromProperties(Properties properties) { 222 | SortedMap result = Maps.newTreeMap(); 223 | properties.forEach((key, value) -> { 224 | result.put((String)key, StringEscapeUtils.unescapeJava((String)value)); 225 | }); 226 | return result; 227 | } 228 | 229 | private static ExtendedProperties toProperties(Map translations) { 230 | ExtendedProperties result = new ExtendedProperties(); 231 | translations.forEach((key, value) -> { 232 | if (!Strings.isNullOrEmpty(value)) { 233 | result.put(key, value); 234 | } 235 | }); 236 | return result; 237 | } 238 | 239 | private static SortedMap fromJson(String json) { 240 | SortedMap result = Maps.newTreeMap(); 241 | JsonElement elem = new JsonParser().parse(json); 242 | fromJson(null, elem, result); 243 | return result; 244 | } 245 | 246 | private static void fromJson(String key, JsonElement elem, Map content) { 247 | if (elem.isJsonObject()) { 248 | elem.getAsJsonObject().entrySet().forEach(entry -> { 249 | String newKey = key == null ? entry.getKey() : ResourceKeys.create(key, entry.getKey()); 250 | fromJson(newKey, entry.getValue(), content); 251 | }); 252 | } else if (elem.isJsonPrimitive()) { 253 | content.put(key, StringEscapeUtils.unescapeJava(elem.getAsString())); 254 | } else if (elem.isJsonNull()) { 255 | content.put(key, ""); 256 | } else { 257 | throw new IllegalArgumentException("Found invalid json element."); 258 | } 259 | } 260 | 261 | private static String toJson(Map translations, boolean prettify, boolean flattenKeys) { 262 | List keys = Lists.newArrayList(translations.keySet()); 263 | JsonElement elem = !flattenKeys ? toJson(translations, null, keys) : toFlatJson(translations, keys); 264 | GsonBuilder builder = new GsonBuilder().disableHtmlEscaping(); 265 | if (prettify) { 266 | builder.setPrettyPrinting(); 267 | } 268 | return builder.create().toJson(elem); 269 | } 270 | 271 | private static JsonElement toFlatJson(Map translations, List keys) { 272 | JsonObject object = new JsonObject(); 273 | if (keys.size() > 0) { 274 | translations.forEach((k, v) -> { 275 | if (!Strings.isNullOrEmpty(translations.get(k))) { 276 | object.add(k, new JsonPrimitive(translations.get(k))); 277 | } 278 | }); 279 | } 280 | return object; 281 | } 282 | 283 | private static JsonElement toJson(Map translations, String key, List keys) { 284 | if (keys.size() > 0) { 285 | JsonObject object = new JsonObject(); 286 | ResourceKeys.uniqueRootKeys(keys).forEach(rootKey -> { 287 | String subKey = ResourceKeys.create(key, rootKey); 288 | List subKeys = ResourceKeys.extractChildKeys(keys, rootKey); 289 | object.add(rootKey, toJson(translations, subKey, subKeys)); 290 | }); 291 | return object; 292 | } 293 | if (key == null) { 294 | return new JsonObject(); 295 | } 296 | if (Strings.isNullOrEmpty(translations.get(key))) { 297 | return JsonNull.INSTANCE; 298 | } 299 | return new JsonPrimitive(translations.get(key)); 300 | } 301 | 302 | private static String es6ToJson(String content) { 303 | return content.replaceAll("export +default", "").replaceAll("} *;", "}"); 304 | } 305 | 306 | private static String jsonToEs6(String content) { 307 | return "export default " + content + ";"; 308 | } 309 | 310 | private static String createChecksum(Resource resource) throws IOException { 311 | MessageDigest digest; 312 | try { 313 | digest = MessageDigest.getInstance("SHA-1"); 314 | } catch (NoSuchAlgorithmException e) { 315 | return null; 316 | } 317 | byte[] buffer = new byte[1024]; 318 | int bytesRead = 0; 319 | try (InputStream is = Files.newInputStream(resource.getPath())) { 320 | while ((bytesRead = is.read(buffer)) != -1) { 321 | digest.update(buffer, 0, bytesRead); 322 | } 323 | } 324 | String result = ""; 325 | byte[] bytes = digest.digest(); 326 | for (int i = 0; i < bytes.length; i++) { 327 | result += Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1); 328 | } 329 | return result; 330 | } 331 | 332 | private static boolean matchesResourceType(Path path, ResourceType type) { 333 | String fileExt = com.google.common.io.Files.getFileExtension(path.getFileName().toString()); 334 | return ("."+fileExt).equals(type.getExtension()); 335 | } 336 | } 337 | --------------------------------------------------------------------------------