Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
351caed00This file is pinned to the selected release. Download the original to open it in your design tool.
package com.marginallyclever.makelangelo;
import com.marginallyclever.convenience.FileAccess;
import com.marginallyclever.util.MarginallyCleverTranslationXmlFileHelper;
import com.marginallyclever.util.PreferencesHelper;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.file.*;
import java.util.*;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Stream;
/**
* <p></p>MultilingualSupport is the translation engine. You ask for a string it finds the matching string in the currently selected language.
* See <a href="http://www.java-samples.com/showtutorial.php?tutorialid=152">XML and Java - Parsing XML using Java Tutorial</a></p>
* <p>TODO: This system loads all language files even though it only displays one at a time. It could load a single language and save memory.</p>
* @author Dan Royer
* @author Peter Colapietro
*/
public final class Translator {
private static final Logger logger = LoggerFactory.getLogger(Translator.class);
public static final String MISSING = "Missing:";
// Working directory. This represents the directory where the java executable launched the jar from.
public static final String WORKING_DIRECTORY = /*File.separator + */"languages"/*+File.separator*/;
// The name of the preferences node containing the user's choice.
private static final String LANGUAGE_KEY = "language";
// TODO get a better way to store user preference.
private static final Preferences languagePreferenceNode = PreferencesHelper.getPreferenceNode(PreferencesHelper.MakelangeloPreferenceKey.LANGUAGE);
// The default choice when nothing has been selected.
private static String defaultLanguage = "English";
// The current choice
private static String currentLanguage;
// a list of all languages and their translations strings
private static final Map<String, TranslatorLanguage> languages = new HashMap<>();
private static TranslatorLanguage currentLanguageContainer;
public static void start() {
logger.info("starting translator...");
Locale locale = Locale.getDefault();
defaultLanguage = locale.getDisplayLanguage(Locale.ENGLISH);
logger.info("Default language = {}", defaultLanguage);
loadLanguages();
loadConfig();
}
/**
* @return true if this is the first time loading language files (probably on install)
*/
public static boolean isThisTheFirstTimeLoadingLanguageFiles() {
// Did the language file disappear? Offer the language dialog.
try {
if (doesLanguagePreferenceExist()) {
// does the list of languages contain the preferred choice?
String languageNameFromPref = languagePreferenceNode.get(LANGUAGE_KEY, defaultLanguage);
if (!languages.containsKey(languageNameFromPref)) {
logger.error("Language '{}' not available ...", languageNameFromPref);
// To avoid some null issues in Translator.get(String key),
// lets say it's the first run (to ask the user to select a valid language name)
return true;
}
return false;
}
} catch (BackingStoreException e) {
logger.error("Failed to load language", e);
}
return true;
}
/**
* @return true if a preferences node exists
* @throws BackingStoreException
*/
static private boolean doesLanguagePreferenceExist() throws BackingStoreException {
return Arrays.asList(languagePreferenceNode.keys()).contains(LANGUAGE_KEY);
}
/**
* save the user's current language choice
*/
public static void saveConfig() {
logger.debug("saveConfig()");
languagePreferenceNode.put(LANGUAGE_KEY, currentLanguage);
}
/**
* load the user's language choice
*/
public static void loadConfig() {
logger.debug("loadConfig: {}={}", languagePreferenceNode.toString(), defaultLanguage);
setCurrentLanguage(languagePreferenceNode.get(LANGUAGE_KEY, defaultLanguage));
}
/**
* Scan folder for language files.
* See http://stackoverflow.com/questions/1429172/how-do-i-list-the-files-inside-a-jar-file
* @throws IllegalStateException No language files found
*/
public static void loadLanguages() {
languages.clear();
try {
int found=0;
Stream<Path> walk = getLanguagePaths();
Iterator<Path> it = walk.iterator();
while (it.hasNext()) {
Path p = it.next();
String name = p.toString();
//if( f.isDirectory() || f.isHidden() ) continue;
if (FilenameUtils.getExtension(name).equalsIgnoreCase("xml") ) {
if (name.endsWith("pom.xml")) {
continue;
}
// found an XML file in the /languages folder. Good sign!
if (attemptToLoadLanguageXML(name)) found++;
}
}
walk.close();
//logger.debug("total found: "+found);
if(found==0) {
throw new IllegalStateException("No translations found.");
}
}
catch(Exception e) {
logger.error("{}. Defaulting to {}. Language folder expected to be located at {}", e.getMessage(), defaultLanguage, WORKING_DIRECTORY);
final TranslatorLanguage languageContainer = new TranslatorLanguage();
String path = MarginallyCleverTranslationXmlFileHelper.getDefaultLanguageFilePath();
logger.debug("default path requested: {}", path);
URL pathFound = Translator.class.getClassLoader().getResource(path);
logger.debug("path found: {}", pathFound);
try (InputStream s = pathFound.openStream()) {
languageContainer.loadFromInputStream(s);
} catch (IOException ie) {
logger.error(ie.getMessage());
}
languages.put(languageContainer.getName(), languageContainer);
}
}
private static Stream<Path> getLanguagePaths() throws Exception {
URI uri = Translator.class.getClassLoader().getResource(WORKING_DIRECTORY).toURI();
logger.trace("Looking for translations in {}", uri.toString());
Path myPath;
if (uri.getScheme().equals("jar")) {
FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
myPath = fileSystem.getPath(WORKING_DIRECTORY);
} else {
myPath = Paths.get(uri);
}
Path rootPath = FileSystems.getDefault().getPath(FileAccess.getWorkingDirectory());
logger.trace("rootDir={}", rootPath);
// we'll look inside the JAR file first, then look in the working directory.
// this way new translation files in the working directory will replace the old JAR files.
Stream<Path> walk = Stream.concat(
Files.walk(myPath, 1), // check inside the JAR file.
Files.walk(rootPath,1) // then check the working directory
);
return walk;
}
private static boolean attemptToLoadLanguageXML(String name) throws Exception {
InputStream stream;
String actualFilename;
File externalFile = new File(name);
if(externalFile.exists()) {
stream = new FileInputStream(name);
actualFilename = name;
} else {
String nameInsideJar = WORKING_DIRECTORY+"/"+FilenameUtils.getName(name);
stream = Translator.class.getClassLoader().getResourceAsStream(nameInsideJar);
actualFilename = "Jar:"+nameInsideJar;
}
if( stream == null ) return false;
logger.trace("Found {}", actualFilename);
TranslatorLanguage lang = new TranslatorLanguage();
try {
lang.loadFromInputStream(stream);
} catch(Exception e) {
logger.error("Failed to load {}", actualFilename, e);
// if the xml file is invalid then an exception can occur.
// make sure lang is empty in case of a partial-load failure.
lang = new TranslatorLanguage();
}
stream.close();
if( !lang.getName().isEmpty() &&
!lang.getAuthor().isEmpty()) {
// we loaded a language file that seems pretty legit.
languages.put(lang.getName(), lang);
return true;
}
return false;
}
/**
* @param key name of key to find in translation list. <b>Keys must be Strings, not variables</b>.
* If you use a variable then the tests for missing and duplicate translations will not
* work in the distant future.
* @return the translated value for key, or "missing:key".
*/
public static String get(String key) {
String translation = currentLanguageContainer.get(key);
if (translation == null) {
logger.warn("Missing translation '{}' in language '{}'", key, currentLanguage);
return MISSING +key;
}
return translation;
}
/**
* Translates a string and fills in some details. String contains the special character sequence "%N", where N is the n-th parameter passed to get()
* A %1 is replaced with the first parameter, %2 with the second, and so on. There is no escape character.
* @param key name of key to find in translation list
* @param params
* @return the translated value for key, or "missing:key".
*/
public static String get(String key,String ... params) {
String modified = get(key);
int n=1;
for(String p : params) {
modified = modified.replaceAll("%"+n, p);
++n;
}
return modified;
}
/**
* @return the list of language names
*/
public static String[] getLanguageList() {
final String[] choices = new String[languages.keySet().size()];
final Object[] lang_keys = languages.keySet().toArray();
for (int i = 0; i < lang_keys.length; ++i) {
choices[i] = (String) lang_keys[i];
}
return choices;
}
/**
* @param language the name of the language to make active.
*/
public static void setCurrentLanguage(String language) {
currentLanguage = language;
currentLanguageContainer = languages.get(language);
}
/**
* @return the index of the current language
*/
public static int getCurrentLanguageIndex() {
String [] set = getLanguageList();
// find the current language
for( int i=0;i<set.length; ++i) {
if( set[i].equals(Translator.currentLanguage)) return i;
}
// now try the default
for( int i=0;i<set.length; ++i) {
if( set[i].equals(Translator.defaultLanguage)) return i;
}
// failed both, return 0 for the first option.
return 0;
}
}