package com.psddev.cms.db;
import com.ibm.icu.text.MessageFormat;
import com.psddev.cms.tool.CmsTool;
import com.psddev.cms.tool.MachineTranslations;
import com.psddev.dari.db.DatabaseEnvironment;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.Query;
import com.psddev.dari.db.Recordable;
import com.psddev.dari.db.State;
import com.psddev.dari.util.CascadingMap;
import com.psddev.dari.util.ClassFinder;
import com.psddev.dari.util.CollectionUtils;
import com.psddev.dari.util.CompactMap;
import com.psddev.dari.util.ObjectUtils;
import com.psddev.dari.util.TypeDefinition;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class LocalizationContext {
private final String baseName;
private final State state;
private final Map<String, Object> overrides;
public LocalizationContext(Object context, Map<String, Object> overrides) {
String baseName = null;
if (context instanceof ObjectField) {
context = ((ObjectField) context).getParent();
}
State state = null;
if (context != null) {
if (context instanceof String) {
baseName = (String) context;
} else if (context instanceof ObjectType) {
baseName = ((ObjectType) context).getInternalName();
} else if (context instanceof DatabaseEnvironment) {
baseName = null;
} else if (context instanceof Class) {
ObjectType type = ObjectType.getInstance((Class<?>) context);
if (type != null) {
baseName = type.getInternalName();
} else {
baseName = ((Class<?>) context).getName();
}
} else if (context instanceof Recordable) {
state = ((Recordable) context).getState();
ObjectType type = state.getType();
if (type != null) {
baseName = type.getInternalName();
}
} else {
baseName = context.getClass().getName();
}
}
this.baseName = baseName;
this.state = state;
this.overrides = overrides;
}
public String text(Locale source, Locale target, String key) {
CascadingMap<String, Object> arguments = new CascadingMap<>();
List<Map<String, Object>> argumentsSources = arguments.getSources();
if (overrides != null) {
argumentsSources.add(overrides);
}
List<ClassLoader> customLoaders = ClassFinder
.findConcreteClasses(LocalizationClassLoaderProvider.class)
.stream()
.map(c -> TypeDefinition.getInstance(c).newInstance().getClassLoader())
.filter(Objects::nonNull)
.collect(Collectors.toList());
ClassLoader defaultLoader = getClass().getClassLoader();
String pattern = null;
if (baseName != null) {
for (ClassLoader customLoader : customLoaders) {
ResourceBundle customOverride = findBundle(baseName + "Override", source, customLoader);
if (customOverride != null) {
argumentsSources.add(createBundleMap(customOverride));
if (pattern == null) {
pattern = findBundleString(customOverride, key);
}
}
}
ResourceBundle baseOverride = findBundle(baseName + "Override", source, defaultLoader);
ResourceBundle baseDefault = findBundle(baseName + "Default", source, defaultLoader);
if (baseOverride != null) {
argumentsSources.add(createBundleMap(baseOverride));
if (pattern == null) {
pattern = findBundleString(baseOverride, key);
}
}
if (baseDefault != null) {
argumentsSources.add(createBundleMap(baseDefault));
if (pattern == null) {
pattern = findBundleString(baseDefault, key);
}
}
}
if (state != null) {
argumentsSources.add(state);
}
if (pattern == null) {
ResourceBundle fallbackOverride = findBundle("FallbackOverride", source, defaultLoader);
if (fallbackOverride != null) {
pattern = findBundleString(fallbackOverride, key);
}
if (pattern == null) {
ResourceBundle fallbackDefault = findBundle("FallbackDefault", source, defaultLoader);
if (fallbackDefault != null) {
pattern = findBundleString(fallbackDefault, key);
}
}
}
ObjectType type = ObjectType.getInstance(baseName);
ObjectTypeResourceBundle bundle = ObjectTypeResourceBundle.getInstance(type);
Map<String, Object> bundleMap = bundle.getMap();
argumentsSources.add(bundleMap);
if (pattern == null && Locale.getDefault().equals(source)) {
pattern = findBundleString(bundle, key);
}
if (pattern == null) {
return null;
} else if (source.equals(target)) {
return new MessageFormat(pattern, target).format(arguments);
} else {
String googleServerApiKey = Query
.from(CmsTool.class)
.first()
.getGoogleServerApiKey();
if (ObjectUtils.isBlank(googleServerApiKey)) {
return new MessageFormat(pattern, source).format(arguments);
}
// Already translated?
UUID translationsId = MachineTranslations.createId(baseName, target);
State translations = State.getInstance(Query
.from(MachineTranslations.class)
.where("_id = ?", translationsId)
.first());
if (translations != null) {
String translation = (String) translations.get(key);
if (translation != null) {
argumentsSources.remove(bundleMap);
argumentsSources.add(new AbstractMap<String, Object>() {
private final Set<Entry<String, Object>> entries = bundleMap.keySet().stream()
.map(k -> new Entry<String, Object>() {
@Override
public String getKey() {
return k;
}
@Override
public Object getValue() {
return text(source, target, k);
}
@Override
public Object setValue(Object value) {
throw new UnsupportedOperationException();
}
})
.collect(Collectors.toSet());
@Nonnull
@Override
public Set<Entry<String, Object>> entrySet() {
return entries;
}
});
return new MessageFormat(translation, target).format(arguments);
}
}
// Convert named arguments to be numbered to prevent the names
// from being translated.
MessageFormat numberedFormat = new MessageFormat(pattern, target);
Map<String, String> numberedToNamed = null;
if (numberedFormat.usesNamedArguments()) {
// e.g. Hi, {name} -> Hi, {0}
Map<String, Object> numberedArgumentByName = new CompactMap<>();
int index = 0;
for (String name : numberedFormat.getArgumentNames()) {
numberedArgumentByName.put(name, "{" + index + "}");
numberedFormat.setFormatByArgumentName(name, null);
++ index;
}
// Use the numbered pattern to create a regex that can find
// the named arguments from the original pattern.
String numberedPattern = numberedFormat.format(numberedArgumentByName);
Matcher numberedArgumentMatcher = Pattern.compile("\\{\\d+\\}").matcher(numberedPattern);
StringBuilder namedArgumentPattern = new StringBuilder();
List<String> numberedArguments = new ArrayList<>();
int lastEnd = 0;
while (numberedArgumentMatcher.find()) {
namedArgumentPattern.append(Pattern.quote(numberedPattern.substring(lastEnd, numberedArgumentMatcher.start())));
namedArgumentPattern.append("(\\{.+?\\})");
numberedArguments.add(numberedArgumentMatcher.group(0));
lastEnd = numberedArgumentMatcher.end();
}
namedArgumentPattern.append(Pattern.quote(numberedPattern.substring(lastEnd)));
// Map all numbered argument to the named ones.
// e.g. {0} = {name}
Matcher namedArgumentMatcher = Pattern.compile(namedArgumentPattern.toString()).matcher(pattern);
pattern = numberedPattern;
if (namedArgumentMatcher.matches()) {
numberedToNamed = new CompactMap<>();
for (int i = 1; i <= namedArgumentMatcher.groupCount(); ++ i) {
numberedToNamed.put(numberedArguments.get(i - 1), namedArgumentMatcher.group(1));
}
} else {
throw new IllegalArgumentException();
}
}
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpUriRequest request = RequestBuilder.get()
.setUri("https://www.googleapis.com/language/translate/v2")
.addParameter("format", "text")
.addParameter("key", googleServerApiKey)
.addParameter("q", pattern)
.addParameter("source", source.getLanguage())
.addParameter("target", target.getLanguage())
.build();
try (CloseableHttpResponse response = client.execute(request)) {
String responseText = EntityUtils.toString(response.getEntity());
String translation = (String) CollectionUtils.getByPath(ObjectUtils.fromJson(responseText), "data/translations/0/translatedText");
// Restore named arguments.
if (numberedToNamed != null) {
for (Map.Entry<String, String> entry : numberedToNamed.entrySet()) {
translation = translation.replace(entry.getKey(), entry.getValue());
}
}
final String finalTranslation = translation;
Thread saveTranslations = new Thread() {
@Override
public void run() {
com.psddev.dari.db.State translations = new MachineTranslations().getState();
translations.setId(translationsId);
translations.putAtomically(key, finalTranslation);
translations.save();
}
};
saveTranslations.start();
return new MessageFormat(translation, target).format(arguments);
}
} catch (IOException error) {
return new MessageFormat(pattern, source).format(arguments);
}
}
}
private ResourceBundle findBundle(String baseName, Locale locale, ClassLoader loader) {
try {
return ResourceBundle.getBundle(
baseName,
locale,
loader,
ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
} catch (MissingResourceException error) {
return null;
}
}
private Map<String, Object> createBundleMap(ResourceBundle bundle) {
Map<String, Object> map = new CompactMap<>();
for (Enumeration<String> keys = bundle.getKeys(); keys.hasMoreElements();) {
String key = keys.nextElement();
map.put(key, findBundleString(bundle, key));
}
return map;
}
private String findBundleString(ResourceBundle bundle, String key) {
try {
String pattern = bundle.getString(key);
if (bundle instanceof ObjectTypeResourceBundle) {
return pattern;
} else {
return new String(pattern.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
} catch (MissingResourceException error) {
return null;
}
}
public String missingText(String key) {
return "{" + baseName + "/" + key + "}";
}
}