package magic.translate;
import groovy.json.StringEscapeUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.CRC32;
import magic.data.GeneralConfig;
import magic.utility.MagicFileSystem;
import magic.utility.MagicSystem;
public final class MText {
private MText() { }
private static final String UTF_CHAR_SET = "UTF-8";
private static final CRC32 crc = new CRC32();
private static final Map<Long, String> translationsMap = new HashMap<>();
private static final Map<Long, String> annotations = new HashMap<>();
static {
try {
loadTranslationFile();
} catch (Exception ex) {
Logger.getLogger(MText.class.getName()).log(Level.WARNING, null, ex);
}
}
/**
* Converts (English) string into CRC32 number which is used
* as the mapping ID to identify the equivalent translation.
*/
private static Long getStringId(final String aString) {
crc.reset();
try {
crc.update(aString.getBytes(UTF_CHAR_SET));
return crc.getValue();
} catch (UnsupportedEncodingException ex) {
System.err.println(ex);
translationsMap.clear();
return 0L;
}
}
public static final String get(final String aString, final Object... args) {
if (translationsMap.isEmpty() == false) {
final Long stringId = getStringId(aString);
if (translationsMap.containsKey(stringId)) {
return String.format(translationsMap.get(stringId), args);
}
}
return String.format(aString, args);
}
public static final String get(final String aString) {
if (translationsMap.isEmpty() == false) {
final Long stringId = getStringId(aString);
if (translationsMap.containsKey(stringId)) {
return translationsMap.get(stringId);
}
}
return aString;
}
/**
* Returns translated string enclosed in {@literal <html>...</html>} tags.
*
* This is useful for automatically wrapping long strings.
*/
public static final String asHtml(final String aString) {
return "<html>" + get(aString) + "</html>";
}
private static Map<Long, String> getStringsMapFromFile(final File txtFile, final boolean unescape) throws FileNotFoundException {
final Map<Long, String> stringsMap = new LinkedHashMap<>();
try (final Scanner sc = new Scanner(txtFile, UTF_CHAR_SET)) {
while (sc.hasNextLine()) {
final String line = sc.nextLine().trim();
if (line.startsWith("#") == false && line.isEmpty() == false) {
final int equalsChar = line.indexOf('=');
final long stringId = Long.valueOf(line.substring(0, equalsChar).trim());
final String translation = line.substring(equalsChar + 1).trim();
stringsMap.put(stringId, unescape ? StringEscapeUtils.unescapeJava(translation) : translation);
}
}
}
return stringsMap;
}
public static Map<Long, String> getUnescapedStringsMap(final File txtFile) throws FileNotFoundException {
return getStringsMapFromFile(txtFile, true);
}
public static Map<Long, String> getEscapedStringsMap(final File txtFile) throws FileNotFoundException {
return getStringsMapFromFile(txtFile, false);
}
public static void loadTranslationFile() throws FileNotFoundException {
translationsMap.clear();
final String language = GeneralConfig.getInstance().getTranslation();
if (language.isEmpty() == false) {
final Path dirPath = MagicFileSystem.getDataPath(MagicFileSystem.DataPath.TRANSLATIONS);
final File txtFile = dirPath.resolve(language + ".txt").toFile();
translationsMap.putAll(getUnescapedStringsMap(txtFile));
}
}
/**
* Returns the names of all classes in specified package (including sub-packages).
*/
public static List<String> getClassNamesInPackage(final File jarFile, String packageName) throws IOException {
final List<String> classes = new ArrayList<>();
if (jarFile == null || !jarFile.exists() || !jarFile.isFile()) {
throw new IOException("Unable to locate JAR file!\n\nTo manually specify the location please use the '-DjarFile' VM option.");
}
try (JarInputStream jarStream = new JarInputStream(new FileInputStream(jarFile))) {
packageName = packageName.replaceAll("\\.", "/");
while (true) {
final JarEntry jarEntry = jarStream.getNextJarEntry();
if (jarEntry == null) {
break;
}
final String entryName = jarEntry.getName();
if (entryName.startsWith(packageName) && entryName.endsWith(".class")) {
classes.add(entryName.replaceAll("/", "\\."));
}
}
}
return classes;
}
/**
* Use reflection to find all _S* strings.
*/
public static Map<Long, String> getUiStringsMap() throws URISyntaxException, IOException {
// Not sure if it is a bug or by design but if no UTF character is written
// then (on Windows 7 anyway) it ignores UTF_CHAR_SET and encodes as ANSI.
// If you subsequently overwrite the template strings with translations that do
// use unicode characters no error is thrown but the translations are not loaded
// either. Therefore since the template file uses a prefix character to highlight
// untranslated strings in the UI use a unicode character to force correct encoding.
final String UTF_PREFIX = "\u25AB"; // small white square ▫
final Map<Long, String> stringsMap = new LinkedHashMap<>();
annotations.clear();
for (final String c : getClassNamesInPackage(MagicSystem.getJarFile(), "magic")) {
final String className = c.substring(0, c.length() - ".class".length());
try {
for (final Field f : Class.forName(className).getDeclaredFields()) {
final boolean isFieldValid =
f.getType() == String.class
&& f.getName().startsWith("_S")
&& Modifier.isStatic(f.getModifiers()); // prevents a UnsafeObjectFieldAccessorImpl error.
if (isFieldValid) {
f.setAccessible(true);
try {
final Long stringId = getStringId((String) f.get(null));
final String stringValue = UTF_PREFIX + StringEscapeUtils.escapeJava((String) f.get(null));
if (stringsMap.containsKey(stringId) == false) {
stringsMap.put(stringId, stringValue);
if (f.getAnnotation(StringContext.class) != null) {
annotations.put(stringId, f.getAnnotation(StringContext.class).eg());
}
} else if (stringValue.equals(stringsMap.get(stringId)) == false) {
throw new RuntimeException(
"Failed to generate translation file because the following strings have the same CRC32 value:-\n" +
stringValue + "\n" + stringsMap.get(stringId));
}
} catch (IllegalAccessException ex) {
System.err.println(ex);
}
}
}
} catch (ClassNotFoundException ex) {
System.err.println(ex);
}
}
return stringsMap;
}
public static void createTranslationFile(File txtFile, Map<Long, String> stringsMap) throws FileNotFoundException, UnsupportedEncodingException {
try (final PrintWriter writer = new PrintWriter(txtFile, UTF_CHAR_SET)) {
for (Map.Entry<Long, String> entry : stringsMap.entrySet()) {
final Long key = entry.getKey();
if (annotations.containsKey(key)) {
writer.println(String.format("# %010d eg. %s", key, annotations.get(key)));
}
// CRC32 function returns 32 bit long = max 10 numerals. Pad if smaller.
writer.println(String.format("%010d = %s", key, entry.getValue()));
}
}
}
public static void createTranslationFIle(File txtFile) throws URISyntaxException, IOException {
createTranslationFile(txtFile, getUiStringsMap());
}
public static void disableTranslations() {
translationsMap.clear();
}
public static boolean isEnglish() {
return translationsMap.isEmpty();
}
}