package org.deeplearning4j.ui.i18n;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.deeplearning4j.ui.api.I18N;
import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.util.ConfigurationBuilder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;
/**
* Default internationalization implementation.<br>
* Content for internationalization is implemented using resource files.<br>
* For the resource files: they should be specified as follows:<br>
* 1. In the /dl4j_i18n/ directory in resources<br>
* 2. Filenames should be "somekey.langcode" - for example, "index.en" or "index.ja"<br>
* 3. Each key should be unique across all files. Any key can appear in any file; files may be split for convenience<br>
* <p>
* Loading of these UI resources is done as follows:<br>
* - On initialization of the DefaultI18N:<br>
* - Resource files for the default language are loaded<br>
* - If a different language is requested, the content will be loaded on demand (and stored in memory for future use)<br>
* Note that if a specified language does not have the specified key, the result from the defaultfallback language (English)
* will be used instead.
*
* @author Alex Black
*/
@Slf4j
public class DefaultI18N implements I18N {
public static final String DEFAULT_LANGUAGE = "en";
public static final String FALLBACK_LANGUAGE = "en"; //use this if the specified language doesn't have the requested message
public static final String DEFAULT_I8N_RESOURCES_DIR = "dl4j_i18n";
private static DefaultI18N instance;
private Map<String, Map<String, String>> messagesByLanguage = new HashMap<>();
public static synchronized I18N getInstance() {
if (instance == null)
instance = new DefaultI18N();
return instance;
}
private String currentLanguage = DEFAULT_LANGUAGE;
private Set<String> loadedLanguages = Collections.synchronizedSet(new HashSet<>());
private DefaultI18N() {
//Load default language...
loadLanguageResources(currentLanguage);
}
private synchronized void loadLanguageResources(String languageCode) {
if (loadedLanguages.contains(languageCode))
return;
//Scan classpath for resources in the /dl4j_i18n/ directory...
URL url = this.getClass().getResource("/" + DEFAULT_I8N_RESOURCES_DIR + "/");
Reflections reflections =
new Reflections(new ConfigurationBuilder().setScanners(new ResourcesScanner()).setUrls(url));
String pattern = ".*" + languageCode;
Set<String> resources = reflections.getResources(Pattern.compile(pattern));
Map<String, String> messages = new HashMap<>();
for (String s : resources) {
if (!s.endsWith(languageCode))
continue;
log.trace("Attempting to parse file: {}", s);
parseFile(s, messages);
}
messagesByLanguage.put(languageCode, messages);
loadedLanguages.add(languageCode);
}
private void parseFile(String filename, Map<String, String> results) {
List<String> lines;
try {
String path;
if (filename.startsWith(DEFAULT_I8N_RESOURCES_DIR)) {
//As a resource from JAR file - already has dir at the start...
path = "/" + filename;
} else {
//Run in dev environment - no dir at the start...
path = "/" + DEFAULT_I8N_RESOURCES_DIR + "/" + filename;
}
InputStream is = this.getClass().getResourceAsStream(path);
lines = IOUtils.readLines(is);
} catch (Exception e) {
log.debug("Error parsing UI I18N content file; skipping: {}", filename, e.getMessage());
return;
}
//TODO need to think more carefully about how to parse this, with multi-line messages, etc
int count = 0;
for (String line : lines) {
if (!line.matches(".+=.*")) {
log.debug("Invalid line in I18N file: {}, \"{}\"", filename, line);
continue;
}
int idx = line.indexOf('=');
String key = line.substring(0, idx);
String value = line.substring(Math.min(idx + 1, line.length()));
results.put(key, value);
count++;
}
log.trace("Loaded {} messages from file {}", count, filename);
}
@Override
public String getMessage(String key) {
return getMessage(currentLanguage, key);
}
@Override
public String getMessage(String langCode, String key) {
Map<String, String> messagesForLanguage = messagesByLanguage.get(langCode);
if (messagesForLanguage == null) {
synchronized (this) {
//Synchronized to avoid loading multiple times in case of multi-threaded requests
if (messagesByLanguage.get(langCode) == null) {
loadLanguageResources(langCode);
}
}
messagesForLanguage = messagesByLanguage.get(langCode);
}
String msg = messagesForLanguage.get(key);
if (msg == null && !FALLBACK_LANGUAGE.equals(langCode)) {
//Try getting the result from the fallback language
return getMessage(FALLBACK_LANGUAGE, key);
}
return msg;
}
@Override
public String getDefaultLanguage() {
return currentLanguage;
}
@Override
public void setDefaultLanguage(String langCode) {
this.currentLanguage = langCode;
log.debug("UI: Set language to {}", langCode);
}
}