package util; // Java imports import java.util.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.text.*; // UI display text manager public class Localizer { // Instance fields private HashMap controls; // Control mapping private Locale locale; // Current message store /////////////////////////////////////////////////////////////////////////// // Constants // /////////////////////////////////////////////////////////////////////////// // Class reference for JComboBox JComboBox JCOMBOBOX = new JComboBox(); /////////////////////////////////////////////////////////////////////////// // Classes // /////////////////////////////////////////////////////////////////////////// // Control settings private static class Control { String key; // Single-string dictionary key String[] keys; // Multiple-string dictionary key String tipKey; // Tooltip dictionary key HashMap tags; // Message overrides Control(String tipKey) { tags = new HashMap(); this.tipKey = tipKey; } } // Locale container public static class Locale implements Comparable { // Public fields public final String id; // Unique identifier public final String name; // Display name // Private fields private HashMap messages; // Message dictionary // Constructor private Locale(HashMap messages) { id = messages.get("locale.id"); this.messages = messages; name = messages.get("locale.name"); } // Comparator public int compareTo(Locale o) { return id.compareTo(o.id); } // Represent this object as a string public String toString() { return id + " - " + name; } } /////////////////////////////////////////////////////////////////////////// // Static Methods // /////////////////////////////////////////////////////////////////////////// // Parse a text file to produce a Locale object public static Locale parse(String text) { var chars = text.replaceAll("\\r\\n?", "\n").toCharArray(); var ret = new HashMap(); // Output dictionary int start = 0; // Position of first character in key or value var stack = new Stack(); // Nested key chain int state = 0; // Current parsing context // Process all characters for (int x = 0, line = 1, col = 1; x < chars.length; x++, col++) { char c = chars[x]; boolean end = x == chars.length - 1; String pos = line + ":" + col + ": "; boolean white = c == ' ' || c == '\t'; // Processing for newline if (c == '\n') { col = 0; line++; } // Comment if (state == -1) { if (c == '\n' || end) state = 0; } // Pre-key else if (state == 0) { // Ignore leading whitespace if (white) continue; // The line is a comment if (c == '#') { state = -1; continue; } // End of input has been reached if (end) break; // The line is empty if (c == '\n') continue; // Proceed to key start = x; state = 1; } // Key else if (state == 1) { // Any non-whitespace character is valid in a key if (!white && c != '\n' && !end) continue; // Produce the key as a string String key = new String(chars, start, x - start).trim(); // Close a key group if (key.equals("}")) { // Nesting error if (stack.size() == 0) throw new RuntimeException( pos + "Unexpected '}'"); // Syntax error outer: for (int y = x; y < chars.length; y++) { char h = chars[y]; if (h == '\n') break; if (h != ' ' && h != '\t') throw new RuntimeException( pos + "Newline expected"); } // Proceed to key state = 0; stack.pop(); continue; } // Curly braces are not valid in keys if (key.contains("{") || key.contains("}")) throw new RuntimeException( line + ": Curly braces are not allowed in keys"); // Proceed to interim stack.push(key); state = 2; } // Post-key, pre-value if (state == 2) { // Ignore any whitespace between key and value if (white) continue; // No value is present if (c == '\n') throw new RuntimeException( pos + "Unexpected newline"); if (end) throw new RuntimeException( pos + "Unexpected end of input"); // Proceed to value start = x; state = 3; } // Value if (state == 3) { // Escape sequence if (c == '\\') { // EOF if (x == chars.length - 1) throw new RuntimeException( pos + "Unexpected end of input"); // Cannot escape a newline if (chars[x + 1] == '\n') throw new RuntimeException( pos + "Unexpected newline"); // Skip the next character x++; continue; } // The end of the value has not yet been reached if (c != '\n' && !end) continue; // Produce the value as a string if (end) x++; String value = new String(chars, start, x - start).trim(); state = 0; // Open a key group if (value.equals("{")) { state = 0; continue; } // Check for nesting errors int depth = 0; for (int y = start; y < x; y++) { switch (chars[y]) { case '\\': y++; continue; case '{' : depth++; continue; case '}' : if (--depth == -1) throw new RuntimeException( (line - 1) + ": Unexpected '}'" ); continue; } } if (depth != 0) throw new RuntimeException( pos + "'}' expected"); // Produce the full key as a string var pkey = new StringBuilder(); for (String item : stack) pkey.append((pkey.length() != 0 ? "." : "") + item); String key = pkey.toString(); // Check for duplicate keys String lkey = key.toLowerCase(); if (ret.get(lkey) != null) throw new RuntimeException( (line-1)+ ": Key '" + key + "' has already been defined."); // Add the pair to the dictionary ret.put(lkey, value); stack.pop(); } } // Perform post-processing error checks if (state != 0 || !stack.empty()) throw new RuntimeException( "Unexpected end of input"); if (!ret.containsKey("locale.id")) throw new RuntimeException( "Required key not found: 'locale.id'"); if (!ret.containsKey("locale.name")) throw new RuntimeException( "Required key not found: 'locale.name'"); ret.put("null", ""); return new Locale(ret); } /////////////////////////////////////////////////////////////////////////// // Constructors // /////////////////////////////////////////////////////////////////////////// // Default constructor public Localizer() { controls = new HashMap(); } // Parsing constructor public Localizer(String text) { this(); setLocale(parse(text)); } /////////////////////////////////////////////////////////////////////////// // Public Methods // /////////////////////////////////////////////////////////////////////////// // Add a control to the collection public boolean add(Object control, Object key) { return add(control, key, null); } // Add a control with a tooltip to the collection public boolean add(Object control, Object key, String tipKey) { var ctrl = controls.get(control); if (ctrl == null) ctrl = new Control(tipKey); // Error checking if (control == null || key == null) return false; // Control takes a single string if (key instanceof String) { // Type validation if (!( control instanceof AbstractButton || control instanceof JFrame || control instanceof JInternalFrame || control instanceof JLabel || control instanceof JPanel || // TitledBorder control instanceof JTextComponent )) return false; // Configure key ctrl.key = (String) key; } // Control takes an array of strings else if (key instanceof String[]) { // Type validation if (!( JCOMBOBOX.getClass().isAssignableFrom(control.getClass()) )) return false; // Configure keys String[] keys = (String[]) key; ctrl.keys = new String[keys.length]; System.arraycopy(keys, 0, ctrl.keys, 0, keys.length); } // Invalid control type else return false; // Add the control to the collection controls.put(control, ctrl); update(control); return true; } // Remove all controls from being managed public void clearControls() { controls.clear(); } // Evaluate the message for a given key public String get(String key) { return key == null ? null : evaluate(null, key); } // Retrieve the currently loaded locale public Locale getLocale() { return locale; } // Configure a control tag public String put(Object control, String key, String value) { var ctrl = controls.get(control); // Error checking if (ctrl == null || key == null) return null; // Update the control's tags key = key.toLowerCase(); String ret = value == null ? ctrl.tags.remove(key) : ctrl.tags.put(key, value) ; // Refresh the text on the control update(control); return ret; } // Remove a control from the collection public boolean remove(Object control) { return controls.remove(control) != null; } // Specify a message dictionary public void setLocale(Locale locale) { this.locale = locale; } /////////////////////////////////////////////////////////////////////////// // Private Methods // /////////////////////////////////////////////////////////////////////////// // Process substitutions and escapes on a message for a given key private String evaluate(Control control, String key) { // No locale is loaded if (locale == null) return key; // Check that the key exists String ret = locale.messages.get(key.toLowerCase()); if (ret == null) return key; // Perform all substitutions outer: for (;;) { var chars = ret.toCharArray(); int start = 0; // Look for the first complete substitution for (int x = 0; x < chars.length; x++) { char c = chars[x]; // Escape sequence if (c == '\\') { x++; continue; } // Begin substitution if (c == '{') start = x + 1; // Substitution has not yet ended if (c != '}') continue; // Determine the substitution key = new String(chars, start, x - start); String lkey = key.toLowerCase(); String value = control == null ? null : control.tags.get(lkey); if (value == null) value = locale.messages.get(lkey); if (value == null) value = "\\{" + key + "\\}"; // Apply the substitution to the message ret = new String(chars, 0, start - 1) + value + new String(chars, x + 1, chars.length - x - 1) ; continue outer; } // No further substitutions break; } // Unescape all escape sequences var chars = ret.toCharArray(); int length = 0; for (int x = 0; x < chars.length; x++, length++) { char c = chars[x]; if (c == '\\') switch (c = chars[++x]) { case 'n': c = '\n'; break; case 't': c = '\t'; break; } chars[length] = c; } return new String(chars, 0, length); } // Update the text for all controls private void update() { for (var control : controls.keySet()) update(control); } // Update the text for a control private void update(Object control) { var ctrl = controls.get(control); String[] keys = ctrl.key==null ? ctrl.keys : new String[]{ctrl.key}; String[] values = new String[keys.length]; // Evaluate all messages for (int x = 0; x < keys.length; x++) values[x] = evaluate(ctrl, keys[x]); // Update the control's text if (control instanceof AbstractButton) ((AbstractButton) control).setText (values[0]); if (control instanceof JFrame) ((JFrame ) control).setTitle(values[0]); if (control instanceof JInternalFrame) ((JInternalFrame) control).setTitle(values[0]); if (control instanceof JLabel ) ((JLabel ) control).setText (values[0]); if (control instanceof JTextComponent) ((JTextComponent) control).setText (values[0]); // JPanel must be wrapped in a TitledBorder if (control instanceof JPanel) { var border = ((JPanel) control).getBorder(); if (border instanceof TitledBorder) ((TitledBorder) border).setTitle(values[0]); } // Replace the contents of a JComboBox without firing events if (JCOMBOBOX.getClass().isAssignableFrom(control.getClass())) { // The type is explicitly verified above @SuppressWarnings("unchecked") var box = (JComboBox) control; // Configure working variables var action = box.getActionListeners(); int index = box.getSelectedIndex(); var item = box.getItemListeners(); // Remove event listeners for (var lst : action) box.removeActionListener(lst); for (var lst : item ) box.removeItemListener (lst); // Update contents box.setModel(new DefaultComboBoxModel(values)); box.setSelectedIndex(index); // Restore event listeners for (var lst : action) box.addActionListener(lst); for (var lst : item ) box.addItemListener (lst); } // Update the control's tooltip text if (control instanceof JComponent) ((JComponent) control).setToolTipText( ctrl.tipKey == null ? null : evaluate(ctrl, ctrl.tipKey)); } }