/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.language;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.KeyStroke;
import org.apache.log4j.Logger;
import com.t3.client.AppActions;
import com.t3.client.TabletopTool;
/**
* This class is the front-end for all string handling. The goal is for all text
* to be external from the application source so that translations can be done
* without editing source code. To that end, this class will locate the
* <b>i18n.properties</b> file for the current locale and read the string values
* from it, returning the results.
* <p>
* As TabletopTool uses a base name for the string and extensions for alternate
* pieces (such as <code>action.loadMap</code> as the base and
* <code>action.loadMap.accel</code> as the menu accelerator key) there are
* different methods used to return the different components.
* <p>
* The ResourceBundle name is <b>com.t3.language.i18n</b>.
*
* @author tcroft
*/
public class I18N {
//for eclipse to find this class as a i18n accessor
private static final String BUNDLE_NAME = "com.t3.language.i18n"; //$NON-NLS-1$
private static final ResourceBundle BUNDLE;
private static final Logger log = Logger.getLogger(I18N.class);
private static final String ACCELERATOR_EXTENSION = ".accel";
private static final String DESCRIPTION_EXTENSION = ".description";
static {
// Put here to make breakpointing easier. :)
BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);
}
/**
* Returns a <b>JMenu</b> object to represent the given <b>Action</b> key.
* The key is used to locate the menu text in the properties file. The menu
* mnemonic is extracted as well and assigned to the JMenu object that is
* then returned.
*
* @param key
* the component to search for
* @return the JMenu created from the key's property values
*/
public static JMenu createMenu(String key) {
JMenu menu = new JMenu(getText(key));
int mnemonic = getMnemonic(key);
if (mnemonic != -1 && !TabletopTool.MAC_OS_X) {
menu.setMnemonic(mnemonic);
}
// Should we set the SHORT_DESCRIPTION and use it as a tooltip?
return menu;
}
/**
* Returns the text associated with the given key that is to be used as the
* menu accelerator for the <b>Action</b>. This method uses the same key as
* used by the prior methods, but appends the string
* <code>ACCELERATOR_EXTENSION</code> to the end. This allows a single key
* to be composed of multiple (optional) parts.
* <p>
* Note that the modifier key used by the platform to initiate the sequence
* is not included in the properties file. This is because the key changes
* from platform to platform. For example, on Windows the key is the Control
* key, while on Mac OSX the key is the Command key. The AWT Toolkit
* represents these differently ("ctrl" on Windows and "meta" on OSX) so our
* only choice is to eliminate the modifiers from the properties file and
* add them back in programmatically. The application does this by
* retrieving the platform's <code>menuShortcut</code> and saving it in a
* <code>final</code> field. The value of that field is used as the modifier
* throughout the application. The properties file may still specify other
* modifiers, such as "Shift" or "Alt".
* <p>
* This seems a little bogus, though. Shouldn't <b>all</b> keystrokes be
* definable from outside the application? So then shouldn't there be some
* text that can appear in the properties file that means
* "use menuShortcutKey"? But what text should that be? And can it be
* automatically parsed by the library code so that it becomes Ctrl or Cmd
* as appropriate?
*
* @param key
* the component to search for
* @return the String value of the given key's accelerator
*/
public static String getAccelerator(String key) {
return getString(key + ACCELERATOR_EXTENSION);
}
/**
* Returns the description text for the given key. This text normally
* appears in the statusbar of the main application frame. As described by
* the {@link #getAccelerator(String)} method, the input key has the string
* DESCRIPTION_EXTENSION appended to it.
*
* @param key
* @return
*/
public static String getDescription(String key) {
return getString(key + DESCRIPTION_EXTENSION);
}
/**
* Returns the character to use as the menu mnemonic for the given key. This
* method searches the properties file for the given key. If the value
* contains an ampersand ("&") the character following the ampersand is
* converted to uppercase and returned.
*
* @param key
* the component to search for
* @return the character to use as the mnemonic (as an <code>int</code>)
*/
public static int getMnemonic(String key) {
String value = getString(key);
if (value == null || value.length() < 2)
return -1;
int index = value.indexOf('&');
if (index != -1 && index + 1 < value.length()) {
return Character.toUpperCase(value.charAt(index + 1));
}
return -1;
}
/**
* Returns the String that results from a lookup within the properties file.
*
* @param key
* the component to search for
* @return the String found or <code>null</code>
*/
public static String getString(String key) {
try {
return BUNDLE.getString(key);
} catch (MissingResourceException e) {
return null;
}
}
/**
* Returns the text associated with the given key after removing any menu
* mnemonic. So for the key <b>action.loadMap</b> that has the value
* "&Load Map" in the properties file, this method returns "Load Map".
*
* @param key
* the component to search for
* @return the String found with mnemonics removed, or the input key if not
* found
*/
public static String getText(String key) {
String value = getString(key);
if (value == null) {
log.debug("Cannot find key '" + key + "' in properties file.");
return key;
}
return value.replaceAll("\\&", "");
}
/**
* Functionally identical to {@link #getText(String key)} except that this
* one bundles the formatting calls into this code. This version of the
* method is truly only needed when the string being retrieved contains
* parameters. In TabletopTool, this commonly means the player's name or a
* filename. See the "Parameterized Strings" section of the
* <b>i18n.properties</b> file for example usage. Full documentation for
* this technique can be found under {@link MessageFormat#format}.
*
* @param key
* the <code>propertyKey</code> to use for lookup in the
* properties file
* @param args
* parameters needed for formatting purposes
* @return the formatted String
*/
public static String getText(String key, Object... args) {
// If the key doesn't exist in the file, the key becomes the format and
// nothing will be substituted; it's a little extra work, but is not the normal case
// anyway.
String msg = MessageFormat.format(getText(key), args);
return msg;
}
/**
* <p>
* Set all of the I18N values on an Action by retrieving said values from
* the properties file.
* </p>
* <p>
* This is a compatibility function that calls
* {@link #setAction(String, Action, boolean)}.
* </p>
*
* @param key
* Key used to look up values
* @param action
* Action being modified
*/
public static void setAction(String key, Action action) {
setAction(key, action, true);
}
/**
* <p>
* Set all of the I18N values on an <code>Action</code> by retrieving said
* values from the properties file.
* </p>
* <p>
* Uses the <code>key</code> as the index for the properties file to set the
* <b>Action.NAME</b> field of <b>action</b>.
* </p>
* <p>
* The string used for the <b>NAME</b> is searched for an ampersand
* ("&") to determine the mnemonic used by any menu item (no mnemonic is
* set if there is no ampersand). If there is one, the
* <b>Action.MNEMONIC_KEY</b> property is set.
* </p>
* <p>
* The <code>key</code> string has "<code>.accel</code>" appended to it and
* the properties file is searched again, this time to obtain a string
* representing the shortcut key. If there is one, the
* <b>Action.ACCELERATOR_KEY</b> property is set.
* </p>
* <p>
* The <code>key</code> string has "<code>.description</code>" appended to
* it and the properties file is searched again, this time to obtain a
* string representing the status bar help message. If there is one, the
* <b>Action.SHORT_DESCRIPTION</b> property is set.
* </p>
* <p>
* If <b>addMenuShortcut</b> is <code>true</code> then the proper shortcut
* key for the platform is added to the modifiers for the keystroke (
* {@link AppActions#menuShortcut} and any menu items that do not require
* modifiers, such as {@link AppActions#ZOOM_IN}).
* </p>
*
* @param key
* String to use as an index into the <b>i18n.properties</b> file
* @param action
* Action used to store the retrieved settings
* @param addMenuShortcut
* whether to add the platform's menu shortcut key mask (usually
* <code>true</code>)
*/
public static void setAction(String key, Action action, boolean addMenuShortcut) {
action.putValue(Action.NAME, getText(key));
int mnemonic = getMnemonic(key);
if (mnemonic != -1)
action.putValue(Action.MNEMONIC_KEY, mnemonic);
String accel = getAccelerator(key);
if (accel != null) {
KeyStroke k = KeyStroke.getKeyStroke(accel);
if (k == null) {
log.error("Bad accelerator '" + accel + "' for " + key);
} else if (addMenuShortcut) {
int modifiers = k.getModifiers() | AppActions.menuShortcut;
if (k.getKeyCode() != 0)
k = KeyStroke.getKeyStroke(k.getKeyCode(), modifiers);
else
k = KeyStroke.getKeyStroke(k.getKeyChar(), modifiers);
}
action.putValue(Action.ACCELERATOR_KEY, k);
// System.err.println("I18N.setAction(\"" + key + "\") = " + k);
}
String description = getDescription(key);
if (description != null)
action.putValue(Action.SHORT_DESCRIPTION, description);
}
/**
* Returns all matching keys when given a string regular expression.
*/
public static List<String> getMatchingKeys(String regex) {
return getMatchingKeys(Pattern.compile(regex));
}
/**
* Returns all matching keys when given a compiled regular expression
* pattern.
*/
public static List<String> getMatchingKeys(Pattern regex) {
Enumeration<String> keys = BUNDLE.getKeys();
List<String> menuItemKeys = new LinkedList<String>();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
if (regex.matcher(key).find()) {
menuItemKeys.add(key);
}
}
return menuItemKeys;
}
}