mirror of
https://github.com/yacy/yacy_search_server.git
synced 2026-10-25 05:54:15 +00:00
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.
This commit is contained in:
1 parent
2ca403a1fc
commit
57d188e69a
25 files changed
+50494
-26136
No files matched your search
@@ -44,14 +44,16 @@
|
||||
<input type="radio" name="language" value="default" id="lang_en" onchange="this.form.submit()" #(lang_en)#::checked="checked"#(/lang_en)# /><label for="lang_en" #(active_en)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_en)#>English</label>
|
||||
<input type="radio" name="language" value="de" id="lang_de" onchange="this.form.submit()" #(lang_de)#::checked="checked"#(/lang_de)# /><label for="lang_de" #(active_de)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_de)#>Deutsch</label>
|
||||
<input type="radio" name="language" value="fr" id="lang_fr" onchange="this.form.submit()" #(lang_fr)#::checked="checked"#(/lang_fr)# /><label for="lang_fr" #(active_fr)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_fr)#>Français</label>
|
||||
<input type="radio" name="language" value="zh" id="lang_zh" onchange="this.form.submit()" #(lang_zh)#::checked="checked"#(/lang_zh)# /><label for="lang_zh" #(active_zh)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_zh)#>中文</label>
|
||||
<input type="radio" name="language" value="ru" id="lang_ru" onchange="this.form.submit()" #(lang_ru)#::checked="checked"#(/lang_ru)# /><label for="lang_ru" #(active_ru)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_ru)#>Русский</label>
|
||||
<input type="radio" name="language" value="uk" id="lang_uk" onchange="this.form.submit()" #(lang_uk)#::checked="checked"#(/lang_uk)# /><label for="lang_uk" #(active_uk)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_uk)#>Українська</label>
|
||||
<input type="radio" name="language" value="hi" id="lang_hi" onchange="this.form.submit()" #(lang_hi)#::checked="checked"#(/lang_hi)# /><label for="lang_hi" #(active_hi)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_hi)#>हिन्दी</label>
|
||||
<input type="radio" name="language" value="ja" id="lang_ja" onchange="this.form.submit()" #(lang_ja)#::checked="checked"#(/lang_ja)# /><label for="lang_ja" #(active_ja)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_ja)#>日本語</label>
|
||||
<input type="radio" name="language" value="pl" id="lang_pl" onchange="this.form.submit()" #(lang_pl)#::checked="checked"#(/lang_pl)# /><label for="lang_pl" #(active_pl)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_pl)#>Polski</label>
|
||||
<input type="radio" name="language" value="el" id="lang_el" onchange="this.form.submit()" #(lang_el)#::checked="checked"#(/lang_el)# /><label for="lang_el" #(active_el)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_el)#>Greek</label>
|
||||
<input type="radio" name="language" value="it" id="lang_it" onchange="this.form.submit()" #(lang_it)#::checked="checked"#(/lang_it)# /><label for="lang_it" #(active_el)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_el)#>Italiano</label>
|
||||
<input type="radio" name="language" value="es" id="lang_es" onchange="this.form.submit()" #(lang_es)#::checked="checked"#(/lang_es)# /><label for="lang_es" #(active_es)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_es)#>Español</label>
|
||||
<input type="radio" name="language" value="es" id="lang_es" onchange="this.form.submit()" #(lang_es)#::checked="checked"#(/lang_es)# /><label for="lang_es" #(active_es)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_es)#>Español</label>
|
||||
<input type="radio" name="language" value="uk" id="lang_uk" onchange="this.form.submit()" #(lang_uk)#::checked="checked"#(/lang_uk)# /><label for="lang_uk" #(active_uk)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_uk)#>Українська</label>
|
||||
<input type="radio" name="language" value="ru" id="lang_ru" onchange="this.form.submit()" #(lang_ru)#::checked="checked"#(/lang_ru)# /><label for="lang_ru" #(active_ru)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_ru)#>Русский</label>
|
||||
<input type="radio" name="language" value="zh" id="lang_zh" onchange="this.form.submit()" #(lang_zh)#::checked="checked"#(/lang_zh)# /><label for="lang_zh" #(active_zh)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_zh)#>中文</label>
|
||||
<input type="radio" name="language" value="hi" id="lang_hi" onchange="this.form.submit()" #(lang_hi)#::checked="checked"#(/lang_hi)# /><label for="lang_hi" #(active_hi)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_hi)#>हिन्दी</label>
|
||||
<input type="radio" name="language" value="ja" id="lang_ja" onchange="this.form.submit()" #(lang_ja)#::checked="checked"#(/lang_ja)# /><label for="lang_ja" #(active_ja)#::title="Click to generate translated pages"::class="label-success" title="Active : translated pages are available"#(/active_ja)#>日本語</label>
|
||||
|
||||
</fieldset>
|
||||
</li>
|
||||
<!-- take care that no other items are changed, but also change the former if no js is enabled -->
|
||||
|
||||
+88
-15
@@ -70,12 +70,18 @@ Wichtig:
|
||||
englische Text auf mehreren Seiten vorkommt, braucht **jede** Seite ihren
|
||||
eigenen Abschnitt mit diesem Eintrag (Übersetzung ist pro Datei-Abschnitt).
|
||||
|
||||
### Lokalisierter Abschnitts-Header (`#Dosya:`)
|
||||
Ausnahme: `tr.lng` verwendet für viele Abschnitte das türkische Schlüsselwort
|
||||
**`#Dosya:`** statt `#File:`. Werkzeuge, die Abschnitte parsen, müssen **beide**
|
||||
erkennen, z. B. per Regex `^#(?:File|Dosya):\s*(.*)$`. Andere `#Xxx:`-Präfixe
|
||||
(`#YaCy:`, `#Subject:`, `#URL:` …) sind **keine** Header, sondern Übersetzungs-
|
||||
schlüssel, die zufällig mit `#` beginnen.
|
||||
### Abschnitts-Header: nur `#File:`
|
||||
Der Runtime-Loader (`Translator.loadTranslationsLists`) erkennt derzeit **nur**
|
||||
`#File:` als Abschnitts-Header. Zeilen mit anderen Präfixen, z. B. `#Dosya:`,
|
||||
werden wie normale Kommentarzeilen behandelt; die folgenden Einträge landen dann
|
||||
nicht im beabsichtigten Dateiabschnitt und greifen zur Laufzeit nicht korrekt.
|
||||
|
||||
Hinweis: Ältere oder importierte Dateien können lokalisierte Header wie
|
||||
`#Dosya:` enthalten. Solche Header müssen vor einem Runtime-Test nach `#File:`
|
||||
normalisiert werden (oder der Java-Loader muss explizit erweitert werden).
|
||||
Andere `#Xxx:`-Präfixe (`#YaCy:`, `#Subject:`, `#URL:` …) sind ebenfalls keine
|
||||
Header. Da alle Zeilen mit `#` ignoriert werden, sind auch `#key==wert`-Zeilen
|
||||
auskommentierte Einträge und keine aktiven Übersetzungen.
|
||||
|
||||
---
|
||||
|
||||
@@ -137,6 +143,17 @@ Ablauf (`Translator.translate` / `translateFilesRecursive`):
|
||||
Es gibt **keinen** automatischen Extraktor, der „übersetzbare Strings“
|
||||
erkennt — die Schlüssel werden von Hand gepflegt.
|
||||
|
||||
Wichtig: Beim Erzeugen der lokalisierten Dateien werden nur Quelldateien
|
||||
geschrieben, für die in der Sprachdatei ein passender `#File:`-Abschnitt
|
||||
existiert. Fehlt der Abschnitt, wird diese Datei nicht als lokalisierte Kopie
|
||||
erzeugt.
|
||||
|
||||
Die erzeugten Seiten liegen zur Laufzeit unter `DATA/LOCALE/htroot/<sprache>`.
|
||||
Beim Umschalten oder automatischen Refresh einer Sprache muss dieser Ordner
|
||||
vorher gelöscht werden; sonst können nicht mehr erzeugte Altdateien weiterhin
|
||||
ausgeliefert werden. Aktuelle YaCy-Versionen erledigen das beim Sprachwechsel
|
||||
und beim versionsbedingten Startup-Refresh automatisch.
|
||||
|
||||
---
|
||||
|
||||
## 5. Template-Markup in den Quelldateien
|
||||
@@ -206,21 +223,70 @@ Leerzeichen, bis zum `#[path]#`), **nicht** die ganze Zeile inkl. `#[path]#`.
|
||||
JSON-Snippets) — nur die umgebende Prosa übersetzen.
|
||||
- **Test-/Demo-Dateien** und rein technische Bezeichner (Feldnamen wie
|
||||
`num_ctx`, `max_tokens`, Rollennamen wie `search-query`).
|
||||
- **Technische Link-Ziele, Pfade, Servlets und URLs** bleiben literal. Nicht
|
||||
übersetzen oder durch Leerzeichen beschädigen: `Network.html` bleibt
|
||||
`Network.html`, `sharedBlacklist.html` bleibt `sharedBlacklist.html`,
|
||||
`share.json` bleibt `share.json`, `styles/prosilver/template/overall_header.html`
|
||||
bleibt unverändert und URLs wie `http://localhost:8090/proxy.html?...`
|
||||
dürfen nicht lokalisiert werden.
|
||||
|
||||
Prüfung aus dem Repository-Root:
|
||||
|
||||
```bash
|
||||
python3 locales/validate-locale-links.py --exclude pl.lng
|
||||
```
|
||||
|
||||
`--exclude` ist nützlich, wenn eine Sprache parallel in einem anderen Arbeitszweig
|
||||
bearbeitet wird. Für einzelne Sprachen kann `--include de.lng --include fr.lng`
|
||||
verwendet werden. Ein sauberer Lauf endet mit `OK: ... no link target issues found.`
|
||||
|
||||
---
|
||||
|
||||
## 8. `master.lng.xlf`
|
||||
|
||||
`master.lng.xlf` ist eine XLIFF-Datei, die den **Gesamtbestand** aller
|
||||
übersetzbaren Strings pro Datei als `<source>`-Elemente führt.
|
||||
`master.lng.xlf` ist die source-basierte XLIFF-Referenz der übersetzbaren
|
||||
Strings pro Datei. Die Wahrheit für diesen Master liegt in den Quellen unter
|
||||
`htroot`, nicht in bereits vorhandenen `.lng`-Dateien.
|
||||
|
||||
- Sie wird **generiert** (`TranslationManager.createMasterTranslationLists`):
|
||||
Grundlage sind die vorhandenen `.lng`-Schlüssel, gefiltert danach, ob sie noch
|
||||
als Teilstring in der jeweiligen Quelldatei vorkommen (`content.indexOf >= 0`).
|
||||
- Sie wird mit `GenerateSourceMasterXliff` aus sichtbaren Textknoten und
|
||||
ausgewählten UI-Attributen (`alt`, `title`, `placeholder`, `aria-label`,
|
||||
Button-`value`) erzeugt.
|
||||
- Jeder Kandidat wird gegen den Roh-Quelltext und die Runtime-Wortgrenzen der
|
||||
Übersetzung geprüft. Nicht darstellbare `.lng`-Keys, z. B. Keys mit `==` oder
|
||||
einem abschließenden `=`, werden verworfen.
|
||||
- **Nicht** von Hand mit Hash-/Zeilen-IDs pflegen — nach Änderungen an Quellen
|
||||
oder `.lng`-Dateien besser über das YaCy-Tooling neu erzeugen. Ein kompletter
|
||||
`<file>…</file>`-Block darf jedoch sauber entfernt werden (z. B. wenn die
|
||||
zugehörige Seite gelöscht wurde).
|
||||
über das YaCy-Tooling neu erzeugen und das Delta prüfen.
|
||||
- Beim Refresh wird die Zieldatei ersetzt. Stale Master-Einträge fallen dadurch
|
||||
weg, auch wenn sie noch in alten `.lng`-Dateien stehen.
|
||||
|
||||
Refresh aus dem Repository-Root:
|
||||
|
||||
```bash
|
||||
java -cp 'build/classes/java/main:lib/*' \
|
||||
net.yacy.utils.translation.GenerateSourceMasterXliff \
|
||||
htroot locales/master.lng.xlf
|
||||
```
|
||||
|
||||
Falls die Klassen noch nicht kompiliert sind, vorher `ant compile` ausführen.
|
||||
Das zweite Argument ist wichtig: ohne `locales/master.lng.xlf` schreibt das Tool
|
||||
standardmäßig nach `./source-master.lng.xlf` im Repository-Root. Existiert die
|
||||
Zieldatei bereits, wird sie ersetzt.
|
||||
|
||||
### Legacy: bestandbasierter Master
|
||||
|
||||
`GenerateMasterXliff` erzeugt nur einen bestandbasierten Master aus vorhandenen
|
||||
`.lng`-Schlüsseln, gefiltert danach, ob sie noch als Teilstring in der
|
||||
jeweiligen Quelldatei vorkommen (`content.indexOf >= 0`):
|
||||
|
||||
```bash
|
||||
java -cp 'build/classes/java/main:lib/*' \
|
||||
net.yacy.utils.translation.GenerateMasterXliff \
|
||||
locales /tmp/master-from-lng.lng.xlf
|
||||
```
|
||||
|
||||
Dieses Tool ist nützlich zur Diagnose von Altbestand, aber nicht als
|
||||
Vollständigkeitsreferenz: englische UI-Texte, die noch in keiner `.lng`-Datei als
|
||||
Schlüssel vorkommen, erscheinen dort nicht.
|
||||
|
||||
---
|
||||
|
||||
@@ -240,7 +306,14 @@ Leerzeichen, bis zum `#[path]#`), **nicht** die ganze Zeile inkl. `#[path]#`.
|
||||
5. **Einordnen:** neuen `#File:`-Abschnitt anlegen; Einträge längster-zuerst
|
||||
sortieren (Abschnitt 6, Regel 5). Neue Abschnitte können am Dateiende
|
||||
angehängt werden (die Datei ist nicht streng sortiert).
|
||||
6. **Zeilenenden beachten** (Abschnitt 10).
|
||||
6. **Vollständigkeit beidseitig prüfen:**
|
||||
- `master.lng.xlf -> <sprache>.lng`: fehlende Source-Schlüssel ergänzen.
|
||||
- `<sprache>.lng -> master.lng.xlf`: Extras prüfen und in der Regel entfernen;
|
||||
sie sind stale oder stammen aus einem nicht frisch generierten Master.
|
||||
- Für jeden aktiven Sprach-Key prüfen: `key in htroot/<#File>`.
|
||||
- Doppelte Schlüssel und auskommentierte `#...==...`-Einträge entfernen oder
|
||||
bewusst reaktivieren.
|
||||
7. **Zeilenenden beachten** (Abschnitt 10).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2195
-1933
File diff suppressed because it is too large.
Load diff
+4701
-101
File diff suppressed because it is too large.
Load diff
+3793
-691
File diff suppressed because it is too large.
Load diff
+3753
-1390
File diff suppressed because it is too large.
Load diff
+270
-456
File diff suppressed because it is too large.
Load diff
+3818
-689
File diff suppressed because it is too large.
Load diff
+290
-792
File diff suppressed because it is too large.
Load diff
+11511
-9584
File diff suppressed because it is too large.
Load diff
+4777
File diff suppressed because it is too large.
Load diff
+2548
-1896
File diff suppressed because it is too large.
Load diff
+4367
-1204
File diff suppressed because it is too large.
Load diff
+4437
-3540
File diff suppressed because it is too large.
Load diff
+3046
-1805
File diff suppressed because it is too large.
Load diff
Executable
+163
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that locale translations preserve technical link targets.
|
||||
|
||||
The .lng format is plain text replacement. File names, servlet endpoints,
|
||||
paths and URLs inside a source string must therefore remain literal in the
|
||||
translation. This script detects translated or damaged targets such as
|
||||
Network.html -> Netzwerk.html, servletshare.json, or URLs with inserted spaces.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TARGET_EXTENSIONS = (
|
||||
"html",
|
||||
"inc",
|
||||
"json",
|
||||
"xml",
|
||||
"rss",
|
||||
"css",
|
||||
"js",
|
||||
"pac",
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate literal link targets in YaCy .lng locale files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--locales",
|
||||
type=Path,
|
||||
default=Path("locales"),
|
||||
help="Locale directory containing .lng files (default: locales).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
type=Path,
|
||||
default=Path("htroot"),
|
||||
help="YaCy htroot directory used to identify valid source targets (default: htroot).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="FILE",
|
||||
help="Only validate this .lng file name. Can be used multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="FILE",
|
||||
help="Skip this .lng file name. Can be used multiple times.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def collect_known_targets(source_dir: Path, locale_files: list[Path]) -> set[str]:
|
||||
known: set[str] = set()
|
||||
|
||||
if source_dir.exists():
|
||||
suffixes = {f".{ext}" for ext in TARGET_EXTENSIONS}
|
||||
for path in source_dir.rglob("*"):
|
||||
if path.is_file() and path.suffix.lower() in suffixes:
|
||||
relative = path.relative_to(source_dir).as_posix()
|
||||
known.add(relative)
|
||||
known.add(path.name)
|
||||
|
||||
for locale_file in locale_files:
|
||||
for line in locale_file.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
if line.startswith("#File:"):
|
||||
target = line[6:].strip()
|
||||
known.add(target)
|
||||
known.add(Path(target).name)
|
||||
|
||||
return known
|
||||
|
||||
|
||||
def locale_files(locales_dir: Path, include: list[str], exclude: list[str]) -> list[Path]:
|
||||
include_set = set(include)
|
||||
exclude_set = set(exclude)
|
||||
files = sorted(locales_dir.glob("*.lng"))
|
||||
if include_set:
|
||||
files = [path for path in files if path.name in include_set]
|
||||
if exclude_set:
|
||||
files = [path for path in files if path.name not in exclude_set]
|
||||
return files
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
files = locale_files(args.locales, args.include, args.exclude)
|
||||
known_targets = collect_known_targets(args.source, files)
|
||||
|
||||
extensions = "|".join(TARGET_EXTENSIONS)
|
||||
file_token = re.compile(
|
||||
rf"(?<![\w./:-])([A-Za-z0-9_./-]+\.(?:{extensions}))(?![\w./-])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
value_token = re.compile(
|
||||
rf"(?<![\w./:-])([^\s\"'<>(),;]+\.(?:{extensions}))(?![\w./-])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
url_token = re.compile(r"https?://[^\s\"'<>),]*")
|
||||
|
||||
failures = 0
|
||||
for locale_file in files:
|
||||
current_section: str | None = None
|
||||
missing_key_targets: list[tuple[int, str | None, list[str], str]] = []
|
||||
unknown_value_targets: list[tuple[int, str | None, str, str]] = []
|
||||
|
||||
for line_number, line in enumerate(
|
||||
locale_file.read_text(encoding="utf-8", errors="ignore").splitlines(),
|
||||
1,
|
||||
):
|
||||
if line.startswith("#File:"):
|
||||
current_section = line[6:].strip()
|
||||
continue
|
||||
if "==" not in line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
source, target = line.split("==", 1)
|
||||
source_targets = set(file_token.findall(source)) | set(url_token.findall(source))
|
||||
target_targets = set(file_token.findall(target)) | set(url_token.findall(target))
|
||||
|
||||
missing = sorted(token for token in source_targets if token not in target_targets)
|
||||
if missing:
|
||||
missing_key_targets.append((line_number, current_section, missing, line))
|
||||
|
||||
source_file_targets = set(value_token.findall(source))
|
||||
for token in value_token.findall(target):
|
||||
clean = token.strip(".,:;!?")
|
||||
if clean in known_targets or clean in source_file_targets:
|
||||
continue
|
||||
if clean.startswith(("http://", "https://")):
|
||||
continue
|
||||
unknown_value_targets.append((line_number, current_section, clean, line))
|
||||
|
||||
if missing_key_targets or unknown_value_targets:
|
||||
failures += len(missing_key_targets) + len(unknown_value_targets)
|
||||
print(f"\n## {locale_file.name}")
|
||||
for line_number, section, missing, line in missing_key_targets:
|
||||
print(f"{line_number}: missing technical target(s) {missing} in {section}")
|
||||
print(f" {line}")
|
||||
for line_number, section, token, line in unknown_value_targets:
|
||||
print(f"{line_number}: unknown translated/damaged target {token!r} in {section}")
|
||||
print(f" {line}")
|
||||
|
||||
if failures:
|
||||
print(f"\nFAILED: {failures} locale link target issue(s) found.")
|
||||
return 1
|
||||
|
||||
print(f"OK: checked {len(files)} locale file(s), no link target issues found.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+189
-2023
File diff suppressed because it is too large.
Load diff
@@ -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"));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)(?: | | | |\\s)+");
|
||||
|
||||
private static final Pattern HTML_SPACE_ENTITY_EDGE_PATTERN = Pattern.compile(
|
||||
"(?i)^(?: | | | |\\s)+|(?: | | | |\\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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))));
|
||||
|
||||
@@ -60,4 +60,21 @@ public class TranslatorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTranslateSkipsTechnicalTargets() {
|
||||
final Translator translator = new Translator();
|
||||
final Map<String, String> translationTable = new HashMap<String, String>();
|
||||
translationTable.put("Network", "Réseau");
|
||||
translationTable.put("share", "partager");
|
||||
translationTable.put("localhost", "hôte-local");
|
||||
|
||||
assertEquals("Réseau", translator.translate(new StringBuilder("Network"), translationTable));
|
||||
assertEquals("<a href=\"Network.html\">Réseau</a>",
|
||||
translator.translate(new StringBuilder("<a href=\"Network.html\">Network</a>"), translationTable));
|
||||
assertEquals("servlet share.json",
|
||||
translator.translate(new StringBuilder("servlet share.json"), translationTable));
|
||||
assertEquals("http://localhost:8090/Network.html",
|
||||
translator.translate(new StringBuilder("http://localhost:8090/Network.html"), translationTable));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.yacy.utils.translation;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
public class GenerateSourceMasterXliffTest {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void testMainExtractsSourceVisibleTextAndVerifiesOutput() throws Exception {
|
||||
final File sourceRoot = this.temporaryFolder.newFolder("htroot");
|
||||
final File sourceFile = new File(sourceRoot, "Network.html");
|
||||
Files.write(sourceFile.toPath(), (
|
||||
"<html><body>\n"
|
||||
+ "<a href=\"Network.html\" title=\"Network link\">Network</a>\n"
|
||||
+ "<input type=\"submit\" value=\"Save\">\n"
|
||||
+ "<input type=\"hidden\" value=\"Internal secret\">\n"
|
||||
+ "<script>Ignored script text</script>\n"
|
||||
+ "</body></html>\n").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final File outputFile = this.temporaryFolder.newFile("source-master.lng.xlf");
|
||||
GenerateSourceMasterXliff.main(new String[] { sourceRoot.getAbsolutePath(), outputFile.getAbsolutePath() });
|
||||
|
||||
final Map<String, Map<String, String>> master = new TranslatorXliff().loadTranslationsListsFromXliff(outputFile);
|
||||
assertTrue(master.containsKey("Network.html"));
|
||||
|
||||
final Map<String, String> entries = master.get("Network.html");
|
||||
assertTrue(entries.containsKey("Network"));
|
||||
assertTrue(entries.containsKey("\"Network link\""));
|
||||
assertTrue(entries.containsKey("\"Save\""));
|
||||
assertFalse(entries.containsKey("\"Internal secret\""));
|
||||
assertFalse(entries.containsKey("Ignored script text"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user