Repository navigation
SubRip (srt): edits to existing units are never written to the file since 2026.10.1 (SubtitleUnit.target reads unit.source) #22272
Description
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.discoveryon the main component;weblate.cleanup.genericon the discovered components. The behaviour is identical withweblate.cleanup.genericremoved 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()callspounit.set_target(...). - L1830-L1836:
if not updated: updated = self.has_stored_content_changed(original_content), thenif not updated: return changes_status, False. The change status isTrue, 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 nocommitted ...line is logged. Thendelete_successful_pending_changes()(L1353, L1365) deletes the pending change because its status isTrue. updatedis 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 addedTranslation.has_stored_content_changed,read_stored_content, andTranslationUnit.get_stored_content. - Contained in tag
weblate-2026.10.1(PyPI 2026-10-09). Not contained inweblate-2026.10(PyPI 2026-10-01) orweblate-2026.9.1; checked with the GitHub compare API and by searching the released packages forhas_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(HEADe377ce58, 2026-10-20) still has the sameSubtitleUnit.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:
SubtitleUnitis shared bySubRipFormat,MicroDVDFormat,AdvSubStationAlphaFormatandSubStationAlphaFormat(the last three subclassSubRipFormat), so they are presumably affected too. I only testedsrt.- I did not use a
target or sourcefallback on purpose: clearing a translation would then show the source text as the translation. - Please confirm
unit.targetis populated on every load and add-unit path with the pinned translate-toolkit 3.20.0. I only tested loading existing files andset_targeton 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
.srtfile 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.targetfixes it. Also verified: the introducing commit, the releases that contain it, and thatmainis 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
- Create a monolingual SubRip component with a base
subtitles.srtand a translationsubtitles_<lang>.srtthat has translated cues. - Open any translated cue in the editor, change its text and save. The editor shows the new text.
- Manage → Repository maintenance → Commit (or Files → Download translation).
- 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
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
fix(formats): persist edits to existing subtitle cues
fix(formats): persist edits to existing subtitle cues
github-actions commentedon Oct 22, 2026
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.