I18nWorker.java
/*
** Module : I18nWorker.java
** Abstract : i18n helper
**
** Copyright (c) 2021-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------------------Description-----------------------------------
** 001 GES 20211003 Created initial version.
** TJD 20220504 Upgrade do Java 11
** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.convert;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.util.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;
/**
* A pattern worker whose purpose is to provide helpers for conversion of localizable strings.
*/
public class I18nWorker
extends AbstractPatternWorker
{
/** Logger */
private static ConversionStatus LOG = ConversionStatus.get(I18nWorker.class);
/** map of languages of procedure file name to list of translations mapped by source messages */
private Map<String, Map<String, HashMap<String, List<Translation>>>> translations = new HashMap<>();
/** Flag that indicates TM translations load state */
private boolean loaded = false;
/** String expansion ratio */
private double expansionRatio = 0;
/**
* Default ctor.
*/
public I18nWorker()
{
String expStr = Configuration.getParameter("text-expansion", null);
if (expStr != null)
{
String errMsg = "Failed to load i18n configuration, 'text-expansion' must be a positive double " +
"value.";
try
{
double percent = Double.parseDouble(expStr);
if (percent < 0)
{
LOG.log(Level.WARNING, errMsg);
}
else
{
expansionRatio = percent / 100.0;
}
}
catch (NumberFormatException ex)
{
LOG.log(Level.WARNING, errMsg, ex);
}
}
setLibrary(new Library());
}
/**
* The library.
*/
public class Library
{
/**
* Loads Translation Manager translations data exported in CSV. The method reads {@code tm-translations}
* configuration value, which holds comma separated list of CSV files with the exported translations.
* All translations are kept in memory for subsequent calls of {@link #getTargetTMLanguages()} and
* {@link #resolveTMTranslation(String, String, int, String)}.
*
* @throws IOException
* For any IO error during reading the CSV files.
*/
public void loadTMTranslations()
throws IOException
{
// load only once
if (loaded)
{
return;
}
loaded = true;
String tmFiles = Configuration.getParameter("tm-translations", null);
if (tmFiles == null || tmFiles.trim().isEmpty())
{
return;
}
String[] files = tmFiles.split(",");
for (String file : files)
{
try (Reader r = new InputStreamReader(new FileInputStream(file)))
{
Map<String, HashMap<String, List<Translation>>> translations = new HashMap<>();
HashMap<String, List<Translation>> procTranslations = null;
String lastProc = null;
// first line holds target language and name of Translation Manager project,
// second line has csv columns
List<String> line = Utils.readCSVLine(r);
String targetLanguage = TranslationManager.getIdentifier(line.get(1).trim());
// skip columns
Utils.readCSVLine(r);
while ((line = Utils.readCSVLine(r)) != null)
{
String proc = line.get(0).replace('\\', '/');
Translation tr = new Translation();
tr.source = line.get(3);
tr.target = line.get(4);
tr.line = -1;
try
{
tr.line = Integer.parseInt(line.get(6));
}
catch (NumberFormatException ex)
{
// skip
}
if (procTranslations == null || !Objects.equals(lastProc, proc))
{
procTranslations = translations.get(proc);
if (procTranslations == null)
{
procTranslations = new HashMap<>();
translations.put(proc, procTranslations);
}
lastProc = proc;
}
List<Translation> lineTranslations = procTranslations.get(tr.source);
if (lineTranslations == null)
{
lineTranslations = new ArrayList<>();
procTranslations.put(tr.source, lineTranslations);
}
lineTranslations.add(tr);
}
I18nWorker.this.translations.put(targetLanguage, translations);
}
}
// second pass - deduplicate translations which map all source messages to the same target message,
// such entries are not subject of disambiguation during translation resolution
for (Map<String, HashMap<String, List<Translation>>> proc : I18nWorker.this.translations.values())
{
for (HashMap<String, List<Translation>> procTranslations : proc.values())
{
for (List<Translation> sourceTranslations : procTranslations.values())
{
Translation firstTr = null;
for (Translation tr : sourceTranslations)
{
if (firstTr == null)
{
firstTr = tr;
continue;
}
if (Objects.equals(firstTr.target, tr.target))
{
continue;
}
firstTr = null;
break;
}
// the list of translations all translate the source to the same target, keep only
// the first translation
if (firstTr != null)
{
firstTr.line = -1;
sourceTranslations.clear();
sourceTranslations.add(firstTr);
}
}
}
}
}
/**
* Returns the list of target languages as specified in the exported Translation Manager translation
* data loaded by {@link #loadTMTranslations()}.
*
* @return See above.
*/
public List<String> getTargetTMLanguages()
{
return new ArrayList<>(translations.keySet());
}
/**
* Resolves the translation for the supplied target language, procedure and source message. The
* resolution is performed on the data loaded by {@link #loadTMTranslations()}. If the translation is
* found it is returned by the method, otherwise the method returns {@code null}.
*
* @param targetLanguage
* The target language.
* @param proc
* Path of the procedure which is the origin of the source message to translate.
* @param line
* The line of the source message in the specified procedure.
* @param source
* The source message to translate.
*
* @return See above.
*/
public String resolveTMTranslation(String targetLanguage, String proc, int line, String source)
{
if (translations.isEmpty())
{
return null;
}
Translation foundTranslation = null;
Map<String, HashMap<String, List<Translation>>> translations =
I18nWorker.this.translations.get(targetLanguage);
if (translations != null)
{
proc = proc.trim();
// TODO: will this work on Windows?
if (proc.startsWith("./"))
{
proc = proc.substring(2);
}
HashMap<String, List<Translation>> procTranslations = translations.get(proc);
if (procTranslations != null)
{
List<Translation> targetTranslations = procTranslations.get(source);
if (targetTranslations != null)
{
Translation noLineTr = null;
for (Translation tr : targetTranslations)
{
if (tr.line == -1)
{
noLineTr = tr;
continue;
}
if (tr.line == line)
{
foundTranslation = tr;
break;
}
}
if (foundTranslation == null)
{
foundTranslation = noLineTr;
}
}
}
}
if (foundTranslation == null)
{
LOG.log(Level.WARNING, "Failed to resolve TM translation for '" + targetLanguage + "' '" +
source + "' in '" + proc + ":" + line + "'.");
return null;
}
else
{
return foundTranslation.target;
}
}
/**
* Expands the supplied string.
*
* The string is expanded with empty space characters. The number of characters is calculated from
* the expansion {@linkplain #expansionRatio} and the expansion table held by
* {@link TranslationManager}, for more details see
* {@link TranslationManager#getExpandedLength(String, double)}.
*
* @param str
* String to be expanded.
*
* @return The expanded string.
*/
public String expandString(String str)
{
if (expansionRatio > 0)
{
int length = TranslationManager.getExpandedLength(str, expansionRatio);
I18nString i18nString = new I18nString(str, null, length);
return i18nString.getTranslatedExpanded();
}
else
{
return str;
}
}
}
/**
* Holds a single translation entry.
*/
private static class Translation
{
/** Location of the source message in the source procedure */
public int line;
// TODO: TM doesn't export column number
public int column;
/** The source message */
public String source;
/** The target message */
public String target;
}
}