/** * FormatTranslation.java * * Copyright (C) 2010, Volker Boerchers * * FormatTranslation.java is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * FormatTranslation.java 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 General Public License for more details. */ package org.freeplane.ant; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.regex.Pattern; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; /** formats a translation file and writes the result to another file. * The following transformations are made: * <ol> * <li> sort lines (case insensitive) * <li> remove duplicates * <li> if a key is present multiple times entries marked as [translate me] * and [auto] are removed in favor of normal entries. * <li> newline style is changed to <eolStyle>. * </ol> * * Attributes: * <ul> * <li> dir: the input directory (default: ".") * <li> outputDir: the output directory. Overwrites existing files if outputDir * equals the input directory (default: the input directory) * <li> includes: wildcard pattern (default: all regular files). * <li> excludes: wildcard pattern, overrules includes (default: no excludes). * <li> eolStyle: unix|mac|windows (default: platform default). * </ul> * * Build messages: * <table border=1> * <tr><th>Message</th><th>Action</th><th>Description</th></tr> * <tr><td><file>: no key/val: <line></td><td>drop line</td><td>broken line with an empty key or without an '=' sign</td></tr> * <tr><td><file>: drop duplicate: <line></td><td>drop line</td><td>two completely identical lines</td></tr> * <tr><td><file>: drop: <line></td><td>drop line</td> * <td>this translation is dropped since a better one was found * (quality: [translate me] -> [auto] -> manually translated) * </td> * </tr> * <tr><td><file>: drop one of two of equal quality (revisit!):keep: <line></td><td>keep line</td> * <td>for one key two manual translations were found. This one (arbitrarily chosen) will be kept. * Printout of the complete line allows to correct an action of FormatTranslation via Copy and Past * if it chose the wrong tranlation. * </td> * </tr> * <tr><td><file>: drop one of two of equal quality (revisit!):drop: <line></td><td>drop line</td> * <td>accompanies the :keep: line: This is the line that is dropped. * </td> * </tr> * </table> * Note that CheckTranslation does not remove anything but produces the same messages! */ public class FormatTranslation extends Task { static Comparator<String> KEY_COMPARATOR = new Comparator<String>() { public int compare(String s1, String s2) { int n1 = s1.length(), n2 = s2.length(); for (int i1 = 0, i2 = 0; i1 < n1 && i2 < n2; i1++, i2++) { char c1 = s1.charAt(i1); char c2 = s2.charAt(i2); boolean c1Terminated = c1 == ' ' || c1 == '\t' || c1 == '='; boolean c2Terminated = c2 == ' ' || c2 == '\t' || c2 == '='; if (c1Terminated && c2Terminated) return 0; if (c1Terminated && !c2Terminated) return -1; if (c2Terminated && !c1Terminated) return 1; if (c1 != c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { return c1 - c2; } } } } return n1 - n2; } }; private final static int QUALITY_NULL = 0; // for empty values private final static int QUALITY_TRANSLATE_ME = 1; private final static int QUALITY_AUTO_TRANSLATED = 2; private final static int QUALITY_MANUALLY_TRANSLATED = 3; private File outputDir; private boolean writeIfUnchanged = false; private File inputDir = new File("."); private ArrayList<Pattern> includePatterns = new ArrayList<Pattern>(); private ArrayList<Pattern> excludePatterns = new ArrayList<Pattern>(); private String lineSeparator = System.getProperty("line.separator"); public void execute() { final int countFormatted = executeImpl(false); log(inputDir + ": formatted " + countFormatted + " file" + (countFormatted == 1 ? "" : "s")); } public int checkOnly() { return executeImpl(true); } /** returns the number of unformatted files. */ private int executeImpl(boolean checkOnly) { validate(); File[] inputFiles = inputDir.listFiles(new TaskUtils.IncludeFileFilter(includePatterns, excludePatterns)); return process(inputFiles, checkOnly); } static public void main(final String argc[]) { File[] inputFiles = new File[argc.length]; int i = 0; for (String arg : argc) { inputFiles[i++] = new File(arg); } new FormatTranslation().configureFromDefines().process(inputFiles, false); } private FormatTranslation configureFromDefines() { final String eolStyle = getConfigurationProperty("eolStyle"); if(eolStyle != null) setEolStyle(eolStyle); final String dir = getConfigurationProperty("dir"); if(dir != null) setDir(dir); final String includes = getConfigurationProperty("includes"); if(includes != null) setIncludes(includes); final String excludes = getConfigurationProperty("excludes"); if(excludes != null) setExcludes(excludes); final String outputDir = getConfigurationProperty("outputdir"); if(outputDir != null) setOutputDir(outputDir); final String writeIfUnchanged = getConfigurationProperty("writeIfUnchanged"); if(writeIfUnchanged != null) setWriteIfUnchanged(Boolean.parseBoolean(writeIfUnchanged)); return this; } protected String getConfigurationProperty(String key) { String propertyName = getClass().getName() + "." + key; return System.getProperty(propertyName, null); } private int process(File[] inputFiles, boolean checkOnly) { try { int countFormattingRequired = 0; for (int i = 0; i < inputFiles.length; i++) { File inputFile = inputFiles[i]; log("processing " + inputFile + "...", Project.MSG_DEBUG); final String input = TaskUtils.readFile(inputFile); final ArrayList<String> lines = new ArrayList<String>(2048); boolean eolStyleMatches = TaskUtils.checkEolStyleAndReadLines(input, lines, lineSeparator); final ArrayList<String> sortedLines = processLines(inputFile.getName(), new ArrayList<String>(lines)); final boolean contentChanged = !lines.equals(sortedLines); final boolean formattingRequired = !eolStyleMatches || contentChanged; if (formattingRequired) { ++countFormattingRequired; if (checkOnly) warn(inputFile + " requires formatting - " + formatCause(contentChanged, eolStyleMatches)); else log(inputFile + "formatted - " + formatCause(contentChanged, eolStyleMatches), Project.MSG_DEBUG); } if (!checkOnly && (formattingRequired || writeIfUnchanged)) { File outputFile; if (outputDir != null) outputFile = new File(outputDir, inputFile.getName()); else outputFile = inputFile; TaskUtils.writeFile(outputFile, sortedLines, lineSeparator); } } return countFormattingRequired; } catch (IOException e) { throw new BuildException(e); } } private String formatCause(boolean contentChanged, boolean eolStyleMatches) { final String string1 = eolStyleMatches ? "" : "wrong eol style"; final String string2 = contentChanged ? "content changed" : ""; return string1 + (string1.length() > 0 && string2.length() > 0 ? ", " : "") + string2; } private void validate() { if (inputDir == null) throw new BuildException("missing attribute 'dir'"); if (outputDir == null) outputDir = inputDir; if (!inputDir.isDirectory()) throw new BuildException("input directory '" + inputDir + "' does not exist"); if (!outputDir.isDirectory() && !outputDir.mkdirs()) throw new BuildException("cannot create output directory '" + outputDir + "'"); } ArrayList<String> processLines(final String filename, ArrayList<String> lines) { Collections.sort(lines, KEY_COMPARATOR); ArrayList<String> result = new ArrayList<String>(lines.size()); String lastKey = null; String lastValue = null; for (final String line : lines) { if (line.indexOf('#') == 0 || line.matches("\\s*")) continue; final String[] keyValue = line.split("\\s*=\\s*", 2); if (keyValue.length != 2 || keyValue[0].length() == 0) { // broken line: no '=' sign or empty key (we had " = ======") warn(filename + ": no key/val: " + line); continue; } final String thisKey = keyValue[0]; String thisValue = keyValue[1]; if (thisValue.matches("(\\[auto\\]|\\[translate me\\])?")) { warn(filename + ": drop empty translation: " + line); continue; } if (thisValue.indexOf("{1}") != -1 && keyValue[1].indexOf("{0}") == -1) { warn(filename + ": errorneous placeholders usage: {1} used without {0}: " + line); } if (thisValue.matches(".*\\$\\d.*")) { warn(filename + ": use '{0}' instead of '$1' as placeholder! (likewise for $2...): " + line); thisValue = thisValue.replaceAll("\\$1", "{0}").replaceAll("\\$2", "{1}"); } if (thisValue.matches(".*\\{\\d[^},]*")) { warn(filename + ": mismatched braces in placeholder: '{' not closed by '}': " + line); } if (thisValue.matches(".*[^']'[^'].*\\{\\d\\}.*") || thisValue.matches(".*\\{\\d\\}.*[^']'[^'].*")) { warn(filename + ": replaced single quotes in strings containing placeholders by two: " + "\"'{0}' n'a\" -> \"''{0}'' n''a\": " + line); thisValue = thisValue.replaceAll("([^'])'([^'])", "$1''$2"); } if (lastKey != null && thisKey.equals(lastKey)) { if (quality(thisValue) < quality(lastValue)) { log(filename + ": drop " + TaskUtils.toLine(lastKey, thisValue)); continue; } else if (quality(thisValue) == quality(lastValue)) { if (thisValue.equals(lastValue)) { log(filename + ": drop duplicate " + TaskUtils.toLine(lastKey, thisValue)); } else if (quality(thisValue) == QUALITY_MANUALLY_TRANSLATED) { warn(filename // + ": drop one of two of equal quality (revisit!):keep: " + TaskUtils.toLine(lastKey, lastValue)); warn(filename // + ": drop one of two of equal quality (revisit!):drop: " + TaskUtils.toLine(thisKey, thisValue)); } else { log(filename + ": drop " + TaskUtils.toLine(lastKey, thisValue)); } continue; } else { log(filename + ": drop " + TaskUtils.toLine(lastKey, lastValue)); } lastValue = thisValue; } else { if (lastKey != null) result.add(TaskUtils.toLine(lastKey, lastValue)); lastKey = thisKey; lastValue = thisValue; } } if (lastKey != null) result.add(TaskUtils.toLine(lastKey, lastValue)); return result; } private int quality(String value) { if (value.length() == 0) return QUALITY_NULL; if (value.indexOf("[translate me]") > 0) return QUALITY_TRANSLATE_ME; if (value.indexOf("[auto]") > 0) return QUALITY_AUTO_TRANSLATED; return QUALITY_MANUALLY_TRANSLATED; } private void warn(String msg) { log(msg, Project.MSG_WARN); } /** per default output files will only be created if the output would * differ from the input file. Set attribute <code>writeIfUnchanged</code> * to "true" to enforce file creation. */ public void setWriteIfUnchanged(boolean writeIfUnchanged) { this.writeIfUnchanged = writeIfUnchanged; } public void setDir(String inputDir) { setDir(new File(inputDir)); } public void setDir(File inputDir) { this.inputDir = inputDir; } public void setIncludes(String pattern) { includePatterns.add(Pattern.compile(TaskUtils.wildcardToRegex(pattern))); } public void setExcludes(String pattern) { excludePatterns.add(Pattern.compile(TaskUtils.wildcardToRegex(pattern))); } /** parameter is set in the build file via the attribute "outputDir" */ public void setOutputDir(String outputDir) { setOutputDir(new File(outputDir)); } /** parameter is set in the build file via the attribute "outputDir" */ public void setOutputDir(File outputDir) { this.outputDir = outputDir; } /** parameter is set in the build file via the attribute "eolStyle" */ public void setEolStyle(String eolStyle) { if (eolStyle.toLowerCase().startsWith("unix")) lineSeparator = "\n"; else if (eolStyle.toLowerCase().startsWith("win")) lineSeparator = "\r\n"; else if (eolStyle.toLowerCase().startsWith("mac")) lineSeparator = "\r"; else throw new BuildException("unknown eolStyle, known: unix|win|mac"); } }