Skip to content

+semver:minor Replace retired BingTranslator with alternative on-the-fly localization - #167

Open
tombogle wants to merge 3 commits into
masterfrom
163-replace-retired-bing-translator
Open

+semver:minor Replace retired BingTranslator with alternative on-the-fly localization#167
tombogle wants to merge 3 commits into
masterfrom
163-replace-retired-bing-translator

Conversation

@tombogle

@tombogle tombogle commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The Bing/Microsoft Translator v1 SOAP API used by the internal BingTranslator class (LanguageChoosingDialog's fail-safe, on-the-fly translation of its title/message/OK button) was retired years ago, so this had been silently falling back to English the whole time.

Replaces it with:

  • MyMemoryTranslator (internal, new default): a free, keyless REST API with no provisioning required, replacing the also-dead GoogleTranslator.cs in place.
  • MicrosoftTranslator (new public class): the real Azure AI Translator v3 REST API, opt-in via a subscription key (MicrosoftTranslator.SubscriptionKey or the L10NSHARP_TRANSLATOR_KEY/L10NSHARP_TRANSLATOR_REGION env vars), for host apps that want more robust translation.

TranslatorBase is now public (required for MicrosoftTranslator to subclass it publicly). LanguageChoosingDialog picks MicrosoftTranslator when configured, else MyMemoryTranslator.

Also removes BingTranslator.cs, the generated WCF service reference, and the System.ServiceModel/System.Security.Cryptography.Xml dependencies they required, since nothing REST-based needs WCF.

Adds a "Show Language Chooser" button to the SampleApp (randomly demoing "de", "it", or "fr") so the fix is visible without faking a missing-locale scenario. "ar" is deliberately excluded: LanguageChoosingDialog doesn't set RightToLeft, so Arabic would demo a layout bug rather than the translator.

Fixes #163.


This change is Reviewable

…MicrosoftTranslator

The Bing/Microsoft Translator v1 SOAP API used by the internal BingTranslator
class (LanguageChoosingDialog's fail-safe, on-the-fly translation of its
title/message/OK button) was retired years ago, so this had been silently
falling back to English the whole time.

Replaces it with:
- MyMemoryTranslator (internal, new default): a free, keyless REST API with
  no provisioning required, replacing the also-dead GoogleTranslator.cs in
  place.
- MicrosoftTranslator (new public class): the real Azure AI Translator v3
  REST API, opt-in via a subscription key (MicrosoftTranslator.SubscriptionKey
  or the L10NSHARP_TRANSLATOR_KEY/L10NSHARP_TRANSLATOR_REGION env vars), for
  host apps that want more robust translation.

TranslatorBase is now public (required for MicrosoftTranslator to subclass
it publicly). LanguageChoosingDialog picks MicrosoftTranslator when
configured, else MyMemoryTranslator.

Also removes BingTranslator.cs, the generated WCF service reference, and the
System.ServiceModel/System.Security.Cryptography.Xml dependencies they
required, since nothing REST-based needs WCF.

Adds a "Show Language Chooser" button to the SampleApp (randomly demoing
"de", "it", or "fr") so the fix is visible without faking a missing-locale
scenario. "ar" is deliberately excluded: LanguageChoosingDialog doesn't set
RightToLeft, so Arabic would demo a layout bug rather than the translator.

Fixes #163.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tombogle tombogle self-assigned this Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Test Results

    7 files  ±  0  147 suites  +3   33s ⏱️ +11s
229 tests +  6  224 ✔️ +  6    5 💤 ±0  0 ±0 
876 runs  +18  861 ✔️ +18  15 💤 ±0  0 ±0 

Results for commit 40577b5. ± Comparison against base commit b67ed52.

♻️ This comment has been updated with latest results.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk partially reviewed 19 files and all commit messages, and made 4 comments.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on imnasnainaec and tombogle).


src/L10NSharp.Windows.Forms/Translators/MicrosoftTranslator.cs line 97 at r1 (raw file):

				var requestBody = Encoding.UTF8.GetString(ms.ToArray());

				using var client = new HttpClient();

devin says


src/L10NSharp.Windows.Forms/Translators/MicrosoftTranslator.cs:97
Connections cannot be reused
Each translation creates a new HttpClient. Repeated dialogs open separate connections for every string and add avoidable network overhead.


src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs line 29 at r1 (raw file):

			else
				translator = new MyMemoryTranslator("en", targetCultureId);
			_model.TranslateStrings(translator);

devin says


src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:R29

Translation freezes the language dialog

When translation starts, TranslateStrings runs synchronous HTTP requests on the UI thread. A slow endpoint can freeze the dialog for nearly 300 seconds.


src/L10NSharp.Windows.Forms/Translators/MyMemoryTranslator.cs line 67 at r1 (raw file):

				var translation = ser.ReadObject(ms) as JSONResponse;

				if (translation == null || translation.quotaFinished)

devin says


src/L10NSharp.Windows.Forms/Translators/MyMemoryTranslator.cs:67
Provider errors can reach users
MyMemory returns some failures as HTTP 200 responses. Check responseStatus before displaying translatedText, or unsupported languages can show provider error messages.


src/L10NSharp.Windows.Forms/Translators/MyMemoryTranslator.cs line 90 at r1 (raw file):

		public int responseStatus;
		/// ------------------------------------------------------------------------------------
		public bool quotaFinished;

This looks like a partial implementation which got left behind.

…r error leakage

Responds to 4 review comments on the BingTranslator replacement:

- MicrosoftTranslator/MyMemoryTranslator now share a single static HttpClient
  (with a 10s timeout) instead of creating a new one per call. MicrosoftTranslator
  moves its per-call headers (subscription key/region) onto the HttpRequestMessage
  instead of the shared client's DefaultRequestHeaders, since those are read fresh
  from mutable static properties on every call and mustn't race across calls.

- LanguageChoosingDialog.Application_Idle now runs TranslateStrings on a background
  thread (Task.Run) and marshals the UI update back via BeginInvoke once done, so a
  slow/unresponsive translation endpoint can no longer block the dialog's UI thread
  at all (previously it ran synchronously on Application.Idle with no bound).
  Guards against the dialog being closed before translation completes.

- MyMemoryTranslator now checks responseStatus (!= 200) in addition to the existing
  quotaFinished check before trusting responseData.translatedText. MyMemory always
  returns HTTP 200, signaling errors (invalid language pair, quota exhaustion, etc.)
  only in the body; without this, a provider error/warning string could flow into
  the dialog as if it were a translation. quotaFinished is now bool? since MyMemory
  sends JSON null (not false) for it on error responses, which a non-nullable bool
  can't deserialize. Verified live against real MyMemory success/error responses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tombogle

tombogle commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all 4 review comments in 97916d4:

  1. Connections cannot be reused (MicrosoftTranslator.cs:97) — both MicrosoftTranslator and MyMemoryTranslator now use a shared static readonly HttpClient instead of creating a new one per call. MicrosoftTranslator's per-call headers (subscription key/region) moved onto a per-request HttpRequestMessage rather than the shared client's DefaultRequestHeaders, since those come from mutable static properties read fresh on every call.

  2. Translation freezes the language dialog (LanguageChoosingDialog.cs:29) — rather than just bounding the block with a timeout, Application_Idle now runs TranslateStrings on a background thread (Task.Run) and marshals the UI update back via BeginInvoke once it's done. The UI thread is never blocked on the network call at all now. (A 10s HttpClient.Timeout is still set on both translators as secondary hygiene, so a hung connection doesn't tie up a thread-pool thread indefinitely.)

  3. Provider errors can reach users (MyMemoryTranslator.cs:67) — verified live against the real API: MyMemory always returns HTTP 200, even for errors (invalid language pair, quota exhaustion), signaling failure only via body fields. InternalTranslate now checks responseStatus != 200 in addition to the existing quotaFinished check before trusting translatedText, so a provider error/warning string can no longer flow into the dialog as if it were a translation.

  4. Partial implementation left behind (MyMemoryTranslator.cs:90) — same root cause as Fix broken Group tests #3: responseStatus was captured but never checked. It's now used in the guard above. Also widened quotaFinished from bool to bool?, since MyMemory sends JSON null (not false) for it on error responses, which a non-nullable bool can't deserialize.

All changes verified: full solution build + test suite pass, plus a live round-trip against the real MyMemory API for both a valid translation and an intentionally-invalid language pair (confirming the new guard filters the provider's raw error text rather than leaking it).

@tombogle
tombogle requested a review from andrew-polk September 4, 2026 19:12

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are stale Bing comments in files not touched by this pr.

@imnasnainaec reviewed 11 files and all commit messages, and made 2 comments.
Reviewable status: 16 of 19 files reviewed, 5 unresolved discussions (waiting on andrew-polk and tombogle).


src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs line 20 at r2 (raw file):
Recommended code from Devin:

private readonly LanguageChoosingDialogViewModel _model;
private bool _translationNeeded;

public LanguageChoosingDialog(L10NCultureInfo requestedCulture, Icon icon)
{
    InitializeComponent();
    Icon = icon;
    // The callback just records that translation is needed; we wait to hook
    // Application.Idle until the handle exists (see OnHandleCreated) so the
    // background BeginInvoke can never run against a not-yet-created handle.
    _model = new LanguageChoosingDialogViewModel(_messageLabel.Text, _OKButton.Text, Text,
        requestedCulture, () => { _translationNeeded = true; });
    _messageLabel.Text = _model.Message;
}

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);
    if (_translationNeeded)
    {
        _translationNeeded = false;
        Application.Idle += Application_Idle;
    }
}

to address

Application.Idle is subscribed in the constructor (src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:17), before the dialog's window handle exists. If idle fires (app-wide event) and background translation finishes before the handle is created, BeginInvoke (src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:41) throws InvalidOperationException, which isn't caught (only ObjectDisposedException is, at src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:51) — so the dialog stays English. Moving the subscription to OnHandleCreated guarantees the handle exists before BeginInvoke runs, eliminating the race.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk partially reviewed 3 files and all commit messages, made 1 comment, and resolved 4 discussions.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on tombogle).


src/L10NSharp.Windows.Forms/Translators/MyMemoryTranslator.cs line 90 at r1 (raw file):

Previously, andrew-polk wrote…

This looks like a partial implementation which got left behind.

Sorry; I completely missed that this was coming from deserialization.

…ists

- Reword remaining "Bing"/"native language" comments in
  LanguageChoosingDialogViewModel.cs and its tests to reflect the current
  generic on-the-fly translator (MyMemoryTranslator), now that BingTranslator
  is gone. Verified live that MyMemoryTranslator doesn't exhibit either of
  the old Bing-specific quirks the test comment described (choking on a
  literal "{0}", or substituting "English" for the target language name).

- LanguageChoosingDialog: defer subscribing to Application.Idle until
  OnHandleCreated instead of doing it in the constructor. Per @imnasnainaec's
  review, Application.Idle is a process-wide event; if it fired (and the
  background translation from a previous commit's fix completed) before this
  dialog's window handle existed, BeginInvoke would throw
  InvalidOperationException, which nothing catches -- silently leaving the
  dialog in English. The constructor now just records that translation is
  needed via a flag; OnHandleCreated hooks Idle once the handle is guaranteed
  to exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tombogle

tombogle commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 40577b5, addressing @imnasnainaec's finding (via Devin) on LanguageChoosingDialog.cs:20:

  • Application.Idle is now subscribed in OnHandleCreated rather than in the constructor, using Devin's suggested approach almost verbatim. The constructor's callback just sets a _translationNeeded flag; OnHandleCreated hooks Idle (and clears the flag) once the window handle is guaranteed to exist, so the background-thread BeginInvoke added in the previous commit can never race against a not-yet-created handle.

Also batched in two small doc-only cleanups while I was in the area (per @tombogle):

  • Reworded the last remaining "Bing"/"native language" comments in LanguageChoosingDialogViewModel.cs and its test file, now that BingTranslator is gone. Verified live that MyMemoryTranslator doesn't exhibit either of the old Bing-specific quirks the test comment described.

Full build + test suite pass.

@tombogle

tombogle commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs line 20 at r2 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

Recommended code from Devin:

private readonly LanguageChoosingDialogViewModel _model;
private bool _translationNeeded;

public LanguageChoosingDialog(L10NCultureInfo requestedCulture, Icon icon)
{
    InitializeComponent();
    Icon = icon;
    // The callback just records that translation is needed; we wait to hook
    // Application.Idle until the handle exists (see OnHandleCreated) so the
    // background BeginInvoke can never run against a not-yet-created handle.
    _model = new LanguageChoosingDialogViewModel(_messageLabel.Text, _OKButton.Text, Text,
        requestedCulture, () => { _translationNeeded = true; });
    _messageLabel.Text = _model.Message;
}

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);
    if (_translationNeeded)
    {
        _translationNeeded = false;
        Application.Idle += Application_Idle;
    }
}

to address

Application.Idle is subscribed in the constructor (src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:17), before the dialog's window handle exists. If idle fires (app-wide event) and background translation finishes before the handle is created, BeginInvoke (src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:41) throws InvalidOperationException, which isn't caught (only ObjectDisposedException is, at src/L10NSharp.Windows.Forms/UIComponents/LanguageChoosingDialog.cs:51) — so the dialog stays English. Moving the subscription to OnHandleCreated guarantees the handle exists before BeginInvoke runs, eliminating the race.

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BingTranslator API has been retired

3 participants