/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander 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. * * muCommander 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.text; import com.mucommander.AppLogger; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.commons.io.bom.BOMReader; import com.mucommander.conf.MuConfiguration; import java.io.*; import java.util.*; /** * This class takes care of all text localization issues by loading all text entries from a dictionary file on startup * and translating them into the current language on demand. * * <p>All public methods are static to make it easy to call them throughout the application.</p> * * <p>See dictionary file for more information about th dictionary file format.</p> * * @author Maxence Bernard */ public class Translator { /** Contains key/value pairs for the current language */ private static Map<String, String> dictionary; /** Contains key/value pairs for the default language, for entries that are not defined in the current language */ private static Map<String, String> defaultDictionary; /** List of all available languages in the dictionary file */ private static List<String> availableLanguages; /** Current language */ private static String language; /** Default language */ private final static String DEFAULT_LANGUAGE = "EN"; /** Key for available languages */ private final static String AVAILABLE_LANGUAGES_KEY = "available_languages"; /** * Prevents instance creation. */ private Translator() {} /** * Determines and sets the current language based on the given list of available languages * and the current language set in the preferences if it has been set, else on the system's language. * <p> * If the language set in the preferences or the system's language is not available, the default language as * defined by {@link #DEFAULT_LANGUAGE} will be used. * </p> * * @param availableLanguages list of available languages */ private static void setCurrentLanguage(List<String> availableLanguages) { String lang = MuConfiguration.getVariable(MuConfiguration.LANGUAGE); if(lang==null) { // language is not set in preferences, use system's language // Try to match language with the system's language, only if the system's language // has values in dictionary, otherwise use default language (English). lang = Locale.getDefault().getLanguage(); AppLogger.info("Language not set in preferences, trying to match system's language ("+lang+")"); } else { AppLogger.info("Using language set in preferences: "+lang); } // Determines if the list of available languages contains the language (case-insensitive) boolean containsLanguage = false; int nbAvailableLanguages = availableLanguages.size(); for(int i=0; i<nbAvailableLanguages; i++) { if(availableLanguages.get(i).equalsIgnoreCase(lang)) { containsLanguage = true; lang = availableLanguages.get(i); // Use the proper case variation break; } } // Determines if language is one of the languages declared as available if(containsLanguage) { // Language is available Translator.language = lang; AppLogger.fine("Language "+lang+" is available."); } else { // Language is not available, fall back to default language (English) Translator.language = DEFAULT_LANGUAGE; AppLogger.fine("Language "+lang+" is not available, falling back to default language "+DEFAULT_LANGUAGE); } // Set preferred language in configuration file MuConfiguration.setVariable(MuConfiguration.LANGUAGE, Translator.language); AppLogger.config("Current language has been set to "+Translator.language); } /** * Loads the default dictionary file. * * @throws IOException thrown if an IO error occurs. */ public static void loadDictionaryFile() throws IOException { loadDictionaryFile(com.mucommander.RuntimeConstants.DICTIONARY_FILE); } /** * Loads the specified dictionary file, which contains localized text entries. * * @param filePath path to the dictionary file * @throws IOException thrown if an IO error occurs. */ public static void loadDictionaryFile(String filePath) throws IOException { availableLanguages = new Vector<String>(); dictionary = new Hashtable<String, String>(); defaultDictionary = new Hashtable<String, String>(); BufferedReader br = new BufferedReader(new BOMReader(ResourceLoader.getResourceAsStream(filePath))); String line; String keyLC; String lang; String text; StringTokenizer st; while((line = br.readLine())!=null) { if (!line.trim().startsWith("#") && !line.trim().equals("")) { st = new StringTokenizer(line); try { // Sets delimiter to ':' keyLC = st.nextToken(":").trim().toLowerCase(); // Special key that lists available languages, must // be defined before any other entry if(Translator.language==null && keyLC.equals(AVAILABLE_LANGUAGES_KEY)) { // Parse comma separated languages st = new StringTokenizer(st.nextToken(), ",\n"); while(st.hasMoreTokens()) availableLanguages.add(st.nextToken().trim()); AppLogger.finer("Available languages= "+availableLanguages); // Determines current language based on available languages and preferred language (if set) or system's language setCurrentLanguage(availableLanguages); continue; } lang = st.nextToken().trim(); // Delimiter is now line break text = st.nextToken("\n"); text = text.substring(1, text.length()); // Replace "\n" strings in the text by \n characters int pos = 0; while ((pos = text.indexOf("\\n", pos))!=-1) text = text.substring(0, pos)+"\n"+text.substring(pos+2, text.length()); // Replace "\\uxxxx" unicode strings by the designated character pos = 0; while ((pos = text.indexOf("\\u", pos))!=-1) text = text.substring(0, pos)+(char)(Integer.parseInt(text.substring(pos+2, pos+6), 16))+text.substring(pos+6, text.length()); // Add entry for current language, or for default language if a value for current language wasn't already set if(lang.equalsIgnoreCase(language)) { dictionary.put(keyLC, text); // Remove the default dictionary entry as it will not be used (saves some memory). defaultDictionary.remove(keyLC); } else if(lang.equalsIgnoreCase(DEFAULT_LANGUAGE) && dictionary.get(keyLC)==null) { defaultDictionary.put(keyLC, text); } } catch(Exception e) { AppLogger.info("error in line " + line + " (" + e + ")"); throw new IOException("Syntax error in line " + line); } } } br.close(); } /** * Returns the current language as a language code ("EN", "FR", "pt_BR", ...). * * @return lang a language code */ public static String getLanguage() { return language; } /** * Returns an array of available languages, expressed as language codes ("EN", "FR", "pt_BR"...). * The returned array is sorted by language codes in case insensitive order. * * @return an array of language codes. */ public static String[] getAvailableLanguages() { String[] languages = availableLanguages.toArray(new String[availableLanguages.size()]); Arrays.sort(languages, String.CASE_INSENSITIVE_ORDER); return languages; } /** * Returns <code>true</code> if the given entry's key has a value in the current language. * If the <code>useDefaultLanguage</code> parameter is <code>true</code>, entries that have no value in the * {@link #getLanguage() current language} but one in the {@link #DEFAULT_LANGUAGE} will be considered as having * a value (<code>true</code> will be returned). * * @param key key of the requested dictionary entry (case-insensitive) * @param useDefaultLanguage if <code>true</code>, entries that have no value in the {@link #getLanguage() current * language} but one in the {@link #DEFAULT_LANGUAGE} will be considered as having a value * @return <code>true</code> if the given key has a corresponding value in the current language. */ public static boolean hasValue(String key, boolean useDefaultLanguage) { return dictionary.get(key.toLowerCase())!=null || (useDefaultLanguage && defaultDictionary.get(key.toLowerCase())!=null); } /** * Returns the localized text String for the given key expressd in the current language, or in the default language * if there is no value for the current language. Entry parameters (%1, %2, ...), if any, are replaced by the * specified values. * * @param key key of the requested dictionary entry (case-insensitive) * @param paramValues array of parameters which will be used as values for variables. * @return the localized text String for the given key expressd in the current language */ public static String get(String key, String... paramValues) { // Returns the localized text String text = dictionary.get(key.toLowerCase()); if (text==null) { text = defaultDictionary.get(key.toLowerCase()); if(text==null) { AppLogger.fine("No value for "+key+", returning key"); return key; } else { AppLogger.fine("No value for "+key+" in language "+language+", using "+DEFAULT_LANGUAGE+" value"); // Don't return yet, parameters need to be replaced } } // Replace %1, %2 ... parameters by their value if (paramValues!=null) { int pos = -1; for(int i=0; i<paramValues.length; i++) { while(++pos<text.length()-1 && (pos = text.indexOf("%"+(i+1), pos))!=-1) text = text.substring(0, pos)+paramValues[i]+text.substring(pos+2, text.length()); } } // Replace $[key] occurrences by their value int pos = 0; int pos2; String variable; while ((pos = text.indexOf("$[", pos))!=-1) { pos2 = text.indexOf("]", pos+1); variable = text.substring(pos+2, pos2); text = text.substring(0, pos)+get(variable, paramValues)+text.substring(pos2+1, text.length()); } return text; } /** * Based on the number of supplied command line parameters, this method either : * <ul> * <li>Looks for and reports any missing or unused dictionary entry, * using the supplied source folder path to look inside source files * for references to dictionary entries. * <li>Merges a new language's entries from a dictionary file into a new one. * </ul> */ public static void main(String args[]) throws IOException { /* // Looks for missing and unused entries if(args.length<4) { Enumeration languages = dictionaries.keys(); Vector langsV = new Vector(); while(languages.hasMoreElements()) langsV.add(languages.nextElement()); String langs[] = new String[langsV.size()]; langsV.toArray(langs); com.mucommander.commons.file.AbstractFile sourceFolder = com.mucommander.commons.file.AbstractFile.getFile(args[0]); System.out.println("\n##### Looking for missing entries #####"); checkMissingEntries(sourceFolder, langs); System.out.println("\n##### Looking for unused entries #####"); checkUnusedEntries(sourceFolder, langs); } // Integrates a new language into the dictionary else { */ // Parameters order: originalFile newLanguageFile resultingFile newLanguage if(args.length<4) { System.out.println("usage: Translator originalFile newLanguageFile mergedFile newLanguage"); return; } addLanguageToDictionary(args[0], args[1], args[2], args[3]); /* } */ } /** * Checks for missing dictionary entries in the given file or folder and reports them on the standard output. * If the given file is a folder, recurse on each file that it contains, if it's a 'regular' file and the * extension is '.java', looks for any calls to {@link #Translator.get(String), Translator.get()} and checks * that the request entry has a value in each language's dictionary. */ /* private static void checkMissingEntries(com.mucommander.commons.file.AbstractFile file, String languages[]) throws IOException { if(file.isDirectory()) { com.mucommander.commons.file.AbstractFile children[] = file.ls(); for(int i=0; i<children.length; i++) checkMissingEntries(children[i], languages); } else if(file.getName().endsWith(".java")) { BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream())); String line; int pos; String entry; String value; String language; while((line=br.readLine())!=null) { if(!line.trim().startsWith("//") && (pos=line.indexOf("Translator.get(\""))!=-1) { try { entry = line.substring(pos+16, line.indexOf("\"", pos+16)); for(int i=0; i<languages.length; i++) { language = languages[i]; if((String)((Hashtable)dictionaries.get(language)).get(entry)!=null || (!language.equalsIgnoreCase("en") && (value=(String)((Hashtable)dictionaries.get("en")).get(entry))!=null && value.startsWith("$"))) continue; System.out.println("Missing "+language.toUpperCase()+" entry '"+entry+"' in "+file.getAbsolutePath()); } } catch(Exception e) { } } } br.close(); } } */ /** * Checks all enties in all dictionaries, checks that they are used in at least one source file * in or under the supplied folder, and reports unused entries on the standard output. */ /* private static void checkUnusedEntries(com.mucommander.commons.file.AbstractFile sourceFolder, String languages[]) throws IOException { Enumeration entries; String entry; for(int i=0; i<languages.length; i++) { entries = ((Hashtable)dictionaries.get(languages[i])).keys(); while(entries.hasMoreElements()) { entry = (String)entries.nextElement(); if(!isEntryUsed(entry, sourceFolder)) System.out.println("Unused "+languages[i].toUpperCase()+" entry "+entry); } } } */ /** * Checks if the given entry is used in the supplied file or folder. */ /* private static boolean isEntryUsed(String entry, com.mucommander.commons.file.AbstractFile file) throws IOException { if(file.isDirectory()) { com.mucommander.commons.file.AbstractFile children[] = file.ls(); for(int i=0; i<children.length; i++) if(isEntryUsed(entry, children[i])) return true; return false; } else if(file.getName().endsWith(".java")) { BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream())); String line; int pos; while((line=br.readLine())!=null) { if(!line.trim().startsWith("//") && (pos=line.indexOf("\""+entry+"\""))!=-1) { br.close(); return true; } } br.close(); return false; } return false; } */ /** * Merges a dictionary file with another one, adding entries of the specified new language. * <p>This method is used to merge dictionary files sent by contributors. * * @param originalFile current version of the dictionary file * @param newLanguageFile dictionary file containing new language entries * @param resultingFile merged dictionary file * @param newLanguage new language * @throws IOException if an I/O error occurred */ private static void addLanguageToDictionary(String originalFile, String newLanguageFile, String resultingFile, String newLanguage) throws IOException { // Initialize streams BufferedReader originalFileReader = new BufferedReader(new BOMReader(new FileInputStream(originalFile))); BufferedReader newLanguageFileReader = new BufferedReader(new BOMReader(new FileInputStream(newLanguageFile))); PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(resultingFile), "UTF-8")); // Parse new language's entries String line; int lineNum = 0; String key; String lang; String text; StringTokenizer st; Map<String, String> newLanguageEntries = new Hashtable<String, String>(); while ((line = newLanguageFileReader.readLine())!=null) { try { if (!line.trim().startsWith("#") && !line.trim().equals("")) { st = new StringTokenizer(line); // Sets delimiter to ':' key = st.nextToken(":"); lang = st.nextToken(); if(lang.equalsIgnoreCase(newLanguage)) { // Delimiter is now line break text = st.nextToken("\n"); text = text.substring(1, text.length()); newLanguageEntries.put(key, text); } } lineNum++; } catch(Exception e) { AppLogger.warning("caught "+e+" at line "+lineNum); return; } } // Insert new language entries in resulting file boolean keyProcessedForNewLanguage = false; String currentKey = null; while ((line = originalFileReader.readLine())!=null) { boolean emptyLine = line.trim().startsWith("#") || line.trim().equals(""); if (!keyProcessedForNewLanguage && (emptyLine || (currentKey!=null && !line.startsWith(currentKey+":")))) { if(currentKey!=null) { String newLanguageValue = newLanguageEntries.get(currentKey); if(newLanguageValue!=null) { // Insert new language's entry in resulting file AppLogger.info("New language entry for key="+currentKey+" value="+newLanguageValue); pw.println(currentKey+":"+newLanguage+":"+newLanguageValue); } keyProcessedForNewLanguage = true; } } if(!emptyLine) { // Parse entry st = new StringTokenizer(line); // Set delimiter to ':' key = st.nextToken(":"); lang = st.nextToken(); if(!key.equals(currentKey)) { currentKey = key; keyProcessedForNewLanguage = false; } if(lang.equalsIgnoreCase(newLanguage)) { // Delimiter is now line break String existingNewLanguageValue = st.nextToken("\n"); existingNewLanguageValue = existingNewLanguageValue.substring(1, existingNewLanguageValue.length()); String newLanguageValue = newLanguageEntries.get(currentKey); if(newLanguageValue!=null) { if(!existingNewLanguageValue.equals(newLanguageValue)) AppLogger.warning("Warning: found an updated value for key="+currentKey+", using new value="+newLanguageValue+" existing value="+existingNewLanguageValue); pw.println(currentKey+":"+newLanguage+":"+newLanguageValue); } else { AppLogger.warning("Existing dictionary has a value for key="+currentKey+" that is missing in the new dictionary file, using existing value= "+existingNewLanguageValue); pw.println(currentKey+":"+newLanguage+":"+existingNewLanguageValue); } keyProcessedForNewLanguage = true; } else { pw.println(line); } } else { pw.println(line); } } newLanguageFileReader.close(); originalFileReader.close(); pw.close(); } }