From a0a956306d6ee81fe3ddeecb783cd10fa5434390 Mon Sep 17 00:00:00 2001 From: niksedk Date: Sun, 17 May 2026 16:12:27 +0200 Subject: [PATCH 1/2] Add Plugin-Shared library and AmericanToBritish SE5 plugin Plugin-Shared is a small Avalonia 11 / .NET 8 library that future SE5 plugins can reference instead of copy-pasting boilerplate: - PluginContract.cs - request/response/subtitle DTOs (was inlined in Haxor and TypewriterEffect). - PluginApp - Application base that reads request.theme and sets RequestedThemeVariant so plugins match SE's Dark/Light theme. - PluginBootstrap.Run(args) - parses the request JSON from the first command-line argument, starts Avalonia, writes the response on exit, returns 0. - SubRipParser - lifted out of TypewriterEffect. - WindowExtensions.BringToForeground() - the Topmost-toggle pattern used to force the plugin window in front of SE on macOS. AmericanToBritish is the first plugin to use it: ports the SE4 converter (regex word list with case variants + the colour-in-font-tag revert), bundles the ~1000-pair WordList.xml as an embedded resource, and shows a checkable preview list of every proposed change before applying. Apply only the checked ones; Cancel does nothing. UI uses the shared Plugin-Shared boot + theme. Workflow .github/workflows/american-to-british.yml mirrors typewriter.yml: matrix self-contained publish for the six supported RIDs, plugin.json rewritten with per-OS executables block per zip, and a GitHub release on manual dispatch with a tag. se5-plugins.json updated to advertise AmericanToBritish via the per-platform downloads map pointing at the planned se5-american-to-british-v1.0 release tag. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/american-to-british.yml | 109 ++ se5-plugins.json | 17 + .../AmericanToBritish.csproj | 22 + .../AmericanToBritishConverter.cs | 112 ++ se5/AmericanToBritish/App.axaml | 8 + se5/AmericanToBritish/App.axaml.cs | 13 + se5/AmericanToBritish/ChangeProposal.cs | 20 + se5/AmericanToBritish/MainWindow.axaml | 95 ++ se5/AmericanToBritish/MainWindow.axaml.cs | 175 +++ se5/AmericanToBritish/Program.cs | 10 + se5/AmericanToBritish/README.md | 24 + se5/AmericanToBritish/WordList.xml | 1016 +++++++++++++++++ se5/AmericanToBritish/app.manifest | 10 + se5/AmericanToBritish/plugin.json | 12 + se5/Plugin-Shared/Plugin-Shared.csproj | 19 + se5/Plugin-Shared/PluginApp.cs | 46 + se5/Plugin-Shared/PluginBootstrap.cs | 62 + se5/Plugin-Shared/PluginContract.cs | 48 + se5/Plugin-Shared/SubRipParser.cs | 88 ++ se5/Plugin-Shared/WindowExtensions.cs | 26 + 20 files changed, 1932 insertions(+) create mode 100644 .github/workflows/american-to-british.yml create mode 100644 se5/AmericanToBritish/AmericanToBritish.csproj create mode 100644 se5/AmericanToBritish/AmericanToBritishConverter.cs create mode 100644 se5/AmericanToBritish/App.axaml create mode 100644 se5/AmericanToBritish/App.axaml.cs create mode 100644 se5/AmericanToBritish/ChangeProposal.cs create mode 100644 se5/AmericanToBritish/MainWindow.axaml create mode 100644 se5/AmericanToBritish/MainWindow.axaml.cs create mode 100644 se5/AmericanToBritish/Program.cs create mode 100644 se5/AmericanToBritish/README.md create mode 100644 se5/AmericanToBritish/WordList.xml create mode 100644 se5/AmericanToBritish/app.manifest create mode 100644 se5/AmericanToBritish/plugin.json create mode 100644 se5/Plugin-Shared/Plugin-Shared.csproj create mode 100644 se5/Plugin-Shared/PluginApp.cs create mode 100644 se5/Plugin-Shared/PluginBootstrap.cs create mode 100644 se5/Plugin-Shared/PluginContract.cs create mode 100644 se5/Plugin-Shared/SubRipParser.cs create mode 100644 se5/Plugin-Shared/WindowExtensions.cs diff --git a/.github/workflows/american-to-british.yml b/.github/workflows/american-to-british.yml new file mode 100644 index 0000000..793c4e5 --- /dev/null +++ b/.github/workflows/american-to-british.yml @@ -0,0 +1,109 @@ +name: Build AmericanToBritish (SE5) + +on: + push: + branches: [main] + paths: + - 'se5/AmericanToBritish/**' + - 'se5/Plugin-Shared/**' + - '.github/workflows/american-to-british.yml' + pull_request: + paths: + - 'se5/AmericanToBritish/**' + - 'se5/Plugin-Shared/**' + - '.github/workflows/american-to-british.yml' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to publish the zips under (e.g. se5-american-to-british-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: AmericanToBritish.exe + - rid: win-arm64 + os: windows + exe: AmericanToBritish.exe + - rid: linux-x64 + os: linux + exe: AmericanToBritish + - rid: linux-arm64 + os: linux + exe: AmericanToBritish + - rid: osx-x64 + os: macos + exe: AmericanToBritish + - rid: osx-arm64 + os: macos + exe: AmericanToBritish + + 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/AmericanToBritish + run: | + dotnet publish AmericanToBritish.csproj \ + -c Release \ + -r ${{ matrix.rid }} \ + --self-contained true \ + -p:DebugType=None \ + -p:DebugSymbols=false \ + -o staging/AmericanToBritish + + - name: Rewrite plugin.json for ${{ matrix.os }} + working-directory: se5/AmericanToBritish + run: | + jq ' + del(.runtime, .entry) | + .executables = { "${{ matrix.os }}": "${{ matrix.exe }}" } + ' plugin.json > staging/AmericanToBritish/plugin.json + + - name: Package zip + working-directory: se5/AmericanToBritish/staging + run: zip -r "$GITHUB_WORKSPACE/AmericanToBritish-${{ matrix.rid }}.zip" AmericanToBritish + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: AmericanToBritish-${{ matrix.rid }} + path: AmericanToBritish-${{ 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: AmericanToBritish-* + merge-multiple: true + + - name: Create release and upload zips + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.event.inputs.tag }}" dist/AmericanToBritish-*.zip \ + --repo "${{ github.repository }}" \ + --target "${{ github.sha }}" \ + --title "AmericanToBritish ${{ github.event.inputs.tag }}" \ + --notes "Self-contained AmericanToBritish plugin builds for win/linux/osx (x64 + arm64)." diff --git a/se5-plugins.json b/se5-plugins.json index c34c129..24f1ac5 100644 --- a/se5-plugins.json +++ b/se5-plugins.json @@ -1,5 +1,22 @@ { "plugins": [ + { + "name": "American to British", + "description": "Converts American English spellings to British English in the subtitle. Shows a checkable preview of every proposed change.", + "version": "1.0.0", + "author": "Subtitle Edit", + "url": "https://github.com/SubtitleEdit/plugins/tree/main/se5/AmericanToBritish", + "date": "2026-05-17", + "minSeVersion": "5.0.0", + "downloads": { + "win-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-win-x64.zip", + "win-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-win-arm64.zip", + "linux-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-linux-x64.zip", + "linux-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-linux-arm64.zip", + "osx-x64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-osx-x64.zip", + "osx-arm64": "https://github.com/SubtitleEdit/plugins/releases/download/se5-american-to-british-v1.0/AmericanToBritish-osx-arm64.zip" + } + }, { "name": "Typewriter effect", "description": "Splits each subtitle line into short timed parts that progressively reveal the text, character by character.", diff --git a/se5/AmericanToBritish/AmericanToBritish.csproj b/se5/AmericanToBritish/AmericanToBritish.csproj new file mode 100644 index 0000000..dc88a0e --- /dev/null +++ b/se5/AmericanToBritish/AmericanToBritish.csproj @@ -0,0 +1,22 @@ + + + + WinExe + net8.0 + enable + enable + AmericanToBritish + SubtitleEdit.Plugins.AmericanToBritish + true + app.manifest + + + + + + + + + + + diff --git a/se5/AmericanToBritish/AmericanToBritishConverter.cs b/se5/AmericanToBritish/AmericanToBritishConverter.cs new file mode 100644 index 0000000..ee8dc3d --- /dev/null +++ b/se5/AmericanToBritish/AmericanToBritishConverter.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace SubtitleEdit.Plugins.AmericanToBritish; + +/// +/// Converts US English spellings to UK English using the bundled WordList.xml +/// (~1000 word pairs). Each pair becomes three case-aware regexes: lowercase, +/// UPPERCASE, and Titlecase, all matched as whole words. +/// +public sealed class AmericanToBritishConverter +{ + private readonly List<(Regex Pattern, string Replacement)> _rules = new(); + + public AmericanToBritishConverter() + { + LoadBuiltInWordList(); + } + + public int RuleCount => _rules.Count; + + public string Convert(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return text; + } + + foreach (var (pattern, replacement) in _rules) + { + if (pattern.IsMatch(text)) + { + text = pattern.Replace(text, replacement); + } + } + + return RevertColourInFontTags(text); + } + + /// + /// Try converting ; if it changed, return the new text via . + /// + public bool TryConvert(string text, out string converted) + { + converted = Convert(text); + return !string.Equals(text, converted, StringComparison.Ordinal); + } + + private void LoadBuiltInWordList() + { + using var stream = typeof(AmericanToBritishConverter).Assembly + .GetManifestResourceStream("SubtitleEdit.Plugins.AmericanToBritish.WordList.xml") + ?? throw new InvalidOperationException("Embedded WordList.xml not found."); + + var xml = XDocument.Load(stream); + if (xml.Root?.Name != "Words") + { + return; + } + + foreach (var element in xml.Root.Elements("Word")) + { + var us = element.Attribute("us")?.Value; + var br = element.Attribute("br")?.Value; + if (string.IsNullOrEmpty(us) || string.IsNullOrEmpty(br) || us!.Length < 2 || br!.Length < 2) + { + continue; + } + + AddRule(us, br); + AddRule(us.ToUpperInvariant(), br.ToUpperInvariant()); + AddRule(char.ToUpperInvariant(us[0]) + us.Substring(1), char.ToUpperInvariant(br[0]) + br.Substring(1)); + } + } + + private void AddRule(string american, string british) + { + _rules.Add((new Regex("\\b" + Regex.Escape(american) + "\\b", RegexOptions.ExplicitCapture | RegexOptions.Compiled), british)); + } + + /// + /// "color" inside <font ... color="..."> is HTML attribute syntax and must not be Britishized; + /// the word-list conversion would otherwise corrupt the tag. Undo "colour" back to "color" inside <font ...>. + /// + private static string RevertColourInFontTags(string s) + { + var tagIndex = s.IndexOf("= 0) + { + var tagEndIndex = s.IndexOf('>', tagIndex + 5); + if (tagEndIndex < 0) + { + break; + } + + var tag = s.Substring(tagIndex, tagEndIndex - tagIndex); + var colourIndex = tag.IndexOf("colour", StringComparison.OrdinalIgnoreCase); + while (colourIndex >= 0) + { + tag = tag.Remove(colourIndex + 4, 1); + colourIndex = tag.IndexOf("colour", colourIndex + 5, StringComparison.OrdinalIgnoreCase); + } + s = s.Remove(tagIndex, tagEndIndex - tagIndex).Insert(tagIndex, tag); + tagIndex = s.IndexOf(" + + + + diff --git a/se5/AmericanToBritish/App.axaml.cs b/se5/AmericanToBritish/App.axaml.cs new file mode 100644 index 0000000..1afdd50 --- /dev/null +++ b/se5/AmericanToBritish/App.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using SubtitleEdit.Plugins.Shared; + +namespace SubtitleEdit.Plugins.AmericanToBritish; + +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/AmericanToBritish/ChangeProposal.cs b/se5/AmericanToBritish/ChangeProposal.cs new file mode 100644 index 0000000..2b2581a --- /dev/null +++ b/se5/AmericanToBritish/ChangeProposal.cs @@ -0,0 +1,20 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SubtitleEdit.Plugins.AmericanToBritish; + +public partial class ChangeProposal : ObservableObject +{ + [ObservableProperty] private bool _include = true; + + public int LineIndex { get; } + public string LineNumber => (LineIndex + 1).ToString(); + public string OriginalText { get; } + public string ConvertedText { get; } + + public ChangeProposal(int lineIndex, string originalText, string convertedText) + { + LineIndex = lineIndex; + OriginalText = originalText; + ConvertedText = convertedText; + } +} diff --git a/se5/AmericanToBritish/MainWindow.axaml b/se5/AmericanToBritish/MainWindow.axaml new file mode 100644 index 0000000..d2912c7 --- /dev/null +++ b/se5/AmericanToBritish/MainWindow.axaml @@ -0,0 +1,95 @@ + + + + + +