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 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"),