Add qps-ploc pseudolocalization support - #158
Conversation
|
[Claude Fable 5] Consulted Devin on 2026-08-21 up to commit |
|
[Claude Fable 5] Consulted Devin on 2026-08-24 up to commit |
|
[Claude Fable 5] Consulted Devin on 2026-08-24 up to commit |
3e71b88 to
0b956b2
Compare
|
[Claude Fable 5] Consulted Devin on 2026-08-24 up to commit |
303b495 to
23cd79b
Compare
imnasnainaec
left a comment
There was a problem hiding this comment.
Looks good. Lots of ⛏️ nitpicks, some inspired from Devin informational flags, other from me looking for over-wordy prose. All optional: worth a moment of consideration, but take-or-leave as you like.
@imnasnainaec reviewed 5 files and all commit messages, and made 13 comments.
Reviewable status: 5 of 12 files reviewed, 13 unresolved discussions (waiting on andrew-polk).
CHANGELOG.md line 24 at r2 (raw file):
- [L10NSharp] Added `net8.0` as a target framework, enabling use on non-Windows platforms, and added cross-platform CI/CD coverage for `net8.0`. - [L10NSharp] Added UiLanguageChanged event to ILocalizationManager. This provides a way for clients to deal with changes now that (in Windows) LocalizeItemDlg<XLiffDocument>.StringsLocalized no longer exists. - [L10NSharp] Added pseudolocalization support: any lookup for the standard `qps-ploc` pseudo-locale (`LocalizationManager.PseudoLocalizationLanguageId`) returns the English text pseudolocalized at runtime — every vowel doubled with an accent on the first of the pair, wrapped in brackets (e.g. `[Tîitlée Mîissîing]`) — so testers can spot non-internationalized strings and layout problems. No translation files exist or are created for it. Set `LocalizationManager.OfferPseudoLocalization = true` to include it in `GetAvailableLocalizedLanguages()`/`GetUILanguages()`; `LocalizationManager.PseudoLocalize(string)` exposes the transform directly. The transform is self-contained (its placeholder/markup-skipping logic was adapted from the MIT-licensed PseudoLocalizer project — see `src/L10NSharp/Pseudo/README.md`), so no new package dependency is added.
⛏️ This CHANGELOG entry perhaps gives more details than necessary.
src/L10NSharp/L10NCultureInfo.cs line 115 at r2 (raw file):
EnglishName = "Pseudo-English (qps-ploc)"; DisplayName = EnglishName; NativeName = EnglishName;
⛏️ This sets the four name fields but leaves TwoLetterISOLanguageName, ThreeLetterISOLanguageName, IetfLanguageTag, NumberFormat platform-dependent. Consider also pinning them here so the pseudo culture is identical everywhere.
src/L10NSharp/LocalizationManagerInternal.cs line 391 at r2 (raw file):
{ if (LocalizationManager.IsPseudoLanguageId(lang)) lang = LocalizationManager.kDefaultLang; // pseudo is exactly as complete as English
⛏️ This
if (LocalizationManager.IsPseudoLanguageId(lang))
lang = LocalizationManager.kDefaultLang; // pseudo is exactly as complete as Englishcould also be added to FractionApproved and FractionTranslated for consistency and future-proofing (though not strictly necessary now, by coincidence).
src/L10NSharp/LocalizationManagerInternal.cs line 497 at r2 (raw file):
{ if (string.IsNullOrWhiteSpace(id)) return string.IsNullOrEmpty(englishText) ? string.Empty : englishText;
⛏️ When no managers are loaded, this early-exit only special-cases en, so a qps-ploc request falls through and the caller gets the raw string id instead of pseudolocalized text (even when englishText was supplied).
This doesn't match how GetString(...) was modified in this pr. Claude-suggest extension of the guard:
if (!string.IsNullOrEmpty(englishText))
{
if (langId == LocalizationManager.kDefaultLang)
return englishText;
if (LocalizationManager.IsPseudoLanguageId(langId))
return PseudoLocalization.Transform(englishText);
}
return id;
src/L10NSharp/LocalizationManagerInternal.cs line 526 at r2 (raw file):
} return string.IsNullOrEmpty(englishText) ? id : englishText;
⛏️ The two instances of
return string.IsNullOrEmpty(englishText) ? id : englishText;
could use PseudoLocalization.Transform when
IsPseudoLanguageId(langId), so pseudo mode stays visually obvious.
src/L10NSharp/LocalizationManagerInternal.cs line 536 at r2 (raw file):
// caller-supplied englishText wins over the cache. Note that this never engages the // dynamic-string collection machinery below: no files exist or are written for the // pseudo-locale.
⛏️ Since not engaging dynamic-string collection is intended, it's probably worth adding a note to Pseudo/README.md, so devs won't rely on exercising the app to collect dynamic strings and be surprised when a pseudo run quietly produce nothing.
src/L10NSharp/Pseudo/EscapeHelpers.cs line 20 at r2 (raw file):
int j = i; while (j < array.Length - 1 && IsPlaceholderNameChar(array[++j]))
⛏️ The name scan accepts only letters/digits/underscore plus an optional:format, so a placeholder with an alignment segment (e.g., {name,10}) or a space in the name (e.g., {first name}) fails the closing } check and gets pseudolocalized character by character.
Consuming an optional ,alignment segment (and anything up to } after the name) would close the gap in a couple of lines and keep the parser's fail-open behavior for genuinely malformed braces.
src/L10NSharp/Pseudo/PseudoLocalization.cs line 9 at r2 (raw file):
/// untouched. E.g. "Title Missing" becomes "[Tîitlée Mîissîing]". The transform is /// deterministic, and self-contained in this folder (see its README.md), so L10NSharp /// carries no extra dependency for this feature.
⛏️ The last sentence in this summary feels unnecessary here.
src/L10NSharp/Pseudo/README.md line 28 at r2 (raw file):
and others. Earlier iterations of this feature (see git history) used the full accent map and per-word padding of the PseudoLocalizer project, which we used as a starting point but replaced for readability.
⛏️ Some of the latter half of the "Design notes:" paragraph feels unnecessary.
src/L10NSharp/Pseudo/README.md line 34 at r2 (raw file):
`EscapeHelpers` (the placeholder/markup-skipping logic) is adapted from the MIT-licensed [PseudoLocalizer](https://github.com/martincostello/Pseudolocalizer) project, Copyright (C) 2012, Anders Kaplan, and extended here to also recognize `%0`-style and named
⛏️ The license/copyright detail in this Provenance sentence is unnecessary, given the full license copied below (though the author's name is probably nice to have in this prose.
src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs line 513 at r2 (raw file):
// the English entry. Doing it here rather than only in GetLocalizedString matters // because the WinForms component localizers (which set the Text of designer-created // controls, tool strip items and column headers) call straight into the string cache;
⛏️ This parenthetical may be unnecessary.
src/L10NSharp.Tests/PseudoLocalizationTests.cs line 302 at r2 (raw file):
/// <summary> /// The WinForms component localizers -- what sets the Text of controls, tool strip items /// and column headers created in the designer -- read the string cache directly rather
⛏️ This -- aside may be unnecessary.
andrew-polk
left a comment
There was a problem hiding this comment.
@andrew-polk+AI made 12 comments and resolved 12 discussions.
Reviewable status: 4 of 13 files reviewed, 1 unresolved discussion (waiting on andrew-polk and imnasnainaec).
CHANGELOG.md line 24 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ This CHANGELOG entry perhaps gives more details than necessary.
[Claude Fable 5] Agreed. Trimmed in 984e8f1 to the what/why plus the two entry points, with a pointer to src/L10NSharp/Pseudo/README.md for the rest.
src/L10NSharp/L10NCultureInfo.cs line 115 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ This sets the four name fields but leaves
TwoLetterISOLanguageName,ThreeLetterISOLanguageName,IetfLanguageTag,NumberFormatplatform-dependent. Consider also pinning them here so the pseudo culture is identical everywhere.
[Claude Fable 5] Done in 984e8f1: IetfLanguageTag, TwoLetterISOLanguageName ("en"), ThreeLetterISOLanguageName ("eng"), IsNeutralCulture (false) and NumberFormat (English) are now pinned, matching what Windows reports for qps-ploc. The GetUILanguages test asserts them.
src/L10NSharp/LocalizationManagerInternal.cs line 391 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ This
if (LocalizationManager.IsPseudoLanguageId(lang)) lang = LocalizationManager.kDefaultLang; // pseudo is exactly as complete as Englishcould also be added to
FractionApprovedandFractionTranslatedfor consistency and future-proofing (though not strictly necessary now, by coincidence).
[Claude Fable 5] Added the same mapping to FractionApproved and FractionTranslated in 984e8f1.
src/L10NSharp/LocalizationManagerInternal.cs line 497 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ When no managers are loaded, this early-exit only special-cases
en, so aqps-plocrequest falls through and the caller gets the raw string id instead of pseudolocalized text (even whenenglishTextwas supplied).This doesn't match how
GetString(...)was modified in this pr. Claude-suggest extension of the guard:if (!string.IsNullOrEmpty(englishText)) { if (langId == LocalizationManager.kDefaultLang) return englishText; if (LocalizationManager.IsPseudoLanguageId(langId)) return PseudoLocalization.Transform(englishText); } return id;
[Claude Fable 5] Good catch; fixed in 984e8f1. The no-manager path now treats qps-ploc like English and returns the supplied englishText pseudolocalized (id when there is none), via a small EnglishTextOrFallback helper shared with the disposed-manager returns. Regression test: GetDynamicStringOrEnglish_NoManagerLoaded_Pseudo_PseudolocalizesSuppliedEnglish.
src/L10NSharp/LocalizationManagerInternal.cs line 526 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ The two instances of
return string.IsNullOrEmpty(englishText) ? id : englishText;could use
PseudoLocalization.Transformwhen
IsPseudoLanguageId(langId), so pseudo mode stays visually obvious.
[Claude Fable 5] Done in 984e8f1: both disposed-manager returns go through EnglishTextOrFallback, which pseudolocalizes englishText when langId is qps-ploc.
src/L10NSharp/LocalizationManagerInternal.cs line 536 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ Since not engaging dynamic-string collection is intended, it's probably worth adding a note to
Pseudo/README.md, so devs won't rely on exercising the app to collect dynamic strings and be surprised when a pseudo run quietly produce nothing.
[Claude Fable 5] Added a short "No files, no dynamic-string collection" section to Pseudo/README.md in 984e8f1 saying a pseudo run never collects dynamic strings and to harvest under English as before.
src/L10NSharp/Pseudo/EscapeHelpers.cs line 20 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ The name scan accepts only letters/digits/underscore plus an optional
:format, so a placeholder with an alignment segment (e.g.,{name,10}) or a space in the name (e.g.,{first name}) fails the closing}check and gets pseudolocalized character by character.Consuming an optional
,alignmentsegment (and anything up to}after the name) would close the gap in a couple of lines and keep the parser's fail-open behavior for genuinely malformed braces.
[Claude Fable 5] Partly taken in 984e8f1: the scanner now consumes a .NET alignment segment, so {0,10:n0} and {name,-8} pass through (test cases added). I stopped short of accepting arbitrary content up to the closing brace: spaces in names are not valid composite-format placeholders and no consumer uses them, and accepting anything would also skip brace-wrapped prose like {see below}. Andrew reviewed that trade-off and chose to leave it there.
src/L10NSharp/Pseudo/PseudoLocalization.cs line 9 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ The last sentence in this summary feels unnecessary here.
[Claude Fable 5] Removed in 984e8f1; the summary now just notes determinism and points at the README.
src/L10NSharp/Pseudo/README.md line 28 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ Some of the latter half of the "Design notes:" paragraph feels unnecessary.
[Claude Fable 5] Dropped the earlier-iterations history in 984e8f1; the design notes now stop after the Mozilla/qps-ploc/en-XA precedent.
src/L10NSharp/Pseudo/README.md line 34 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ The license/copyright detail in this Provenance sentence is unnecessary, given the full license copied below (though the author's name is probably nice to have in this prose.
[Claude Fable 5] Done in 984e8f1: the sentence now credits Anders Kaplan's PseudoLocalizer and leaves the license and copyright detail to the license block below.
src/L10NSharp/XLiffUtils/XliffLocalizationManager.cs line 513 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ This parenthetical may be unnecessary.
[Claude Fable 5] Removed the parenthetical in 984e8f1.
src/L10NSharp.Tests/PseudoLocalizationTests.cs line 302 at r2 (raw file):
Previously, imnasnainaec (D. Ror.) wrote…
⛏️ This
--aside may be unnecessary.
[Claude Fable 5] Removed the aside in 984e8f1.
|
[Claude Fable 5] Consulted Devin on 2026-09-01 up to commit |
imnasnainaec
left a comment
There was a problem hiding this comment.
@imnasnainaec reviewed 9 files and all commit messages, and resolved 1 discussion.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on andrew-polk).
Lookups for the standard qps-ploc pseudo-locale return the live English
text pseudolocalized at runtime — every vowel doubled with an accent on
the first of the pair, wrapped in brackets ("Title Missing" ->
"[Tîitlée Mîissîing]") — so testers can spot non-internationalized
strings (plain English), truncation (missing "]"), concatenation
(brackets mid-sentence), and layout problems (~30-40% in-word
expansion). Format placeholders ({0}, {0:n0}, named {app_title}-style,
and %0-style) and HTML/XML markup pass through untouched.
- Derived from English at lookup time: no translation files exist, are
loaded, or are written for the pseudo-locale; it never maps to or from
a real language; availability and counts report it exactly as complete
as English.
- Lookups work by simply setting the language code; all lookup funnels
are hooked (static, preferred-languages, dynamic, and the WinForms
paths — both runtime lookups and designer-created controls, whose
component localizers read the string cache directly), transforming
exactly once, with code-supplied English winning over the cache as it
does for "en".
- LocalizationManager.OfferPseudoLocalization (default false) gates only
whether the locale is advertised in language lists; display name is
hard-coded as "Pseudo-English (qps-ploc)".
- LocalizationManager.PseudoLocalize exposes the transform for strings
that bypass L10NSharp.
- Self-contained in src/L10NSharp/Pseudo/ with no new package
dependency; the placeholder-skipping logic is adapted from the
MIT-licensed PseudoLocalizer project (attribution in the folder's
README). 31 tests pin the transform and lookup semantics.
Smoke-tested end to end in Bloom (WinForms chrome incl. designer-created
dialogs, React UI, live placeholder substitution; no pseudo files
written).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Pseudo-locale now pseudolocalizes the supplied English text on the
no-manager-loaded and disposed-manager paths of GetDynamicStringOrEnglish,
matching GetString (new EnglishTextOrFallback helper).
- FractionApproved/FractionTranslated map qps-ploc to English like the
other completeness helpers.
- L10NCultureInfo pins the pseudo culture's ISO names, IETF tag, neutral
flag and NumberFormat so it is identical on every platform.
- EscapeHelpers passes through placeholders with an alignment segment
({0,10:n0}, {name,-8}).
- README documents that a pseudo run never collects dynamic strings; trims
design-notes and provenance prose. Shorter CHANGELOG entry and comments.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
.NET composite format allows whitespace around the alignment ("{0, 10:E2}").
The scanner stopped at the space, fell open, and pseudolocalized the format
code. Found by Devin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
98dcc75 to
75db5a4
Compare
imnasnainaec
left a comment
There was a problem hiding this comment.
@imnasnainaec reviewed 1 file and all commit messages.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on andrew-polk).
L10nSharp gained qps-ploc pseudolocalization support in #158, but nothing in the repo showed it working. Set LocalizationManager.OfferPseudoLocalization in the SampleApp so "Pseudo-English (qps-ploc)" appears in its language combo, and point the CHANGELOG entry at the sample app. The comment on the flag explains what the pseudo-locale is for, and notes one non-obvious consequence: while it is selected, the app's "Get Name Dynamically" demo no longer harvests its string into the English XLIFF, because pseudo lookups return before the collection step. That is intended and documented in Pseudo/README.md, but the sample app is read as documentation, so it belongs where someone trying the demo will see it. No library code changes; only the sample app opts in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
L10nSharp gained qps-ploc pseudolocalization support in #158, but nothing in the repo showed it working. Set LocalizationManager.OfferPseudoLocalization in the SampleApp so "Pseudo-English (qps-ploc)" appears in its language combo, and point the CHANGELOG entry at the sample app. The comment on the flag explains what the pseudo-locale is for, and notes one non-obvious consequence: while it is selected, the app's "Get Name Dynamically" demo no longer harvests its string into the English XLIFF, because pseudo lookups return before the collection step. That is intended and documented in Pseudo/README.md, but the sample app is read as documentation, so it belongs where someone trying the demo will see it. No library code changes; only the sample app opts in. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
Applications built on L10NSharp have no good way for testers to verify internationalization. Hard-coded (non-localizable) strings, clipped layouts, broken format placeholders, and runtime string concatenation are invisible in an English UI and only surface after real translations arrive.
Fix
This PR adds the industry-standard qps-ploc pseudo-locale. Any lookup for language
qps-plocreturns the live English text pseudolocalized — every vowel doubled with an accent on the first of the pair for ~30-40% in-word expansion, all wrapped in brackets (e.g.Title Missing→[Tîitlée Mîissîing]) — with format placeholders ({0},{0:n0}, named{app_title}-style, and%0-style) and HTML/XML tags passed through untouched. Plain English on screen then means a hard-coded string; a missing]means truncation; brackets mid-sentence mean concatenation.SetUILanguage("qps-ploc")or a per-call language list); all the lookup funnels (static, preferred-languages, dynamic, and the WinForms paths — both runtime lookups and designer-created controls, whose localizers read the string cache directly) are hooked, and the transform is applied exactly once. The code-supplied English wins over the cache, mirroring the existingenrule.LocalizationManager.OfferPseudoLocalization(default false) gates only whether it is advertised inGetAvailableLocalizedLanguages()/GetUILanguages(), so end users never see the test locale unless the app opts in (e.g. alpha channels). Display name is hard-coded as "Pseudo-English (qps-ploc)".src/L10NSharp/Pseudo/silo and adds no package dependency. The vowel-doubling style follows Mozilla’s pseudolocalization approach, chosen for readability after trying several styles live in Bloom; the MIT-licensed PseudoLocalizer project was the starting point, and its adapted placeholder/markup-skipping logic remains (attribution in the silo’s README).LocalizationManager.PseudoLocalize(string)exposes the transform for strings that bypass L10NSharp.Additive only — no behavior change for consumers that never request qps-ploc. 31 new tests pin the transform behaviors and the lookup/gating semantics.
Devin review
This change is