Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/apt-install
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ apt-get update
# Install dependencies
apt-get install -y \
gettext \
po4a \
git \
git-svn \
gnupg \
Expand Down
32 changes: 15 additions & 17 deletions weblate/formats/asciidoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from weblate.formats.convert import ConvertFormat
from weblate.utils.errors import report_error
from weblate.utils.state import FUZZY_STATES


class AsciiDocFormat(ConvertFormat):
Expand All @@ -27,46 +28,43 @@ class AsciiDocFormat(ConvertFormat):
format_id = "asciidoc"
monolingual = True

def _merge_translations(self, store, template_store):
def _merge_translations(self, store, _template_store):
"""
Add missing translation units from database to the store.
Merge Weblate database translations into the PO store from po4a-gettextize.

Only adds units that don't exist in the store. Does not merge/overwrite
existing units in the store. This ensures all database translations are
preserved even if po4a-gettextize didn't extract them.
For each ``existing_units`` entry, match on ``(source, context)``. If a unit
with that key is already in the store (typical after gettextize), overwrite
``target`` so database edits win—gettextize often sets ``msgstr`` from the
on-disk file only, which is wrong when the template and translation paths are
the same file. If no unit matches, add one so nothing from the DB is lost.
"""
# Create index of units already in store (by source + context) for quick lookup
store_units_index = {}
for unit in store.units:
if unit.isheader():
continue
# Use source + context as key for matching
key = (unit.source, unit.getcontext())
key = (unit.source, unit.getcontext() or "")
store_units_index[key] = unit

# Add missing units from database that are not in the store
for existing_unit in self.existing_units:
sources = existing_unit.get_source_plurals()
if not sources:
continue
source = sources[0] # Use first source for matching
context = existing_unit.context or ""

# Check if this unit exists in store
key = (source, context)
if key not in store_units_index:
# Unit is missing from store, add it with its translation from database
if key in store_units_index:
thepo = store_units_index[key]
thepo.target = existing_unit.target
thepo.markfuzzy(existing_unit.state in FUZZY_STATES)
else:
thepo = store.addsourceunit(source)
if context:
thepo.setcontext(context)
# Set the translation from database
thepo.target = existing_unit.target
# Set fuzzy flag if unit is STATE_FUZZY
from weblate.utils.state import STATE_FUZZY

if existing_unit.state == STATE_FUZZY:
thepo.markfuzzy(True)
# Update index
thepo.markfuzzy(existing_unit.state in FUZZY_STATES)
store_units_index[key] = thepo

return store
Expand Down
121 changes: 67 additions & 54 deletions weblate/formats/quickbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,68 @@
from weblate.formats.base import TranslationFormat


def _empty_quickbook_po() -> pofile:
empty = pofile()
empty.updateheader(add=True, x_accelerator_marker=None, x_previous_msgid=None)
return empty


def _resolve_template_path(
storefile: IO[bytes],
template_store: TranslationFormat | None,
) -> str | None:
template_path: str | None = None
if template_store is not None and hasattr(template_store, "storefile"):
tf = template_store.storefile
if hasattr(tf, "name"):
template_path = tf.name
elif isinstance(tf, str):
template_path = tf
if template_path is None:
template_path = getattr(storefile, "name", None)
return template_path


def _fill_targets_same_template_file(
store: TranslationStore,
existing_units: object | None,
) -> None:
# Same template + translation path (gettextize-style): source-language fill,
# but preserve targets merged from ``existing_units`` where present.
for unit in store.units:
if unit.isheader():
continue
if existing_units:
if not unit.target:
unit.target = unit.source
else:
unit.target = unit.source


def _merge_positional_translated_qbk(
store: TranslationStore,
storefile_path: str,
) -> None:
"""Pair translated .qbk segments positionally with template segments."""
try:
translated_content = Path(storefile_path).read_text(encoding="utf-8")
translated_store = qbk_to_po(translated_content, Path(storefile_path).name)
trans_units = [u for u in translated_store.units if not u.isheader()]
tmpl_units = [u for u in store.units if not u.isheader()]
if len(tmpl_units) != len(trans_units):
report_error(
"QuickBook: refusing positional import: segment count mismatch "
f"(file={storefile_path!s}, name={Path(storefile_path).name!s}, "
f"template_units={len(tmpl_units)}, translated_units={len(trans_units)})"
)
return
for tmpl_unit, trans_unit in zip(tmpl_units, trans_units, strict=True):
if trans_unit.source:
tmpl_unit.target = trans_unit.source
except Exception as exc:
report_error(f"QuickBook: cannot read translated file {storefile_path}: {exc}")


class QuickBookFormat(ConvertFormat):
"""
QuickBook (.qbk) documentation file format with built-in PO converter.
Expand All @@ -51,78 +113,29 @@ def convertfile(
template_store: TranslationFormat | None,
) -> TranslationStore:
"""Extract translatable strings from a .qbk file, returning a ``pofile``."""
# Resolve the template (source-language) .qbk file path.
template_path: str | None = None
if template_store is not None and hasattr(template_store, "storefile"):
tf = template_store.storefile
if hasattr(tf, "name"):
template_path = tf.name
elif isinstance(tf, str):
template_path = tf

if template_path is None:
# Fall back: use storefile path as the template.
template_path = getattr(storefile, "name", None)

template_path = _resolve_template_path(storefile, template_store)
if template_path is None:
report_error("QuickBook: cannot determine template file path")
empty = pofile()
empty.updateheader(
add=True, x_accelerator_marker=None, x_previous_msgid=None
)
return empty
return _empty_quickbook_po()

try:
content = Path(template_path).read_text(encoding="utf-8")
except Exception as exc:
report_error(f"QuickBook: cannot read template {template_path}: {exc}")
empty = pofile()
empty.updateheader(
add=True, x_accelerator_marker=None, x_previous_msgid=None
)
return empty
return _empty_quickbook_po()

filename = Path(template_path).name
store = qbk_to_po(content, filename, self.existing_units)

storefile_path: str | None = getattr(storefile, "name", None)
if storefile_path == template_path:
# Loading the source-language file: set target = source on every unit
# so Weblate stores a non-empty translation for the source language.
for unit in store.units:
if not unit.isheader():
unit.target = unit.source
# Loading a translated .qbk file: parse it and pair its segments
# positionally with the template segments to populate msgstr values.
# This mirrors what po4a-gettextize does when given both -m and -l.
_fill_targets_same_template_file(store, self.existing_units)
elif storefile_path is None:
report_error(
"QuickBook: cannot load translated .qbk without a filesystem path"
)
else:
try:
translated_content = Path(storefile_path).read_text(encoding="utf-8")
translated_store = qbk_to_po(
translated_content, Path(storefile_path).name
)
trans_units = [u for u in translated_store.units if not u.isheader()]
tmpl_units = [u for u in store.units if not u.isheader()]
if len(tmpl_units) != len(trans_units):
report_error(
"QuickBook: refusing positional import: segment count mismatch "
f"(file={storefile_path!s}, name={Path(storefile_path).name!s}, "
f"template_units={len(tmpl_units)}, translated_units={len(trans_units)})"
)
else:
for tmpl_unit, trans_unit in zip(
tmpl_units, trans_units, strict=True
):
if trans_unit.source:
tmpl_unit.target = trans_unit.source
except Exception as exc:
report_error(
f"QuickBook: cannot read translated file {storefile_path}: {exc}"
)
_merge_positional_translated_qbk(store, storefile_path)

return store

Expand Down
Loading
Loading