Establishing app module, adjustments to localization support
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import java.util.*;
|
||||
import javax.swing.*;
|
||||
// Project imports
|
||||
import app.*;
|
||||
import util.*;
|
||||
|
||||
// Desktop application primary class
|
||||
@@ -7,13 +7,8 @@ public class Main {
|
||||
|
||||
// Program entry point
|
||||
public static void main(String[] args) {
|
||||
var loc = new Localizer();
|
||||
loc.set(Util.textRead("locale/en_US.txt"));
|
||||
var window = new JFrame();
|
||||
loc.add(window, "app.title.mixed");
|
||||
loc.put(window, "ctrl.filename", "wario.vb");
|
||||
loc.put(window, "ctrl.number", "1");
|
||||
System.out.println(window.getTitle());
|
||||
Util.setSystemLAF();
|
||||
new App();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package app;
|
||||
|
||||
// Java imports
|
||||
import java.util.*;
|
||||
|
||||
// Project imports
|
||||
import util.*;
|
||||
|
||||
// Top-level software state manager
|
||||
public class App {
|
||||
|
||||
// Instance fields
|
||||
private Localizer.Locale[] locales; // Language translations
|
||||
private Localizer localizer; // UI localization manager
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public App() {
|
||||
|
||||
// Instance fields
|
||||
localizer = new Localizer();
|
||||
|
||||
// Additional processing
|
||||
initLocales();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Associate a control with the localizer
|
||||
public boolean addControl(Object control, Object key) {
|
||||
return localizer.add(control, key);
|
||||
}
|
||||
|
||||
// Retrieve the currently active locale
|
||||
public Localizer.Locale getLocale() {
|
||||
return localizer.getLocale();
|
||||
}
|
||||
|
||||
// Retrieve a list of registered locales
|
||||
public Localizer.Locale[] listLocales() {
|
||||
var ret = new Localizer.Locale[locales.length];
|
||||
System.arraycopy(locales, 0, ret, 0, locales.length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Remove a control from the localizer
|
||||
public boolean removeControl(Object control) {
|
||||
return localizer.remove(control);
|
||||
}
|
||||
|
||||
// Specify a new locale
|
||||
public void setLocale(Localizer.Locale locale) {
|
||||
localizer.setLocale(locale);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Private Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Load and parse all locale translations
|
||||
private void initLocales() {
|
||||
|
||||
// Process all locale files
|
||||
var locales = new HashMap<String, Localizer.Locale>();
|
||||
for (String file : Util.listFiles("locale")) {
|
||||
|
||||
// Process the file
|
||||
try {
|
||||
var locale = Localizer.parse(Util.textRead("locale/" + file));
|
||||
String key = locale.id.toLowerCase();
|
||||
|
||||
// Register the locale
|
||||
if (locales.containsKey(key)) throw new RuntimeException(
|
||||
"Locale with ID '" + locale.id + "' already registered");
|
||||
locales.put(key, locale);
|
||||
|
||||
// The locale matches the one in the config
|
||||
if (key.equals("en-us"))
|
||||
localizer.setLocale(locale);
|
||||
}
|
||||
|
||||
// Could not process the file
|
||||
catch (Exception e) {
|
||||
System.err.println("Error parsing locale " +
|
||||
file + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Produce the list of registered locales
|
||||
this.locales = locales.values().toArray(
|
||||
new Localizer.Locale[locales.size()]);
|
||||
Arrays.sort(this.locales);
|
||||
|
||||
// Select a default locale
|
||||
if (localizer.getLocale() != null || this.locales.length == 0)
|
||||
return;
|
||||
var locale = locales.get("en-us");
|
||||
localizer.setLocale(locale != null ? locale : this.locales[0]);
|
||||
}
|
||||
|
||||
}
|
||||
+155
-112
@@ -11,8 +11,8 @@ public class Localizer {
|
||||
|
||||
// Instance fields
|
||||
private HashMap<Object, Object> controls; // Control mapping
|
||||
private HashMap<String, String> messages; // Message dictionary
|
||||
private HashMap<Object, HashMap<String, String>> tags; // Control overrides
|
||||
private Locale locale; // Current message store
|
||||
private HashMap<Object, HashMap<String, String>> tags; // Control messages
|
||||
|
||||
|
||||
|
||||
@@ -26,124 +26,69 @@ public class Localizer {
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Constructors //
|
||||
// Classes //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Default constructor
|
||||
public Localizer() {
|
||||
controls = new HashMap<Object, Object>();
|
||||
messages = new HashMap<String, String>();
|
||||
tags = new HashMap<Object, HashMap<String, String>>();
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Public Methods //
|
||||
// Static Methods //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Add a control to the collection
|
||||
public boolean add(Object control, Object key) {
|
||||
|
||||
// Error checking
|
||||
if (control == null || key == null)
|
||||
return false;
|
||||
|
||||
// Control takes a single string
|
||||
if (key instanceof String) {
|
||||
if (!(
|
||||
control instanceof AbstractButton ||
|
||||
control instanceof JFrame ||
|
||||
control instanceof JInternalFrame ||
|
||||
control instanceof JPanel || // TitledBorder
|
||||
control instanceof JTextComponent
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Control takes an array of strings
|
||||
else if (key instanceof String[]) {
|
||||
if (!(
|
||||
JCOMBOBOX.getClass().isAssignableFrom(control.getClass())
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Invalid control type
|
||||
else return false;
|
||||
|
||||
// Add the control to the collection
|
||||
controls.put(control, key);
|
||||
tags .put(control, new HashMap<String, String>());
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove all controls from being managed
|
||||
public void clearControls() {
|
||||
controls.clear();
|
||||
}
|
||||
|
||||
// Configure a control tag
|
||||
public String put(Object control, String key, String value) {
|
||||
if (controls.get(control) == null || key == null)
|
||||
return null;
|
||||
var tags = this.tags.get(control);
|
||||
key = key.toLowerCase();
|
||||
String ret = value == null ?
|
||||
tags.remove(key) :
|
||||
tags.put(key, value)
|
||||
;
|
||||
update();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Configure a dictionary entry
|
||||
public String put(String key, String value) {
|
||||
String ret = value == null ?
|
||||
messages.remove(key) :
|
||||
messages.put(key, value)
|
||||
;
|
||||
update();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Remove a control from the collection
|
||||
public boolean remove(Object control) {
|
||||
tags.remove(control);
|
||||
return controls.remove(control) != null;
|
||||
}
|
||||
|
||||
// Specify a message dictionary
|
||||
public void set(String text) {
|
||||
|
||||
// Pre-processing
|
||||
messages.clear();
|
||||
if (text == null) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure working variables
|
||||
// Parse a text file to produce a Locale object
|
||||
public static Locale parse(String text) {
|
||||
var chars = text.replaceAll("\\r\\n?", "\n").toCharArray();
|
||||
int col = 1; // Current character on the current line
|
||||
int line = 1; // Current line in the file
|
||||
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; x < chars.length; x++, col++) {
|
||||
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)
|
||||
continue;
|
||||
col = 1;
|
||||
state = 0;
|
||||
line++;
|
||||
if (c == '\n' || end)
|
||||
state = 0;
|
||||
}
|
||||
|
||||
// Pre-key
|
||||
@@ -164,11 +109,8 @@ public class Localizer {
|
||||
break;
|
||||
|
||||
// The line is empty
|
||||
if (c == '\n') {
|
||||
col = 1;
|
||||
line++;
|
||||
if (c == '\n')
|
||||
continue;
|
||||
}
|
||||
|
||||
// Proceed to key
|
||||
start = x;
|
||||
@@ -263,9 +205,7 @@ public class Localizer {
|
||||
if (end)
|
||||
x++;
|
||||
String value = new String(chars, start, x - start).trim();
|
||||
col = 1;
|
||||
state = 0;
|
||||
line++;
|
||||
|
||||
// Open a key group
|
||||
if (value.equals("{")) {
|
||||
@@ -281,8 +221,7 @@ public class Localizer {
|
||||
case '{' : depth++; continue;
|
||||
case '}' : if (--depth == -1)
|
||||
throw new RuntimeException(
|
||||
line + ":" + (x - start + y + 1) +
|
||||
": Unexpected '}'"
|
||||
(line - 1) + ": Unexpected '}'"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -298,18 +237,118 @@ public class Localizer {
|
||||
|
||||
// Check for duplicate keys
|
||||
String lkey = key.toLowerCase();
|
||||
if (messages.get(lkey) != null) throw new RuntimeException(
|
||||
if (ret.get(lkey) != null) throw new RuntimeException(
|
||||
(line-1)+ ": Key '" + key + "' has already been defined.");
|
||||
|
||||
// Add the pair to the dictionary
|
||||
messages.put(lkey, value);
|
||||
ret.put(lkey, value);
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update all controls
|
||||
// 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, Object>();
|
||||
tags = new HashMap<Object, HashMap<String, String>>();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// Error checking
|
||||
if (control == null || key == null)
|
||||
return false;
|
||||
|
||||
// Control takes a single string
|
||||
if (key instanceof String) {
|
||||
if (!(
|
||||
control instanceof AbstractButton ||
|
||||
control instanceof JFrame ||
|
||||
control instanceof JInternalFrame ||
|
||||
control instanceof JPanel || // TitledBorder
|
||||
control instanceof JTextComponent
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Control takes an array of strings
|
||||
else if (key instanceof String[]) {
|
||||
if (!(
|
||||
JCOMBOBOX.getClass().isAssignableFrom(control.getClass())
|
||||
)) return false;
|
||||
}
|
||||
|
||||
// Invalid control type
|
||||
else return false;
|
||||
|
||||
// Add the control to the collection
|
||||
controls.put(control, key);
|
||||
tags .put(control, new HashMap<String, String>());
|
||||
update();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove all controls from being managed
|
||||
public void clearControls() {
|
||||
controls.clear();
|
||||
}
|
||||
|
||||
// Retrieve the currently loaded locale
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
// Configure a control tag
|
||||
public String put(Object control, String key, String value) {
|
||||
if (controls.get(control) == null || key == null)
|
||||
return null;
|
||||
var tags = this.tags.get(control);
|
||||
key = key.toLowerCase();
|
||||
String ret = value == null ?
|
||||
tags.remove(key) :
|
||||
tags.put(key, value)
|
||||
;
|
||||
update();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Remove a control from the collection
|
||||
public boolean remove(Object control) {
|
||||
tags.remove(control);
|
||||
return controls.remove(control) != null;
|
||||
}
|
||||
|
||||
// Specify a message dictionary
|
||||
public void setLocale(Locale locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,9 +359,13 @@ public class Localizer {
|
||||
|
||||
// Process substitutions and escapes on a message for a given key
|
||||
private String evaluate(Object control, String key) {
|
||||
String ret = messages.get(key.toLowerCase());
|
||||
|
||||
// The topmost key does not exist in the dictionary
|
||||
// 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;
|
||||
|
||||
@@ -354,7 +397,7 @@ public class Localizer {
|
||||
String lkey = key.toLowerCase();
|
||||
String value = tags.get(control).get(lkey);
|
||||
if (value == null)
|
||||
value = messages.get(lkey);
|
||||
value = locale.messages.get(lkey);
|
||||
if (value == null)
|
||||
value = "\\{" + key + "\\}";
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import javax.imageio.*;
|
||||
import javax.swing.*;
|
||||
@@ -53,6 +55,23 @@ public final class Util {
|
||||
new BOM(new byte[] { - 1, - 2 }, StandardCharsets.UTF_16LE),
|
||||
};
|
||||
|
||||
// Filesystem state manager for the current .jar (if any)
|
||||
private static final FileSystem JARFS;
|
||||
|
||||
// Static initializer
|
||||
static {
|
||||
FileSystem fs = null;
|
||||
try {
|
||||
fs = FileSystems.newFileSystem(
|
||||
new URI("jar:" + Util.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toString()),
|
||||
new HashMap<String, String>(),
|
||||
Util.class.getClassLoader()
|
||||
);
|
||||
} catch (Exception e) { }
|
||||
JARFS = fs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -138,6 +157,34 @@ public final class Util {
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// Produce a list of files contained in a directory
|
||||
// If the directory is not found on disk, looks for it in the .jar
|
||||
public static String[] listFiles(String path) {
|
||||
|
||||
// Check for the directory on disk
|
||||
var file = new File(path);
|
||||
if (file.exists() && file.isDirectory())
|
||||
return file.list();
|
||||
|
||||
// Not executing out of a .jar
|
||||
if (JARFS == null)
|
||||
return null;
|
||||
|
||||
// Check for the directory in the .jar
|
||||
try {
|
||||
var list = Files.list(JARFS.getPath("/" + path)).toArray();
|
||||
var ret = new String[list.length];
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
path = "/" + ((Path) list[x]).toString();
|
||||
ret[x] = path.substring(path.lastIndexOf("/") + 1);
|
||||
}
|
||||
return ret;
|
||||
} catch (Exception e) { }
|
||||
|
||||
// The directory was not found
|
||||
return null;
|
||||
}
|
||||
|
||||
// Produce a WindowListener with a functional interface
|
||||
public static WindowListener onClose(OnClose close) {
|
||||
return new WindowListener() {
|
||||
@@ -246,7 +293,7 @@ public final class Util {
|
||||
return split;
|
||||
}
|
||||
|
||||
// Read a text file, accounting for BOMs and guaranteeing Unix line endings
|
||||
// Read a text file, accounting for BOMs
|
||||
public static String textRead(String filename) {
|
||||
|
||||
// Read the file
|
||||
@@ -277,8 +324,8 @@ public final class Util {
|
||||
data = temp;
|
||||
}
|
||||
|
||||
// Produce a string with LF-style (Unix) line endings
|
||||
return new String(data, set).replaceAll("\\r\\n?", "\n");
|
||||
// Produce a string
|
||||
return new String(data, set);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user