From 4f4c22006d2d600a74a0ad23fd6870af6eb2e2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Wed, 23 Nov 2022 17:28:59 +0100 Subject: [PATCH] Added Translator tool for easier localization --- .gitignore | 1 + CHANGELOG.md | 1 + TRANSLATIONS.md | 23 +- build.properties | 5 + build.xml | 54 +- libsrc/ffdec_lib/lexers/properties.flex | 158 ++ .../helpers/properties/ParsedSymbol.java | 25 + .../helpers/properties/PropertiesLexer.java | 742 +++++++++ .../properties/PropertiesParseException.java | 9 + .../jpexs/helpers/properties/SymbolType.java | 13 + resources/translator.bat | 2 + resources/translator.sh | 105 ++ .../console/CommandLineArgumentParser.java | 5 +- src/com/jpexs/decompiler/flash/gui/View.java | 2 +- .../flash/gui/translator/Translator.java | 1329 +++++++++++++++++ 15 files changed, 2457 insertions(+), 17 deletions(-) create mode 100644 libsrc/ffdec_lib/lexers/properties.flex create mode 100644 libsrc/ffdec_lib/src/com/jpexs/helpers/properties/ParsedSymbol.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesLexer.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesParseException.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/helpers/properties/SymbolType.java create mode 100644 resources/translator.bat create mode 100644 resources/translator.sh create mode 100644 src/com/jpexs/decompiler/flash/gui/translator/Translator.java diff --git a/.gitignore b/.gitignore index 3ea7f8424..780cda6bc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ Thumbs.db /testdata/recompile/ /build_exe.xml /build_exe64.xml +/build_translator_exe.xml /coverage.ec /libsrc/jsyntaxpane/jsyntaxpane/src/target/ hs_err_pid*.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e933fc29..ba0168bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ All notable changes to this project will be documented in this file. ##[Unreleased] +- Translator tool for easier localization ##[17.0.2] - 2022-11-22 ### Fixed diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md index ba0e45ac6..8664f0f94 100644 --- a/TRANSLATIONS.md +++ b/TRANSLATIONS.md @@ -21,18 +21,15 @@ - Turkish - Ukrainian -## New translation +## Help translating If you would like to translate FFDec to your language, please follow these steps: -1. Check whether your language is not already present in the development branch: -[dev/TRANSLATIONS.md](https://github.com/jindrapetrik/jpexs-decompiler/blob/dev/TRANSLATIONS.md) -2. Find out your language code (See [table](http://www.loc.gov/standards/iso639-2/php/code_list.php) ) -4. Download `Language pack for translators (zipped)` from latest (including nightly) version on [releases page](https://github.com/jindrapetrik/jpexs-decompiler/releases) -5. The archive contains all language files for newest version of FFDec. Each language in this pack has files with its own suffix which is standard language code. -6. Extract Language pack ZIP file -7. Copy each `.properties` file without `_xx` suffix (english), to new file which has your language code suffix (`_cs` is for czech, etc...) -8. Open `.properties` files with an editor. (`.properties` editor bundled with some Java IDE is better than standard text editor) -9. In order to `.properties` to work in FFDec, all nonascii characters should be replaced with unicode escapes (like `\u1234`). IDE editors like Netbeans do this automatically. If you have classic text editor, you can skip this phase, I will do it later myself. -10. Don't forget to place your name in `AboutDialog_xx.properties` file. -11. Create branch from `dev` and place .properties files to correct locations. TODO: specify what's correct location -12. Create pull request +1. Download latest nightly version of FFDec to have the latest english strings. +2. In the FFDec installation directory, run FFDec Translator: + `translator.exe`, `translator.bat`, `translator.sh` or `java -jar ffdec.jar -translator` will do +3. Use GUI editor to edit existing translations and/or add new Locale +4. If you create brand new locale, you will be asked for its code + See [table](http://www.loc.gov/standards/iso639-2/php/code_list.php) (ISO 639-1 Code) for available codes. +5. When you are ready, use `Export JPT` button to export modified strings to an archive (.jpt extension) +6. Send that archive to us, you can use either Issue tracker (preffered) or e-mail contact `jindra.petrik@gmail.com` + diff --git a/build.properties b/build.properties index b342c1630..ef4f7e3d1 100644 --- a/build.properties +++ b/build.properties @@ -34,6 +34,11 @@ core.lib.version_info = libsrc/ffdec_lib/version.properties version_info = version.properties exe.config = build_exe.xml +translator_exe.config = build_translator_exe.xml +translator_exe.filename = translator + +translator_internal.name = FFDec +translator_product.name = JPEXS Free Flash Decompiler website.upload.url = - website.version.url = - diff --git a/build.xml b/build.xml index 8c8ee617a..081fe9d6c 100644 --- a/build.xml +++ b/build.xml @@ -444,16 +444,66 @@ - + + + + + true + gui + ${jar.filename}.jar + ${basedir}/${dist.dir}/${translator_exe.filename}.exe + + -translator + + normal + http://java.com/download + + false + false + false + + + + + ${min.java.version} + + preferJre + 64/32 + ${max.heap.size.percent} + + + ${version.number} + ${version}${version.suffix} + ${translator_product.name} + ${vendor} + ${version.number} + ${version}${version.suffix} + ${translator_product.name} + + ${translator_internal.name} + ${translator_exe.filename}.exe + + + + + + + + - + + + + + + diff --git a/libsrc/ffdec_lib/lexers/properties.flex b/libsrc/ffdec_lib/lexers/properties.flex new file mode 100644 index 000000000..731fbeea9 --- /dev/null +++ b/libsrc/ffdec_lib/lexers/properties.flex @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.helpers.properties; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +%% + +%public +%class PropertiesLexer +%final +%unicode +%char +%type ParsedSymbol +%throws PropertiesParseException + +%{ + + private String sourceCode; + public PropertiesLexer(String sourceCode){ + this(new StringReader(sourceCode)); + this.sourceCode = sourceCode; + } + + public void yypushbackstr(String s, int state) + { + sourceCode = s + sourceCode.substring(yychar + yylength()); + yyreset(new StringReader(sourceCode)); + yybegin(state); + } + + public void yypushbackstr(String s) + { + yypushbackstr(s, YYINITIAL); + } + + StringBuilder string = new StringBuilder(); + + private static String xmlTagName = ""; + + public int yychar() { + return yychar; + } + + private Stack pushedBack = new Stack<>(); + + public int yyline() { + return yyline + 1; + } + + public void pushback(ParsedSymbol symb) { + pushedBack.push(symb); + last = null; + } + + ParsedSymbol last; + public ParsedSymbol lex() throws java.io.IOException, PropertiesParseException{ + ParsedSymbol ret = null; + if (!pushedBack.isEmpty()){ + ret = last = pushedBack.pop(); + } else { + ret = last = yylex(); + } + return ret; + } + + private int count(String str, String target) { + return (str.length() - str.replace(target, "").length()) / target.length(); + } +%} + +/* main character classes */ +LineTerminator = \r|\n|\r\n +InputCharacter = [^\r\n] + +WhiteSpace = [ \t\f]+ + +NonWhiteSpaceChar = [^ \t\f] + +Separator = {WhiteSpace} | ({WhiteSpace}? [:=] {WhiteSpace}?) +NewLineEscape = "\\" {LineTerminator} {WhiteSpace}? + +Comment = [#!] {InputCharacter}* + +UnicodeEscape = "\\" u[0-9a-fA-F]{4} + +/*return new ParsedSymbol(SymbolGroup.KEYWORD, SymbolType.BREAK, yytext());*/ +%state COMMENT, KEY, VALUE + +%% + + { + {WhiteSpace} {} + {Comment} { yybegin(COMMENT); + return new ParsedSymbol(SymbolType.COMMENT, yytext().substring(1));} + {LineTerminator} { return new ParsedSymbol(SymbolType.EMPTY_LINE, null); } + {NonWhiteSpaceChar} {string.setLength(0); string.append(yytext()); yybegin(KEY);} + +} + + { + {LineTerminator} {yybegin(YYINITIAL);} +} + + { + "\\t" { string.append("\\t"); } + "\\f" { string.append("\\f"); } + "\\r" { string.append("\\r"); } + "\\n" { string.append("\\n"); } + "\\\\" { string.append("\\\\"); } + "\\ " { string.append(" "); } + "\\!" { string.append("!"); } + "\\#" { string.append("#"); } + "\\=" { string.append("="); } + "\\:" { string.append(":"); } + {UnicodeEscape} { string.append((char)Integer.parseInt(yytext().substring(2), 16));} + {Separator} { String key = string.toString(); + yybegin(VALUE); + string.setLength(0); + return new ParsedSymbol(SymbolType.KEY, key);} + {NonWhiteSpaceChar} { string.append(yytext());} +} + + { + "\\t" { string.append("\\t"); } + "\\f" { string.append("\\f"); } + "\\r" { string.append("\\r"); } + "\\n" { string.append("\\n"); } + "\\\\" { string.append("\\\\"); } + "\\ " { string.append(" "); } + {UnicodeEscape} { string.append((char)Integer.parseInt(yytext().substring(2), 16));} + {NewLineEscape} { string.append("\r\n"); } + {LineTerminator} {yybegin(YYINITIAL); + return new ParsedSymbol(SymbolType.VALUE, string.toString());} + {InputCharacter} { string.append(yytext());} + <> {yybegin(YYINITIAL); + return new ParsedSymbol(SymbolType.VALUE, string.toString());} +} + +/* error fallback */ +[^] { } +<> { return new ParsedSymbol(SymbolType.EOF, null); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/ParsedSymbol.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/ParsedSymbol.java new file mode 100644 index 000000000..41a8b91c4 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/ParsedSymbol.java @@ -0,0 +1,25 @@ +package com.jpexs.helpers.properties; + +/** + * + * @author JPEXS + */ +public class ParsedSymbol { + public Object value; + + public SymbolType type; + + public ParsedSymbol(SymbolType type, Object value) { + this.type = type; + this.value = value; + } + + @Override + public String toString() { + return type+": " + value; + } + + + + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesLexer.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesLexer.java new file mode 100644 index 000000000..6c9986cc8 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesLexer.java @@ -0,0 +1,742 @@ +/* The following code was generated by JFlex 1.6.0 */ + +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.helpers.properties; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + + +/** + * This class is a scanner generated by + * JFlex 1.6.0 + * from the specification file C:/Dropbox/Programovani/JavaSE/FFDec/libsrc/ffdec_lib/lexers/properties.flex + */ +public final class PropertiesLexer { + + /** This character denotes the end of file */ + public static final int YYEOF = -1; + + /** initial size of the lookahead buffer */ + private static final int ZZ_BUFFERSIZE = 16384; + + /** lexical states */ + public static final int YYINITIAL = 0; + public static final int COMMENT = 2; + public static final int KEY = 4; + public static final int VALUE = 6; + + /** + * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l + * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l + * at the beginning of a line + * l is of the form l = 2*k, k a non negative integer + */ + private static final int ZZ_LEXSTATE[] = { + 0, 0, 1, 1, 2, 2, 3, 3 + }; + + /** + * Translates characters to character classes + */ + private static final String ZZ_CMAP_PACKED = + "\11\0\1\3\1\2\1\0\1\3\1\1\22\0\1\15\1\16\1\0"+ + "\1\6\14\0\12\10\1\4\2\0\1\17\3\0\6\10\25\0\1\5"+ + "\4\0\5\10\1\12\7\0\1\14\3\0\1\13\1\0\1\11\1\7"+ + "\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uff9a\0"; + + /** + * Translates characters to character classes + */ + private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED); + + /** + * Translates DFA states to action switch labels. + */ + private static final int [] ZZ_ACTION = zzUnpackAction(); + + private static final String ZZ_ACTION_PACKED_0 = + "\4\0\1\1\2\2\1\3\1\4\1\3\2\5\1\6"+ + "\2\7\1\6\2\10\1\6\1\11\1\12\1\13\1\0"+ + "\1\14\1\15\1\16\1\17\1\20\1\21\1\22\2\23"+ + "\3\0\1\24"; + + private static int [] zzUnpackAction() { + int [] result = new int[36]; + int offset = 0; + offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAction(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /** + * Translates a state to a row index in the transition table + */ + private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); + + private static final String ZZ_ROWMAP_PACKED_0 = + "\0\0\0\20\0\40\0\60\0\100\0\120\0\100\0\140"+ + "\0\160\0\100\0\200\0\100\0\100\0\220\0\240\0\260"+ + "\0\300\0\100\0\320\0\100\0\100\0\100\0\340\0\100"+ + "\0\100\0\100\0\100\0\100\0\100\0\100\0\360\0\u0100"+ + "\0\u0110\0\u0120\0\u0130\0\100"; + + private static int [] zzUnpackRowMap() { + int [] result = new int[36]; + int offset = 0; + offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackRowMap(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int high = packed.charAt(i++) << 16; + result[j++] = high | packed.charAt(i++); + } + return j; + } + + /** + * The transition table of the DFA + */ + private static final int [] ZZ_TRANS = zzUnpackTrans(); + + private static final String ZZ_TRANS_PACKED_0 = + "\1\5\1\6\1\7\1\10\2\5\1\11\6\5\1\10"+ + "\1\11\1\5\1\12\1\13\1\14\15\12\3\15\1\16"+ + "\1\17\1\20\7\15\1\16\1\15\1\17\1\15\1\21"+ + "\1\22\2\15\1\23\12\15\22\0\1\7\20\0\1\10"+ + "\11\0\1\10\2\0\1\11\2\0\15\11\2\0\1\14"+ + "\20\0\1\16\1\17\10\0\1\16\1\0\1\17\3\0"+ + "\1\17\11\0\1\17\6\0\1\24\1\25\1\26\1\27"+ + "\1\0\1\30\1\31\1\32\1\33\1\34\1\35\1\36"+ + "\2\0\1\22\16\0\1\37\1\40\2\0\1\25\1\0"+ + "\1\27\1\0\1\30\1\31\1\32\1\33\1\34\12\0"+ + "\1\41\1\0\1\41\7\0\2\40\11\0\1\40\5\0"+ + "\1\40\11\0\1\40\12\0\1\42\1\0\1\42\15\0"+ + "\1\43\1\0\1\43\15\0\1\44\1\0\1\44\5\0"; + + private static int [] zzUnpackTrans() { + int [] result = new int[320]; + int offset = 0; + offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackTrans(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + value--; + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /* error codes */ + private static final int ZZ_UNKNOWN_ERROR = 0; + private static final int ZZ_NO_MATCH = 1; + private static final int ZZ_PUSHBACK_2BIG = 2; + + /* error messages for the codes above */ + private static final String ZZ_ERROR_MSG[] = { + "Unkown internal scanner error", + "Error: could not match input", + "Error: pushback value was too large" + }; + + /** + * ZZ_ATTRIBUTE[aState] contains the attributes of state aState + */ + private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); + + private static final String ZZ_ATTRIBUTE_PACKED_0 = + "\4\0\1\11\1\1\1\11\2\1\1\11\1\1\2\11"+ + "\4\1\1\11\1\1\3\11\1\0\7\11\2\1\3\0"+ + "\1\11"; + + private static int [] zzUnpackAttribute() { + int [] result = new int[36]; + int offset = 0; + offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAttribute(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + /** the input device */ + private java.io.Reader zzReader; + + /** the current state of the DFA */ + private int zzState; + + /** the current lexical state */ + private int zzLexicalState = YYINITIAL; + + /** this buffer contains the current text to be matched and is + the source of the yytext() string */ + private char zzBuffer[] = new char[ZZ_BUFFERSIZE]; + + /** the textposition at the last accepting state */ + private int zzMarkedPos; + + /** the current text position in the buffer */ + private int zzCurrentPos; + + /** startRead marks the beginning of the yytext() string in the buffer */ + private int zzStartRead; + + /** endRead marks the last character in the buffer, that has been read + from input */ + private int zzEndRead; + + /** number of newlines encountered up to the start of the matched text */ + private int yyline; + + /** the number of characters up to the start of the matched text */ + private int yychar; + + /** + * the number of characters from the last newline up to the start of the + * matched text + */ + private int yycolumn; + + /** + * zzAtBOL == true <=> the scanner is currently at the beginning of a line + */ + private boolean zzAtBOL = true; + + /** zzAtEOF == true <=> the scanner is at the EOF */ + private boolean zzAtEOF; + + /** denotes if the user-EOF-code has already been executed */ + private boolean zzEOFDone; + + /** + * The number of occupied positions in zzBuffer beyond zzEndRead. + * When a lead/high surrogate has been read from the input stream + * into the final zzBuffer position, this will have a value of 1; + * otherwise, it will have a value of 0. + */ + private int zzFinalHighSurrogate = 0; + + /* user code: */ + + private String sourceCode; + public PropertiesLexer(String sourceCode){ + this(new StringReader(sourceCode)); + this.sourceCode = sourceCode; + } + + public void yypushbackstr(String s, int state) + { + sourceCode = s + sourceCode.substring(yychar + yylength()); + yyreset(new StringReader(sourceCode)); + yybegin(state); + } + + public void yypushbackstr(String s) + { + yypushbackstr(s, YYINITIAL); + } + + StringBuilder string = new StringBuilder(); + + private static String xmlTagName = ""; + + public int yychar() { + return yychar; + } + + private Stack pushedBack = new Stack<>(); + + public int yyline() { + return yyline + 1; + } + + public void pushback(ParsedSymbol symb) { + pushedBack.push(symb); + last = null; + } + + ParsedSymbol last; + public ParsedSymbol lex() throws java.io.IOException, PropertiesParseException{ + ParsedSymbol ret = null; + if (!pushedBack.isEmpty()){ + ret = last = pushedBack.pop(); + } else { + ret = last = yylex(); + } + return ret; + } + + private int count(String str, String target) { + return (str.length() - str.replace(target, "").length()) / target.length(); + } + + + /** + * Creates a new scanner + * + * @param in the java.io.Reader to read input from. + */ + public PropertiesLexer(java.io.Reader in) { + this.zzReader = in; + } + + + /** + * Unpacks the compressed character translation table. + * + * @param packed the packed character translation table + * @return the unpacked character translation table + */ + private static char [] zzUnpackCMap(String packed) { + char [] map = new char[0x110000]; + int i = 0; /* index in packed string */ + int j = 0; /* index in unpacked array */ + while (i < 94) { + int count = packed.charAt(i++); + char value = packed.charAt(i++); + do map[j++] = value; while (--count > 0); + } + return map; + } + + + /** + * Refills the input buffer. + * + * @return false, iff there was new input. + * + * @exception java.io.IOException if any I/O-Error occurs + */ + private boolean zzRefill() throws java.io.IOException { + + /* first: make room (if you can) */ + if (zzStartRead > 0) { + zzEndRead += zzFinalHighSurrogate; + zzFinalHighSurrogate = 0; + System.arraycopy(zzBuffer, zzStartRead, + zzBuffer, 0, + zzEndRead-zzStartRead); + + /* translate stored positions */ + zzEndRead-= zzStartRead; + zzCurrentPos-= zzStartRead; + zzMarkedPos-= zzStartRead; + zzStartRead = 0; + } + + /* is the buffer big enough? */ + if (zzCurrentPos >= zzBuffer.length - zzFinalHighSurrogate) { + /* if not: blow it up */ + char newBuffer[] = new char[zzBuffer.length*2]; + System.arraycopy(zzBuffer, 0, newBuffer, 0, zzBuffer.length); + zzBuffer = newBuffer; + zzEndRead += zzFinalHighSurrogate; + zzFinalHighSurrogate = 0; + } + + /* fill the buffer with new input */ + int requested = zzBuffer.length - zzEndRead; + int totalRead = 0; + while (totalRead < requested) { + int numRead = zzReader.read(zzBuffer, zzEndRead + totalRead, requested - totalRead); + if (numRead == -1) { + break; + } + totalRead += numRead; + } + + if (totalRead > 0) { + zzEndRead += totalRead; + if (totalRead == requested) { /* possibly more input available */ + if (Character.isHighSurrogate(zzBuffer[zzEndRead - 1])) { + --zzEndRead; + zzFinalHighSurrogate = 1; + } + } + return false; + } + + // totalRead = 0: End of stream + return true; + } + + + /** + * Closes the input stream. + */ + public final void yyclose() throws java.io.IOException { + zzAtEOF = true; /* indicate end of file */ + zzEndRead = zzStartRead; /* invalidate buffer */ + + if (zzReader != null) + zzReader.close(); + } + + + /** + * Resets the scanner to read from a new input stream. + * Does not close the old reader. + * + * All internal variables are reset, the old input stream + * cannot be reused (internal buffer is discarded and lost). + * Lexical state is set to ZZ_INITIAL. + * + * Internal scan buffer is resized down to its initial length, if it has grown. + * + * @param reader the new input stream + */ + public final void yyreset(java.io.Reader reader) { + zzReader = reader; + zzAtBOL = true; + zzAtEOF = false; + zzEOFDone = false; + zzEndRead = zzStartRead = 0; + zzCurrentPos = zzMarkedPos = 0; + zzFinalHighSurrogate = 0; + yyline = yychar = yycolumn = 0; + zzLexicalState = YYINITIAL; + if (zzBuffer.length > ZZ_BUFFERSIZE) + zzBuffer = new char[ZZ_BUFFERSIZE]; + } + + + /** + * Returns the current lexical state. + */ + public final int yystate() { + return zzLexicalState; + } + + + /** + * Enters a new lexical state + * + * @param newState the new lexical state + */ + public final void yybegin(int newState) { + zzLexicalState = newState; + } + + + /** + * Returns the text matched by the current regular expression. + */ + public final String yytext() { + return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead ); + } + + + /** + * Returns the character at position pos from the + * matched text. + * + * It is equivalent to yytext().charAt(pos), but faster + * + * @param pos the position of the character to fetch. + * A value from 0 to yylength()-1. + * + * @return the character at position pos + */ + public final char yycharat(int pos) { + return zzBuffer[zzStartRead+pos]; + } + + + /** + * Returns the length of the matched text region. + */ + public final int yylength() { + return zzMarkedPos-zzStartRead; + } + + + /** + * Reports an error that occured while scanning. + * + * In a wellformed scanner (no or only correct usage of + * yypushback(int) and a match-all fallback rule) this method + * will only be called with things that "Can't Possibly Happen". + * If this method is called, something is seriously wrong + * (e.g. a JFlex bug producing a faulty scanner etc.). + * + * Usual syntax/scanner level error handling should be done + * in error fallback rules. + * + * @param errorCode the code of the errormessage to display + */ + private void zzScanError(int errorCode) { + String message; + try { + message = ZZ_ERROR_MSG[errorCode]; + } + catch (ArrayIndexOutOfBoundsException e) { + message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; + } + + throw new Error(message); + } + + + /** + * Pushes the specified amount of characters back into the input stream. + * + * They will be read again by then next call of the scanning method + * + * @param number the number of characters to be read again. + * This number must not be greater than yylength()! + */ + public void yypushback(int number) { + if ( number > yylength() ) + zzScanError(ZZ_PUSHBACK_2BIG); + + zzMarkedPos -= number; + } + + + /** + * Resumes scanning until the next regular expression is matched, + * the end of input is encountered or an I/O-Error occurs. + * + * @return the next token + * @exception java.io.IOException if any I/O-Error occurs + */ + public ParsedSymbol yylex() throws java.io.IOException, PropertiesParseException { + int zzInput; + int zzAction; + + // cached fields: + int zzCurrentPosL; + int zzMarkedPosL; + int zzEndReadL = zzEndRead; + char [] zzBufferL = zzBuffer; + char [] zzCMapL = ZZ_CMAP; + + int [] zzTransL = ZZ_TRANS; + int [] zzRowMapL = ZZ_ROWMAP; + int [] zzAttrL = ZZ_ATTRIBUTE; + + while (true) { + zzMarkedPosL = zzMarkedPos; + + yychar+= zzMarkedPosL-zzStartRead; + + zzAction = -1; + + zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; + + zzState = ZZ_LEXSTATE[zzLexicalState]; + + // set up zzAction for empty match case: + int zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + } + + + zzForAction: { + while (true) { + + if (zzCurrentPosL < zzEndReadL) { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); + zzCurrentPosL += Character.charCount(zzInput); + } + else if (zzAtEOF) { + zzInput = YYEOF; + break zzForAction; + } + else { + // store back cached positions + zzCurrentPos = zzCurrentPosL; + zzMarkedPos = zzMarkedPosL; + boolean eof = zzRefill(); + // get translated positions and possibly new buffer + zzCurrentPosL = zzCurrentPos; + zzMarkedPosL = zzMarkedPos; + zzBufferL = zzBuffer; + zzEndReadL = zzEndRead; + if (eof) { + zzInput = YYEOF; + break zzForAction; + } + else { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); + zzCurrentPosL += Character.charCount(zzInput); + } + } + int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ]; + if (zzNext == -1) break zzForAction; + zzState = zzNext; + + zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + zzMarkedPosL = zzCurrentPosL; + if ( (zzAttributes & 8) == 8 ) break zzForAction; + } + + } + } + + // store back cached position + zzMarkedPos = zzMarkedPosL; + + switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { + case 1: + { string.setLength(0); string.append(yytext()); yybegin(KEY); + } + case 21: break; + case 2: + { return new ParsedSymbol(SymbolType.EMPTY_LINE, null); + } + case 22: break; + case 3: + { + } + case 23: break; + case 4: + { yybegin(COMMENT); + return new ParsedSymbol(SymbolType.COMMENT, yytext().substring(1)); + } + case 24: break; + case 5: + { yybegin(YYINITIAL); + } + case 25: break; + case 6: + { string.append(yytext()); + } + case 26: break; + case 7: + { String key = string.toString(); + yybegin(VALUE); + string.setLength(0); + return new ParsedSymbol(SymbolType.KEY, key); + } + case 27: break; + case 8: + { yybegin(YYINITIAL); + return new ParsedSymbol(SymbolType.VALUE, string.toString()); + } + case 28: break; + case 9: + { string.append(":"); + } + case 29: break; + case 10: + { string.append("\\\\"); + } + case 30: break; + case 11: + { string.append("#"); + } + case 31: break; + case 12: + { string.append("\\t"); + } + case 32: break; + case 13: + { string.append("\\f"); + } + case 33: break; + case 14: + { string.append("\\r"); + } + case 34: break; + case 15: + { string.append("\\n"); + } + case 35: break; + case 16: + { string.append(" "); + } + case 36: break; + case 17: + { string.append("!"); + } + case 37: break; + case 18: + { string.append("="); + } + case 38: break; + case 19: + { string.append("\r\n"); + } + case 39: break; + case 20: + { string.append((char)Integer.parseInt(yytext().substring(2), 16)); + } + case 40: break; + default: + if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { + zzAtEOF = true; + switch (zzLexicalState) { + case VALUE: { + yybegin(YYINITIAL); + return new ParsedSymbol(SymbolType.VALUE, string.toString()); + } + case 37: break; + default: + { + return new ParsedSymbol(SymbolType.EOF, null); + } + } + } + else { + zzScanError(ZZ_NO_MATCH); + } + } + } + } + + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesParseException.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesParseException.java new file mode 100644 index 000000000..58b0beae5 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/PropertiesParseException.java @@ -0,0 +1,9 @@ +package com.jpexs.helpers.properties; + +/** + * + * @author JPEXS + */ +public class PropertiesParseException extends Exception { + +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/SymbolType.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/SymbolType.java new file mode 100644 index 000000000..b7c1a3101 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/properties/SymbolType.java @@ -0,0 +1,13 @@ +package com.jpexs.helpers.properties; + +/** + * + * @author JPEXS + */ +public enum SymbolType { + KEY, + VALUE, + COMMENT, + EMPTY_LINE, + EOF +} diff --git a/resources/translator.bat b/resources/translator.bat new file mode 100644 index 000000000..bbaad0efe --- /dev/null +++ b/resources/translator.bat @@ -0,0 +1,2 @@ +@echo off +start javaw -jar "%~dp0\ffdec.jar" -translator diff --git a/resources/translator.sh b/resources/translator.sh new file mode 100644 index 000000000..01248bdea --- /dev/null +++ b/resources/translator.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Based on Freerapid Downloader startup script - created by Petris 2009 + +# FFDec requires Oracle Java 8 +# Look for java in these directories +LOOKUP_JRE_DIRS="/usr/lib/jvm/* /opt/java* /opt/jre*" +# Required version +REQ_JVER1=1 +REQ_JVER2=8 +REQ_JVER3=0 +REQ_JVER4=0 + +search_jar_file() { + JAR_FILE_CANDIDATES='./ffdec.jar ../dist/ffdec.jar /usr/share/java/ffdec.jar /usr/share/java/ffdec/ffdec.jar /usr/share/java/jpexs-decompiler/ffdec.jar' + for f in $JAR_FILE_CANDIDATES ; do + [ -r "$f" ] && JAR_FILE="$f" && return 0 + done + echo Unable to find ffdec.jar in the following locations: + echo "${JAR_FILE_CANDIDATES// /$'\n'}" + return 1 +} + +check_java_version () { + JVER1=`echo $JAVA_VERSION_OUTPUT | sed 's/java version "\([0-9]*\)\.[0-9]*\.[0-9]*_[0-9]*".*/\1/'` + JVER2=`echo $JAVA_VERSION_OUTPUT | sed 's/java version "[0-9]*\.\([0-9]*\)\.[0-9]*_[0-9]*".*/\1/'` + JVER3=`echo $JAVA_VERSION_OUTPUT | sed 's/java version "[0-9]*\.[0-9]*\.\([0-9]*\)_[0-9]*".*/\1/'` + JVER4=`echo $JAVA_VERSION_OUTPUT | sed 's/java version "[0-9]*\.[0-9]*\.[0-9]*_\([0-9]*\)".*/\1/'` + + if [ $JVER1 -gt $REQ_JVER1 ]; then + return 0 + elif [ $JVER1 -lt $REQ_JVER1 ]; then + return 1 + fi + + if [ $JVER2 -gt $REQ_JVER2 ]; then + return 0 + elif [ $JVER2 -lt $REQ_JVER2 ]; then + return 1 + fi + + if [ $JVER3 -gt $REQ_JVER3 ]; then + return 0 + elif [ $JVER3 -lt $REQ_JVER3 ]; then + return 1 + fi + + if [ $JVER4 -lt $REQ_JVER4 ]; then + return 1 + fi + + return 0 +} + +# Handle symlinks +PROGRAM="$0" +while [ -L "$PROGRAM" ]; do + PROGRAM=`readlink -f "$PROGRAM"` +done +pushd "`dirname \"$PROGRAM\"`" > /dev/null + +search_jar_file || exit 1 + +if [ ${JAR_FILE:0:1} != '/' ] ; then + JAR_FILE=`pwd`/$JAR_FILE +fi + +popd > /dev/null + +args=(-jar $JAR_FILE -translator) + +if [ "`uname`" = "Darwin" ]; then + args=(-Xdock:name=FFDec -Xdock:icon=icon.png "${args[@]}") +fi + +# Check default java +if [ -x "`which java`" ]; then + JAVA_VERSION_OUTPUT=`java -version 2>&1` + JAVA_VERSION_OUTPUT=`echo $JAVA_VERSION_OUTPUT | sed 's/openjdk version/java version/'` + check_java_version && exec java "${args[@]}" +fi + +# Test other possible Java locations +for JRE_PATH in $LOOKUP_JRE_DIRS; do + if [ -x "$JRE_PATH/bin/java" ]; then + JAVA_VERSION_OUTPUT=`"$JRE_PATH/bin/java" -version 2>&1` + JAVA_VERSION_OUTPUT=`echo $JAVA_VERSION_OUTPUT | sed 's/openjdk version/java version/'` + check_java_version && { + export JRE_PATH + exec $JRE_PATH/bin/java "${args[@]}" + } + fi +done + +# Failed +if [ -x "`which xmessage`" ]; then + xmessage -nearmouse -file - <. + */ +package com.jpexs.decompiler.flash.gui.translator; + +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.gui.View; +import com.jpexs.decompiler.flash.gui.ViewMessages; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.properties.ParsedSymbol; +import com.jpexs.helpers.properties.PropertiesLexer; +import com.jpexs.helpers.properties.PropertiesParseException; +import com.jpexs.helpers.properties.SymbolType; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import javax.swing.AbstractCellEditor; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import static javax.swing.WindowConstants.EXIT_ON_CLOSE; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.filechooser.FileFilter; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumnModel; + +/** + * + * @author JPEXS + */ +public class Translator extends JFrame implements ItemListener { + + private TreeSet locales = new TreeSet<>(); + private Map> resourceLocales = new TreeMap<>(); + private Set resourceKeys = new TreeSet<>(); + private Map>> resourceValues = new TreeMap<>(); + + private Map>> comments = new TreeMap<>(); + + private Map>> newValues = new TreeMap<>(); + private Map>> newLinesBeforeComment = new TreeMap<>(); + private Map>> newLinesAfterComment = new TreeMap<>(); + + private Map> hiddenKeys = new TreeMap<>(); + + private JTable table; + private DefaultTableModel tableModel; + private JComboBox localeComboBox; + private JComboBox resourcesComboBox; + + private String lastSaveDir = ""; + + private static final String DO_NOT_EDIT = "!!!! FFDec translators - please do not edit anything below this line !!!"; + + private List ignoredResources = Arrays.asList( + "project", + "META-INF/services/jsyntaxpane/kitsfortypes", + "META-INF/services/jsyntaxpane/syntaxkits/actionscript3syntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/actionscriptsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/flasm3methodinfosyntaxkit/abbreviations", + "META-INF/services/jsyntaxpane/syntaxkits/bashsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/clojuresyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/dosbatchsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/flasmsyntaxkit/abbreviations", + "META-INF/services/jsyntaxpane/syntaxkits/flasm3syntaxkit/abbreviations", + "META-INF/services/jsyntaxpane/syntaxkits/groovysyntaxkit/abbreviations", + "META-INF/services/jsyntaxpane/syntaxkits/groovysyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/javascriptsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/javasyntaxkit/abbreviations", + "META-INF/services/jsyntaxpane/syntaxkits/javasyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/luasyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/plainsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/propertiessyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/pythonsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/rubysyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/scalasyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/sqlsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/talsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/xhtmlsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/xmlsyntaxkit/config", + "META-INF/services/jsyntaxpane/syntaxkits/xpathsyntaxkit/config", + "META-INF/maven/jsyntaxpane/jsyntaxpane/pom" + ); + + private String readStreamAsString(InputStream is) throws IOException { + byte buf[] = new byte[1024]; + int cnt = 0; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while ((cnt = is.read(buf)) > 0) { + baos.write(buf, 0, cnt); + } + return new String(baos.toByteArray(), "UTF-8"); + } + + private void loadJar(File file, String fileTitle) throws FileNotFoundException, IOException, URISyntaxException { + ZipInputStream zis = new ZipInputStream(new FileInputStream(file)); + ZipEntry zipEntry = zis.getNextEntry(); + Pattern pat = Pattern.compile("(?(.*/)?[^/_]+)(_(?[^/\\.]+))?\\.properties"); + + while (zipEntry != null) { + if (!zipEntry.isDirectory()) { + String name = zipEntry.getName(); + Matcher m = pat.matcher(name); + if (m.matches()) { + String resource = m.group("resource"); + if (!ignoredResources.contains(resource)) { + resource = fileTitle + ": " + resource; + String locale = m.group("locale"); + if (locale == null) { + locale = "en"; + } + if (!resourceLocales.containsKey(resource)) { + resourceLocales.put(resource, new TreeSet<>()); + resourceKeys.add(resource); + resourceValues.put(resource, new LinkedHashMap<>()); + hiddenKeys.put(resource, new HashSet<>()); + } + if (!resourceValues.get(resource).containsKey(locale)) { + resourceValues.get(resource).put(locale, new LinkedHashMap<>()); + } + resourceLocales.get(resource).add(locale); + locales.add(locale); + String propertiesData = readStreamAsString(zis); + PropertiesLexer lexer = new PropertiesLexer(propertiesData); + try { + ParsedSymbol s = lexer.lex(); + boolean hidden = false; + String comment = ""; + int numEmptyLinesBeforeComment = 0; + int numEmptyLinesAfterComment = 0; + while (s.type != SymbolType.EOF) { + if (s.type == SymbolType.COMMENT) { + if (((String) s.value).trim().equals(DO_NOT_EDIT)) { + hidden = true; + } else { + if (comment.isEmpty()) { + comment = (String) s.value; + } else { + comment = comment + "\r\n" + s.value; + } + } + s = lexer.lex(); + continue; + } + if (s.type == SymbolType.EMPTY_LINE) { + if (comment.isEmpty()) { + numEmptyLinesBeforeComment++; + } else { + numEmptyLinesAfterComment++; + } + s = lexer.lex(); + continue; + } + //System.out.println(s); + if (s.type == SymbolType.EOF) { + break; + } + if (s.type != SymbolType.KEY) { + throw new RuntimeException("KEY EXPECTED"); + //break; + } + String key = (String) s.value; + s = lexer.lex(); + + if (s.type != SymbolType.VALUE) { + throw new RuntimeException("VALUE EXPECTED"); + //break; + } + String value = (String) s.value; + + resourceValues.get(resource).get(locale).put(key, value); + if (hidden) { + hiddenKeys.get(resource).add(key); + } + + if (!comment.isEmpty()) { + if (!comments.containsKey(resource)) { + comments.put(resource, new LinkedHashMap<>()); + } + if (!comments.get(resource).containsKey(locale)) { + comments.get(resource).put(locale, new LinkedHashMap<>()); + } + comments.get(resource).get(locale).put(key, comment); + } + + if (numEmptyLinesAfterComment > 0) { + if (!newLinesAfterComment.containsKey(resource)) { + newLinesAfterComment.put(resource, new LinkedHashMap<>()); + } + if (!newLinesAfterComment.get(resource).containsKey(locale)) { + newLinesAfterComment.get(resource).put(locale, new LinkedHashMap<>()); + } + newLinesAfterComment.get(resource).get(locale).put(key, numEmptyLinesAfterComment); + } + + if (numEmptyLinesBeforeComment > 0) { + if (!newLinesBeforeComment.containsKey(resource)) { + newLinesBeforeComment.put(resource, new LinkedHashMap<>()); + } + if (!newLinesBeforeComment.get(resource).containsKey(locale)) { + newLinesBeforeComment.get(resource).put(locale, new LinkedHashMap<>()); + } + newLinesBeforeComment.get(resource).get(locale).put(key, numEmptyLinesBeforeComment); + } + numEmptyLinesAfterComment = 0; + numEmptyLinesBeforeComment = 0; + + comment = ""; + //System.out.println(resource+": locale="+locale+" key="+key+" value="+value); + s = lexer.lex(); + } + //System.exit(0); + } catch (PropertiesParseException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + zis.close(); + } + + private void loadJarTry(String path) throws FileNotFoundException, IOException, URISyntaxException { + File file; + file = new File("dist/" + path); + if (!file.exists()) { + file = new File(path); + } + loadJar(file, path); + } + + private void loadItems() throws FileNotFoundException, IOException, URISyntaxException { + loadJarTry("ffdec.jar"); + loadJarTry("lib/ffdec_lib.jar"); + loadJarTry("lib/jsyntaxpane-0.9.5.jar"); + } + + public Translator() throws IOException, FileNotFoundException, URISyntaxException { + + loadItems(); + + setDefaultCloseOperation(EXIT_ON_CLOSE); + Container cnt = getContentPane(); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + try { + save(); + saveWindow(); + } catch (IOException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + } + + }); + + List localeItems = new ArrayList(); + for (String locale : locales) { + localeItems.add(new LocaleItem(locale)); + } + localeComboBox = new JComboBox<>(localeItems.toArray(new LocaleItem[localeItems.size()])); + + List resources = new ArrayList<>(); + for (String resource : resourceLocales.keySet()) { + resources.add(new ResourceItem(resource)); + } + + resourcesComboBox = new JComboBox<>(resources.toArray(new ResourceItem[resources.size()])); + resourcesComboBox.addItemListener(this); + localeComboBox.addItemListener(this); + + tableModel = new DefaultTableModel(); + table = new JTable(tableModel) { + @Override + public boolean isCellEditable(int row, int column) { + return column >= 1; + } + + }; + + tableModel.addColumn("key"); + tableModel.addColumn("en"); + tableModel.addColumn("translated"); + + tableModel.addRow(new Object[]{"menu.new", "New", "Nový"}); + tableModel.addRow(new Object[]{"error.missing", "Error: missing xy", "Chyba: chybí xy"}); + + table.getColumn("key").setCellRenderer(new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + String key = (String) value; + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + + if (comments.containsKey(resource) + && comments.get(resource).containsKey(locale) + && comments.get(resource).get(locale).containsKey(key)) { + + //"General Public License" + String comment = comments.get(resource).get(locale).get(key); + if (comment.contains("General Public License")) { + label.setIcon(null); + label.setToolTipText(null); + } else { + label.setIcon(View.getIcon("about16")); + label.setToolTipText(comment); + } + } else { + label.setIcon(null); + label.setToolTipText(null); + } + + return label; + } + + }); + + table.getColumn("en").setCellRenderer(new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + String valueStr = "" + value; + label.setText("" + valueStr.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\r\n", "
") + ""); + + return label; + } + + }); + table.getColumn("translated").setCellRenderer(new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + String key = (String) table.getValueAt(row, 0); + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + String oldValue = null; + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key)) { + oldValue = resourceValues.get(resource).get(locale).get(key); + } + String enValue = resourceValues.get(resource).get("en").get(key); + if (enValue == null) { + enValue = ""; + } + + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key)) { + if (!Objects.equals(oldValue, value)) { + label.setBackground(new Color(0xaa, 0xaa, 0xff)); + } else { + label.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + } + } else if (newValues.containsKey(resource) + && newValues.get(resource).containsKey(locale) + && newValues.get(resource).get(locale).containsKey(key)) { + label.setBackground(new Color(0xaa, 0xff, 0xaa)); + } else if ((!resourceValues.get(resource).containsKey(locale) + || !resourceValues.get(resource).get(locale).containsKey(key)) + && !enValue.equals("")) { + label.setBackground(new Color(0xff, 0xaa, 0xaa)); + } else { + label.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + } + + String valueStr = "" + value; + label.setText("" + valueStr.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\r\n", "
") + ""); + + return label; + } + + }); + + table.putClientProperty("terminateEditOnFocusLost", true); + table.getColumn("translated").setCellEditor(new JTextAreaColumn(resourceValues, localeComboBox, resourcesComboBox)); + + table.getColumn("en").setCellEditor(new JTextAreaColumn(resourceValues, localeComboBox, resourcesComboBox)); + + TableCellEditor editor = table.getColumn("translated").getCellEditor(); + editor.addCellEditorListener(getEditorListener()); + editor.addCellEditorListener(new CellEditorListener() { + @Override + public void editingStopped(ChangeEvent e) { + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + String key = (String) table.getModel().getValueAt(table.getSelectedRow(), 0); + String value = (String) editor.getCellEditorValue(); + String oldValue = null; + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key)) { + oldValue = resourceValues.get(resource).get(locale).get(key); + } + String enValue = null; + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey("en") + && resourceValues.get(resource).get("en").containsKey(key)) { + enValue = resourceValues.get(resource).get("en").get(key); + } + if (Objects.equals(oldValue, value) || ("".equals(value) && !enValue.equals(""))) { + if (newValues.containsKey(resource) + && newValues.get(resource).containsKey(locale) + && newValues.get(resource).get(locale).containsKey(key)) { + newValues.get(resource).get(locale).remove(key); + if (newValues.get(resource).get(locale).isEmpty()) { + newValues.get(resource).remove(locale); + } + if (newValues.get(resource).isEmpty()) { + newValues.remove(resource); + } + } + } else { + if (!newValues.containsKey(resource)) { + newValues.put(resource, new LinkedHashMap<>()); + } + if (!newValues.get(resource).containsKey(locale)) { + newValues.get(resource).put(locale, new LinkedHashMap<>()); + } + newValues.get(resource).get(locale).put(key, value); + } + try { + save(); + } catch (IOException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + updateCounts(); + } + + @Override + public void editingCanceled(ChangeEvent e) { + + } + }); + + cnt.setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + + c.gridx = 0; + c.gridy = 0; + topPanel.add(new JLabel("Locale:"), c); + + c.gridx = 1; + c.gridy = 0; + topPanel.add(localeComboBox, c); + + c.gridx = 2; + c.gridy = 0; + + JButton newLocaleButton = new JButton("New"); + newLocaleButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String newLocale = ViewMessages.showInputDialog(Translator.this, "Enter new locale identifier", "New locale", ""); + if (newLocale == null) { + return; + } + newLocale = newLocale.trim(); + if (newLocale.equals("")) { + return; + } + + for (int i = 0; i < localeComboBox.getItemCount(); i++) { + String locale = localeComboBox.getItemAt(i).locale; + if (locale.equals(newLocale)) { + ViewMessages.showMessageDialog(Translator.this, "Locale already exists", "Already exists", JOptionPane.ERROR_MESSAGE); + return; + } + } + locales.add(newLocale); + + List localeItems = new ArrayList(); + int index = -1; + int i = 0; + for (String locale : locales) { + if (locale.equals(newLocale)) { + index = i; + } + i++; + localeItems.add(new LocaleItem(locale)); + } + localeComboBox.setModel(new DefaultComboBoxModel<>(localeItems.toArray(new LocaleItem[localeItems.size()]))); + localeComboBox.setSelectedIndex(index); + } + }); + topPanel.add(newLocaleButton, c); + + c.gridx = 0; + c.gridy = 1; + topPanel.add(new JLabel("Resource:"), c); + + c.gridx = 1; + c.gridy = 1; + topPanel.add(resourcesComboBox, c); + + c.gridx = 2; + c.gridy = 1; + + JButton nextMissingButton = new JButton("Next missing"); + nextMissingButton.addActionListener(new ActionListener() { + private void selectFirstMissing() { + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + int row = 0; + for (String key : resourceValues.get(resource).get("en").keySet()) { + if (!resourceValues.containsKey(resource) + || !resourceValues.get(resource).containsKey(locale) + || !resourceValues.get(resource).get(locale).containsKey(key)) { + if (!newValues.containsKey(resource) + || !newValues.get(resource).containsKey(locale) + || !newValues.get(resource).get(locale).containsKey(key)) { + table.scrollRectToVisible(table.getCellRect(row, 0, false)); + break; + } + } + row++; + } + } + + @Override + public void actionPerformed(ActionEvent e) { + for (int i = resourcesComboBox.getSelectedIndex() + 1; i < resourcesComboBox.getItemCount(); i++) { + ResourceItem item = resourcesComboBox.getItemAt(i); + if (item.missingCount > 0) { + resourcesComboBox.setSelectedIndex(i); + selectFirstMissing(); + return; + } + } + for (int i = 0; i < resourcesComboBox.getItemCount(); i++) { + ResourceItem item = resourcesComboBox.getItemAt(i); + if (item.missingCount > 0) { + resourcesComboBox.setSelectedIndex(i); + selectFirstMissing(); + return; + } + } + } + }); + + topPanel.add(nextMissingButton, c); + + cnt.add(topPanel, BorderLayout.NORTH); + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + JButton exportButton = new JButton("Export JPT"); + exportButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser fc = new JFileChooser(); + FileFilter jptFilter = new FileFilter() { + @Override + public boolean accept(File f) { + return (f.getName().toLowerCase(Locale.ENGLISH).endsWith(".jpt")) || (f.isDirectory()); + } + + @Override + public String getDescription() { + return "JPEXS translation files (*.jpt)"; + } + }; + fc.setFileFilter(jptFilter); + fc.setAcceptAllFileFilterUsed(false); + fc.setSelectedFile(new File("translation.jpt")); + if (!lastSaveDir.isEmpty()) { + fc.setCurrentDirectory(new File(lastSaveDir)); + } + if (fc.showSaveDialog(Translator.this) == JFileChooser.APPROVE_OPTION) { + File file = Helper.fixDialogFile(fc.getSelectedFile()); + String fileName = file.getAbsolutePath(); + FileFilter selFilter = fc.getFileFilter(); + if (selFilter == jptFilter) { + if (!fileName.toLowerCase(Locale.ENGLISH).endsWith(".jpt")) { + fileName += ".jpt"; + } + } + File targetFile = new File(fileName); + if (targetFile.exists()) { + targetFile.delete(); + } + try { + Files.copy(new File(getStorageFile()).toPath(), targetFile.toPath()); + } catch (IOException ex) { + ViewMessages.showMessageDialog(Translator.this, "Error during saving: " + ex.getLocalizedMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + lastSaveDir = file.getParentFile().getAbsolutePath(); + } + } + }); + + JButton importButton = new JButton("Import JPT"); + importButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (ViewMessages.showConfirmDialog(Translator.this, "WARNING: This will erase all your previous work and imports data from other JPT file", "WARNING", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) { + + JFileChooser fc = new JFileChooser(); + FileFilter jptFilter = new FileFilter() { + @Override + public boolean accept(File f) { + return (f.getName().toLowerCase(Locale.ENGLISH).endsWith(".jpt")) || (f.isDirectory()); + } + + @Override + public String getDescription() { + return "JPEXS translation files (*.jpt)"; + } + }; + fc.setFileFilter(jptFilter); + fc.setAcceptAllFileFilterUsed(false); + fc.setSelectedFile(new File("translation.jpt")); + if (!lastSaveDir.isEmpty()) { + fc.setCurrentDirectory(new File(lastSaveDir)); + } + if (fc.showOpenDialog(Translator.this) == JFileChooser.APPROVE_OPTION) { + File file = Helper.fixDialogFile(fc.getSelectedFile()); + String fileName = file.getAbsolutePath(); + File targetFile = new File(getStorageFile()); + File tmpFile = new File(getStorageFile() + ".tmp"); + try { + if (targetFile.exists()) { + targetFile.renameTo(tmpFile); + } + Files.copy(new File(fileName).toPath(), targetFile.toPath()); + tmpFile.delete(); + load(); + } catch (IOException ex) { + tmpFile.renameTo(targetFile); + ViewMessages.showMessageDialog(Translator.this, "Error during importing: " + ex.getLocalizedMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + } + } + } + }); + + JButton startOverButton = new JButton("Start over"); + startOverButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (ViewMessages.showConfirmDialog(Translator.this, "Do you really want to lose all your work? This is unreversable!", "WARNING", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) { + newValues.clear(); + try { + save(); + load(); + } catch (IOException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + + } + } + }); + buttonsPanel.add(importButton); + buttonsPanel.add(exportButton); + buttonsPanel.add(startOverButton); + + cnt.add(buttonsPanel, BorderLayout.SOUTH); + + cnt.add(new JScrollPane(table), BorderLayout.CENTER); + resizeColumnWidth(table); + setSize(800, 600); + setTitle("JPEXS Free Flash Decompiler Translator"); + itemStateChanged(null); + table.setRowHeight(table.getFont().getSize() + 10); + updateRowHeights(table); + View.centerScreen(this); + + List images = new ArrayList<>(); + images.add(View.loadImage("icon16")); + images.add(View.loadImage("icon32")); + images.add(View.loadImage("icon48")); + images.add(View.loadImage("icon256")); + setIconImages(images); + load(); + loadWindow(); + + saveAll();//fixme + } + + private void updateCounts() { + String selectedLocale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + for (int j = 0; j < localeComboBox.getItemCount(); j++) { + LocaleItem localeItem = localeComboBox.getItemAt(j); + String locale = localeItem.locale; + int localeMissingCount = 0; + int localeNewCount = 0; + int localeModifiedCount = 0; + + for (int i = 0; i < resourcesComboBox.getItemCount(); i++) { + ResourceItem resourceItem = resourcesComboBox.getItemAt(i); + String resource = resourceItem.resource; + + int missingCount = 0; + int newCount = 0; + int modifiedCount = 0; + for (String key : resourceValues.get(resource).get("en").keySet()) { + if (resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key)) { + if (newValues.containsKey(resource) + && newValues.get(resource).containsKey(locale) + && newValues.get(resource).get(locale).containsKey(key)) { + modifiedCount++; + } + } else { + if (newValues.containsKey(resource) + && newValues.get(resource).containsKey(locale) + && newValues.get(resource).get(locale).containsKey(key)) { + newCount++; + } else { + missingCount++; + } + } + } + if (locale.equals(selectedLocale)) { + resourceItem.missingCount = missingCount; + resourceItem.modifiedCount = modifiedCount; + resourceItem.newCount = newCount; + } + localeMissingCount += missingCount; + localeModifiedCount += modifiedCount; + localeNewCount += newCount; + } + + localeItem.missingCount = localeMissingCount; + localeItem.modifiedCount = localeModifiedCount; + localeItem.newCount = localeNewCount; + } + repaint(); + + } + + private CellEditorListener getEditorListener() { + return new CellEditorListener() { + + @Override + public void editingStopped(ChangeEvent e) { + updateRowHeights(table); + } + + @Override + public void editingCanceled(ChangeEvent e) { + updateRowHeights(table); + } + }; + } + + private void updateRowHeights(JTable table) { + try { + for (int row = 0; row < table.getRowCount(); row++) { + int rowHeight = table.getRowHeight(); + + for (int column = 0; column < table.getColumnCount(); column++) { + Component comp = table.prepareRenderer( + table.getCellRenderer(row, column), row, column); + rowHeight = Math.max(rowHeight, + comp.getPreferredSize().height); + } + + table.setRowHeight(row, rowHeight); + } + } catch (ClassCastException e) { + } + } + + @Override + public void itemStateChanged(ItemEvent e) { + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + tableModel.setRowCount(0); + for (String key : resourceValues.get(resource).get("en").keySet()) { + String value = ""; + if (newValues.containsKey(resource) && newValues.get(resource).containsKey(locale) && newValues.get(resource).get(locale).containsKey(key)) { + value = newValues.get(resource).get(locale).get(key); + } else if (resourceValues.get(resource).containsKey(locale) && resourceValues.get(resource).get(locale).containsKey(key)) { + value = resourceValues.get(resource).get(locale).get(key); + } + if (hiddenKeys.get(resource).contains(key)) { + continue; + } + tableModel.addRow(new Object[]{key, resourceValues.get(resource).get("en").get(key), value}); + } + updateRowHeights(table); + updateCounts(); + } + + public void resizeColumnWidth(JTable table) { + final TableColumnModel columnModel = table.getColumnModel(); + for (int column = 0; column < table.getColumnCount(); column++) { + int width = 15; // Min width + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer renderer = table.getCellRenderer(row, column); + Component comp = table.prepareRenderer(renderer, row, column); + width = Math.max(comp.getPreferredSize().width + 1, width); + } + if (width > 300) { + width = 300; + } + columnModel.getColumn(column).setPreferredWidth(width); + } + } + + private String getStorageFile() { + return Configuration.getFFDecHome() + "translated.zip"; + } + + private String getWindowFile() { + return Configuration.getFFDecHome() + "translated.ini"; + } + + private String escapeUnicode(String s) { + StringBuilder b = new StringBuilder(); + + for (char c : s.toCharArray()) { + if (c >= 128) { + b.append("\\u").append(String.format("%04x", (int) c)); + } else { + b.append(c); + } + } + + return b.toString(); + } + + private String escapeKey(String s) { + if (s.startsWith("#") || s.startsWith("!")) { + s = "\\" + s; + } + s = escapeUnicode(s); + return s; + } + + private String escapeValue(String s) { + if (s.startsWith(" ")) { + s = "\\" + s; + } + s = s.replace("\r\n", "\n"); + s = s.replace("\n", "\\\r\n "); + s = escapeUnicode(s); + return s; + } + + private void load() throws IOException { + newValues.clear(); + ZipInputStream zis = new ZipInputStream(new FileInputStream(getStorageFile())); + ZipEntry zipEntry = zis.getNextEntry(); + Pattern pat = Pattern.compile("(?(.*/)?[^/_]+)(_(?[^/\\.]+))?\\.properties"); + + while (zipEntry != null) { + if (!zipEntry.isDirectory()) { + String name = zipEntry.getName(); + Matcher m = pat.matcher(name); + if (m.matches()) { + String resource = m.group("resource"); + resource = resource.replace(".jar/", ".jar: "); + String locale = m.group("locale"); + if (locale == null) { + locale = "en"; + } + locales.add(locale); + if (!newValues.containsKey(resource)) { + newValues.put(resource, new LinkedHashMap<>()); + } + if (!newValues.get(resource).containsKey(locale)) { + newValues.get(resource).put(locale, new LinkedHashMap<>()); + } + resourceLocales.get(resource).add(locale); + locales.add(locale); + String propertiesData = readStreamAsString(zis); + PropertiesLexer lexer = new PropertiesLexer(propertiesData); + try { + ParsedSymbol s = lexer.lex(); + boolean hidden = false; + while (s.type != SymbolType.EOF) { + if (s.type == SymbolType.COMMENT) { + if (((String) s.value).trim().equals(DO_NOT_EDIT)) { + hidden = true; + } + s = lexer.lex(); + continue; + } + if (s.type == SymbolType.EMPTY_LINE) { + s = lexer.lex(); + continue; + } + //System.out.println(s); + if (s.type == SymbolType.EOF) { + break; + } + if (s.type != SymbolType.KEY) { + throw new RuntimeException("KEY EXPECTED"); + //break; + } + String key = (String) s.value; + s = lexer.lex(); + + if (s.type != SymbolType.VALUE) { + throw new RuntimeException("VALUE EXPECTED"); + //break; + } + String value = (String) s.value; + if (!hidden) { + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key) + && Objects.equals(resourceValues.get(resource).get(locale).get(key), value)) { + //same, ignore + } else { + newValues.get(resource).get(locale).put(key, value); + } + } + //System.out.println(resource+": locale="+locale+" key="+key+" value="+value); + s = lexer.lex(); + } + //System.exit(0); + } catch (PropertiesParseException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + + } + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + zis.close(); + + List localeItems = new ArrayList(); + for (String locale : locales) { + localeItems.add(new LocaleItem(locale)); + } + localeComboBox.setModel(new DefaultComboBoxModel<>(localeItems.toArray(new LocaleItem[localeItems.size()]))); + + itemStateChanged(null); + repaint(); + } + + private void loadWindow() throws FileNotFoundException, IOException { + if (!new File(getWindowFile()).exists()) { + return; + } + + FileReader fileReader = new FileReader(getWindowFile()); + BufferedReader reader = new BufferedReader(fileReader); + String s = null; + while ((s = reader.readLine()) != null) { + if (!s.contains("=")) { + continue; + } + String key = s.substring(0, s.indexOf("=")); + String value = s.substring(s.indexOf("=") + 1); + + int valueInt; + switch (key) { + case "locale": + for (int i = 0; i < localeComboBox.getItemCount(); i++) { + LocaleItem item = (LocaleItem) localeComboBox.getItemAt(i); + if (item.locale.equals(value)) { + localeComboBox.setSelectedIndex(i); + break; + } + } + break; + case "resource": + resourcesComboBox.setSelectedItem(value); + break; + case "window.x": + int x = Integer.parseInt(value); + setLocation(x, getLocation().y); + break; + case "window.y": + int y = Integer.parseInt(value); + setLocation(getLocation().x, y); + break; + case "window.width": + int width = Integer.parseInt(value); + setSize(width, getSize().height); + break; + case "window.height": + int height = Integer.parseInt(value); + setSize(getSize().width, height); + break; + case "window.maximized": + if (value.equals("true")) { + setExtendedState(getExtendedState() | JFrame.MAXIMIZED_BOTH); + } + break; + case "column.key.width": + valueInt = Integer.parseInt(value); + table.getColumn("key").setPreferredWidth(valueInt); + break; + case "column.en.width": + valueInt = Integer.parseInt(value); + table.getColumn("en").setPreferredWidth(valueInt); + break; + case "column.translated.width": + valueInt = Integer.parseInt(value); + table.getColumn("translated").setPreferredWidth(valueInt); + break; + case "export.dir": + lastSaveDir = value; + break; + } + + } + fileReader.close(); + } + + private void saveWindow() throws IOException { + + FileWriter writer = new FileWriter(new File(getWindowFile())); + PrintWriter pw = new PrintWriter(writer); + pw.println("locale=" + ((LocaleItem) localeComboBox.getSelectedItem()).locale); + pw.println("resource=" + ((ResourceItem) resourcesComboBox.getSelectedItem()).resource); + pw.println("window.x=" + getLocation().x); + pw.println("window.y=" + getLocation().y); + pw.println("window.width=" + getSize().width); + pw.println("window.height=" + getSize().height); + if ((getExtendedState() & MAXIMIZED_BOTH) > 0) { + pw.println("window.maximized=true"); + } else { + pw.println("window.maximized=false"); + } + pw.println("column.key.width=" + table.getColumn("key").getWidth()); + pw.println("column.en.width=" + table.getColumn("en").getWidth()); + pw.println("column.translated.width=" + table.getColumn("translated").getWidth()); + pw.println("export.dir=" + lastSaveDir); + + writer.close(); + } + + private void save() throws FileNotFoundException, IOException { + save(getStorageFile(), newValues); + } + + private void saveAll() throws FileNotFoundException, IOException { + String storageFile = Configuration.getFFDecHome() + "alltranslated.zip"; + save(storageFile, resourceValues); + } + + private void save(String storageFile, Map>> values) throws FileNotFoundException, IOException { + String storageFileTmp = storageFile + ".tmp"; + FileOutputStream fos = new FileOutputStream(storageFileTmp); + ZipOutputStream zipOut = new ZipOutputStream(fos); + for (String resource : values.keySet()) { + for (String locale : values.get(resource).keySet()) { + String resourceZipName = resource.replace(": ", "/"); + String propertiesPath = resourceZipName + (locale.equals("en") ? "" : "_" + locale) + ".properties"; + ZipEntry zipEntry = new ZipEntry(propertiesPath); + zipOut.putNextEntry(zipEntry); + for (String key : resourceValues.get(resource).get("en").keySet()) { + String value; + if (values.containsKey(resource) + && values.get(resource).containsKey(locale) + && values.get(resource).get(locale).containsKey(key)) { + value = values.get(resource).get(locale).get(key); + } else if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey(locale) + && resourceValues.get(resource).get(locale).containsKey(key)) { + value = resourceValues.get(resource).get(locale).get(key); + } else { + continue; + } + + if (newLinesBeforeComment.containsKey(resource) + && newLinesBeforeComment.get(resource).containsKey(locale) + && newLinesBeforeComment.get(resource).get(locale).containsKey(key)) { + int numLines = newLinesBeforeComment.get(resource).get(locale).get(key); + for (int i = 0; i < numLines; i++) { + zipOut.write("\r\n".getBytes("UTF-8")); + } + } + if (comments.containsKey(resource) + && comments.get(resource).containsKey(locale) + && comments.get(resource).get(locale).containsKey(key)) { + String comment = comments.get(resource).get(locale).get(key); + comment = "#" + comment.replace("\r\n", "\r\n#") + "\r\n"; + zipOut.write(comment.getBytes("UTF-8")); + } + if (newLinesAfterComment.containsKey(resource) + && newLinesAfterComment.get(resource).containsKey(locale) + && newLinesAfterComment.get(resource).get(locale).containsKey(key)) { + int numLines = newLinesAfterComment.get(resource).get(locale).get(key); + for (int i = 0; i < numLines; i++) { + zipOut.write("\r\n".getBytes("UTF-8")); + } + } + zipOut.write((escapeKey(key) + " = " + escapeValue(value) + "\r\n").getBytes("UTF-8")); + } + } + } + zipOut.close(); + fos.close(); + if (!new File(storageFile).exists() || new File(storageFile).delete()) { + new File(storageFileTmp).renameTo(new File(storageFile)); + } + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + try { + + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + } catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { + } + try { + new Translator().setVisible(true); + } catch (IOException | URISyntaxException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex); + } + } +} + +class LocaleItem { + + public String locale; + public int missingCount = 0; + public int modifiedCount = 0; + public int newCount = 0; + + public LocaleItem(String locale) { + this.locale = locale; + } + + @Override + public String toString() { + String[] parts = locale.split("_"); + Locale loc; + if (parts.length == 2) { + loc = new Locale(parts[0], parts[1]); + } else { + loc = new Locale(parts[0]); + } + + String ret = loc.getDisplayName() + " [" + locale + "]"; + if (missingCount > 0 || modifiedCount > 0 || newCount > 0) { + ret += " ("; + List parts2 = new ArrayList<>(); + if (missingCount > 0) { + parts2.add("" + missingCount + " missing"); + } + if (modifiedCount > 0) { + parts2.add("" + modifiedCount + " modified"); + } + if (newCount > 0) { + parts2.add("" + newCount + " new"); + } + ret += String.join(", ", parts2); + + ret += ")"; + + } + + return ret; + } + +} + +class ResourceItem { + + public String resource; + public int missingCount = 0; + public int modifiedCount = 0; + public int newCount = 0; + + public ResourceItem(String resource) { + this.resource = resource; + } + + @Override + public String toString() { + String ret = resource; + if (missingCount > 0 || modifiedCount > 0 || newCount > 0) { + ret += " ("; + List parts = new ArrayList<>(); + if (missingCount > 0) { + parts.add("" + missingCount + " missing"); + } + if (modifiedCount > 0) { + parts.add("" + modifiedCount + " modified"); + } + if (newCount > 0) { + parts.add("" + newCount + " new"); + } + ret += String.join(", ", parts); + + ret += ")"; + + } + + return ret; + } +} + +class JTextAreaColumn extends AbstractCellEditor implements TableCellEditor { + + private JTextArea textArea = new JTextArea(); + private JTextField textField = new JTextField(); + private JScrollPane pane = new JScrollPane(textArea); + private boolean areaMode = false; + private Map>> resourceValues; + private JComboBox localeComboBox; + private JComboBox resourcesComboBox; + + public JTextAreaColumn( + Map>> resourceValues, + JComboBox localeComboBox, + JComboBox resourcesComboBox + ) { + pane.setBorder(null); + this.resourceValues = resourceValues; + this.localeComboBox = localeComboBox; + this.resourcesComboBox = resourcesComboBox; + } + + @Override + public Object getCellEditorValue() { + String val; + if (areaMode) { + return textArea.getText().replace("\r\n", "\n").replace("\n", "\r\n"); + } + return textField.getText(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, + Object value, boolean isSelected, int row, int column) { + + String locale = ((LocaleItem) localeComboBox.getSelectedItem()).locale; + String resource = ((ResourceItem) resourcesComboBox.getSelectedItem()).resource; + String key = (String) table.getModel().getValueAt(row, 0); + + String enValue = ""; + if (resourceValues.containsKey(resource) + && resourceValues.get(resource).containsKey("en") + && resourceValues.get(resource).get("en").containsKey(key)) { + enValue = resourceValues.get(resource).get("en").get(key); + } + + String valueStr = value == null ? "" : value.toString(); + if (enValue.contains("\n") || valueStr.contains("\n")) { + textArea.setText(valueStr); + textArea.setEditable(column == 2); + areaMode = true; + return pane; + } + areaMode = false; + textField.setText(valueStr); + textField.setEditable(column == 2); + return textField; + } +}