Repository: SubtitleEdit/subtitleedit (2026-07-04T08:45:26Z)

Issue:
Subtitle Edit 5.1.0 beta 8 (Linux)
After deleting all previous configuration files and letting Subtitle Edit recreate them, the application behavior changed in several areas, so the configuration was successfully reset.

However, OCR corrections from spa_OCRFixReplaceList.xml are still not applied.

Test subtitle:

Hola, aca estoy.
Adios.
Algun dia volvere.

Run:
Tools -> Fix common errors

Enable only:
Fix common OCR errors using OCR replace list

Expected:

Hola, acá estoy.
Adiós.
Algún día volveré.

Actual:

No corrections are made.

The same subtitle and the same
spa_OCRFixReplaceList.xml file work correctly in Subtitle Edit 4.0.16 (Mono).

MD5 of spa_OCRFixReplaceList.xml:
cf66622261fa292b75b79799acb202b1

The OCR replacement file exists and is identical in both installations.


Discussion:
Some screenshots? Do you have a Spanish spell check dictionary?



Patch:
diff --git a/change-log.txt b/change-log.txt
index 202e4d37bd..0957ce15bd 100644
--- a/change-log.txt
+++ b/change-log.txt
@@ -3,6 +3,7 @@ Subtitle Edit Changelog
 
 v5.1.0-beta8 (3rd of July 2026)
 
+* Fix "Fix common OCR errors" doing nothing when no Hunspell dictionary is installed for the language - the OCR replace list (e.g. spa_OCRFixReplaceList.xml) is now applied on its own again - thx perkesfurmah-collab
 * Fix UI freeze when switching to layout 12 (no video pane) with a video loaded
 * Text to speech: review window - Space no longer clicks OK on key release either (Avalonia raises a focused button's click on Space key-up, independent of the tunnel-stage fix in beta7) - thx cvrle77
 * Text to speech: fix the Import/Export/merge-output file dialogs and the TTS window dropping behind the main window on Windows - thx cvrle77
diff --git a/src/seconv/Core/FixCommonErrorsRunner.cs b/src/seconv/Core/FixCommonErrorsRunner.cs
index dfbc1b1eb5..dd76cae06a 100644
--- a/src/seconv/Core/FixCommonErrorsRunner.cs
+++ b/src/seconv/Core/FixCommonErrorsRunner.cs
@@ -130,7 +130,8 @@ public static void Run(
     /// <summary>
     /// Applies the OCR-fix engine (OCR replace lists + Hunspell spell-check guessing) to every line,
     /// the headless equivalent of the GUI's "Fix common OCR errors". No-op when no dictionary folder
-    /// is configured or no Hunspell dictionary matches the detected language. (#11744)
+    /// is configured. When no Hunspell dictionary matches the language the OCR replace list is still
+    /// applied (the spell-check-driven guessing is skipped). (#11744, #12126)
     /// </summary>
     private static void RunOcrFix(Subtitle subtitle, string twoLetterLanguage)
     {
@@ -147,12 +148,14 @@ private static void RunOcrFix(Subtitle subtitle, string twoLetterLanguage)
         }
 
         var spellChecker = new SpellChecker();
+
+        // Fall back to an empty dictionary display when no Hunspell dictionary is installed for the
+        // language: the OCR replace list is still applied, the spell-check-driven "guess" path is just a
+        // no-op. Only en_US.dic ships with SE, so gating on a matching .dic made this silently do nothing
+        // for other languages on a fresh config - a regression from SE 4 (issue #12126).
         var dictionary = spellChecker.GetDictionaryLanguages(folder)
-            .FirstOrDefault(d => d.GetThreeLetterCode() == threeLetter);
-        if (dictionary == null)
-        {
-            return;
-        }
+            .FirstOrDefault(d => d.GetThreeLetterCode() == threeLetter)
+            ?? new SpellCheckDictionaryDisplay();
 
         IOcrFixEngine engine = new OcrFixEngine(spellChecker);
         engine.Initialize(subtitle, threeLetter, dictionary);
diff --git a/src/ui/Features/Tools/FixCommonErrors/FixCommonOcrErrors.cs b/src/ui/Features/Tools/FixCommonErrors/FixCommonOcrErrors.cs
index 7d4063b635..cb76dce55f 100644
--- a/src/ui/Features/Tools/FixCommonErrors/FixCommonOcrErrors.cs
+++ b/src/ui/Features/Tools/FixCommonErrors/FixCommonOcrErrors.cs
@@ -33,11 +33,14 @@ public void Fix(Subtitle subtitle, IFixCallbacks callbacks)
 
             var spellCheckManager = new SpellCheckManager();
             var spellCheckers = spellCheckManager.GetDictionaryLanguages(Se.DictionariesFolder);
-            var spellChecker = spellCheckers.FirstOrDefault(x => x.GetThreeLetterCode() == threeLetterCode);
-            if (spellChecker == null)
-            {
-                return;
-            }
+
+            // Fall back to an empty dictionary display when no Hunspell dictionary is installed for the
+            // language. The OCR replace list (e.g. spa_OCRFixReplaceList.xml) is applied regardless of
+            // spell-checking; without a dictionary the spell-check-driven "guess" path is simply a no-op.
+            // Requiring a matching .dic here made the feature silently do nothing on a fresh config, since
+            // only en_US.dic ships with SE - a regression from SE 4 (issue #12126).
+            var spellChecker = spellCheckers.FirstOrDefault(x => x.GetThreeLetterCode() == threeLetterCode)
+                               ?? new SpellCheckDictionaryDisplay();
 
             OcrFixEngine.Initialize(subtitle, threeLetterCode, spellChecker);
 


Test patch:
diff --git a/tests/UI/Features/Ocr/FixEngine/OcrFixNoDictionaryReplaceListTests.cs b/tests/UI/Features/Ocr/FixEngine/OcrFixNoDictionaryReplaceListTests.cs
new file mode 100644
index 0000000000..8a12329fa8
--- /dev/null
+++ b/tests/UI/Features/Ocr/FixEngine/OcrFixNoDictionaryReplaceListTests.cs
@@ -0,0 +1,100 @@
+using Nikse.SubtitleEdit.Core.Common;
+using Nikse.SubtitleEdit.Core.Forms.FixCommonErrors;
+using Nikse.SubtitleEdit.Core.Interfaces;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
+using Nikse.SubtitleEdit.Features.Ocr.FixEngine;
+using Nikse.SubtitleEdit.Features.SpellCheck;
+using Nikse.SubtitleEdit.Logic.Config;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace UITests.Features.Ocr.FixEngine;
+
+// #12126: "Fix common OCR errors" applied no corrections on a fresh config because it required a
+// matching Hunspell dictionary (.dic) - and only en_US.dic ships with SE. The OCR replace list is
+// spell-check independent, so its replacements must still be applied when no dictionary is installed
+// (this worked in SE 4). Drives the real FixCommonOcrErrors.Fix path with only the bundled-style
+// spa_OCRFixReplaceList.xml present and no Spanish dictionary - the exact reporter scenario.
+public class OcrFixNoDictionaryReplaceListTests : IDisposable
+{
+    private readonly string _originalSeDictionariesFolder;
+    private readonly Func<string> _originalSpellCheckDictionariesFolder;
+    private readonly IOcrFixEngine? _originalOcrFixEngine;
+    private readonly string _tempDictionariesFolder;
+
+    public OcrFixNoDictionaryReplaceListTests()
+    {
+        _originalSeDictionariesFolder = Se.DictionariesFolder;
+        _originalSpellCheckDictionariesFolder = SpellCheckConfig.DictionariesFolder;
+        _originalOcrFixEngine = FixCommonOcrErrors.OcrFixEngine;
+
+        _tempDictionariesFolder = Path.Combine(
+            Path.GetTempPath(),
+            "SeOcrFixNoDicTest_" + Guid.NewGuid().ToString("N"));
+        Directory.CreateDirectory(_tempDictionariesFolder);
+        Se.DictionariesFolder = _tempDictionariesFolder;
+        SpellCheckConfig.DictionariesFolder = () => _tempDictionariesFolder;
+
+        // Spanish OCR replace list with the reporter's whole-word accent fixes. No spa .dic/.aff -
+        // so there is no Hunspell dictionary for the language.
+        File.WriteAllText(
+            Path.Combine(_tempDictionariesFolder, "spa_OCRFixReplaceList.xml"),
+            "<ReplaceList><WholeWords>" +
+            "<Word from=\"aca\" to=\"acá\" />" +
+            "<Word from=\"dia\" to=\"día\" />" +
+            "</WholeWords></ReplaceList>");
+    }
+
+    [Fact]
+    public void Fix_WithoutHunspellDictionary_StillAppliesOcrReplaceList()
+    {
+        FixCommonOcrErrors.OcrFixEngine = new OcrFixEngine(new SpellChecker());
+
+        var subtitle = new Subtitle();
+        subtitle.Paragraphs.Add(new Paragraph("Hola, aca estoy. Un dia volvere.", 0, 3000));
+
+        var callbacks = new FakeFixCallbacks("es");
+        new FixCommonOcrErrors().Fix(subtitle, callbacks);
+
+        Assert.Equal("Hola, acá estoy. Un día volvere.", subtitle.Paragraphs[0].Text);
+        Assert.Equal(1, callbacks.FixCount);
+    }
+
+    public void Dispose()
+    {
+        Se.DictionariesFolder = _originalSeDictionariesFolder;
+        SpellCheckConfig.DictionariesFolder = _originalSpellCheckDictionariesFolder;
+        FixCommonOcrErrors.OcrFixEngine = _originalOcrFixEngine;
+        try
+        {
+            Directory.Delete(_tempDictionariesFolder, recursive: true);
+        }
+        catch
+        {
+            // Best-effort cleanup.
+        }
+    }
+
+    private sealed class FakeFixCallbacks : IFixCallbacks
+    {
+        public FakeFixCallbacks(string language) => Language = language;
+
+        public int FixCount { get; private set; }
+
+        public bool AllowFix(Paragraph p, string action) => true;
+        public void AddFixToListView(Paragraph p, string action, string before, string after) => FixCount++;
+        public void AddFixToListView(Paragraph p, string action, string before, string after, bool isChecked) => FixCount++;
+        public void LogStatus(string sender, string message) { }
+        public void LogStatus(string sender, string message, bool isImportant) { }
+        public void UpdateFixStatus(int fixes, string message) { }
+        public bool IsName(string candidate) => false;
+        public HashSet<string> GetAbbreviations() => new();
+        public void AddToTotalErrors(int count) { }
+        public void AddToDeleteIndices(int index) { }
+        public SubtitleFormat Format => new SubRip();
+        public Encoding Encoding => Encoding.UTF8;
+        public string Language { get; }
+    }
+}