Skip to content

SubRip (srt): edits to existing units are never written to the file since 2026.10.1 (SubtitleUnit.target reads unit.source) #22272

Closed

Description

@hugorezende

Describe the issue

On Weblate 2026.10.1, editing an existing cue of a SubRip (srt) translation saves the text in the database, but the change is never written to the translation file. Commit prints committing N pending changes, then nothing: no committed ... line, no commit, no push. The pending change is removed from the queue, so the edit is silently lost for the repository. No error, alert or "failed save" entry is produced.

  • File format: SubRip subtitle file (srt), monolingual. Base file <dir>/subtitles.srt, file mask <dir>/subtitles_*.srt.
  • Components were created by the Component Discovery add-on and are linked to a main component (weblate://<project>/<component>).
  • Add-ons present: weblate.discovery.discovery on the main component; weblate.cleanup.generic on the discovered components. The behaviour is identical with weblate.cleanup.generic removed from the component.
  • Static (non-SRT) components in the same project, e.g. a JSON component, commit and push normally.
  • The cue timings of translation and base file match 1:1 (184/184 cues in the affected component), there are no duplicate keys, and the file format settings are correct. The unit is found in the file; it is just never written.

Operation log, SRT component (nothing after the pending-changes line):

<project>/app-localizable: running repository operation: commit
<project>/app-tutorial-...bracketmountwithmouse/zh_Hant: committing 1 pending changes (commit)
<project>/app-tutorial-...bracketmount/zh_Hant: committing 1 pending changes (commit)

Same operation on a JSON component in the same project:

<project>/app-kernel-strings/pt_BR: committing 1 pending changes (commit)
<project>/app-kernel-strings/pt_BR: committed ['.../pt-BR.lproj/KernelStrings.json'] as <user>
<project>/app-localizable: pushing to remote repo

Expected behavior

Screenshots

Not applicable; log excerpts are above.

Exception traceback

None. Nothing is logged for the dropped change, which is why it is hard to notice.

How do you run Weblate?

weblate.org service (hosted).

Weblate version

2026.10.1 (shown in the page footer).

Weblate deploy checks

Not available (hosted instance).

Additional context

Root cause

SubtitleUnit.target and TTKitUnit.set_target() use different attributes of the translate-toolkit unit (paths at tag weblate-2026.10.1):

Code Attribute
Read SubtitleUnit.target, weblate/formats/ttkit.py L3319-L3324 self.unit.source
Write TTKitUnit.set_target, weblate/formats/ttkit.py L388-L393 self.unit.target = target

In translate-toolkit's subtitle unit, __init__ sets both (self.source = source; self.target = source), but only target is serialized. With translate-toolkit 3.20.0, setting unit.target changes the saved file; setting unit.source does not. After set_target(), SubtitleUnit.target still returns the old text from the untouched unit.source.

Since 2026.10.1, Translation.update_units() decides whether to write the file by comparing a snapshot of get_stored_content() (which starts with self.target) taken before and after the edit:

  • L1799-L1800: snapshot taken via read_stored_content(pounit).
  • L1804: update_pending_store_unit() calls pounit.set_target(...).
  • L1830-L1836: if not updated: updated = self.has_stored_content_changed(original_content), then if not updated: return changes_status, False. The change status is True, so the pending change is deleted and nothing is written.
  • Back in _commit_pending_with_filename(), git_commit() still runs (L1322-L1330) but finds nothing to commit, so no committed ... line is logged. Then delete_successful_pending_changes() (L1353, L1365) deletes the pending change because its status is True.
  • updated is only set directly for newly added units (updated = updated or add), so adding cues still works and editing existing ones does not.

When it was introduced

  • Commit aa51da6 "fix(translations): skip commits for state changes files cannot store" (authored 2026-09-29), merged as #21882 on 2026-10-07. It added Translation.has_stored_content_changed, read_stored_content, and TranslationUnit.get_stored_content.
  • Contained in tag weblate-2026.10.1 (PyPI 2026-10-09). Not contained in weblate-2026.10 (PyPI 2026-10-01) or weblate-2026.9.1; checked with the GitHub compare API and by searching the released packages for has_stored_content_changed.
  • The changelog entry ("Avoided commits for translation state and metadata changes that the file format cannot store") is listed under Weblate 2026.10, but the code first ships in 2026.10.1.
  • The intent of the commit is fine (skip state-only changes for formats such as JSON). SubRip is simply not covered: its text accessor was never consistent with its setter, which did not matter until the result was compared.
  • Upstream main (HEAD e377ce58, 2026-10-20) still has the same SubtitleUnit.target.

Proposed fix

 class SubtitleUnit(MonolingualIDUnit):
@@
     @cached_property
     def target(self):
         """Return target string from a Translate Toolkit unit."""
         if not self.has_unit():
             return ""
-        return get_string(self.unit.source)
+        return get_string(self.unit.target)

With this change the standalone script above reports changed=True and the edit is written. Notes for the maintainers:

  • SubtitleUnit is shared by SubRipFormat, MicroDVDFormat, AdvSubStationAlphaFormat and SubStationAlphaFormat (the last three subclass SubRipFormat), so they are presumably affected too. I only tested srt.
  • I did not use a target or source fallback on purpose: clearing a translation would then show the source text as the translation.
  • Please confirm unit.target is populated on every load and add-unit path with the pinned translate-toolkit 3.20.0. I only tested loading existing files and set_target on existing units.

Suggested regression test

There is currently no SubRip or other subtitle test data or test class in the upstream tree (no .srt/.sub/.ssa/.ass file and no test mentioning SubRip or SubtitleUnit; searched at main e377ce58), which is why this was not caught. A test in the style of XliffFormatTest.test_stored_content_includes_state_markup from the same commit (untested sketch):

def test_stored_content_changes_on_edit(self) -> None:
    storage = self.parse_file(self.FILE)
    unit = storage.content_units[0]
    unit.invalidate_all_caches()
    before = unit.get_stored_content()
    unit.set_target("Edited")
    unit.invalidate_all_caches()
    self.assertNotEqual(unit.get_stored_content(), before)

More generally, a format-agnostic test asserting that set_target(new_text) changes get_stored_content() for every format would guard this whole class of problem.

Impact and workaround

  • Every edit to an existing cue made through the web UI or API on SRT components is dropped silently after the commit queue runs, so translators' work is lost without any indication. The edit remains in the database but not in the repository. I have not verified what happens to it on the next repository update.
  • Workaround: change the .srt file directly in the repository; Weblate imports repository changes normally.

Verified vs not verified

  • Verified (Weblate 2026.10.1, translate-toolkit 3.20.0, aeidon 2.0.1): the snapshot comparison reports no change for an SRT edit, the file is not written, and reading unit.target fixes it. Also verified: the introducing commit, the releases that contain it, and that main is unchanged.
  • Not verified: behaviour of the other subtitle formats, the full Weblate test suite with the proposed fix, and what the hosted instance does with the orphaned database edits after a repository update.

I already tried

  • I've read and searched the documentation.
  • I've searched for similar filed issues in this repository.

Steps to reproduce the behavior

In the UI

  1. Create a monolingual SubRip component with a base subtitles.srt and a translation subtitles_<lang>.srt that has translated cues.
  2. Open any translated cue in the editor, change its text and save. The editor shows the new text.
  3. Manage → Repository maintenance → Commit (or Files → Download translation).
  4. No commit is created and the downloaded file still has the old text.

Standalone, no server and no database (synthetic data, uses Weblate's own code; this is the same sequence Translation.update_units() runs for an edit of an existing unit):

# repro_srt_stored_content.py
# uv run --no-project --python 3.12 --with "weblate==2026.10.1" --with "psycopg[binary]" python repro_srt_stored_content.py
import os, sys, tempfile
from functools import cached_property
from importlib.metadata import version

settings_dir = tempfile.mkdtemp()
with open(os.path.join(settings_dir, "repro_settings.py"), "w") as fh:
    fh.write(
        "from weblate.settings_example import *\n"
        "DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}}\n"
        "SECRET_KEY = 'repro'\n"
    )
sys.path.insert(0, settings_dir)
os.environ["DJANGO_SETTINGS_MODULE"] = "repro_settings"

import django
django.setup()

from weblate.formats import ttkit
from weblate.formats.ttkit import SubRipFormat, SubtitleUnit
from weblate.trans.models.translation import Translation, read_stored_content

TEMPLATE = """1
00:00:01,000 --> 00:00:03,000
Hello world.

2
00:00:03,500 --> 00:00:05,000
Second cue.

3
00:00:05,500 --> 00:00:07,000
Third cue.
"""
TRANSLATION = """1
00:00:01,000 --> 00:00:03,000
Hallo Welt.

2
00:00:03,500 --> 00:00:05,000
Zweite Zeile.

3
00:00:05,500 --> 00:00:07,000
Dritte Zeile.
"""

def run(label):
    tmp = tempfile.mkdtemp()
    base, target = os.path.join(tmp, "subtitles.srt"), os.path.join(tmp, "subtitles_de.srt")
    open(base, "w", encoding="utf-8").write(TEMPLATE)
    open(target, "w", encoding="utf-8").write(TRANSLATION)
    template = SubRipFormat(base, is_template=True, source_language="en")
    store = SubRipFormat(target, template_store=template, language_code="de", source_language="en")
    unit = list(store.content_units)[1]
    pounit, _ = store.find_unit(unit.context, unit.source)
    snapshot = {1: (pounit, read_stored_content(pounit))}   # update_units(): original_content
    pounit.set_target("Edited translation")                 # update_pending_store_unit()
    changed = Translation.has_stored_content_changed(snapshot)
    if changed:                                             # update_units(): otherwise returns (status, False)
        store.save()
    saved = "Edited translation" in open(target, encoding="utf-8").read()
    print(f"{label:38} changed={changed!s:5}  edit written to file={saved}")

print("weblate", version("weblate"), "| translate-toolkit", version("translate-toolkit"))
run("as shipped (target reads unit.source)")

# proposed fix: read the text from where set_target() stores it
patched = cached_property(lambda self: "" if not self.has_unit() else ttkit.get_string(self.unit.target))
patched.__set_name__(SubtitleUnit, "target")
SubtitleUnit.target = patched
run("patched (target reads unit.target)")

Output with weblate==2026.10.1 (pins translate-toolkit==3.20.0, aeidon==2.0.1):

weblate 2026.10.1 | translate-toolkit 3.20.0
as shipped (target reads unit.source)  changed=False  edit written to file=False
patched (target reads unit.target)     changed=True   edit written to file=True

Expected behavior

Editing an existing cue and committing writes the new text to subtitles_<lang>.srt, creates a commit and pushes it, as in 2026.9.1 and earlier.

Screenshots

No response

Exception traceback

How do you run Weblate?

weblate.org service

Weblate version

2026.10.1

Weblate deploy checks

Additional context

No response

Activity

  1. added
    Waiting for: TriageAssigned automatically for any new issue, needs attention from a Weblate member.
    on Oct 21, 2026
  2. self-assigned this
    on Oct 22, 2026
  3. added theissue type on Oct 22, 2026
  4. added this to the 2026.11 milestone on Oct 22, 2026
  5. added
    Waiting for: ImplementationAdded to a milestone, will be resolved according to the milestone timeline.
    and removed
    Waiting for: TriageAssigned automatically for any new issue, needs attention from a Weblate member.
    on Oct 22, 2026
  6. added 2 commits that reference this issue on Oct 22, 2026
    84fb072
    f5bc4b9
  7. added
    Waiting for: ReleaseThe issue is fixed and waiting to be released.
    and removed
    Waiting for: ImplementationAdded to a milestone, will be resolved according to the milestone timeline.
    on Oct 22, 2026
  8. github-actions commented on Oct 22, 2026

    @github-actions

    Thank you for your report — the issue has now been resolved.

    The fix has been merged into the repository and will be included in the upcoming Weblate 2026.11 release.

    • If you encounter any issues with the fix, please comment in this thread.
    • If you experience a similar but separate issue, open a new report so we can track it properly.
    • If you find Weblate helpful, consider supporting its development.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Area: File format supportIssues related to file formats implemented in Weblate.Waiting for: ReleaseThe issue is fixed and waiting to be released.

Type

Fields

Priority

None yet

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions