pvbemu/src/desktop/util/Localizer.java

538 lines
17 KiB
Java

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<Object, Control> controls; // Control mapping
private Locale locale; // Current message store
///////////////////////////////////////////////////////////////////////////
// Constants //
///////////////////////////////////////////////////////////////////////////
// Class reference for JComboBox<String>
JComboBox<String> JCOMBOBOX = new JComboBox<String>();
///////////////////////////////////////////////////////////////////////////
// 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<String, String> tags; // Message overrides
Control(String tipKey) {
tags = new HashMap<String, String>();
this.tipKey = tipKey;
}
}
// Locale container
public static class Locale implements Comparable<Locale> {
// Public fields
public final String id; // Unique identifier
public final String name; // Display name
// Private fields
private HashMap<String, String> messages; // Message dictionary
// Constructor
private Locale(HashMap<String, String> 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<String, String>(); // Output dictionary
int start = 0; // Position of first character in key or value
var stack = new Stack<String>(); // 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'");
return new Locale(ret);
}
///////////////////////////////////////////////////////////////////////////
// Constructors //
///////////////////////////////////////////////////////////////////////////
// Default constructor
public Localizer() {
controls = new HashMap<Object, Control>();
}
// 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<String>) 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<String>(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));
}
}