summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-07 21:36:59 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-07 21:36:59 +0200
commit57d188e69a17788ff78fd1b9ad1afa2b232f6536 (patch)
treeace1676fa45c488f126893a6d15feaee49535a51 /source
parent2ca403a1fc80cb9e1d65764cbb42c5dbc7c030ea (diff)
Major fix for localization: finalizing German, French, Spanish, Italian,
Greek, Slovak, Ukrainian, Turkish and Russian. Also added full support for Polish language. All servlets are now fully supported by those languages.
Diffstat (limited to 'source')
-rw-r--r--source/net/yacy/data/Translator.java43
-rwxr-xr-xsource/net/yacy/utils/translation/GenerateMasterXliff.java6
-rw-r--r--source/net/yacy/utils/translation/GenerateSourceMasterXliff.java428
-rw-r--r--source/net/yacy/utils/translation/TranslationManager.java9
-rw-r--r--source/net/yacy/utils/translation/TranslatorXliff.java2
-rw-r--r--source/net/yacy/yacy.java1
6 files changed, 478 insertions, 11 deletions
diff --git a/source/net/yacy/data/Translator.java b/source/net/yacy/data/Translator.java
index 03e55c61b..448c24b1d 100644
--- a/source/net/yacy/data/Translator.java
+++ b/source/net/yacy/data/Translator.java
@@ -47,6 +47,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.regex.Pattern;
import net.yacy.cora.util.CommonPattern;
import net.yacy.cora.util.ConcurrentLog;
@@ -68,6 +69,10 @@ public class Translator {
public final static String LANG_FILENAME_FILTER = "^.*\\.lng$";
+ private static final Pattern TECHNICAL_TARGET_PATTERN = Pattern.compile(
+ "^(?:https?://.*|[A-Za-z0-9_./:%?#=&;,+~@!$'()\\[\\]-]+\\.(?:html|inc|json|xml|rss|css|js|pac)(?:[?#].*)?)$",
+ Pattern.CASE_INSENSITIVE);
+
/**
* Translate source using entries in translationTable
* @param source text to translate. Must be non null.
@@ -112,8 +117,12 @@ public class Translator {
}
if (boundary) { // boundary check ok -> translate
- builder.replace(index, index + key.length(), translation);
- index = builder.indexOf(key, index + translation.length());
+ if (isInsideTechnicalTarget(builder, index, key.length())) {
+ index = builder.indexOf(key, index + key.length());
+ } else {
+ builder.replace(index, index + key.length(), translation);
+ index = builder.indexOf(key, index + translation.length());
+ }
} else { // otherwise just skip to next occurence
index = builder.indexOf(key, index + key.length());
}
@@ -124,6 +133,35 @@ public class Translator {
return builder.toString();
}
+ private static boolean isInsideTechnicalTarget(final CharSequence content, final int index, final int length) {
+ int start = index;
+ while (start > 0 && isTechnicalTargetCharacter(content.charAt(start - 1))) {
+ start--;
+ }
+
+ int end = index + length;
+ while (end < content.length() && isTechnicalTargetCharacter(content.charAt(end))) {
+ end++;
+ }
+
+ if (start == index && end == index + length) {
+ return false;
+ }
+
+ final String token = content.subSequence(start, end).toString();
+ return TECHNICAL_TARGET_PATTERN.matcher(token).matches();
+ }
+
+ private static boolean isTechnicalTargetCharacter(final char c) {
+ return Character.isLetterOrDigit(c)
+ || c == '_' || c == '-' || c == '.' || c == '/'
+ || c == ':' || c == '%' || c == '?' || c == '#'
+ || c == '=' || c == '&' || c == ';' || c == ','
+ || c == '+' || c == '~' || c == '@' || c == '!'
+ || c == '$' || c == '\'' || c == '(' || c == ')'
+ || c == '[' || c == ']';
+ }
+
/**
* Load multiple translationLists from one File. Each List starts with #File: relative/path/to/file
* (within each file section translation is done in order of the language file entries, on conflicts
@@ -300,6 +338,7 @@ public class Translator {
final File destDir = new File(env.getDataPath("locale.translated_html", "DATA/LOCALE/htroot"), lang.substring(0, lang.length() - 4));// cut .lng
final File translationFile = new File(langPath, lang);
+ FileUtils.deletedelete(destDir);
if (translateFilesRecursive(sourceDir, destDir, translationFile, "html,template,inc", "locale")) {
env.setConfig("locale.language", lang.substring(0, lang.length() - 4));
Formatter.setLocale(env.getConfig("locale.language", "en"));
diff --git a/source/net/yacy/utils/translation/GenerateMasterXliff.java b/source/net/yacy/utils/translation/GenerateMasterXliff.java
index 9cc1b691c..8a9d3c1e0 100755
--- a/source/net/yacy/utils/translation/GenerateMasterXliff.java
+++ b/source/net/yacy/utils/translation/GenerateMasterXliff.java
@@ -39,8 +39,8 @@ public class GenerateMasterXliff {
/**
* Read all translation files in YaCy custom lng format from a given folder
* (locales/ as default), and convert them to a single master xliff file
- * containing entries to translate. When the ouput master file already exists,
- * it is updated with new entries from the lng files.
+ * containing entries to translate. When the output master file already exists,
+ * it is replaced with the currently valid entries from the lng files.
*
* @param args
* runtime optional arguments<br/>
@@ -74,7 +74,7 @@ public class GenerateMasterXliff {
masterXlf = new File("master.lng.xlf");
}
if (masterXlf.exists()) {
- System.out.println("Updating master xliff file at " + masterXlf.getAbsolutePath());
+ System.out.println("Replacing master xliff file at " + masterXlf.getAbsolutePath());
} else {
System.out.println("Generating a new master xliff file at " + masterXlf.getAbsolutePath());
}
diff --git a/source/net/yacy/utils/translation/GenerateSourceMasterXliff.java b/source/net/yacy/utils/translation/GenerateSourceMasterXliff.java
new file mode 100644
index 000000000..1832ad676
--- /dev/null
+++ b/source/net/yacy/utils/translation/GenerateSourceMasterXliff.java
@@ -0,0 +1,428 @@
+// GenerateSourceMasterXliff.java
+// ---------------------------
+// Copyright 2026 by YaCy contributors
+//
+// This is a part of YaCy, a peer-to-peer based web search engine
+//
+// LICENSE
+//
+// This program 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 2 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package net.yacy.utils.translation;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import net.yacy.cora.util.ConcurrentLog;
+import net.yacy.document.SentenceReader;
+
+/**
+ * Generate a source-based XLIFF master candidate by extracting conservative UI
+ * text candidates directly from htroot source files.
+ *
+ * This tool does not use existing *.lng files as input. It is intended to build
+ * and review a source-of-truth candidate before checking individual languages
+ * for completeness.
+ */
+public class GenerateSourceMasterXliff {
+
+ private static final List<String> SOURCE_EXTENSIONS = Arrays.asList(".html", ".template", ".inc");
+
+ private static final Set<String> TEXT_ATTRIBUTES = new LinkedHashSet<>(
+ Arrays.asList("alt", "aria-label", "placeholder", "title", "value"));
+
+ private static final Pattern IGNORED_BLOCK_PATTERN = Pattern.compile(
+ "(?is)<(script|style|pre|code|textarea)\\b[^>]*>.*?</\\1>");
+
+ private static final Pattern HTML_COMMENT_PATTERN = Pattern.compile("(?s)<!--.*?-->");
+
+ private static final Pattern TAG_PATTERN = Pattern.compile("(?s)<[^>]+>");
+
+ private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile(
+ "(?is)</?(?:html|head|body|title|div|p|table|thead|tbody|tfoot|tr|td|th|ul|ol|li|form|fieldset|legend|dl|dt|dd|select|option|button|input|h[1-6]|section|article|nav|header|footer|hr)\\b[^>]*>");
+
+ private static final Pattern LINK_TAG_PATTERN = Pattern.compile("(?is)</?a\\b[^>]*>");
+
+ private static final Pattern UNPRESERVED_TAG_PATTERN = Pattern.compile(
+ "(?is)<(?!/?(?:abbr|b|strong|em|i|br)\\b)[^>]+>");
+
+ private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile(
+ "(?i)\\b([a-zA-Z_:][-a-zA-Z0-9_:.]*)\\s*=\\s*(\"([^\"]*)\"|'([^']*)')");
+
+ private static final Pattern TEMPLATE_BOUNDARY_PATTERN = Pattern.compile(
+ "#\\([^)]*\\)#|#\\(/[^)]*\\)#|#\\{[^}]+\\}#|#\\{/[^}]+\\}#|#%[^%]+%#|::");
+
+ private static final Pattern HTML_SPACE_ENTITY_ONLY_PATTERN = Pattern.compile(
+ "(?i)(?:&nbsp;|&#160;|&ensp;|&emsp;|\\s)+");
+
+ private static final Pattern HTML_SPACE_ENTITY_EDGE_PATTERN = Pattern.compile(
+ "(?i)^(?:&nbsp;|&#160;|&ensp;|&emsp;|\\s)+|(?:&nbsp;|&#160;|&ensp;|&emsp;|\\s)+$");
+
+ private static final Pattern FORMAT_WRAPPER_PATTERN = Pattern.compile(
+ "(?is)^<(strong|b|em|i)>(.*)</\\1>(?:<br\\s*/?>)?$");
+
+ private static final Pattern HTML_LINE_BREAK_EDGE_PATTERN = Pattern.compile(
+ "(?is)^(?:<br\\s*/?>\\s*)+|(?:\\s*<br\\s*/?>)+$");
+
+ private static final Pattern HTML_TAG_ONLY_PATTERN = Pattern.compile("(?is)</?[a-z][^>]*>");
+
+ /**
+ * @param args runtime optional arguments<br/>
+ * <ul>
+ * <li>args[0] : source folder path (htroot/ as default)</li>
+ * <li>args[1] : output xliff master file path
+ * (source-master.lng.xlf as default)</li>
+ * </ul>
+ * @throws IOException when a read/write error occurred
+ */
+ public static void main(final String args[]) throws IOException {
+ try {
+ final File sourceFolder = args.length > 0 && args[0] != null ? new File(args[0]) : new File("htroot");
+ if (!sourceFolder.isDirectory()) {
+ System.err.println(sourceFolder.getPath() + " is not a directory");
+ return;
+ }
+
+ final File masterXlf = args.length > 1 && args[1] != null ? new File(args[1]) : new File("source-master.lng.xlf");
+
+ System.out.println("Extracting source text from folder " + sourceFolder.getAbsolutePath());
+ System.out.println((masterXlf.exists() ? "Replacing" : "Generating") + " source master xliff file at "
+ + masterXlf.getAbsolutePath());
+
+ final ExtractionResult extractionResult = extractSourceMaster(sourceFolder.toPath());
+ final Map<String, Map<String, String>> sourceMaster = extractionResult.sourceMaster;
+ final VerificationResult verificationResult = verifySourceMaster(sourceFolder.toPath(), sourceMaster);
+ if (!verificationResult.isOk()) {
+ throw new IOException("Source master verification failed: " + verificationResult);
+ }
+ new TranslatorXliff().saveAsXliff(null, masterXlf, sourceMaster);
+
+ int unitCount = 0;
+ for (Map<String, String> entries : sourceMaster.values()) {
+ unitCount += entries.size();
+ }
+ System.out.println("Extracted " + unitCount + " source text candidates from " + sourceMaster.size() + " files.");
+ System.out.println("Rejected " + extractionResult.rejectedCandidates
+ + " extracted candidates without a runtime boundary match.");
+ System.out.println("Verified " + verificationResult.verifiedKeys
+ + " source text candidates in " + verificationResult.verifiedFiles + " files.");
+ } finally {
+ ConcurrentLog.shutdown();
+ }
+ }
+
+ static ExtractionResult extractSourceMaster(final Path sourceRoot) throws IOException {
+ final ExtractionResult result = new ExtractionResult();
+ try (Stream<Path> files = Files.walk(sourceRoot)) {
+ files.filter(Files::isRegularFile)
+ .filter(GenerateSourceMasterXliff::hasSupportedExtension)
+ .forEach(path -> extractFile(sourceRoot, path, result.sourceMaster, result));
+ }
+ return result;
+ }
+
+ private static boolean hasSupportedExtension(final Path path) {
+ final String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
+ for (String extension : SOURCE_EXTENSIONS) {
+ if (fileName.endsWith(extension)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void extractFile(final Path sourceRoot, final Path sourceFile,
+ final Map<String, Map<String, String>> sourceMaster, final ExtractionResult result) {
+ try {
+ final String content = new String(Files.readAllBytes(sourceFile), StandardCharsets.UTF_8);
+ final LinkedHashSet<String> candidates = extractCandidates(relativeSourcePath(sourceRoot, sourceFile), content);
+ if (!candidates.isEmpty()) {
+ final String relativePath = relativeSourcePath(sourceRoot, sourceFile);
+ final Map<String, String> entries = new LinkedHashMap<>();
+ for (String candidate : candidates) {
+ if (hasRuntimeBoundaryMatch(content, candidate)) {
+ entries.put(candidate, null);
+ } else {
+ result.rejectedCandidates++;
+ }
+ }
+ if (!entries.isEmpty()) {
+ sourceMaster.put(relativePath, entries);
+ }
+ }
+ } catch (IOException e) {
+ ConcurrentLog.warn("TRANSLATOR", "Could not extract source text from " + sourceFile + ": " + e.getMessage());
+ }
+ }
+
+ private static String relativeSourcePath(final Path sourceRoot, final Path sourceFile) {
+ return sourceRoot.relativize(sourceFile).toString().replace(File.separatorChar, '/');
+ }
+
+ static VerificationResult verifySourceMaster(final Path sourceRoot,
+ final Map<String, Map<String, String>> sourceMaster) throws IOException {
+ final VerificationResult result = new VerificationResult();
+ for (Map.Entry<String, Map<String, String>> fileEntry : sourceMaster.entrySet()) {
+ result.verifiedFiles++;
+ final Path sourceFile = sourceRoot.resolve(fileEntry.getKey());
+ if (!Files.isRegularFile(sourceFile)) {
+ result.missingFiles++;
+ continue;
+ }
+
+ final String content = new String(Files.readAllBytes(sourceFile), StandardCharsets.UTF_8);
+ for (String key : fileEntry.getValue().keySet()) {
+ if (key == null || key.isEmpty()) {
+ result.emptyKeys++;
+ continue;
+ }
+ if (content.indexOf(key) < 0) {
+ result.missingRawMatches++;
+ continue;
+ }
+ if (!hasRuntimeBoundaryMatch(content, key)) {
+ result.missingRuntimeBoundaryMatches++;
+ continue;
+ }
+ result.verifiedKeys++;
+ }
+ }
+ return result;
+ }
+
+ private static boolean hasRuntimeBoundaryMatch(final String content, final String key) {
+ if (key == null || key.isEmpty()) {
+ return false;
+ }
+
+ int index = content.indexOf(key);
+ while (index >= 0) {
+ if (hasRuntimeBoundaries(content, key, index)) {
+ return true;
+ }
+ index = content.indexOf(key, index + key.length());
+ }
+ return false;
+ }
+
+ private static boolean hasRuntimeBoundaries(final String content, final String key, final int index) {
+ boolean boundary = index + key.length() >= content.length();
+
+ if (!boundary) {
+ final char c = content.charAt(index + key.length() - 1);
+ final char lc = content.charAt(index + key.length());
+ boundary |= SentenceReader.punctuation(c) || SentenceReader.invisible(c);
+ boundary |= SentenceReader.punctuation(lc) || SentenceReader.invisible(lc);
+ }
+
+ if (boundary && index > 0) {
+ final char c = content.charAt(index - 1);
+ boundary = SentenceReader.punctuation(c) || SentenceReader.invisible(c);
+ final char fc = content.charAt(index);
+ boundary |= SentenceReader.punctuation(fc) || SentenceReader.invisible(fc);
+ }
+ return boundary;
+ }
+
+ static LinkedHashSet<String> extractCandidates(final String content) {
+ return extractCandidates(null, content);
+ }
+
+ static LinkedHashSet<String> extractCandidates(final String sourcePath, final String content) {
+ final LinkedHashSet<String> candidates = new LinkedHashSet<>();
+ String withoutIgnoredBlocks = HTML_COMMENT_PATTERN.matcher(
+ IGNORED_BLOCK_PATTERN.matcher(content).replaceAll("")).replaceAll("");
+ if ("jslicense.html".equals(sourcePath)) {
+ withoutIgnoredBlocks = withoutIgnoredBlocks.replaceAll("(?is)<td\\b[^>]*>.*?</td>", "");
+ }
+
+ Matcher tagMatcher = TAG_PATTERN.matcher(withoutIgnoredBlocks);
+ while (tagMatcher.find()) {
+ addAttributeCandidates(tagMatcher.group(), candidates);
+ }
+
+ final String textSource = UNPRESERVED_TAG_PATTERN.matcher(
+ LINK_TAG_PATTERN.matcher(BLOCK_TAG_PATTERN.matcher(withoutIgnoredBlocks).replaceAll("\n")).replaceAll(""))
+ .replaceAll("\n");
+ addTextCandidates(textSource, candidates);
+ return candidates;
+ }
+
+ private static void addAttributeCandidates(final String tag, final Set<String> candidates) {
+ final String lowerTag = tag.toLowerCase(Locale.ROOT);
+ if (lowerTag.contains("type=\"hidden\"") || lowerTag.contains("type='hidden'")) {
+ return;
+ }
+
+ final Matcher attributeMatcher = ATTRIBUTE_PATTERN.matcher(tag);
+ while (attributeMatcher.find()) {
+ final String attributeName = attributeMatcher.group(1).toLowerCase(Locale.ROOT);
+ if (TEXT_ATTRIBUTES.contains(attributeName)) {
+ if ("value".equals(attributeName) && !isButtonValueAttribute(lowerTag)) {
+ continue;
+ }
+ final String value = attributeMatcher.group(3) != null ? attributeMatcher.group(3) : attributeMatcher.group(4);
+ final String quote = attributeMatcher.group(3) != null ? "\"" : "'";
+ addCandidate(quote + value + quote, candidates);
+ }
+ }
+ }
+
+ private static boolean isButtonValueAttribute(final String lowerTag) {
+ return lowerTag.startsWith("<button") || lowerTag.contains("type=\"submit\"") || lowerTag.contains("type='submit'")
+ || lowerTag.contains("type=\"button\"") || lowerTag.contains("type='button'")
+ || lowerTag.contains("type=\"reset\"") || lowerTag.contains("type='reset'");
+ }
+
+ private static void addTextCandidates(final String text, final Set<String> candidates) {
+ final String[] templateParts = TEMPLATE_BOUNDARY_PATTERN.split(text);
+ for (String templatePart : templateParts) {
+ final String[] lines = templatePart.split("\\r?\\n");
+ for (String line : lines) {
+ addCandidate(line, candidates);
+ }
+ }
+ }
+
+ private static void addCandidate(final String rawCandidate, final Set<String> candidates) {
+ if (rawCandidate == null) {
+ return;
+ }
+
+ final String candidate = normalizeFormattingWrapper(trimHtmlLineBreaks(
+ trimHtmlSpaceEntities(trimInvisible(rawCandidate))));
+ if (!isUsefulCandidate(candidate)) {
+ return;
+ }
+ candidates.add(candidate);
+ }
+
+ private static String trimInvisible(final String text) {
+ int start = 0;
+ int end = text.length();
+ while (start < end && Character.isWhitespace(text.charAt(start))) {
+ start++;
+ }
+ while (end > start && Character.isWhitespace(text.charAt(end - 1))) {
+ end--;
+ }
+ return text.substring(start, end);
+ }
+
+ private static String trimHtmlSpaceEntities(final String text) {
+ return HTML_SPACE_ENTITY_EDGE_PATTERN.matcher(text).replaceAll("");
+ }
+
+ private static String trimHtmlLineBreaks(final String text) {
+ return HTML_LINE_BREAK_EDGE_PATTERN.matcher(text).replaceAll("");
+ }
+
+ private static String normalizeFormattingWrapper(final String text) {
+ final Matcher matcher = FORMAT_WRAPPER_PATTERN.matcher(text);
+ if (matcher.matches()) {
+ return trimHtmlSpaceEntities(trimInvisible(matcher.group(2)));
+ }
+ return text;
+ }
+
+ private static boolean isUsefulCandidate(final String candidate) {
+ if (candidate.length() < 2) {
+ return false;
+ }
+ if (HTML_SPACE_ENTITY_ONLY_PATTERN.matcher(candidate).matches()) {
+ return false;
+ }
+ if (HTML_TAG_ONLY_PATTERN.matcher(candidate).matches()) {
+ return false;
+ }
+ if (candidate.indexOf("==") >= 0) {
+ return false;
+ }
+ if (candidate.endsWith("=")) {
+ return false;
+ }
+ if (candidate.startsWith("#")) {
+ return false;
+ }
+ if (candidate.startsWith("//") || candidate.startsWith("/*") || candidate.startsWith("*")) {
+ return false;
+ }
+ if (candidate.startsWith(":") || candidate.startsWith(".") || candidate.startsWith("-")) {
+ return false;
+ }
+ if (candidate.indexOf("#[") >= 0 || candidate.indexOf("#(") >= 0 || candidate.indexOf("#{") >= 0
+ || candidate.indexOf("#%") >= 0) {
+ return false;
+ }
+
+ boolean hasLetter = false;
+ for (int i = 0; i < candidate.length(); i++) {
+ if (Character.isLetter(candidate.charAt(i))) {
+ hasLetter = true;
+ break;
+ }
+ }
+ return hasLetter;
+ }
+
+ static final class ExtractionResult {
+
+ private final Map<String, Map<String, String>> sourceMaster = new TreeMap<>();
+
+ private int rejectedCandidates;
+ }
+
+ static final class VerificationResult {
+
+ private int verifiedFiles;
+
+ private int verifiedKeys;
+
+ private int missingFiles;
+
+ private int emptyKeys;
+
+ private int missingRawMatches;
+
+ private int missingRuntimeBoundaryMatches;
+
+ private boolean isOk() {
+ return this.missingFiles == 0 && this.emptyKeys == 0 && this.missingRawMatches == 0
+ && this.missingRuntimeBoundaryMatches == 0;
+ }
+
+ @Override
+ public String toString() {
+ return "verifiedFiles=" + this.verifiedFiles + ", verifiedKeys=" + this.verifiedKeys
+ + ", missingFiles=" + this.missingFiles + ", emptyKeys=" + this.emptyKeys
+ + ", missingRawMatches=" + this.missingRawMatches
+ + ", missingRuntimeBoundaryMatches=" + this.missingRuntimeBoundaryMatches;
+ }
+ }
+}
diff --git a/source/net/yacy/utils/translation/TranslationManager.java b/source/net/yacy/utils/translation/TranslationManager.java
index 1b1bbc42f..744dc9a3b 100644
--- a/source/net/yacy/utils/translation/TranslationManager.java
+++ b/source/net/yacy/utils/translation/TranslationManager.java
@@ -162,17 +162,16 @@ public class TranslationManager extends TranslatorXliff {
/**
* Create a master translation list by reading all translation files
- * If a masterOutputFile exists, content is preserved (loaded first)
+ * Existing masterOutputFile content is replaced. This keeps the generated
+ * master list in sync with strings that still occur in the current source
+ * files and avoids carrying stale source entries forward.
*
* @param localesFolder folder containing *.lng translation files
* @param masterOutputFile output file (xliff format). Must not be null.
* @throws IOException
*/
public void createMasterTranslationLists(final File localesFolder, final File masterOutputFile) throws IOException {
- if (masterOutputFile.exists()) // if file exists, conserve existing master content (may be updated by external tool)
- mainTransLists = loadTranslationsListsFromXliff(masterOutputFile);
- else
- mainTransLists = new TreeMap<String, Map<String, String>>();
+ mainTransLists = new TreeMap<String, Map<String, String>>();
List<String> lngFiles = Translator.langFiles(localesFolder);
for (String filename : lngFiles) {
diff --git a/source/net/yacy/utils/translation/TranslatorXliff.java b/source/net/yacy/utils/translation/TranslatorXliff.java
index d30009ae4..77cd7aeaa 100644
--- a/source/net/yacy/utils/translation/TranslatorXliff.java
+++ b/source/net/yacy/utils/translation/TranslatorXliff.java
@@ -227,7 +227,7 @@ public class TranslatorXliff extends Translator {
final OutputStreamWriter output = new OutputStreamWriter(fileOutStream, StandardCharsets.UTF_8.name());
) {
output.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
- output.write("<xliff version='1.2' xmlns='urn:oasis:names:tc:xliff:document:1.2'> \n");
+ output.write("<xliff version='1.2' xmlns='urn:oasis:names:tc:xliff:document:1.2'>\n");
for (String afilemap : lng.keySet()) {
output.write("<file original=\"" + afilemap + "\" " // original required in xliff 1.2
+ " source-language=\"" + sourceLanguage + "\" "); // required in xliff 1.2
diff --git a/source/net/yacy/yacy.java b/source/net/yacy/yacy.java
index 1a7166eb4..1a5babbc4 100644
--- a/source/net/yacy/yacy.java
+++ b/source/net/yacy/yacy.java
@@ -362,6 +362,7 @@ public final class yacy {
try { //is this another version?!
final File sourceDir = new File(sb.getConfig(SwitchboardConstants.HTROOT_PATH, SwitchboardConstants.HTROOT_PATH_DEFAULT));
final File destDir = new File(sb.getDataPath("locale.translated_html", "DATA/LOCALE/htroot"), tmplang);
+ FileUtils.deletedelete(destDir);
if (new TranslatorXliff().translateFilesRecursive(sourceDir, destDir, new File(locale_source, tmplang + ".lng"), "html,template,inc", "locale")) { //translate it
//write the new Versionnumber
final BufferedWriter bw = new BufferedWriter(new PrintWriter(new FileWriter(new File(destDir, "version"))));