Skip to content

Keyman / per-language keyboard integration (BL-16524)#8047

Draft
hatton wants to merge 20 commits into
masterfrom
BL-16524-Keyman-Integration
Draft

Keyman / per-language keyboard integration (BL-16524)#8047
hatton wants to merge 20 commits into
masterfrom
BL-16524-Keyman-Integration

Conversation

@hatton

@hatton hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member

This change is Reviewable

Devin review

hatton and others added 16 commits July 8, 2026 15:32
Proof of concept for embedding KeymanWeb in Bloom's page-editing view.

How far this got (hard-coded to Thai, as far as I'm aware):
- Focusing a .bloom-editable with lang="th" (or th-*) lazy-loads the
  KeymanWeb engine from the Keyman CDN on first use, activates the Thai
  Kedmanee keyboard, and shows the floating on-screen keyboard.
- Physical keys are remapped to Kedmanee (d->ก, k->า, l->ส, etc.).
- Non-Thai fields are left completely alone; the OSK is hidden again when
  focus leaves a Thai field, so the keyboard is active ONLY in Thai fields.
- Verified live in an all-xmatter Thai/English test book: Thai remaps,
  English types literally, OSK shows/hides as expected, no console errors.

Implementation:
- New keymanWebIntegration.ts, called from the delegated focusin handler in
  bloomEditing.ts. attachType "manual"; the exact "thai_kedmanee" stub is
  registered and we wait for its code to load (HasLoaded) before binding, to
  avoid a first-focus race. root/resources/fonts are pointed at the CDN so
  the OSK font loads from there instead of 404ing against Bloom's server.
- A save-time scrub in Cleanup() removes KeymanWeb's attach artifacts
  (keymanweb-font class, inputmode, dir="ltr", empty style) from editable
  divs.

Known limitations / not done:
- Hard-coded to Thai and to the Kedmanee keyboard id; no per-language
  configuration yet.
- Loads from the Keyman CDN at runtime, so it needs internet in the edit
  view; a shipping version should vendor the engine + keyboard for offline.
- The Cleanup() scrub only covers page-body divs. Xmatter content round-
  trips through bloomDataDiv, so KeymanWeb residue still persists in saved
  xmatter fields; that path needs its own scrub (likely C#-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design docs for generalizing the KeymanWeb POC (af6f479) into a real
per-language keyboard setting:

- keyboard-setting-plan.md: the agreed design (Automatic cascade: OS
  input method first, cached KeymanWeb fallback second, resolved per
  machine; active FieldWorks-style OS switching in v1 gated behind a
  WebView2/TSF spike; offline-first with vendored engine and
  collection-cached keyboards) plus ordered work items, verification
  strategy, and ranked risks.
- implementation-handoff.md: context for the implementing agent —
  decision rationale (do not relitigate), live-verified Keyman API and
  libpalaso facts, open questions for John, and suggested skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds src/BloomBrowserUI/keymanweb/: the KMW engine, OSK stylesheets and
OSK font, fetched pinned from s.keyman.com (MIT license included). The
README documents exact sources, how the minimal file set was determined
(grepping the engine for the resources it lazily fetches), the update
procedure, and how the folder reaches output/browser in dev mode (both
dev.mjs's own static-copy watcher and vite build's static copy handle
it; confirmed by a running watcher having already copied it).

The vendored files must stay byte-identical to upstream, so the folder
is excluded from prettier (.prettierignore) and from git EOL/whitespace
munging (.gitattributes) - the pre-commit hook reformatted the minified
engine on the first commit attempt, doubling its size.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the hard-coded Thai POC in keymanWebIntegration.ts with the
general mechanism from Design/Keyboards/keyboard-setting-plan.md item 5:
on every bloom-editable focus, POST keyboarding/fieldFocused {lang} and
either attach the server-chosen KeymanWeb keyboard (lazy vendored-engine
load, local keyboard stub via addKeyboards, HasLoaded poll before
activation, OSK shown) or get out of the way (C# switches the OS input
method; OSK hidden). Langs z/*/empty are skipped. A per-language cache
avoids re-registration while still posting every focus so the C# side
can switch OS keyboards. The C# endpoint lands separately (plan item 4);
this codes against its fixed contract.

Longpress interplay (plan item 8): long-press alternates are variants of
the physical key, which is meaningless under KMW remapping, and both
libraries grab the same key events - so jquery.longpress now bails out
on elements KMW has attached to, tracked via a WeakSet in
longPressShared.ts (extends the BL-1071 window-level policy to field
granularity). OS-switched fields keep longpress.

Vitest specs cover the skip list, cache behavior, the engine-never-loads
path for useKmw:false, and the longpress tracker (7 tests).

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 0 verdict. libpalaso KeyboardController.Activate()
(TF_IPPMF_FORPROCESS) does NOT affect typing in WebView2 - the input
lives in a separate msedgewebview2.exe process, confirmed empirically
via per-thread GetKeyboardLayout and navigator.keyboard.getLayoutMap.
Active switching in v1 remains feasible: WM_INPUTLANGCHANGEREQUEST
posted to Bloom's foreground top-level window switches the webview's
input thread (Bloom-scoped; other apps unaffected). Items 2/4 must use
libpalaso for enumeration only and carry HKLs for activation. A
30-second real-typing confirmation is still recommended (key injection
was not possible during the spike; the user was in a live meeting).

Spiked by a Claude Opus teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 1. WritingSystem gains Keyboard (a resolution policy string:
"" = Automatic, "system:<libpalaso id>" = pinned installed input method,
"kmw:<keyboardId>@<bcp47>" = pinned KeymanWeb keyboard) and
CachedKmwFallbackKeyboard (top Keyman search suggestion, used only by
Automatic). Both serialize in the legacy numbered form and the
unnumbered <Language> form following the FontName pattern, are copied
in Clone(), and are skipped for sign languages. The new
KeyboardSetting value type parses/serializes the policy string,
degrading malformed user data to Automatic rather than throwing.

Tests cover round-trips of both XML forms, defaults, Clone, and Parse
edge cases.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 7, closing the gap flagged in the KeymanWeb POC: KMW's
attachToControl decorates editables with a keymanweb-font class, an
inputmode attribute, and dir="ltr". The JS-side scrub (bloomEditing.ts
Cleanup) strips them from pages, but xmatter/data-div propagation runs
in C#, so BookData now (a) never copies keymanweb-font/inputmode into
the data-div and force-removes them when absent (healing POC-era
pollution), and (b) skips dir="ltr" value-sensitively in
GetAttributesToSave - Bloom itself only writes dir="rtl", which must
survive.

Tests cover fresh pollution (data-div + re-emitted page both clean),
healing of pre-existing pollution, and dir="rtl" survival.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 3. KeymanCloudClient talks to the two verified Keyman
endpoints (search 2.0 for the per-language keyboard list ordered by
finalWeight; cloud 4.0 for download metadata including font/oskFont)
with a 5s metadata timeout, returning empty/null quickly when offline.
JSON parsing lives in pure static methods unit-tested against canned
real API responses (fetched 2026-07-09).

CollectionKeyboardCache stores <collection>/Keyboards/<id>.js, an
<id>.json manifest (incl. display + OSK font families/files) and a
fonts/ subfolder, written atomically (temp+rename, manifest last so
its presence implies completeness); GetJsUrl serves the cached js via
BloomServer's localhost mapping. Offline typing then never needs the
network once a keyboard is cached.

Implemented by a Claude Opus teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds "Keyboards" to every hard-coded synced-folder list (change
watcher, files-to-monitor, local->repo copy, repo->local extract,
repo mod-time). Because Keyboards contains a fonts/ subfolder and the
existing folder sync zipped top-level files only, RobustZip gains a
recursive WriteAllFilesToZip (via BloomZipFile.AddDirectoryContents),
now used for all three synced folders - Allowed Words and Sample Texts
are flat, so their zips are unchanged. Extraction already recreates
subfolders. Known limitation (pre-existing cleanup design): the
delete-if-not-in-repo pruning still scans top-level only.

The TC round-trip test now covers a Keyboards folder including a file
in fonts/.

Implemented by a Claude Opus teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 2, using the spike's corrected mechanism. Program.cs runs
KeyboardController.Initialize() as a low-priority UI-thread startup
action (try/catch + NonFatalProblem; ~160-500ms one-time) and
Shutdown() beside Sldr.Cleanup(). New OsKeyboards uses libpalaso ONLY
for enumeration (AvailableKeyboards/TryGetKeyboard) and resolves an HKL
per language; activation posts WM_INPUTLANGCHANGEREQUEST to Bloom's
Shell window (the only channel the spike proved reaches the separate
msedgewebview2.exe input thread - libpalaso's process-scoped Activate()
does not). TIP-backed definitions (e.g. installed Keyman-for-Windows)
are flagged: HKL posting selects the language but in-language profile
selection was unverified (no Keyman installed on the spike machine).

Implemented by Claude Sonnet teammates under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 4. KeyboardResolver runs the per-machine cascade (pinned
system -> pinned kmw -> Automatic: best OS match, else cached KMW
fallback, else default) with a per-session ConcurrentDictionary cache
(safe because keyboard changes require restart) and a background fetch
of the top Keyman suggestion when Automatic finds no OS keyboard
online - writing only CachedKmwFallbackKeyboard, marshalled to the UI
thread and skipped while the settings dialog is open (plan risk #6).

KeyboardingConfigApi drops all spike code and adds the real
POST keyboarding/fieldFocused (handleOnUiThread): resolve, switch the
OS keyboard only when the target differs from the last activation
(never per keystroke), and reply the fixed contract the browser
integration consumes. useKmw is true only when the keyboard is fully
cached locally; otherwise it replies useKmw:false, activates default,
and kicks a deduped background download so the next focus succeeds.
CollectionKeyboardCache gains IsCached/GetFontUrl for the endpoint.

Implemented by Claude Sonnet teammates under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 6 (browser half). keyboardSection.tsx renders under each
language's font row (languages 1-3) in the Book Making tab: an MUI
select offering Automatic (with dynamic "Currently: <resolves-to>"
subtext), installed OS input methods, and Keyman online keyboards
ordered by popularity, with an offline hint when the cloud list is
empty. It reads settings/keyboardsForLanguage and posts
settings/setKeyboardForLanguage (the C# endpoints land separately).

Also completes the font wiring the earlier JS slice deferred: the
fieldFocused response's font/oskFont family+urls are now passed into
keyman.addKeyboards' language spec so the OSK renders non-Latin
keycaps. The KMW font-spec property names (font/oskFont with
family + source) were verified against the vendored engine's own
internalizeFont helper, not guessed.

New localizable strings went into Bloom.xlf (primary settings UI);
priority-file choice is pending John's confirmation per the
xlf-strings skill.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds render-based unit tests for the keyboard chooser (three groups,
Automatic subtext, correct raw setting string posted for system vs
cloud selections, offline hint). This introduces @testing-library/react
+ @testing-library/dom as devDependencies and is the first render-based
component test in this project - isolated in its own commit so it can be
reverted independently if we prefer a different component-testing
approach. The alternative (pure mocks, no render) could not meaningfully
exercise the MUI Select's grouped menu.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan item 6 (C# half), completing the pairing with the React keyboard
chooser and resolving the "Cannot Find API Endpoint" dialog that
appeared when the UI shipped ahead of its backend.

GET settings/keyboardsForLanguage returns the contract the component
consumes: current setting, language tag, what Automatic resolves to on
this machine (real cascade: best OS keyboard, else cached KMW fallback,
else "Default"), installed OS input methods, and Keyman cloud keyboards
(empty offline). POST settings/setKeyboardForLanguage stores a pending
selection, mirroring PendingFontSelections.

Unlike fonts (which flag restart at POST time), the keyboard restart is
flagged at dialog OK-commit: UpdateLanguageSettings now writes the
pending keyboards into the WritingSystems, returns whether any changed,
and for a newly-pinned kmw: keyboard kicks a background
EnsureDownloaded so its files are cached before the restart. This
avoids a spurious restart if the user tries a keyboard and reverts
before clicking OK. The keyboard params are optional so existing font
callers/tests are unaffected.

Implemented by a Claude Sonnet teammate under Claude Fable orchestration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…man)

Book Making settings tab layout:
- Give the font, keyboard, and page-numbering selects a shared width and
  height so they line up (the plain-text keyboard/page-number boxes were
  sizing narrower than the font boxes). Adds bookMakingSelectCss in
  commonTabSettings and lets WinFormsStyleSelect/FontSelectComponent accept
  a className so the size can be applied (the old width prop was dropped).
- Add vertical spacing between each language block and before Page Numbering.
- Reorder each language block: font, then keyboard, then Special Script
  Settings last.

Also snapshots ongoing Keyman work on this branch:
- Editor Keyman-web integration and canvas control text menu wiring.
- New editableControls (EditableControls + manager) with tests.
- Keyboarding backend (KeyboardResolver, KeymanCloudClient, KeyboardSetting)
  and the keyboarding/collection-settings API endpoints, with tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Group the font, keyboard, and page-numbering controls on the left of the
Book Making settings tab into a light rounded panel with thin dividers
between language blocks, and match the select controls' rounded/light
border appearance ("Refined rhythm", design option 1A). Bump the settings
dialog's tab height to fit the taller panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a full per-language keyboard integration for Bloom, covering both the edit view (live keyboard switching while typing) and the Collection Settings Book Making tab. The C# side adds a KeyboardResolver cascade (Off → pinned System → pinned KMW → Automatic), a CollectionKeyboardCache for offline keyboard storage, a KeymanCloudClient for the Keyman API, OsKeyboards for Windows input-language switching via WM_INPUTLANGCHANGEREQUEST, and two new API controllers. The TypeScript side adds keymanWebIntegration.ts (engine loading, keyboard attachment, OSK visibility management) and a new KeyboardSection React component in the settings dialog.

  • New Keyboarding/ subsystem (C#): All five new classes are fully unit-tested and cleanly dependency-injected; network calls are always on background threads; OS-keyboard enumeration is correctly marshalled to the UI thread only where required.
  • New keymanWebIntegration.ts: Engine loads lazily on first KMW field focus; a bounded poll handles the engine's lazy code download; the oskHideIsProgrammatic flag correctly distinguishes programmatic from user-initiated OSK hides.
  • Settings UI (KeyboardSection.tsx): Uses useMountEffect, handles offline gracefully, and posts keyboard changes immediately to the server for restart-flag tracking.

Important Files Changed

Filename Overview
src/BloomExe/Keyboarding/KeyboardResolver.cs New class implementing the keyboard cascade. Well-structured with injected dependencies for testability; background fetch is properly isolated via a _runInBackground delegate so the UI thread is never blocked.
src/BloomExe/Keyboarding/KeymanCloudClient.cs New Keyman API client using a shared static HttpClient. JSON parsers are pure static methods for isolated unit testing. Blocking .Result calls are safe since the client is always called from background threads.
src/BloomExe/Keyboarding/CollectionKeyboardCache.cs New on-disk keyboard cache. Writes are atomic (temp-file-then-rename), manifest is written last to ensure integrity, and partial downloads never leave corrupt state.
src/BloomExe/web/controllers/KeyboardingConfigApi.cs New edit-view keyboard API. fieldFocused correctly runs on the UI thread; network/download work is dispatched to Task.Run via the resolver's background delegate.
src/BloomExe/web/controllers/CollectionSettingsApi.cs Adds two settings-dialog keyboard endpoints. keyboardsForLanguage correctly runs off the UI thread; minor unsynchronized double-check in EnsureKeyboardServicesBuilt (see comment).
src/BloomBrowserUI/bookEdit/js/keymanWebIntegration.ts Browser-side KMW integration. Engine loads lazily; oskHideIsProgrammatic flag correctly distinguishes programmatic from user-initiated hides. Unhandled rejections are caught by .catch in the caller.
src/BloomBrowserUI/react_components/keyboardSection.tsx New settings UI for per-language keyboard selection. Uses useMountEffect correctly and handles the offline case gracefully.
src/BloomExe/Keyboarding/OsKeyboards.cs New OS keyboard enumeration and activation using WM_INPUTLANGCHANGEREQUEST. TIP residual risk is clearly documented. All methods are correctly UI-thread-only.
src/BloomExe/Collection/CollectionSettingsDialog.cs Adds PendingKeyboardSelections and wires it through UpdateLanguageSettings. EnsureKeyboardCachedInBackground fire-and-forgets a pre-warm download for pinned KMW keyboards on OK.
src/BloomTests/Keyboarding/KeyboardResolverTests.cs Comprehensive resolver tests covering all cascade branches and background fetch side effects with fake collaborators.

Reviews (2): Last reviewed commit: "Run the Book Making keyboard cloud searc..." | Re-trigger Greptile

Comment thread src/BloomExe/web/controllers/CollectionSettingsApi.cs
Comment thread src/BloomExe/web/controllers/CollectionSettingsApi.cs
Comment on lines +396 to +440
<trans-unit id="CollectionSettingsDialog.BookMakingTab.KeyboardFor" translate="no">
<source xml:lang="en">Keyboard for {0}</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.KeyboardFor</note>
<note>{0} is a language name.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.Automatic" translate="no">
<source xml:lang="en">Automatic</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.Automatic</note>
<note>First item of the per-language Keyboard dropdown in the Book Making tab of Settings. Means "let Bloom figure out the best keyboard for this language on this machine".</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.Off" translate="no">
<source xml:lang="en">Off</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.Off</note>
<note>An item in the per-language Keyboard dropdown in the Book Making tab of Settings. Means Bloom should not change or supply a keyboard for this language; it leaves whatever keyboard the user has active.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.AutomaticResolvesTo" translate="no">
<source xml:lang="en">Currently: {0}</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.AutomaticResolvesTo</note>
<note>Secondary/subtext line under "Automatic" in the per-language Keyboard dropdown. {0} is the name of the keyboard or input method that Automatic currently resolves to on this machine (may itself be untranslated, e.g. an OS input method name).</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.InstalledGroup" translate="no">
<source xml:lang="en">Installed input methods</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.InstalledGroup</note>
<note>Group header in the per-language Keyboard dropdown, above the list of keyboards/input methods already installed on this machine.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.CloudGroup" translate="no">
<source xml:lang="en">Keyman (online) keyboards</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.CloudGroup</note>
<note>Group header in the per-language Keyboard dropdown, above the list of keyboards available from the Keyman online keyboard service. "Keyman" is a product name and must not be translated.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.CloudUnavailable" translate="no">
<source xml:lang="en">Not available offline</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.CloudUnavailable</note>
<note>Shown in place of the online Keyman keyboard list, in the per-language Keyboard dropdown, when Bloom cannot reach the Keyman server (e.g. no internet connection).</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.DownloadCount" translate="no">
<source xml:lang="en">{0} downloads</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.DownloadCount</note>
<note>Small subtext shown under a Keyman online keyboard's name in the per-language Keyboard dropdown. {0} is a number of times that keyboard has been downloaded.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.BookMakingTab.Keyboard.NoneResolvesTo" translate="no">
<source xml:lang="en">Default</source>
<note>ID: CollectionSettingsDialog.BookMakingTab.Keyboard.NoneResolvesTo</note>
<note>Used as the "Currently: {0}" value under "Automatic" in the per-language Keyboard dropdown, when this machine has no matching OS input method and no Keyman keyboard has been suggested yet for this language.</note>
</trans-unit>

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.

P2 All new keyboard strings carry translate="no"

Every one of the ten new <trans-unit> entries — "Keyboard for {0}", "Automatic", "Off", "Currently: {0}", "Installed input methods", "Keyman (online) keyboards", etc. — has translate="no". That attribute tells Crowdin to skip them, so they will never be translated into any other language. Comparable existing strings in this file (e.g. CollectionSettingsDialog.BookMakingTab.DefaultFontFor) do not carry this attribute. If the intent is to ship these strings un-translated initially and add translations later, the translate="no" needs to be removed when they are ready for Crowdin pickup.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Bug: Keyboard font file changes in Team Collections are not detected for syncing (TeamCollection.cs:903)

The file-monitoring list for Team Collection sync uses Directory.EnumerateFiles(folderPath, "*") without SearchOption.AllDirectories, so files inside subfolders are omitted. Changes to cached keyboard fonts in the Keyboards/fonts/ subfolder are therefore never included in the sync checksum, and teammates never receive an updated font.

Suggested fix: enumerate with SearchOption.AllDirectories for the Keyboards folder (or the relevant monitored roots).

Comment on lines +70 to +72
const langName = getLanguageDisplayName(
editable?.getAttribute("lang") ?? null,
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Investigate: Language-tag replacement removed data-languageTipContent; context controls now compute the name at render time (CanvasElementContextControls.tsx:70-72)

The old AddLanguageTags stamped data-languageTipContent on each editable and this component read it directly. The new code calls getLanguageDisplayName(editable?.getAttribute('lang'))theOneLocalizationManager.getLanguageName(langCode) at render time instead. Believed functionally equivalent, but the lookup path and timing changed. Worth verifying the displayed names still match the old behavior for CJK and other non-Latin scripts.


// SearchKeyboardsForLanguage already treats offline/failure as "no results" internally, so
// no extra try/catch is needed here to keep this endpoint responsive.
var cloudResults = _keymanCloudClient.SearchKeyboardsForLanguage(tag);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Devin] Investigate: Settings dialog blocks the UI thread with a synchronous HTTP call to the Keyman cloud API (CollectionSettingsApi.cs:675)

GetKeyboardsForLanguage calls _keymanCloudClient.SearchKeyboardsForLanguage(tag), which does s_httpClient.GetAsync(url).Result. The endpoint is registered with handleOnUiThread: true, so the WinForms UI thread blocks for up to the 5s HttpClient timeout — the settings dialog freezes while the cloud list loads (worst case when offline). The OS-keyboard enumeration genuinely needs the UI thread, but the cloud search does not; consider moving just the cloud search off-thread.

(Greptile flagged the same line as P1.)

@hatton

hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8] Consulted Devin on 2026-07-10 19:04 UTC up to commit 19f7fb7. Findings (1 Bug, 2 Investigate) mirrored as inline review threads; Informational items skipped.

hatton and others added 2 commits July 10, 2026 14:50
Master migrated the front-end from yarn to pnpm (delete yarn.lock, add
pnpm-lock.yaml + pnpm-workspace.yaml, packageManager pnpm@11.5.2, volta ->
vite-plus). Adopt that on this branch:
- take master's pnpm-based package.json and re-add this branch's two new
  devDependencies (@testing-library/dom 10.4.1, @testing-library/react 16.3.2,
  pinned to the versions pnpm resolved);
- remove yarn.lock; regenerate pnpm-lock.yaml via pnpm install.

Also brings in the rest of master (57 commits) including the React 18 and
vite-plus changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…BL-16524)

Separate keyboard acquisition from resolution. KeyboardResolver now re-ensures
a KeymanWeb keyboard's files are on disk on EVERY focus (new EnsureAcquired
step) rather than only once per session, so a keyboard is (re)acquired on
demand whenever it is needed but missing: a deleted .keyboards cache, a
teammate who has the setting but not the files, or an offline first focus all
self-heal on the next focus. Acquisition already runs in the background and
no-ops when the files are present, so calling it every focus is cheap. Removes
the two now-redundant in-cascade EnsureCached calls and adds a regression test.

Also: the Book Making keyboard chooser now shows a pinned Keyman keyboard's id
when the cloud list is unavailable (offline) instead of misreporting
"Automatic"; it self-heals to the friendly name once back online.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hatton

hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8]

Bug: HandleGetLanguageNames throws NRE when no book is selected

src/BloomExe/web/controllers/CollectionSettingsApi.cs:473 dereferences _bookSelection.CurrentSelection with no null guard:

x["language1Name"] = _bookSelection.CurrentSelection.CollectionSettings.Language1.Name;

If settings/languageNames is requested while no book is selected, this throws System.NullReferenceException and pops the "Bloom had a problem" dialog.

Repro (observed at runtime): switch collections mid-session (workspace/openCollection). Immediately afterward Bloom is on the Collections tab with CurrentSelection == null; a request to settings/languageNames in that window crashes:

System.NullReferenceException: Object reference not set to an instance of an object.
  at Bloom.web.controllers.CollectionSettingsApi.HandleGetLanguageNames(ApiRequest request)
     in .../CollectionSettingsApi.cs:line 473

Discovered while runtime-checking the canvas-element context control on a CJK book (BL-16524). It's a fail-fast crash rather than a graceful no-op. Worth confirming whether any normal UI flow can request this endpoint with no current selection (e.g. transient states during collection switch / book close); if so, guard CurrentSelection/return an empty result.

@hatton

hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8]

Question/possible bug: Language & Field-type submenus never appear on ordinary pages (bloom-customLayout gating)

The canvas-element context menu's language and fieldType rows are gated by ctx.isCustomPage, defined in src/BloomBrowserUI/bookEdit/toolbox/canvas/buildCanvasElementControlRegistryContext.ts:222 as:

isCustomPage: page?.classList.contains("bloom-customLayout") ?? false,

and enforced in canvasControlAvailabilityRules.ts (language.visible and fieldType.visible both require ctx.isCustomPage).

Observed at runtime: on real pages in a normal book, the page element is classed customPage bloom-combinedPage …not bloom-customLayout. So even with a proper multilingual text canvas element selected (verified: khb/zh-CN/en), the context menu shows only Format text / Copy text / Paste text / Auto height / Duplicate / Delete — no "Language:" and no "Field:" submenu.

Note the language-name label on the context control (the langName div in CanvasElementContextControls.tsx:692) does render fine everywhere; it's specifically the language-switch submenu that's unreachable on these pages.

This may be intentional (perhaps bloom-customLayout is reserved for specific page types), but it means the language-submenu code path isn't exercised on typical pages. Flagging to confirm the class check is correct — customPage/bloom-combinedPage vs bloom-customLayout.

@hatton

hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8]

Data oddity: stray z language editable in a CJK book

While inspecting a trilingual CJK book (Language1 khb, Language2 zh-CN, Language3 en), a translation-group text field on the page contained four .bloom-editable children, not three:

lang visible
khb yes
z no (hidden)
en yes
zh-CN yes

The z tag looks like a truncated/invalid language code (possibly a mangled zh-CN), and its editable is hidden. This may just be pre-existing junk in this particular downloaded book rather than anything this PR introduced — but given it's a CJK/zh-CN book and the tag looks like a truncation of zh-CN, flagging it in case language-tag handling somewhere is producing/leaving a z editable. Worth a quick check that nothing in the keyboarding/language paths truncates zh-CNz.

(Surfaced during the BL-16524 runtime check of the canvas-element context control.)

hatton and others added 2 commits July 10, 2026 16:55
…ons (BL-16524)

The per-collection KeymanWeb keyboard cache is renamed Keyboards -> .keyboards and
is no longer synced through Team Collections. With keyboards now acquired on demand
(KeyboardResolver re-ensures a keyboard's files on every focus), a teammate does not
need the lead's cached files, so syncing the cache added weight and a subfolder
change-detection bug for no benefit.

- Rename the cache folder to .keyboards; the leading dot marks it local-only, per the
  convention that dot-prefixed folders are never synced.
- Remove the .keyboards entries from Team Collection sync (FilesToMonitorForCollection,
  the OnChanged filter, CopyLocalFolderToRepo, ExtractFolder, GetMaxModifyTime). This
  returns FolderTeamCollection.cs, TeamCollection.cs and RobustZip.cs (dropping the
  now-unused WriteAllFilesToZip helper) to their master state.
- Update FolderTeamCollectionTests to assert .keyboards is NOT synced to teammates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The keyboardsForLanguage settings endpoint did a synchronous Keyman cloud HTTP
search on the WinForms UI thread (handleOnUiThread:true), freezing the settings
dialog for up to the 5s HTTP timeout every time the Book Making tab opened,
worst case when offline. Register the endpoint handleOnUiThread:false so the
cloud search runs on a server worker thread, and marshal only the UI-thread-only
OS keyboard enumeration back to the UI thread via a new RunOnUiThread helper
(DialogBeingEdited.Invoke). Add KeymanCloudClientNetworkTests to pin the
offline-degrades-to-empty / reconnect-repopulates behavior the chooser depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant