diff --git a/.gitattributes b/.gitattributes
index 8a740a1587a..5d8e059b5f0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -231,6 +231,8 @@ forge-core/src/main/java/forge/util/ITriggerEvent.java -text
forge-core/src/main/java/forge/util/ItemPool.java -text
forge-core/src/main/java/forge/util/ItemPoolSorter.java -text
forge-core/src/main/java/forge/util/Lang.java -text
+forge-core/src/main/java/forge/util/LocalizationChangeObserver.java -text
+forge-core/src/main/java/forge/util/Localizer.java -text
forge-core/src/main/java/forge/util/MyRandom.java svneol=native#text/plain
forge-core/src/main/java/forge/util/NameGenerator.java -text
forge-core/src/main/java/forge/util/PredicateString.java -text
@@ -15450,6 +15452,7 @@ forge-gui/res/editions/Worldwake.txt -text
forge-gui/res/editions/Zendikar.txt -text
forge-gui/res/effects/lightning.gif -text
forge-gui/res/howto.txt svneol=native#text/plain
+forge-gui/res/languages/en-US.properties -text
forge-gui/res/licenses/java-yield-license.txt svneol=native#text/plain
forge-gui/res/licenses/log4j-license.txt svneol=native#text/plain
forge-gui/res/licenses/multiline-label-license.txt svneol=native#text/plain
diff --git a/forge-core/pom.xml b/forge-core/pom.xml
index 25ff0b525b5..6e7e0044d52 100644
--- a/forge-core/pom.xml
+++ b/forge-core/pom.xml
@@ -23,5 +23,10 @@
commons-lang3
3.3
+
+ com.ibm.icu
+ icu4j
+ 53.1
+
diff --git a/forge-core/src/main/java/forge/CardStorageReader.java b/forge-core/src/main/java/forge/CardStorageReader.java
index 704e6b618a1..14099de189d 100644
--- a/forge-core/src/main/java/forge/CardStorageReader.java
+++ b/forge-core/src/main/java/forge/CardStorageReader.java
@@ -19,6 +19,7 @@ package forge;
import forge.card.CardRules;
import forge.util.FileUtil;
+import forge.util.Localizer;
import forge.util.ThreadUtil;
import org.apache.commons.lang3.time.StopWatch;
@@ -71,21 +72,6 @@ public class CardStorageReader {
private final Observer observer;
-
- // 8/18/11 10:56 PM
-
-
- /**
- *
- * Constructor for CardReader.
- *
- *
- * @param theCardsFolder
- * indicates location of the cardsFolder
- * @param useZip
- * if true, attempts to load cards from a zip file, if one
- * exists.
- */
public CardStorageReader(String cardDataDir, CardStorageReader.ProgressObserver progressObserver, Observer observer) {
this.progressObserver = progressObserver != null ? progressObserver : CardStorageReader.ProgressObserver.emptyObserver;
this.cardsfolder = new File(cardDataDir);
@@ -114,10 +100,10 @@ public class CardStorageReader {
} // CardReader()
- private final List loadCardsInRange(final List files, int from, int to) {
+ private List loadCardsInRange(final List files, int from, int to) {
CardRules.Reader rulesReader = new CardRules.Reader();
- List result = new ArrayList();
+ List result = new ArrayList<>();
for(int i = from; i < to; i++) {
File cardTxtFile = files.get(i);
result.add(this.loadCard(rulesReader, cardTxtFile));
@@ -125,10 +111,10 @@ public class CardStorageReader {
return result;
}
- private final List loadCardsInRangeFromZip(final List files, int from, int to) {
+ private List loadCardsInRangeFromZip(final List files, int from, int to) {
CardRules.Reader rulesReader = new CardRules.Reader();
- List result = new ArrayList();
+ List result = new ArrayList<>();
for(int i = from; i < to; i++) {
ZipEntry ze = files.get(i);
// if (ze.getName().endsWith(CardStorageReader.CARD_FILE_DOT_EXTENSION)) // already filtered!
@@ -146,13 +132,16 @@ public class CardStorageReader {
* @return the Card or null if it was not found.
*/
public final Iterable loadCards() {
- progressObserver.setOperationName("Loading cards, examining folder", true);
+
+ Localizer localizer = Localizer.getInstance();
+
+ progressObserver.setOperationName(localizer.getMessage("splash.loading.examining-cards"), true);
// Iterate through txt files or zip archive.
// Report relevant numbers to progress monitor model.
- Set result = new TreeSet(new Comparator() {
+ Set result = new TreeSet<>(new Comparator() {
@Override
public int compare(CardRules o1, CardRules o2) {
return String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName());
@@ -166,7 +155,7 @@ public class CardStorageReader {
fileParts = allFiles.size() / 100; // to avoid creation of many threads for a dozen of files
final CountDownLatch cdlFiles = new CountDownLatch(fileParts);
List>> taskFiles = makeTaskListForFiles(allFiles, cdlFiles);
- progressObserver.setOperationName("Loading cards from folders", true);
+ progressObserver.setOperationName(localizer.getMessage("splash.loading.cards-folders"), true);
progressObserver.report(0, taskFiles.size());
StopWatch sw = new StopWatch();
sw.start();
@@ -178,10 +167,10 @@ public class CardStorageReader {
if( this.zip != null ) {
final CountDownLatch cdlZip = new CountDownLatch(NUMBER_OF_PARTS);
- List>> taskZip = new ArrayList<>();
+ List>> taskZip;
ZipEntry entry;
- List entries = new ArrayList();
+ List entries = new ArrayList<>();
// zipEnum was initialized in the constructor.
Enumeration extends ZipEntry> zipEnum = this.zip.entries();
while (zipEnum.hasMoreElements()) {
@@ -192,7 +181,7 @@ public class CardStorageReader {
}
taskZip = makeTaskListForZip(entries, cdlZip);
- progressObserver.setOperationName("Loading cards from archive", true);
+ progressObserver.setOperationName(localizer.getMessage("splash.loading.cards-archive"), true);
progressObserver.report(0, taskZip.size());
StopWatch sw = new StopWatch();
sw.start();
@@ -220,9 +209,7 @@ public class CardStorageReader {
result.addAll(c.call());
}
}
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
+ } catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} catch (Exception e) { // this clause comes from non-threaded branch
throw new RuntimeException(e);
@@ -233,7 +220,7 @@ public class CardStorageReader {
int totalFiles = entries.size();
final int maxParts = (int) cdl.getCount();
int filesPerPart = totalFiles / maxParts;
- final List>> tasks = new ArrayList>>();
+ final List>> tasks = new ArrayList<>();
for (int iPart = 0; iPart < maxParts; iPart++) {
final int from = iPart * filesPerPart;
final int till = iPart == maxParts - 1 ? totalFiles : from + filesPerPart;
@@ -254,7 +241,7 @@ public class CardStorageReader {
int totalFiles = allFiles.size();
final int maxParts = (int) cdl.getCount();
int filesPerPart = totalFiles / maxParts;
- final List>> tasks = new ArrayList>>();
+ final List>> tasks = new ArrayList<>();
for (int iPart = 0; iPart < maxParts; iPart++) {
final int from = iPart * filesPerPart;
final int till = iPart == maxParts - 1 ? totalFiles : from + filesPerPart;
@@ -297,10 +284,7 @@ public class CardStorageReader {
/**
* Load a card from a txt file.
- *
- * @param pathToTxtFile
- * the full or relative path to the file to load
- *
+ *
* @return a new Card instance
*/
protected final CardRules loadCard(final CardRules.Reader reader, final File file) {
@@ -317,7 +301,8 @@ public class CardStorageReader {
throw new RuntimeException("CardReader : run error -- file not found: " + file.getPath(), ex);
} finally {
try {
- fileInputStream.close();
+ assert fileInputStream != null;
+ fileInputStream.close();
} catch (final IOException ignored) {
// 11:08
// PM
@@ -338,9 +323,8 @@ public class CardStorageReader {
try {
zipInputStream = this.zip.getInputStream(entry);
rulesReader.reset();
- CardRules rules = rulesReader.readCard(readScript(zipInputStream));
- return rules;
+ return rulesReader.readCard(readScript(zipInputStream));
} catch (final IOException exn) {
throw new RuntimeException(exn);
// PM
diff --git a/forge-core/src/main/java/forge/util/LocalizationChangeObserver.java b/forge-core/src/main/java/forge/util/LocalizationChangeObserver.java
new file mode 100644
index 00000000000..64cac513344
--- /dev/null
+++ b/forge-core/src/main/java/forge/util/LocalizationChangeObserver.java
@@ -0,0 +1,5 @@
+package forge.util;
+
+public interface LocalizationChangeObserver {
+ public void localizationChanged();
+}
diff --git a/forge-core/src/main/java/forge/util/Localizer.java b/forge-core/src/main/java/forge/util/Localizer.java
new file mode 100644
index 00000000000..2c804822635
--- /dev/null
+++ b/forge-core/src/main/java/forge/util/Localizer.java
@@ -0,0 +1,122 @@
+package forge.util;
+
+import com.ibm.icu.text.MessageFormat;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.*;
+
+public class Localizer {
+
+ private static Localizer instance;
+
+ private List observers = new ArrayList<>();
+
+ private Locale locale;
+ private ResourceBundle resourceBundle;
+
+ public static Localizer getInstance() {
+ if (instance == null) {
+ synchronized (Localizer.class) {
+ instance = new Localizer();
+ }
+ }
+ return instance;
+ }
+
+ private Localizer() {
+ }
+
+ public void initialize(String localeID, String languagesDirectory) {
+ setLanguage(localeID, languagesDirectory);
+ }
+
+ public String getMessage(final String key, final Object... messageArguments) {
+
+ MessageFormat formatter = null;
+
+ try {
+ formatter = new MessageFormat(resourceBundle.getString(key.toLowerCase()), locale);
+ } catch (final IllegalArgumentException | MissingResourceException e) {
+ e.printStackTrace();
+ }
+
+ if (formatter == null) {
+ return "INVALID PROPERTY: " + key;
+ }
+
+ formatter.setLocale(locale);
+
+ String formattedMessage = "CHAR ENCODING ERROR";
+ try {
+ //Support non-English-standard characters
+ formattedMessage = new String(formatter.format(messageArguments).getBytes("ISO-8859-1"), "UTF-8");
+ } catch (final UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ return formattedMessage;
+
+ }
+
+ public void setLanguage(final String languageRegionID, final String languagesDirectory) {
+
+ String[] splitLocale = languageRegionID.split("-");
+
+ Locale oldLocale = locale;
+ locale = new Locale(splitLocale[0], splitLocale[1]);
+
+ //Don't reload the language if nothing changed
+ if (oldLocale == null || !oldLocale.equals(locale)) {
+
+ File file = new File(languagesDirectory);
+ URL[] urls = null;
+
+ try {
+ urls = new URL[] { file.toURI().toURL() };
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+
+ ClassLoader loader = new URLClassLoader(urls);
+
+ try {
+ resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
+ } catch (NullPointerException | MissingResourceException e) {
+ //If the language can't be loaded, default to US English
+ resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
+ e.printStackTrace();
+ }
+
+ System.out.println("Language '" + resourceBundle.getBaseBundleName() + "' loaded successfully.");
+
+ notifyObservers();
+
+ }
+
+ }
+
+ public List getLanguages() {
+ //TODO List all languages by getting their files
+ return null;
+ }
+
+ public void registerObserver(LocalizationChangeObserver observer) {
+ observers.add(observer);
+ }
+
+ private void notifyObservers() {
+ for (LocalizationChangeObserver observer : observers) {
+ observer.localizationChanged();
+ }
+ }
+
+ public static class Langauge {
+ public String languageName;
+ public String langaugeID;
+ }
+
+}
diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties
new file mode 100644
index 00000000000..e5da054e69c
--- /dev/null
+++ b/forge-gui/res/languages/en-US.properties
@@ -0,0 +1,6 @@
+language.name = English (US)
+
+splash.loading.examining-cards = Loading cards, examining folder
+splash.loading.cards-folders = Loading cards from folders
+splash.loading.cards-archive = Loading cards from archive
+splash.loading.decks = Loading Decks...
\ No newline at end of file
diff --git a/forge-gui/src/main/java/forge/model/FModel.java b/forge-gui/src/main/java/forge/model/FModel.java
index 465cd1051d8..2414a37ffd6 100644
--- a/forge-gui/src/main/java/forge/model/FModel.java
+++ b/forge-gui/src/main/java/forge/model/FModel.java
@@ -38,6 +38,7 @@ import forge.quest.QuestController;
import forge.quest.QuestWorld;
import forge.quest.data.QuestPreferences;
import forge.util.FileUtil;
+import forge.util.Localizer;
import forge.util.storage.IStorage;
import forge.util.storage.StorageBase;
@@ -74,6 +75,18 @@ public class FModel {
private static GameFormat.Collection formats;
public static void initialize(final IProgressBar progressBar) {
+
+ // Instantiate preferences: quest and regular
+ //Preferences are initialized first so that the splash screen can be translated.
+ try {
+ preferences = new ForgePreferences();
+ }
+ catch (final Exception exn) {
+ throw new RuntimeException(exn);
+ }
+
+ Localizer.getInstance().initialize(FModel.getPreferences().getPref(FPref.UI_LANGUAGE), ForgeConstants.LANG_DIR);
+
//load card database
final ProgressObserver progressBarBridge = (progressBar == null) ?
ProgressObserver.emptyObserver : new ProgressObserver() {
@@ -115,14 +128,6 @@ public class FModel {
}
}
- // Instantiate preferences: quest and regular
- try {
- preferences = new ForgePreferences();
- }
- catch (final Exception exn) {
- throw new RuntimeException(exn);
- }
-
ForgePreferences.DEV_MODE = preferences.getPrefBoolean(FPref.DEV_MODE_ENABLED);
ForgePreferences.UPLOAD_DRAFT = ForgePreferences.NET_CONN; // && preferences.getPrefBoolean(FPref.UI_UPLOAD_DRAFT);
@@ -139,7 +144,7 @@ public class FModel {
FThreads.invokeInEdtLater(new Runnable() {
@Override
public void run() {
- progressBar.setDescription("Loading decks...");
+ progressBar.setDescription(Localizer.getInstance().getMessage("splash.loading.decks"));
}
});
}
diff --git a/forge-gui/src/main/java/forge/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/properties/ForgeConstants.java
index bab74503353..6c28213f8c3 100644
--- a/forge-gui/src/main/java/forge/properties/ForgeConstants.java
+++ b/forge-gui/src/main/java/forge/properties/ForgeConstants.java
@@ -17,11 +17,11 @@
*/
package forge.properties;
+import forge.GuiBase;
+
import java.util.Collections;
import java.util.Map;
-import forge.GuiBase;
-
public final class ForgeConstants {
public static final String ASSETS_DIR = GuiBase.getInterface().getAssetsDir();
public static final String PROFILE_FILE = ASSETS_DIR + "forge.profile.properties";
@@ -56,6 +56,7 @@ public final class ForgeConstants {
public static final String AI_PROFILE_DIR = RES_DIR + "ai/";
public static final String SOUND_DIR = RES_DIR + "sound/";
public static final String MUSIC_DIR = RES_DIR + "music/";
+ public static final String LANG_DIR = RES_DIR + "languages/";
public static final String EFFECTS_DIR = RES_DIR + "effects/";
private static final String QUEST_DIR = RES_DIR + "quest/";
diff --git a/forge-gui/src/main/java/forge/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/properties/ForgePreferences.java
index af8e7b02748..810947bc600 100644
--- a/forge-gui/src/main/java/forge/properties/ForgePreferences.java
+++ b/forge-gui/src/main/java/forge/properties/ForgePreferences.java
@@ -79,6 +79,8 @@ public class ForgePreferences extends PreferencesStore {
UI_VIBRATE_ON_LIFE_LOSS("true"),
UI_VIBRATE_ON_LONG_PRESS("true"),
+
+ UI_LANGUAGE("en-US"),
MATCHPREF_PROMPT_FREE_BLOCKS("false"),