diff --git a/.github/workflows/word-censor.yml b/.github/workflows/word-censor.yml
new file mode 100644
index 0000000..fc33b39
--- /dev/null
+++ b/.github/workflows/word-censor.yml
@@ -0,0 +1,109 @@
+name: Build WordCensor (SE5)
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'se5/WordCensor/**'
+ - 'se5/Plugin-Shared/**'
+ - '.github/workflows/word-censor.yml'
+ pull_request:
+ paths:
+ - 'se5/WordCensor/**'
+ - 'se5/Plugin-Shared/**'
+ - '.github/workflows/word-censor.yml'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Release tag to publish the zips under (e.g. se5-word-censor-v1.0). Leave empty to build artifacts only.'
+ required: false
+ type: string
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - rid: win-x64
+ os: windows
+ exe: WordCensor.exe
+ - rid: win-arm64
+ os: windows
+ exe: WordCensor.exe
+ - rid: linux-x64
+ os: linux
+ exe: WordCensor
+ - rid: linux-arm64
+ os: linux
+ exe: WordCensor
+ - rid: osx-x64
+ os: macos
+ exe: WordCensor
+ - rid: osx-arm64
+ os: macos
+ exe: WordCensor
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Publish self-contained (${{ matrix.rid }})
+ working-directory: se5/WordCensor
+ run: |
+ dotnet publish WordCensor.csproj \
+ -c Release \
+ -r ${{ matrix.rid }} \
+ --self-contained true \
+ -p:DebugType=None \
+ -p:DebugSymbols=false \
+ -o staging/WordCensor
+
+ - name: Rewrite plugin.json for ${{ matrix.os }}
+ working-directory: se5/WordCensor
+ run: |
+ jq '
+ del(.runtime, .entry) |
+ .executables = { "${{ matrix.os }}": "${{ matrix.exe }}" }
+ ' plugin.json > staging/WordCensor/plugin.json
+
+ - name: Package zip
+ working-directory: se5/WordCensor/staging
+ run: zip -r "$GITHUB_WORKSPACE/WordCensor-${{ matrix.rid }}.zip" WordCensor
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: WordCensor-${{ matrix.rid }}
+ path: WordCensor-${{ matrix.rid }}.zip
+
+ release:
+ name: Publish GitHub Release
+ needs: build
+ if: github.event_name == 'workflow_dispatch' && github.event.inputs.tag != ''
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Download all zips
+ uses: actions/download-artifact@v4
+ with:
+ path: dist
+ pattern: WordCensor-*
+ merge-multiple: true
+
+ - name: Create release and upload zips
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh release create "${{ github.event.inputs.tag }}" dist/WordCensor-*.zip \
+ --repo "${{ github.repository }}" \
+ --target "${{ github.sha }}" \
+ --title "WordCensor ${{ github.event.inputs.tag }}" \
+ --notes "Self-contained WordCensor plugin builds for win/linux/osx (x64 + arm64)."
diff --git a/se5-plugins.json b/se5-plugins.json
index 5340b4b..fd9457d 100644
--- a/se5-plugins.json
+++ b/se5-plugins.json
@@ -1,5 +1,22 @@
{
"plugins": [
+ {
+ "name": "Word censor",
+ "description": "Censors offensive words by replacing the first half of each match with random grawlix characters (#@!$%). Optionally highlights censored words in red.",
+ "version": "1.0.0",
+ "author": "Subtitle Edit",
+ "url": "https://github.com/SubtitleEdit/plugins/tree/main/se5/WordCensor",
+ "date": "2026-05-17",
+ "minSeVersion": "5.0.0",
+ "downloads": {
+ "win-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-win-x64.zip",
+ "win-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-win-arm64.zip",
+ "linux-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-linux-x64.zip",
+ "linux-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-linux-arm64.zip",
+ "osx-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-osx-x64.zip",
+ "osx-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-word-censor-v1.0/WordCensor-osx-arm64.zip"
+ }
+ },
{
"name": "British to American",
"description": "Converts British English spellings to American English in the subtitle. Shows a checkable preview of every proposed change.",
diff --git a/se5/WordCensor/App.axaml b/se5/WordCensor/App.axaml
new file mode 100644
index 0000000..4c29740
--- /dev/null
+++ b/se5/WordCensor/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/se5/WordCensor/App.axaml.cs b/se5/WordCensor/App.axaml.cs
new file mode 100644
index 0000000..df195d3
--- /dev/null
+++ b/se5/WordCensor/App.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using SubtitleEdit.Plugins.Shared;
+
+namespace SubtitleEdit.Plugins.WordCensor;
+
+public partial class App : PluginApp
+{
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ protected override Window CreateMainWindow(PluginRequest request) => new MainWindow(request);
+}
diff --git a/se5/WordCensor/BadWords.txt b/se5/WordCensor/BadWords.txt
new file mode 100644
index 0000000..e59e41c
--- /dev/null
+++ b/se5/WordCensor/BadWords.txt
@@ -0,0 +1,79 @@
+anal
+anus
+arse
+ass
+ballsack
+balls
+bastard
+bitch
+biatch
+bloody
+blowjob
+blow job
+bollock
+bollok
+boner
+boob
+bugger
+bum
+butt
+buttplug
+clitoris
+cock
+coon
+crap
+cunt
+damn
+dick
+dildo
+dyke
+fag
+feck
+fellate
+fellatio
+felching
+fuck
+fucking
+fucked
+f u c k
+fudgepacker
+fudge packer
+flange
+goddamn
+god damn
+hell
+homo
+jerk
+jizz
+knobend
+knob end
+labia
+lmao
+lmfao
+muff
+nigger
+nigga
+omg
+penis
+piss
+poop
+prick
+pube
+pussy
+queer
+scrotum
+sex
+shit
+s hit
+sh1t
+slut
+smegma
+spunk
+tit
+tosser
+turd
+twat
+vagina
+wank
+whore
+wtf
diff --git a/se5/WordCensor/ChangeProposal.cs b/se5/WordCensor/ChangeProposal.cs
new file mode 100644
index 0000000..64b2291
--- /dev/null
+++ b/se5/WordCensor/ChangeProposal.cs
@@ -0,0 +1,20 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SubtitleEdit.Plugins.WordCensor;
+
+public partial class ChangeProposal : ObservableObject
+{
+ [ObservableProperty] private bool _include = true;
+ [ObservableProperty] private string _censoredText;
+
+ public int LineIndex { get; }
+ public string LineNumber => (LineIndex + 1).ToString();
+ public string OriginalText { get; }
+
+ public ChangeProposal(int lineIndex, string originalText, string censoredText)
+ {
+ LineIndex = lineIndex;
+ OriginalText = originalText;
+ _censoredText = censoredText;
+ }
+}
diff --git a/se5/WordCensor/MainWindow.axaml b/se5/WordCensor/MainWindow.axaml
new file mode 100644
index 0000000..f3acdbf
--- /dev/null
+++ b/se5/WordCensor/MainWindow.axaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/se5/WordCensor/MainWindow.axaml.cs b/se5/WordCensor/MainWindow.axaml.cs
new file mode 100644
index 0000000..5261b56
--- /dev/null
+++ b/se5/WordCensor/MainWindow.axaml.cs
@@ -0,0 +1,231 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using SubtitleEdit.Plugins.Shared;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+
+namespace SubtitleEdit.Plugins.WordCensor;
+
+public partial class MainWindow : Window
+{
+ private readonly PluginRequest _request;
+ private readonly List _blocks;
+ private readonly WordCensorEngine _engine;
+ private readonly ObservableCollection _proposals = new();
+
+ private TextBlock _summaryLabel = null!;
+ private TextBlock _subtitleLabel = null!;
+ private TextBlock _noChangesLabel = null!;
+ private ListBox _changesList = null!;
+ private Button _applyButton = null!;
+ private CheckBox _colorRedCheck = null!;
+
+ public MainWindow() : this(new PluginRequest()) { }
+
+ public MainWindow(PluginRequest request)
+ {
+ _request = request;
+ InitializeComponent();
+
+ _engine = new WordCensorEngine();
+ _blocks = SubRipParser.Parse(request.Subtitle.SubRip);
+
+ _colorRedCheck.IsChecked = LoadColorRedSetting(request.Settings);
+ _colorRedCheck.IsCheckedChanged += (_, _) => RebuildProposals();
+
+ BuildProposals();
+ _changesList.ItemsSource = _proposals;
+
+ var scope = request.SelectedIndices.Count > 0
+ ? $"the {request.SelectedIndices.Count} selected line(s)"
+ : "all lines";
+ _subtitleLabel.Text = $"Replace offensive words with grawlix characters (#@!$%) in {scope}.";
+
+ UpdateUiForProposals();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ _summaryLabel = this.FindControl("SummaryLabel")!;
+ _subtitleLabel = this.FindControl("SubtitleLabel")!;
+ _noChangesLabel = this.FindControl("NoChangesLabel")!;
+ _changesList = this.FindControl("ChangesList")!;
+ _applyButton = this.FindControl