From 6b7e33e2e3f3f08d6c656ba6027a98a0980610d9 Mon Sep 17 00:00:00 2001 From: Peter Donald Date: Fri, 2 Nov 2001 23:30:44 +0000 Subject: [PATCH] A task for performing 18n translations on files via preprocessing. Submitted by: "Magesh Umasankar" git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@269860 13f79535-47bb-0310-9956-ffa450edef68 --- docs/manual/OptionalTasks/translate.html | 157 +++++ docs/manual/optionaltasklist.html | 1 + .../ant/taskdefs/optional/i18n/Translate.java | 565 ++++++++++++++++++ 3 files changed, 723 insertions(+) create mode 100644 docs/manual/OptionalTasks/translate.html create mode 100644 src/main/org/apache/tools/ant/taskdefs/optional/i18n/Translate.java diff --git a/docs/manual/OptionalTasks/translate.html b/docs/manual/OptionalTasks/translate.html new file mode 100644 index 000000000..7df9d14d5 --- /dev/null +++ b/docs/manual/OptionalTasks/translate.html @@ -0,0 +1,157 @@ + + + + +Ant User Manual + + + + +

Translate

+

Description

+

Identifies keys in files delimited by special tokens +and translates them with values read from resource bundles. +

+

+A resource bundle contains locale-specific key-value pairs. +A resource bundle is a hierarchical set of property files. +A bundle name makes up its base family name. Each file that +makes up this bundle has this name plus its locale. For example, +if the resource bundle name is MyResources, the file that contains +German text will take the name MyResources_de. In addition to +language, country and variant are also used to form the files in +the bundle. +

+

+The resource bundle lookup searches for resource files with various +suffixes on the basis of (1) the desired locale and (2) the default +locale (basebundlename), in the following order from lower-level +(more specific) to parent-level (less specific): +

+
+basebundlename + "_" + language1 + "_" + country1 + "_" + variant1
+basebundlename + "_" + language1 + "_" + country1
+basebundlename + "_" + language1
+basebundlename
+basebundlename + "_" + language2 + "_" + country2 + "_" + variant2
+basebundlename + "_" + language2 + "_" + country2
+basebundlename + "_" + language2
+
+

+The file names generated thus are appended with the string ".properties" +to make up the file names that are to be used. +

+

+File encoding is supported. The encoding scheme of the source files, +destination files and the bundle files can be specified. + +Destination files can be exlicitly overwritten using the +forceoverwrite attribute. If forceoverwrite +is false, the destination file is overwritten only if either the +source file or any of the files that make up the bundle have been +modified after the destination file was last modified. +

+

FileSets are used to select files to +translate. +

+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescriptionRequired
todirDestination directory where destination files are + to be created.Yes
starttokenThe starting token to identify keys.Yes
endtokenThe ending token to identify keys.Yes
bundleFamily name of resource bundle.Yes
bundlelanguage + Locale specific language of resource bundle. Defaults to + default locale's language. + No
bundlecountry + Locale specific country of resource bundle. Defaults to + default locale's country. + No
bundlevariant + Locale specific variant of resource bundle. Defaults to + the default variant of the country and language being used. + No
srcencodingSource file encoding scheme. Defaults to + system default file encoding.No
destencodingDestination file encoding scheme. Defaults to + source file encoding.No
bundleencodingResource Bundle file encoding scheme. Defaults to + source file encoding.No
forceoverwriteOverwrite existing files even if the destination + files are newer. Defaults to "no".No
+

Parameters specified as nested elements

+ +

fileset

+

FileSets are used to select files that + contain keys for which value translated files are to be generated. +

+

Examples

+

Translate source file encoded in english into its japanese +equivalent using a resource bundle encoded in japanese. +

+
+  <translate toDir="$(dest.dir}/ja"
+        starttoken="#"
+        endtoken="#"
+        bundle="resource/BaseResource"
+        bundlelanguage="ja"
+        forceoverwrite="yes"
+        srcencoding="ISO8859_1"
+        destencoding="SJIS"
+        bundleencoding="SJIS">
+        <fileset dir="${src.dir}">
+            <include name="**/*.jsp"/>
+        </fileset>
+  </translate>
diff --git a/docs/manual/optionaltasklist.html b/docs/manual/optionaltasklist.html
index 8dbe2e71d..f3e50db99 100644
--- a/docs/manual/optionaltasklist.html
+++ b/docs/manual/optionaltasklist.html
@@ -46,6 +46,7 @@
 Stylebook
Telnet
Test
+Translate
Visual Age for Java Tasks
Microsoft Visual SourceSafe Tasks
Weblogic JSP Compiler
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/i18n/Translate.java b/src/main/org/apache/tools/ant/taskdefs/optional/i18n/Translate.java new file mode 100644 index 000000000..12e5ba40d --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/optional/i18n/Translate.java @@ -0,0 +1,565 @@ +/* + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Ant", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ +package org.apache.tools.ant.taskdefs.optional.i18n; + +import java.io.*; +import java.util.*; +import org.apache.tools.ant.*; +import org.apache.tools.ant.types.*; +import org.apache.tools.ant.util.*; +import org.apache.tools.ant.taskdefs.MatchingTask; + +/** + * Translates text embedded in files using Resource Bundle files. + * + * @author Magesh Umasankar + */ +public class Translate extends MatchingTask { + + /** + * Family name of resource bundle + */ + private String bundle; + /** + * Locale specific language of the resource bundle + */ + private String bundleLanguage; + /** + * Locale specific country of the resource bundle + */ + private String bundleCountry; + /** + * Locale specific variant of the resource bundle + */ + private String bundleVariant; + /** + * Destination directory + */ + private File toDir; + /** + * Source file encoding scheme + */ + private String srcEncoding; + /** + * Destination file encoding scheme + */ + private String destEncoding; + /** + * Resource Bundle file encoding scheme, defaults to srcEncoding + */ + private String bundleEncoding; + /** + * Starting token to identify keys + */ + private String startToken; + /** + * Ending token to identify keys + */ + private String endToken; + /** + * Create new destination file? Defaults to false. + */ + private boolean forceOverwrite; + /** + * Vector to hold source file sets. + */ + private Vector filesets = new Vector(); + /** + * Holds key value pairs loaded from resource bundle file + */ + private Hashtable resourceMap = new Hashtable(); + /** + * Generated locale based on user attributes + */ + private Locale locale; + /** + * Used to resolve file names. + */ + private FileUtils fileUtils = FileUtils.newFileUtils(); + /** + * Last Modified Timestamp of resource bundle file being used. + */ + private long[] bundleLastModified = new long[7]; + /** + * Last Modified Timestamp of source file being used. + */ + private long srcLastModified; + /** + * Last Modified Timestamp of destination file being used. + */ + private long destLastModified; + + /** + * Sets Family name of resource bundle + */ + public void setBundle(String bundle) { + this.bundle = bundle; + } + + /** + * Sets locale specific language of resource bundle + */ + public void setBundleLanguage(String bundleLanguage ) { + this.bundleLanguage = bundleLanguage; + } + + /** + * Sets locale specific country of resource bundle + */ + public void setBundleCountry(String bundleCountry) { + this.bundleCountry = bundleCountry; + } + + /** + * Sets locale specific variant of resource bundle + */ + public void setBundleVariant(String bundleVariant) { + this.bundleVariant = bundleVariant; + } + + /** + * Sets Destination directory + */ + public void setToDir(File toDir) { + this.toDir = toDir; + } + + /** + * Sets starting token to identify keys + */ + public void setStartToken(String startToken) { + this.startToken = startToken; + } + + /** + * Sets ending token to identify keys + */ + public void setEndToken(String endToken) { + this.endToken = endToken; + } + + /** + * Sets source file encoding scheme + */ + public void setSrcEncoding(String srcEncoding) { + this.srcEncoding = srcEncoding; + } + + /** + * Sets destination file encoding scheme. Defaults to source file + * encoding + */ + public void setDestEncoding(String destEncoding) { + this.destEncoding = destEncoding; + } + + /** + * Sets Resource Bundle file encoding scheme + */ + public void setBundleEncoding(String bundleEncoding) { + this.bundleEncoding = bundleEncoding; + } + + /** + * Overwrite existing file irrespective of whether it is newer than + * the source file as well as the resource bundle file? Defaults to + * false. + */ + public void setForceOverwrite(boolean forceOverwrite) { + this.forceOverwrite = forceOverwrite; + } + + /** + * Adds a set of files (nested fileset attribute). + */ + public void addFileset(FileSet set) { + filesets.addElement(set); + } + + /** + * Check attributes values, load resource map and translate + */ + public void execute() throws BuildException { + if (bundle == null) { + throw new BuildException("The bundle attribute must be set.", + location); + } + + if (startToken == null) { + throw new BuildException("The starttoken attribute must be set.", + location); + } + + if (endToken == null) { + throw new BuildException("The endtoken attribute must be set.", + location); + } + + if (bundleLanguage == null) { + Locale l = Locale.getDefault(); + bundleLanguage = l.getLanguage(); + } + + if (bundleCountry == null) { + bundleCountry = Locale.getDefault().getCountry(); + } + + locale = new Locale(bundleLanguage, bundleCountry); + + if (bundleVariant == null) { + Locale l = new Locale(bundleLanguage, bundleCountry); + bundleVariant = l.getVariant(); + } + + if (toDir == null) { + throw new BuildException("The todir attribute must be set.", + location); + } + + if (!toDir.exists()) { + toDir.mkdirs(); + } else { + if (toDir.isFile()) { + throw new BuildException(toDir + " is not a directory"); + } + } + + if (srcEncoding == null) { + srcEncoding = System.getProperty("file.encoding"); + } + + if (destEncoding == null) { + destEncoding = srcEncoding; + } + + if (bundleEncoding == null) { + bundleEncoding = srcEncoding; + } + + loadResourceMaps(); + + translate(); + } + + /** + * Load resource maps based on resource bundle encoding scheme. + * The resource bundle lookup searches for resource files with various + * suffixes on the basis of (1) the desired locale and (2) the default + * locale (basebundlename), in the following order from lower-level + * (more specific) to parent-level (less specific): + * + * basebundlename + "_" + language1 + "_" + country1 + "_" + variant1 + * basebundlename + "_" + language1 + "_" + country1 + * basebundlename + "_" + language1 + * basebundlename + * basebundlename + "_" + language2 + "_" + country2 + "_" + variant2 + * basebundlename + "_" + language2 + "_" + country2 + * basebundlename + "_" + language2 + * + * To the generated name, a ".properties" string is appeneded and + * once this file is located, it is treated just like a properties file + * but with bundle encoding also considered while loading. + */ + public void loadResourceMaps() throws BuildException { + Locale locale = new Locale(bundleLanguage, + bundleCountry, + bundleVariant); + String language = locale.getLanguage().length() > 0 ? + "_" + locale.getLanguage() : + ""; + String country = locale.getCountry().length() > 0 ? + "_" + locale.getCountry() : + ""; + String variant = locale.getVariant().length() > 0 ? + "_" + locale.getVariant() : + ""; + String bundleFile = bundle + language + country + variant; + processBundle(bundleFile, 0, false); + + bundleFile = bundle + language + country; + processBundle(bundleFile, 1, false); + + bundleFile = bundle + language; + processBundle(bundleFile, 2, false); + + bundleFile = bundle; + processBundle(bundleFile, 3, false); + + //Load default locale bundle files + //using default file encoding scheme. + locale = Locale.getDefault(); + + language = locale.getLanguage().length() > 0 ? + "_" + locale.getLanguage() : + ""; + country = locale.getCountry().length() > 0 ? + "_" + locale.getCountry() : + ""; + variant = locale.getVariant().length() > 0 ? + "_" + locale.getVariant() : + ""; + bundleEncoding = System.getProperty("file.encoding"); + + bundleFile = bundle + language + country + variant; + processBundle(bundleFile, 4, false); + + bundleFile = bundle + language + country; + processBundle(bundleFile, 5, false); + + bundleFile = bundle + language; + processBundle(bundleFile, 6, true); + } + + /** + * Process each file that makes up this bundle. + */ + private void processBundle(String bundleFile, int i, + boolean checkLoaded) throws BuildException { + boolean loaded = false; + bundleFile += ".properties"; + FileInputStream ins = null; + try { + ins = new FileInputStream(bundleFile); + loaded = true; + bundleLastModified[i] = new File(bundleFile).lastModified(); + log("Using " + bundleFile, Project.MSG_DEBUG); + loadResourceMap(ins); + } catch (IOException ioe) { + log(bundleFile + " not found.", Project.MSG_DEBUG); + //if all resource files associated with this bundle + //have been scanned for and still not able to + //find a single resrouce file, throw exception + if (!loaded && checkLoaded) { + throw new BuildException(ioe.getMessage(), location); + } + } + } + + /** + * Load resourceMap with key value pairs. Values of existing keys + * are not overwritten. Bundle's encoding scheme is used. + */ + private void loadResourceMap(FileInputStream ins) throws BuildException { + try { + BufferedReader in = null; + InputStreamReader isr = new InputStreamReader(ins, bundleEncoding); + in = new BufferedReader(isr); + String line = null; + while((line = in.readLine()) != null) { + //So long as the line isn't empty and isn't a comment... + if(line.trim().length() > 1 && + ('#' != line.charAt(0) || '!' != line.charAt(0))) { + //Legal Key-Value separators are :, = and white space. + int sepIndex = line.indexOf('='); + if (-1 == sepIndex) { + sepIndex = line.indexOf(':'); + } + if (-1 == sepIndex) { + for (int k = 0; k < line.length(); k++) { + if (Character.isSpaceChar(line.charAt(k))) { + sepIndex = k; + break; + } + } + } + //Only if we do have a key is there going to be a value + if (-1 != sepIndex) { + String key = line.substring(0, sepIndex).trim(); + String value = line.substring(sepIndex + 1).trim(); + //Handle line continuations, if any + while (value.endsWith("\\")) { + value = value.substring(0, value.length() - 1); + if ((line = in.readLine()) != null) { + value = value + line.trim(); + } else { + break; + } + } + if (key.length() > 0) { + //Has key already been loaded into resourceMap? + if (resourceMap.get(key) == null) { + resourceMap.put(key, value); + } + } + } + } + } + if(in != null) { + in.close(); + } + } catch (IOException ioe) { + throw new BuildException(ioe.getMessage(), location); + } + } + + /** + * Reads source file line by line using the source encoding and + * searches for keys that are sandwiched between the startToken + * and endToken. The values for these keys are looked up from + * the hashtable and substituted. If the hashtable doesn't + * contain the key, they key itself is used as the value. + * Detination files and directories are created as needed. + * The destination file is overwritten only if + * the forceoverwritten attribute is set to true if + * the source file or any associated bundle resource file is + * newer than the destination file. + */ + private void translate() throws BuildException { + for (int i = 0; i < filesets.size(); i++) { + FileSet fs = (FileSet) filesets.elementAt(i); + DirectoryScanner ds = fs.getDirectoryScanner(project); + String[] srcFiles = ds.getIncludedFiles(); + for (int j = 0; j < srcFiles.length; j++) { + try { + File dest = fileUtils.resolveFile(toDir, srcFiles[j]); + //Make sure parent dirs exist, else, create them. + try { + File destDir = new File(dest.getParent()); + if (!destDir.exists()) { + destDir.mkdirs(); + } + } catch (Exception e) { + log("Exception occured while trying to check/create " + + " parent directory. " + e.getMessage(), + Project.MSG_DEBUG); + } + destLastModified = dest.lastModified(); + srcLastModified = new File(srcFiles[i]).lastModified(); + //Check to see if dest file has to be recreated + if (forceOverwrite + || destLastModified < srcLastModified + || destLastModified < bundleLastModified[0] + || destLastModified < bundleLastModified[1] + || destLastModified < bundleLastModified[2] + || destLastModified < bundleLastModified[3] + || destLastModified < bundleLastModified[4] + || destLastModified < bundleLastModified[5] + || destLastModified < bundleLastModified[6]) { + log("Processing " + srcFiles[j], + Project.MSG_DEBUG); + FileOutputStream fos = new FileOutputStream(dest); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(fos, + destEncoding)); + FileInputStream fis = new FileInputStream(srcFiles[j]); + BufferedReader in = new BufferedReader( + new InputStreamReader(fis, + srcEncoding)); + String line; + while((line = in.readLine()) != null) { + StringBuffer newline = new StringBuffer(line); + int startIndex = -1; + int endIndex = -1; + outer: while (true) { + startIndex = line.indexOf(startToken, endIndex + 1); + if (startIndex < 0 || + startIndex + 1 >= line.length()) { + break; + } + endIndex = line.indexOf(endToken, startIndex + 1); + if (endIndex < 0) { + break; + } + String matches = line.substring(startIndex + 1, + endIndex); + //If there is a white space or = or :, then + //it isn't to be treated as a valid key. + for (int k = 0; k < matches.length(); k++) { + char c = matches.charAt(k); + if (c == ':' || + c == '=' || + Character.isSpaceChar(c)) { + endIndex = endIndex - 1; + continue outer; + } + } + String replace = null; + replace = (String) resourceMap.get(matches); + //If the key hasn't been loaded into resourceMap, + //use the key itself as the value also. + if (replace == null) { + log("Warning: The key: " + matches + + " hasn't been defined.", + Project.MSG_DEBUG); + replace = matches; + } + line = line.substring(0, startIndex) + + replace + + line.substring(endIndex + 1); + endIndex = startIndex + replace.length() + 1; + if (endIndex + 1 >= line.length()) { + break; + } + } + out.write(line); + out.newLine(); + } + if(in != null) { + in.close(); + } + if(out != null) { + out.close(); + } + } else { + log("Skipping " + srcFiles[j] + + " as destination file is up to date", + Project.MSG_VERBOSE); + } + } catch (IOException ioe) { + throw new BuildException(ioe.getMessage(), location); + } + } + } + } +}