diff --git a/src/globalization/en-GB.xml b/globalization/en-GB.xml similarity index 100% rename from src/globalization/en-GB.xml rename to globalization/en-GB.xml diff --git a/src/Start.java b/src/Start.java index 585f65c6..5afe3589 100644 --- a/src/Start.java +++ b/src/Start.java @@ -18,8 +18,8 @@ public class Start { apiService.start(); //// testing the API client - //ApiClient client = ApiClient.getInstance(); - //String test = client.executeCommand("GET blocks/height"); - //System.out.println(test); + ApiClient client = ApiClient.getInstance(); + String test = client.executeCommand("GET blocks/height"); + System.out.println(test); } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index e4f7f09b..de4a30e9 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -27,7 +27,7 @@ public class BlocksResource { private ApiErrorFactory apiErrorFactory; public BlocksResource() { - this(new ApiErrorFactory(new Translator())); + this(new ApiErrorFactory(Translator.getInstance())); } public BlocksResource(ApiErrorFactory apiErrorFactory) { diff --git a/src/globalization/ContextPaths.java b/src/globalization/ContextPaths.java new file mode 100644 index 00000000..6f6a35a0 --- /dev/null +++ b/src/globalization/ContextPaths.java @@ -0,0 +1,23 @@ +package globalization; + +import java.nio.file.Paths; + +public class ContextPaths { + + public static boolean isValidKey(String value) { + return !value.contains("/"); + } + + public static String combinePaths(String left, String right) { + return Paths.get("/", left, right).normalize().toString(); + } + + public static String getParent(String path) { + return combinePaths(path, ".."); + } + + public static boolean isRoot(String path) { + return path.equals("/"); + } + +} diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index 50cc86e0..45a95a6b 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -134,7 +134,7 @@ public class TranslationXmlStreamReader { break; case CONTEXT_PATH_ATTRIBUTE_NAME: assureIsValidPathExtension(value); - contextPath = combinePaths(contextPath, value); + contextPath = ContextPaths.combinePaths(contextPath, value); break; default: throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name); @@ -180,7 +180,7 @@ public class TranslationXmlStreamReader { switch(name.toString()) { case TRANSLATION_KEY_ATTRIBUTE_NAME: assureIsValidKey(value); - path = combinePaths(state.path, value); + path = ContextPaths.combinePaths(state.path, value); break; case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME: template = value; @@ -219,14 +219,10 @@ public class TranslationXmlStreamReader { } private void assureIsValidKey(String value) throws XMLStreamException { - if(value.contains("/")) - throw new javax.xml.stream.XMLStreamException("Key must not contain /"); + if(!ContextPaths.isValidKey(value)) + throw new javax.xml.stream.XMLStreamException("Key is not valid"); } - private String combinePaths(String left, String right) { - return Paths.get(left, right).normalize().toString(); - } - private void assureStartElement(XMLEvent event, String name) throws XMLStreamException { if(!isStartElement(event, name)) throw new javax.xml.stream.XMLStreamException("Unexpected start element: " + event.toString() + ", <" + name + "> expected"); diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java index da61c595..fa9a17f2 100644 --- a/src/globalization/Translator.java +++ b/src/globalization/Translator.java @@ -1,13 +1,85 @@ package globalization; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.InputStream; import java.util.AbstractMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.stream.XMLStreamException; import org.apache.commons.text.StringSubstitutor; +import settings.Settings; + public class Translator { + Map> translations = new HashMap>(); + + //XXX: replace singleton pattern by dependency injection? + private static Translator instance; + + private Translator() { + InitializeTranslations(); + } + + public static Translator getInstance() { + if (instance == null) { + instance = new Translator(); + } + + return instance; + } + + private Settings settings() { + return Settings.getInstance(); + } + + private void InitializeTranslations() { + String path = this.settings().translationsPath(); + File dir = new File(path); + File [] files = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".xml"); + } + }); + + Map> translations = new HashMap<>(); + TranslationXmlStreamReader translationReader = new TranslationXmlStreamReader(); + for (File file : files) { + Iterable entries = null; + try { + InputStream stream = new FileInputStream(file); + entries = translationReader.ReadFrom(stream); + } catch (FileNotFoundException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Translation file not found: %s", file), ex); + } catch (XMLStreamException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Error in translation file: %s", file), ex); + } + + for(TranslationEntry entry : entries) { + Map localTranslations = translations.get(entry.locale()); + if(localTranslations == null) { + localTranslations = new HashMap<>(); + translations.put(entry.locale(), localTranslations); + } + + if(localTranslations.containsKey(entry.path())) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Duplicate entry for locale '%s' and path '%s' in translation file '%s'. Falling back to default translations.", entry.locale(), entry.path(), file)); + return; + } + } + } + + // everything is fine, so we store all read translations + this.translations = translations; + } + private Map createMap(Map.Entry[] entries) { HashMap map = new HashMap<>(); for (AbstractMap.Entry entry : entries) { @@ -16,37 +88,59 @@ public class Translator { return map; } - //XXX: replace singleton pattern by dependency injection? - private static Translator instance; + public String translate(Locale locale, String contextPath, String templateKey, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(locale, contextPath, templateKey, map); + } - public static Translator getInstance() { - if (instance == null) { - instance = new Translator(); + public String translate(Locale locale, String contextPath, String templateKey, Map templateValues) { + return translate(locale, contextPath, templateKey, null, templateValues); + } + + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(locale, contextPath, templateKey, defaultTemplate, map); + } + + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, Map templateValues) { + // look for requested language + String template = getTemplateFromNearestPath(locale, contextPath, templateKey); + + if(template == null) { + // scan default languages + for(String language : this.settings().translationsDefaultLocales()) { + Locale defaultLocale = Locale.forLanguageTag(language); + template = getTemplateFromNearestPath(defaultLocale, contextPath, templateKey); + if(template != null) + break; + } } - - return instance; - } - - public String translate(Locale locale, String templateKey, AbstractMap.Entry... templateValues) { - Map map = createMap(templateValues); - return translate(locale, templateKey, map); - } - - public String translate(Locale locale, String templateKey, Map templateValues) { - return translate(locale, templateKey, null, templateValues); - } - - public String translate(Locale locale, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { - Map map = createMap(templateValues); - return translate(locale, templateKey, defaultTemplate, map); - } - - public String translate(Locale locale, String templateKey, String defaultTemplate, Map templateValues) { - String template = defaultTemplate; // TODO: get template for the given locale if available + + if(template == null) + template = defaultTemplate; // fallback template StringSubstitutor sub = new StringSubstitutor(templateValues); String result = sub.replace(template); return result; } + + private String getTemplateFromNearestPath(Locale locale, String contextPath, String templateKey) { + Map localTranslations = this.translations.get(locale); + if(localTranslations == null) + return null; + + String template = null; + while(true) { + String path = ContextPaths.combinePaths(contextPath, templateKey); + template = localTranslations.get(path); + if(template != null) + break; // found template + if(ContextPaths.isRoot(contextPath)) + break; // nothing found + contextPath = ContextPaths.getParent(contextPath); + } + + return template; + } } diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 288020b6..46df2c29 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -24,11 +24,15 @@ public class Settings { private int maxBytePerFee = 1024; private String userpath = ""; - //RPC + // RPC private int rpcPort = 9085; private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6 private boolean rpcEnabled = true; + // Globalization + private String translationsPath = "globalization/"; + private String[] translationsDefaultLocales = {"en-GB"}; + // Constants private static final String SETTINGS_FILENAME = "settings.json"; @@ -129,6 +133,17 @@ public class Settings { { this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue(); } + + // Globalization + if(json.containsKey("translationspath")) + { + this.translationsPath = ((String) json.get("translationspath")); + } + + if(json.containsKey("translationsdefaultlocales")) + { + this.translationsDefaultLocales = ((String[]) json.get("translationsdefaultlocales")); + } } public boolean isTestNet() { @@ -163,4 +178,14 @@ public class Settings { { return this.rpcEnabled; } + + public String translationsPath() + { + return this.translationsPath; + } + + public String[] translationsDefaultLocales() + { + return this.translationsDefaultLocales; + } }