package net.i2p.util; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FilterReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.i2p.I2PAppContext; /** * Translate. * * Strings are tagged with _("translateme") * or _("translate {0} me", "foo") * * Max two parameters. * String and parameters must be double-quoted (no ngettext, no tagged parameters). * Escape quotes inside quote with \". * Commas and spaces between args are optional. * Entire tag (from '_' to ')') must be on one line. * Multiple tags allowed on one line. * * Also will extract strings to a dummy java file for postprocessing by xgettext - see main(). * * @since 0.9.8 */ public class TranslateReader extends FilterReader { /** all states may transition to START */ private enum S { START, /** next state LPAREN */ UNDER, /** next state QUOTE */ LPAREN, /** next state LPAREN or BACK */ QUOTE, /** next state QUOTE */ BACK } private final String _bundle; private final I2PAppContext _ctx; /** parse in progress */ private final StringBuilder _inBuf; /** parsed and translated */ private final StringBuilder _outBuf; /** pending string or parameter for translation */ private final StringBuilder _argBuf; /** parsed string and parameters */ private final List<String> _args; private S _state = S.START; private TagHook _hook; private static final int MAX_ARGS = 9; /** * @param bundle may be null for tagging only * @param in UTF-8 */ public TranslateReader(I2PAppContext ctx, String bundle, InputStream in) throws IOException { super(new BufferedReader(new InputStreamReader(in, "UTF-8"))); _ctx = ctx; _bundle = bundle; _args = new ArrayList<String>(4); _inBuf = new StringBuilder(64); _outBuf = new StringBuilder(64); _argBuf = new StringBuilder(64); } @Override public int read() throws IOException { int rv = popit(); if (rv > 0) return rv; return parse(); } private int parse() throws IOException { while (true) { int c = in.read(); if (c >= 0) pushit((char) c); //System.err.println("State: " + _state + " char: '" + ((char)c) + "'"); switch (c) { case -1: case '\r': case '\n': return flushit(); case '_': switch (_state) { case START: _state = S.UNDER; break; case BACK: _state = S.QUOTE; // fall thru case QUOTE: _argBuf.append((char) c); break; default: return flushit(); } break; case '(': switch (_state) { case UNDER: _args.clear(); _state = S.LPAREN; break; case BACK: _state = S.QUOTE; // fall thru case QUOTE: _argBuf.append((char) c); break; default: return flushit(); } break; case '"': switch (_state) { case LPAREN: // got an opening quote for a parameter if (_args.size() >= MAX_ARGS) return flushit(); _argBuf.setLength(0); _state = S.QUOTE; break; case BACK: _argBuf.append((char) c); _state = S.QUOTE; break; case QUOTE: // got a closing quote for a parameter _args.add(_argBuf.toString()); _state = S.LPAREN; break; default: return flushit(); } break; case '\\': switch (_state) { case QUOTE: _state = S.BACK; break; case BACK: _argBuf.append((char) c); _state = S.QUOTE; break; default: return flushit(); } break; case ' ': case '\t': case ',': switch (_state) { case BACK: _state = S.QUOTE; // fall thru case QUOTE: _argBuf.append((char) c); break; case LPAREN: // ignore whitespace and commas between args break; default: return flushit(); } break; case ')': switch (_state) { case BACK: _state = S.QUOTE; // fall thru case QUOTE: _argBuf.append((char) c); break; case LPAREN: // Finally, we have something to translate! translate(); return popit(); default: return flushit(); } break; default: switch (_state) { case BACK: _state = S.QUOTE; // fall thru case QUOTE: _argBuf.append((char) c); break; default: return flushit(); } break; } } } @Override public int read(char cbuf[], int off, int len) throws IOException { for (int i = 0; i < len; i++) { int c = read(); if (c < 0) { if (i == 0) return -1; return i; } cbuf[off + i] = (char) c; } return len; } @Override public long skip(long n) throws IOException { for (long i = 0; i < n; i++) { int c = read(); if (c < 0) { if (i == 0) return -1; return i; } } return n; } @Override public boolean ready() throws IOException { return _outBuf.length() > 0 || _inBuf.length() > 0 ||in.ready(); } @Override public void close() throws IOException { _inBuf.setLength(0); _outBuf.setLength(0); _state = S.START; in.close(); } @Override public void mark(int readLimit) {} @Override public void reset() throws IOException { throw new IOException(); } @Override public boolean markSupported() { return false; } /** * put in the pending parse buf */ private void pushit(char c) { _inBuf.append(c); } /** * flush _inBuf to _outBuf, * reset state, * and return next char or -1 */ private int flushit() { _state = S.START; if (_inBuf.length() > 0) { _outBuf.append(_inBuf); _inBuf.setLength(0); } return popit(); } /** * return next char from _outBuf or -1 */ private int popit() { if (_outBuf.length() > 0) { int rv = _outBuf.charAt(0) & 0xffff; _outBuf.deleteCharAt(0); return rv; } return -1; } /** * clear _inBuf, translate _args to _outBuf, * reset state */ private void translate() { //System.err.println("Translating: " + _args.toString()); int argCount = _args.size(); if (argCount <= 0 || argCount > MAX_ARGS) { flushit(); return; } _state = S.START; _inBuf.setLength(0); if (_hook != null) { _hook.tag(_args); return; } String tx = null; if (argCount == 1) tx = Translate.getString(_args.get(0), _ctx, _bundle); else tx = Translate.getString(_args.get(0), _ctx, _bundle, _args.subList(1, _args.size()).toArray()); _outBuf.append(tx); } private interface TagHook extends Closeable { public void tag(List<String> args); } private static class Tagger implements TagHook { private final PrintStream _out; private final String _name; private int _count; public Tagger(String file) throws IOException { _name = file; _out = new PrintStream(file, "UTF-8"); _out.println("// Automatically generated, do not edit"); _out.println("package dummy;"); _out.println("class Dummy {"); _out.println(" void dummy() {"); } public void tag(List<String> args) { if (args.size() <= 0) return; _out.print("\t_t("); for (int i = 0; i < args.size(); i++) { if (i > 0) _out.print(", "); _out.print('"'); _out.print(args.get(i).replace("\"", "\\\"")); _out.print('"'); } _out.println(");"); _count++; } public void close() throws IOException { _out.println(" }"); _out.println("}"); if (_out.checkError()) throw new IOException(); _out.close(); System.out.println(_count + " strings written to " + _name); } } /** * Do not comment out, used to extract tags as a part of the build process. */ public static void main(String[] args) { try { if (args.length >= 2 && args[0].equals("test")) test(args[1]); else if (args.length >= 2 && args[0].equals("tag")) tag(args); else System.err.println("Usage:\n" + "\ttest file (output to stdout)\n" + "\ttag file (output to file.java)\n" + "\ttag dir outfile\n" + "\ttag file1 [file2...] outfile"); } catch (IOException ioe) { throw new RuntimeException(ioe); } } private static void test(String file) throws IOException { FileInputStream fio = new FileInputStream(file); TranslateReader r = new TranslateReader(I2PAppContext.getGlobalContext(), "net.i2p.router.web.messages", fio); int c; while ((c = r.read()) >= 0) { System.out.print((char)c); } System.out.flush(); r.close(); } /** @param files ignore 0 */ private static void tag(String[] files) throws IOException { char[] buf = new char[256]; String outfile; List<String> filelist; if (files.length == 2) { outfile = files[1] + ".java"; filelist = Collections.singletonList(files[1]); } else if (files.length == 3 && (new File(files[1])).isDirectory()) { outfile = files[2]; File dir = new File(files[1]); File[] listing = dir.listFiles(); if (listing == null) throw new IOException(); filelist = new ArrayList<String>(listing.length); for (int i = 0; i < listing.length; i++) { File f = listing[i]; if (!f.isDirectory()) filelist.add(f.getAbsolutePath()); } } else { outfile = files[files.length - 1]; filelist = Arrays.asList(files).subList(1, files.length - 1); } TagHook tagger = null; try { tagger = new Tagger(outfile); for (String file : filelist) { TranslateReader r = null; try { r = new TranslateReader(I2PAppContext.getGlobalContext(), null, new FileInputStream(file)); r._hook = tagger; while (r.read(buf, 0, buf.length) >= 0) { // throw away output } } finally { if (r != null) r.close(); } } } finally { if (tagger != null) tagger.close(); } } }