package com.limegroup.gnutella.i18n;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
@SuppressWarnings("unchecked")
class LanguageLoader {
/** @see LanguageInfo#getLink() */
static final String BUNDLE_NAME = "MessagesBundle"; //$NON-NLS-1$
/** @see LanguageInfo#getLink() */
static final String PROPS_EXT = ".properties"; //$NON-NLS-1$
/** @see LanguageInfo#getLink() */
static final String UTF8_EXT = ".UTF-8.txt"; //$NON-NLS-1$
private final Map/* <String, LanguageInfo> */langs;
private final File lib;
/**
* @param directory
*/
LanguageLoader(File directory) {
this.langs = new TreeMap/* <String, LanguageInfo> */();
this.lib = directory;
}
/**
* List and load all available bundles and map them into the languages map.
* Note that resources are not expanded here per base language, and not
* cleaned here from extra keys (needed to support the resources "check"
* option).
*
* @return the languages map (from complete locale codes to LocaleInfo)
*/
Map<String, LanguageInfo> loadLanguages() {
if (!this.lib.isDirectory())
throw new IllegalArgumentException("invalid lib: " + this.lib);
final String[] files = this.lib.list();
for (int i = 0; i < files.length; i++) {
if (!files[i].startsWith(BUNDLE_NAME + '_')
|| !files[i].endsWith(PROPS_EXT)
|| files[i].startsWith(BUNDLE_NAME + "_en")) //$NON-NLS-1$
continue;
/* See if a .UTF-8.txt file exists; if so, use that as the link. */
String linkFileName = files[i];
int idxProperties = linkFileName.indexOf(PROPS_EXT);
final File utf8 = new File(this.lib, linkFileName.substring(0,
idxProperties)
+ UTF8_EXT);
boolean skipUTF8LeadingBOM = false;
if (utf8.exists()) {
/*
* properties files are normally read as streams of ISO-8859-1
* bytes but we want to check the UTF-8 source file. The non
* ASCII characters in key values will be read as sequences of
* Extended Latin 1 characters instead of the actual Unicode
* character coded as Unicode escapes in the .properties file.
* So they won't have the actual run-time value; however it
* allows easier checking and validation here for messages
* printed on the Console, that will output ISO-8859-1; the
* result output still be interpretable as Unicode UTF-8.
*/
linkFileName = utf8.getName();
skipUTF8LeadingBOM = true;
}
try {
final File toRead = new File(this.lib, linkFileName);
final InputStream in = new FileInputStream(toRead);
// skip the three-bytes leading BOM
if (skipUTF8LeadingBOM)
try {
/*
* the leading BOM (U+FEFF), if present, is coded in
* UTF-8 as three bytes 0xEF, 0xBB, 0xBF; they are not
* part of a resource key.
*/
in.mark(3);
if (in.read() != 0xEF || in.read() != 0xBB
|| in.read() != 0xBF)
in.reset();
} catch (java.io.IOException ioe) {/* ignored */}
loadFile(this.langs, in, linkFileName, files[i],
skipUTF8LeadingBOM, toRead);
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
}
return this.langs;
}
/**
* Constructs a list of each line in the default English properties file.
*
* @return a list of Line instances
* @throws IOException
* @see Line
*/
List<Line> getEnglishLines() throws IOException {
final BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(new File(this.lib,
BUNDLE_NAME + PROPS_EXT)), "ISO-8859-1")); //$NON-NLS-1$
final List/* <Line> */lines = new LinkedList/* <Line> */();
String read;
while ((read = reader.readLine()) != null)
lines.add(new Line(read));
return lines;
}
/**
* Scans the file for translations that mistakenly still have a #? sign
* before them and adds them into the properties. This assumes the file is
* ISO-8859-1 encoded, just like Properties.load. If the file is UTF8
* encoded, you will have to manually convert the resulting properties to
* UTF8.
*
* @param file
* @param props
* @throws IOException
*/
private void scanForCommentedTranslations(File file, Properties props)
throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(file));
in.mark(3);
if (in.read() != 0xEF || in.read() != 0xBB || in.read() != 0xBF)
in.reset();
BufferedReader reader = new BufferedReader(new InputStreamReader(in,
"ISO-8859-1")); //$NON-NLS-1$
String read;
while ((read = reader.readLine()) != null) {
Line line = new Line(read);
if (line.hadExtraComment())
props.put(line.getKey(), line.getValue());
}
reader.close();
}
/**
* Retrieves the default properties.
*
* @return the loaded Properties
* @throws IOException
*/
Properties getDefaultProperties() throws java.io.IOException {
Properties p = new Properties();
InputStream in = new FileInputStream(new File(this.lib, BUNDLE_NAME
+ PROPS_EXT));
p.load(in);
in.close();
return p;
}
/**
* Retrieves the advanced keys.
*
* @return a the Set of Strings for the key names of advanced properties.
* @throws IOException
*/
Set getAdvancedKeys() throws java.io.IOException {
final BufferedReader reader;
reader = new BufferedReader(new InputStreamReader(new FileInputStream(
new File(this.lib, BUNDLE_NAME + PROPS_EXT)), "ISO-8859-1")); //$NON-NLS-1$
String read;
while ((read = reader.readLine()) != null)
if (read
.startsWith("## TRANSLATION OF ALL ADVANCED RESOURCE STRINGS AFTER THIS LIMIT IS OPTIONAL")) //$NON-NLS-1$
break;
final StringBuffer sb = new StringBuffer();
while ((read = reader.readLine()) != null) {
if (read.length() == 0 || read.charAt(0) == '#')
continue;
sb.append(read).append("\n"); //$NON-NLS-1$
}
InputStream in = new ByteArrayInputStream(sb.toString().getBytes(
"ISO-8859-1")); //$NON-NLS-1$
Properties p = new Properties();
p.load(in);
in.close();
reader.close();
return p.keySet();
}
/**
* Extend variant resources from *already loaded* base languages.
*/
void extendVariantLanguages() {
/* Extends missing resources with those from the base language */
for (final Iterator/* <Map.Entry<String, LanguageInfo>> */i = this.langs
.entrySet().iterator(); i.hasNext();) {
final Map.Entry entry = (Map.Entry)i.next();
// final String code = (String)entry.getKey();
final LanguageInfo li = (LanguageInfo)entry.getValue();
final Properties props = li.getProperties();
if (li.isVariant()) {
final LanguageInfo liBase = (LanguageInfo)this.langs.get(li
.getBaseCode());
if (liBase != null) {
/* Get a copy of base properties */
final Properties propsBase = new Properties();
propsBase.putAll(liBase.getProperties());
/* Remove properties already defined in the current locale */
propsBase.keySet().removeAll(props.keySet());
/* Add the remaining base properties to the current locale */
props.putAll(propsBase);
}
}
}
}
/**
* Iterates through all languages and retains only those within 'keys'.
*
* @param keys
* a Set of String for key names to retain in properties.
*/
void retainKeys(Set keys) {
/* Extends missing resources with those from the base language */
for (final Iterator/* <Map.Entry<String, LanguageInfo>> */i = this.langs
.entrySet().iterator(); i.hasNext();) {
final Map.Entry/* <String, LanguageInfo> */entry = (Map.Entry)i
.next();
// final String code = (String)entry.getKey();
final LanguageInfo li = (LanguageInfo)entry.getValue();
final Properties props = li.getProperties();
props.keySet().retainAll(keys);
}
}
/**
* Iterates through the properties and removes all entries that have empty
* values.
*
* @param props
*/
private void removeEmptyProperties(Properties props) {
for (Iterator/* <Map.Entry<String, String>> */i = props.entrySet()
.iterator(); i.hasNext();) {
final Map.Entry entry = (Map.Entry)i.next();
if ("".equals(entry.getValue())) {//$NON-NLS-1$
final String key = (String)entry.getKey();
// exceptions for special keys to keep despite an empty value
if (!"LOCALE_COUNTRY_CODE".equals(key)
&& !"LOCALE_VARIANT_CODE".equals(key))
i.remove();
}
}
}
/**
* Loads a single file into the languages map.
*
* @param newlangs
* @param is
* @param filename
* @param baseFileName
* @param isUTF8
* @param toRead
* @return
*/
private LanguageInfo loadFile(final Map newlangs, final InputStream is,
final String filename, final String baseFileName,
final boolean isUTF8, final File toRead) {
try {
final BufferedInputStream in = new BufferedInputStream(is);
final Properties props = new Properties();
props.load(in);
scanForCommentedTranslations(toRead, props);
/* no more needed, as we already check "#? key=" lines without value */
removeEmptyProperties(props);
/*
* note that the file is read in ISO-8859-1 only, even if it is
* encoded with another charset. However, the Properties has its
* unique legacy parser and we want to use it to make sure we use
* the same syntax. So we'll need to correct the parsed values after
* the file is read and interpreted as a set of properties
* (keys,values).
*/
if (isUTF8) {
// actually the file was UTF-8-encoded: convert bytes read
// incorrectly as
// ISO-8859-1 characters, into actual Unicode UTF-16 code units.
for (Iterator i = props.entrySet().iterator(); i.hasNext();) {
final Map.Entry entry = (Map.Entry)i.next();
final String key = (String)entry.getKey();
final String value = (String)entry.getValue();
byte[] bytes = null;
try {
bytes = value.getBytes("ISO-8859-1"); //$NON-NLS-1$
} catch (java.io.IOException ioe) {
ioe.printStackTrace();
}
try {
final String correctedValue = new String(bytes, "UTF-8"); //$NON-NLS-1$
if (!correctedValue.equals(value))
props.put(key, correctedValue);
} catch (java.io.IOException ioe) {
// may occur if .UTF-8.txt file was incorrectly encoded.
ioe.printStackTrace();
}
}
}
String lc = props.getProperty("LOCALE_LANGUAGE_CODE", ""); //$NON-NLS-1$//$NON-NLS-2$
String cc = props.getProperty("LOCALE_COUNTRY_CODE", ""); //$NON-NLS-1$//$NON-NLS-2$
String vc = props.getProperty("LOCALE_VARIANT_CODE", ""); //$NON-NLS-1$//$NON-NLS-2$
String sc = props.getProperty("LOCALE_SCRIPT_CODE", ""); //$NON-NLS-1$//$NON-NLS-2$
String ln = props.getProperty("LOCALE_LANGUAGE_NAME", lc); //$NON-NLS-1$
String cn = props.getProperty("LOCALE_COUNTRY_NAME", cc); //$NON-NLS-1$
String vn = props.getProperty("LOCALE_VARIANT_NAME", vc); //$NON-NLS-1$
String sn = props.getProperty("LOCALE_SCRIPT_NAME", sc); //$NON-NLS-1$
String dn = props.getProperty("LOCALE_ENGLISH_LANGUAGE_NAME", ln); //$NON-NLS-1$
String nsisName = props.getProperty("LOCALE_NSIS_NAME", //$NON-NLS-1$
""); //$NON-NLS-1$
boolean rtl = props.getProperty("LAYOUT_RIGHT_TO_LEFT", //$NON-NLS-1$
"false").equals("true"); //$NON-NLS-1$//$NON-NLS-2$
LanguageInfo li = new LanguageInfo(lc, cc, vc, sc, ln, cn, vn, sn,
dn, nsisName, rtl, filename, props, baseFileName);
newlangs.put(li.getCode(), li);
return li;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null)
try {
is.close();
} catch (IOException ioe) {}
}
return null;
}
}