package com.limegroup.gnutella.i18n; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; /** * Rebuilds the language files, based on the English one. */ class LanguageUpdater { private static final String MARKER = "# TRANSLATIONS START BELOW."; private final File lib; private final Map langs; private final List englishList; private boolean verbose = true; /** * Constructs a new LanguageUpdater. * * @param dir * @param langs * @param englishLines */ LanguageUpdater(File dir, Map langs, List englishLines) { this.lib = dir; this.langs = langs; this.englishList = englishLines; removeInitialComments(englishList); } /** * Determines if stuff should be printed. * * @param silent */ void setSilent(boolean silent) { verbose = !silent; } /** * Prints a message out if we're being verbose. * * @param msg */ void print(String msg) { if (verbose) System.out.print(msg); } /** * @param msg */ void println(String msg) { if (verbose) System.out.println(msg); } /** * */ void println() { if (verbose) System.out.println(); } /** * Updates all languages. */ void updateAllLanguages() { for (Iterator i = langs.values().iterator(); i.hasNext();) { LanguageInfo next = (LanguageInfo) i.next(); // TODO: work with variants. if (next.isVariant()) continue; updateLanguage(next); } } /** * Updates a single language. * * @param info */ void updateLanguage(LanguageInfo info) { if (info == null) { println("Unknown language."); return; } print("Updating language: " + info.getName() + " (" + info.getCode() + ")... "); String filename = info.getFileName(); File f = new File(lib, filename); if (!f.isFile()) throw new IllegalArgumentException("Invalid info: " + info); File temp; BufferedReader reader; PrintWriter printer; try { temp = File.createTempFile("TEMP", info.getCode(), lib); reader = new BufferedReader(new InputStreamReader( new FileInputStream(f), info.isUTF8() ? "UTF-8" : "ISO-8859-1")); printer = new PrintWriter(temp, info.isUTF8() ? "UTF-8" : "ISO-8859-1"); if (info.isUTF8()) { reader.mark(1); if (reader.read() != '\uFEFF') reader.reset(); /* was not a leading BOM */ printer.print('\uFEFF'); /* force a leading BOM */ } printInitialComments(printer, reader, info); printBody(printer, info); reader.close(); printer.close(); if (isDifferent(f, temp)) { println("...changes."); f.delete(); temp.renameTo(f); } else { println("...no changes!"); temp.delete(); } if (info.isUTF8()) native2ascii(info); } catch (IOException ioe) { println("...error! (" + ioe.getMessage() + ")"); } } /** * Home-made native2ascii. */ private void native2ascii(LanguageInfo info) { if (!info.isUTF8()) throw new IllegalArgumentException("requires utf8 language."); InputStream in = null; OutputStream out = null; print("\tConverting to ASCII... "); try { in = new BufferedInputStream( new FileInputStream(info.getFileName())); in.mark(3); if (in.read() != 0xEF || in.read() != 0xBB || in.read() != 0xBF) in.reset(); BufferedReader reader = new BufferedReader(new InputStreamReader( in, "UTF8")); out = new BufferedOutputStream(new FileOutputStream(info .getAlternateFileName())); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( out, "ISO-8859-1")); String read; while ((read = reader.readLine()) != null) { writer.write(ascii(read)); writer.newLine(); } writer.flush(); out.flush(); println("... done!"); } catch (IOException ignored) { println("... error! (" + ignored.getMessage() + ")"); } finally { if (in != null) try { in.close(); } catch (IOException ignored) { } if (out != null) try { out.close(); } catch (IOException ignored) { } } } /** * Determines if there is any difference between file a & file b. */ private boolean isDifferent(File a, File b) { InputStream ia = null, ib = null; try { ia = new BufferedInputStream(new FileInputStream(a)); ib = new BufferedInputStream(new FileInputStream(b)); int c; while ((c = ia.read()) == ib.read()) { // if we got here, both got to EOF at same time if (c == -1) return false; } } catch (IOException ignored) { } finally { if (ia != null) try { ia.close(); } catch (IOException ignored) { } if (ib != null) try { ib.close(); } catch (IOException ignored) { } } // if we didn't exit in the loop, a character was different // or one stream ended before another. return true; } /** * Writes the body of the bundle. */ private void printBody(PrintWriter printer, LanguageInfo info) { Properties props = info.getProperties(); boolean reachedTranslations = false; for (Iterator i = englishList.iterator(); i.hasNext();) { Line line = (Line) i.next(); if (MARKER.equals(line.getLine())) reachedTranslations = true; if (line.isComment()) { printer.println(line.getLine()); } else { String key = line.getKey(); String value = props.getProperty(key); // always write the English version, so translators // have a reference point for possibly needing to update // an older translation. if (reachedTranslations) { printer.print("#### "); printer.print(key); printer.print("="); printer.print(escape(line.getValue())); printer.println(); } if (value != null && line.getBraceCount() == Line.parseBraceCount(value)) { printer.print(key); printer.print("="); printer.println(escape(value)); } else { printer.print("#? "); printer.print(key); printer.print("="); // only write the non-translated value if we didn't // above. if (!reachedTranslations) printer.print(escape(line.getValue())); printer.println(); } } } } /** * Writes the initial comments from a given file to fos. */ private void printInitialComments(PrintWriter printer, BufferedReader reader, LanguageInfo info) throws IOException { // TODO: look into initial comments, to see if more information should // be generated from 'info'. try { String read; // Read through and write the initial lines until we reach a // non-comment while ((read = reader.readLine()) != null) { Line line = new Line(read); if (!line.isComment()) break; printer.println(read); } } finally { } } /** * Removes the initial comments from the English properties file. */ private void removeInitialComments(List l) { for (Iterator i = l.iterator(); i.hasNext();) { Line line = (Line) i.next(); if (line.isComment()) i.remove(); else break; } } /** * Returns a string suitable for insertion into a UTF-8 encoded Properties * file. Some characters will always be escaped. */ private String escape(String s) { final int n = s.length(); StringBuffer sb = new StringBuffer(n); for (int i = 0; i < n; i++) { int cp; if (Character.isSupplementaryCodePoint(cp = s.codePointAt(i))) i++; // TODO: use switch(getType(cp)) for more generic handling. // Whitespace's include all Spacechar's but not non-breaking spaces; // Spacechar's include all Whitespace's but not C0 controls. if (Character.isWhitespace(cp) || Character.isSpaceChar(cp) || Character.isISOControl(cp) || // treat isolated surrogates like controls !Character.isSupplementaryCodePoint(cp) && (Character.isLowSurrogate((char) cp) || Character .isHighSurrogate((char) cp))) { switch (cp) { // only ASCII regular SPACE can be left unchanged; case ' ': sb.append(' '); break; // all other whitespaces and controls must be escaped. case '\n': sb.append("\\n"); break; case '\t': sb.append("\\t"); break; case '\f': sb.append("\\f"); break; case '\r': sb.append("\\r"); break; default: sb.append(hexUnicode(cp)); } } else { // valid non-controls non-whitespaces can be left unchanged. sb.appendCodePoint(cp); } } return sb.toString(); } /** * Converts the input string to ascii, using \\u escapes. */ private String ascii(final String s) { final int n = s.length(); final StringBuffer sb = new StringBuffer(n * 5); for (int i = 0; i < n; i++) { int p = s.codePointAt(i); if (p > 0x00ff || // not Latin-1 p <= 0x001f || // C0 controls p >= 0x007f && p <= 0x00a0 || // DEL, C1 controls, NBSP p == 0x00ad) // SHY (Soft Hyphen) sb.append(hexUnicode(p)); else sb.appendCodePoint(p); } return sb.toString(); } /** * Returns the escaped unicode hex representation of the codepoint. * * @param cp * the codepoint to represent; must be in the 17 first planes, * and not a surrogate. */ private String hexUnicode(final int cp) { if (cp <= 0xffff) { final String hex = Integer.toHexString(cp); final StringBuffer sb = new StringBuffer(6); sb.append("\\u"); for (int j = hex.length(); j < 4; j++) sb.append('0'); sb.append(hex); return sb.toString(); } return new StringBuffer(12).append("\\u").append( Integer.toHexString(((cp - 0x10000) >> 10) + 0xD800)).append( "\\u").append(Integer.toHexString((cp & 0x3ff) + 0xDC00)) .toString(); } }