diff --git a/.gitignore b/.gitignore
index 7b872195880..d66507ce9c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@
/windows/src/**/*.identcache
/windows/src/**/*.vcxproj.user
/windows/src/**/version.res
+/windows/src/**/version*.res
/windows/src/**/*.pch
/windows/src/**/*.wixobj
/windows/src/**/*.sbr
@@ -186,3 +187,16 @@ lcov.info
# flag file for build script
.configured
+
+# Local artifacts from the Delphi 12 CE dev workflow (see docs/build/windows-d12.md)
+/delphi-library-paths.backup*.reg
+/local-delphi-12-patches.patch
+/configure.log
+/core-build.log
+/cpp-engine-build.log
+/install-build-env.log
+/kmcmplib-build.log
+/web-build.log
+
+# Meson wrap dependency lock files
+**/subprojects/.wraplock
diff --git a/common/windows/delphi/components/FixedTrackbar.pas b/common/windows/delphi/components/FixedTrackbar.pas
index 50cb444a55f..8d3c525d842 100644
--- a/common/windows/delphi/components/FixedTrackbar.pas
+++ b/common/windows/delphi/components/FixedTrackbar.pas
@@ -70,10 +70,14 @@ procedure TTntFixedDrawGrid.WMEraseBkgnd(var Message: TMessage);
{$MESSAGE WARN 'Not yet checked against Delphi 10.4'}
{$IFNDEF VER330}
{$IFNDEF VER320}
+{$IFNDEF VER350}
+{$IFNDEF VER360}
{$MESSAGE ERROR 'Check that this fix is still applicable for a new version of Delphi. Checked against Delphi 10.2, 10.3' }
{$ENDIF}
{$ENDIF}
{$ENDIF}
+{$ENDIF}
+{$ENDIF}
procedure TTntFixedDrawGrid.MouseDown(Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
diff --git a/common/windows/delphi/general/CleartypeDrawCharacter.pas b/common/windows/delphi/general/CleartypeDrawCharacter.pas
index 6d1a51976de..44562012ed2 100644
--- a/common/windows/delphi/general/CleartypeDrawCharacter.pas
+++ b/common/windows/delphi/general/CleartypeDrawCharacter.pas
@@ -591,11 +591,11 @@ function TestFont(FFontName: string): Boolean;
StrPCopy(lf.lfFaceName, FFontName); //'Code2000');
hdc := GetDC(0);
//FPlane0FontName := 'Code2000';
-{$IFDEF VER340}
+{$IF Defined(VER340) or Defined(VER350) or Defined(VER360)}
if EnumFontFamiliesEx(hdc, lf, @EnumFallbackFonts, 0, 0) <> 0 then
{$ELSE}
if EnumFontFamiliesEx(hdc, lf, @EnumFallbackFonts, 0, 0) then
-{$ENDIF}
+{$IFEND}
begin
FPlane0FontName := FFontName;
Result := True;
diff --git a/common/windows/delphi/general/JsonUtil.pas b/common/windows/delphi/general/JsonUtil.pas
index ae2b9b0e9c6..16a90bc729d 100644
--- a/common/windows/delphi/general/JsonUtil.pas
+++ b/common/windows/delphi/general/JsonUtil.pas
@@ -52,7 +52,11 @@ function JSONToString(obj: TJSONAncestor; ReplaceSlashes: Boolean = False): stri
begin
builder := TStringBuilder.Create;
try
+{$IF Defined(VER350) or Defined(VER360)}
+ obj.ToChars(builder, []);
+{$ELSE}
obj.ToChars(builder);
+{$IFEND}
Result := builder.ToString;
finally
builder.Free;
diff --git a/common/windows/delphi/tools/devtools/SourceRootPath.pas b/common/windows/delphi/tools/devtools/SourceRootPath.pas
index 360477641e7..c8372c3c6e2 100644
--- a/common/windows/delphi/tools/devtools/SourceRootPath.pas
+++ b/common/windows/delphi/tools/devtools/SourceRootPath.pas
@@ -14,11 +14,19 @@ interface
{$IFDEF VER340}
const DelphiMajorVersion = '21.0';
{$ELSE}
+{$IFDEF VER350}
+const DelphiMajorVersion = '22.0';
+{$ELSE}
+{$IFDEF VER360}
+const DelphiMajorVersion = '23.0';
+{$ELSE}
ERROR: must define Delphi version
{$ENDIF}
{$ENDIF}
{$ENDIF}
{$ENDIF}
+{$ENDIF}
+{$ENDIF}
const DelphiBasePath = 'C:\Program Files (x86)\Embarcadero\Studio\' + DelphiMajorVersion + '\';
diff --git a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas
index d1daf087c39..8df4e732056 100644
--- a/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas
+++ b/common/windows/delphi/web/Keyman.System.HttpServer.Base.pas
@@ -46,7 +46,13 @@ function CrackUTF8ZeroExtendedString(CommandType: THTTPCommandType; const p: str
// Indy's UTF8 handling of URLs is *completely* broken.
// We may need to check this with updated versions of Delphi
{$IFNDEF VER330}
+{$IFNDEF VER340}
+{$IFNDEF VER350}
+{$IFNDEF VER360}
ERROR! Check if this is still needed with Delphi update
+{$ENDIF}
+{$ENDIF}
+{$ENDIF}
{$ENDIF}
SetLength(s, p.Length);
diff --git a/dev-docs/README.md b/dev-docs/README.md
new file mode 100644
index 00000000000..53f7b74c069
--- /dev/null
+++ b/dev-docs/README.md
@@ -0,0 +1,88 @@
+# Keyman Developer Onboarding
+
+Welcome. This folder is the entry point for newcomers to the Keyman
+ecosystem. There are two distinct audiences here:
+
+* **People building tools *around* Keyman from external repos** — AI
+ extensions for keyboard authoring, submission-triage bots for
+ [keymanapp/keyboards](https://github.com/keymanapp/keyboards),
+ validation tools, instrumented test rigs. You're consuming Keyman's
+ formats and CLI/library surface; you're not modifying Keyman itself.
+ Start at [keyboard-anatomy.md](keyboard-anatomy.md) and
+ [external-tooling.md](external-tooling.md).
+
+* **People contributing to Keyman itself** — runtime engines, the
+ developer tooling, KeymanWeb, the Delphi-removal effort. You'll
+ modify code in this repo. Start at [repository-map.md](repository-map.md)
+ and [migration-guide.md](migration-guide.md).
+
+The canonical user-facing documentation is at
+[help.keyman.com](https://help.keyman.com). The canonical build
+instructions are under [docs/build/](../docs/build/). This `dev-docs/`
+folder is *only* for the "where is everything, what shape is it, and
+why" questions.
+
+## The docs
+
+### For external tool builders
+
+> **Strategic frame, read this first.** The CLDR LDML keyboard format
+> is the future-canonical Keyman keyboard format. The spec is complete
+> for both desktop and touch keyboards. Keyman's *read* side for LDML
+> touch isn't finished yet — runtime still drives off the legacy
+> multi-file setup — but LDML is forward-compatible, so the right move
+> for a generation tool is to emit LDML XML as the canonical source and
+> only produce the transitional `.kmn`+`.kvks`+`.keyman-touch-layout`
+> bundle when the deliverable has to run on touch *today*. See
+> [external-tooling.md § The strategic frame](external-tooling.md#the-strategic-frame-for-tools-generating-keyboards)
+> and [migration-guide.md § LDML status](migration-guide.md#status--what-works-today-vs-whats-still-in-flight)
+> for the full picture.
+
+1. **[keyboard-anatomy.md](keyboard-anatomy.md)** — Reference for what
+ files make up a Keyman keyboard project (`.kpj`, `.kmn`, LDML XML,
+ `.kvks`, `.keyboard_info`, `.kps`, `.kmp`, ...), what each does, and
+ where the JSON schemas live for validation.
+
+2. **[external-tooling.md](external-tooling.md)** — How to drive the
+ Keyman compiler and engine programmatically: the `kmc` CLI, the
+ `@keymanapp/kmc-*` npm package surface, schema-based validation,
+ the cross-file consistency contract that any generator must enforce
+ today, triage criteria for keyboard submissions, and the approach
+ for instrumented forks of the compiler or engine.
+
+### For Keyman contributors
+
+3. **[repository-map.md](repository-map.md)** — A guided tour of the
+ top-level directory structure, what each major component does, and
+ how they connect. Includes Mermaid diagrams of the component graph,
+ build dependencies, and the cross-platform matrix. External tool
+ builders should read this too if they want to understand what their
+ tool is hooking *into*.
+
+4. **[migration-guide.md](migration-guide.md)** — Keyman has been
+ actively modernizing for years, replacing Delphi components with
+ C++ (Keyman Core), TypeScript (the kmc compiler family and
+ KeymanWeb), and embracing LDML as the canonical keyboard format.
+ This guide explains what has already moved, what's still in flight,
+ and where the long-running migration branches live.
+
+## Then look at the build docs
+
+Once you've internalized the layout, the standard build instructions live at:
+
+* [docs/build/windows.md](../docs/build/windows.md) — the canonical Windows
+ flow (Delphi 10.3 / Pro)
+* [docs/build/windows-d12.md](../docs/build/windows-d12.md) — fallback for
+ contributors stuck on Delphi 12 Community Edition (the only free Delphi
+ tier today); covers IDE-based workflow plus helper scripts
+* `linux/`, `mac/`, `android/`, `ios/` — platform-specific READMEs in each
+ top-level directory
+
+## Help / Getting Stuck
+
+* [SIL/Keyman Community Forum](https://community.software.sil.org/c/keyman)
+* [Keyman roadmap blog](https://blog.keyman.com/category/roadmap/) — the
+ long-form context for the migrations described in `migration-guide.md`
+* [Issue tracker](https://github.com/keymanapp/keyman/issues) — search for
+ the component or migration branch you're touching before filing new issues
+* [CONTRIBUTING.md](../CONTRIBUTING.md) (at repo root) for PR conventions
diff --git a/dev-docs/external-tooling.md b/dev-docs/external-tooling.md
new file mode 100644
index 00000000000..477a4c93b9f
--- /dev/null
+++ b/dev-docs/external-tooling.md
@@ -0,0 +1,543 @@
+# External Tooling Guide
+
+For people building tools *around* Keyman in other repositories: AI
+extensions that assist keyboard development, automated submission
+triage for the [keymanapp/keyboards](https://github.com/keymanapp/keyboards)
+repo, lint bots, exhaustive test rigs, instrumented forks of the
+compiler or engine for documentation work.
+
+This guide is for *you*. It assumes you know what a Keyman keyboard *is*
+(see [keyboard-anatomy.md](keyboard-anatomy.md) first if not) and
+explains how to drive the Keyman pipeline from external code.
+
+## The strategic frame for tools generating keyboards
+
+Two facts shape every tooling decision here:
+
+1. **The CLDR LDML keyboard spec is complete, including touch.** Layers
+ (hardware + touch), forms, flicks, longpress, multi-tap, transforms —
+ all defined. A spec-correct LDML keyboard with full touch markup is
+ a perfectly valid document today.
+2. **Keyman's *read side* for LDML touch isn't complete yet.** Compiler
+ has TODOs, the Core C API doesn't expose layer/form data to hosts,
+ and the KeymanWeb / mobile OSKs still drive off the legacy
+ `.keyman-touch-layout` JSON, not kmx+. The runtime gap is detailed
+ in [migration-guide.md § LDML](migration-guide.md#what-s-not-working-yet-the-touch-gap-all-layers)
+ and tracked under
+ [#7238](https://github.com/keymanapp/keyman/issues/7238) and the
+ [`epic-ldml` label](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-ldml).
+ Realistic timeline: v21-ish (months out, funding-dependent).
+
+The implication for an AI extension that generates keyboards: **emit
+LDML XML as the canonical source of truth**, even though today Keyman
+can't yet run all of it natively on mobile/web. The LDML is
+forward-compatible — when Keyman closes the read side, the same
+generated keyboards become natively runnable with no source rewrite.
+
+### Recommended generation strategy
+
+```mermaid
+flowchart LR
+ src[AI extension intent
name, language, target, layout, rules]
+ ldml[LDML XML
canonical source]
+ desk{Target
= desktop only?}
+ done[Ship LDML alone
compile with kmc-ldml]
+ bundle[Also emit parallel
.kmn + .kvks +
.keyman-touch-layout]
+ ship[Ship LDML + bundle
kmc build runs
the .kpj path today]
+
+ src --> ldml
+ ldml --> desk
+ desk -- yes --> done
+ desk -- no, touch needed --> bundle
+ bundle --> ship
+
+ style src fill:#28a,color:#fff
+ style ldml fill:#46c,color:#fff
+ style done fill:#284,color:#fff
+ style ship fill:#a52,color:#fff
+```
+
+Concretely:
+
+* **For desktop-only keyboards:** generate LDML XML, compile with
+ `kmc build`. Done. No consistency contract to manage.
+* **For keyboards that need to run on touch today** (mobile, web):
+ generate the LDML XML *and* a derived `.kmn` + `.kvks` +
+ `.keyman-touch-layout` bundle. Treat the LDML as the source of
+ truth; treat the bundle as a transitional deliverable that exists
+ only because the runtime can't read LDML touch yet. When Keyman
+ catches up, you delete the bundle and regenerate.
+
+The cross-file consistency contract documented below is for the
+**transitional bundle** — keeping its files in sync with the LDML and
+with each other. It exists because today's Keyman runtime requires it,
+not because the LDML format does.
+
+### Two practical tactics
+
+1. **Start from a kmc-generated scaffold and mutate consistently.** Run
+ `kmc generate keyboard ...` to produce a known-good starting set,
+ then modify with strict cross-file invariants (see § Cross-file
+ consistency contract below). kmc owns the source of truth for "what
+ a fresh project looks like" and your tool inherits future kmc
+ improvements automatically.
+2. **Validate the LDML XML against the CLDR spec independently of
+ Keyman** — that way the canonical source is correct even if your
+ transitional bundle has gaps. Use the CLDR keyboard schema directly
+ (see [§ Validating metadata with JSON Schemas](#validating-metadata-with-json-schemas)).
+
+## The two integration surfaces
+
+```mermaid
+graph LR
+ subgraph EXT[Your external tool]
+ yt[AI extension / triage bot]
+ end
+ subgraph CLI[CLI surface]
+ kmc[kmc
command-line]
+ end
+ subgraph LIB[Library surface]
+ npm[@keymanapp/kmc-*
npm packages]
+ end
+ subgraph CORE[Native surface]
+ core_so[Keyman Core
libkmcore.so / .dll / .dylib]
+ wasm[Keyman Core
WebAssembly]
+ end
+
+ yt --shell out--> kmc
+ yt --import--> npm
+ yt --FFI / Emscripten--> core_so
+ yt --import wasm--> wasm
+
+ style EXT fill:#28a,color:#fff
+ style CLI fill:#284,color:#fff
+ style LIB fill:#46c,color:#fff
+ style CORE fill:#a52,color:#fff
+```
+
+* **CLI surface** — shell out to `kmc` (or `kmcomp` for legacy `.kmn`-only
+ work). Easiest, least coupling, but you pay process-startup cost per
+ invocation and you parse text output.
+* **Library surface** — import the `@keymanapp/kmc-*` packages from npm
+ in a Node.js tool. Get TypeScript types, programmatic results, no shell
+ parsing. Recommended for any non-trivial tool.
+* **Native surface** — for tools that need to *execute* keyboards (not
+ just compile them), link Keyman Core as a shared library (desktop /
+ server) or import the WebAssembly build (Node.js or browser).
+
+## The `kmc` CLI
+
+`kmc` is the unified TypeScript entry point. Subcommands available:
+
+```
+kmc analyze Analyze a keyboard / project
+kmc build Compile a keyboard, project, model, or package
+kmc copy Duplicate a project with a new id
+kmc generate Scaffold a new project (keyboard, model, etc.)
+kmc message Print info about a kmc compiler message (warning/error)
+```
+
+Install (Node 20+):
+
+```bash
+npm install -g @keymanapp/kmc
+```
+
+Or use directly from a checkout:
+
+```bash
+node /path/to/keyman/developer/src/kmc/build/kmc.js
+```
+
+Get help:
+
+```bash
+kmc --help
+kmc --help
+```
+
+### Compiling a keyboard programmatically
+
+```bash
+# Compile a single .kmn or LDML XML file
+kmc build path/to/keyboard.kmn
+
+# Compile an entire .kpj project (produces .kmx + .js + .kmp)
+kmc build path/to/keyboard.kpj
+
+# JSON output for machine consumption
+kmc build --log-format json path/to/keyboard.kpj
+```
+
+Exit code is 0 on success, non-zero on errors. Use
+`--log-format json` to get a structured stream of messages your tool
+can parse.
+
+### Analyzing a keyboard
+
+```bash
+# Lint / static analysis
+kmc analyze path/to/keyboard.kpj
+
+# Get the rendered Osk image and other artifacts
+kmc analyze osk-char-use path/to/keyboard.kmn
+```
+
+### Scaffolding a new keyboard
+
+```bash
+kmc generate keyboard \
+ --author "Your Name" \
+ --copyright "(c) Your Org" \
+ --bcp47 "und-Latn" \
+ --name "my_keyboard" \
+ --license "MIT" \
+ --target "any" \
+ --description "Demo keyboard"
+```
+
+Result is a directory with `.kpj`, `.kmn`, `.keyboard_info`, `.kps`,
+`.kvks` (basic touch layout), `welcome.htm`, `README.md`,
+`HISTORY.md`, `build.sh`. It compiles out of the box.
+
+This is your starting point for any "reliably produce a Keyman developer
+folder that compiles" tool.
+
+## The `@keymanapp/kmc-*` library surface
+
+Each `kmc-*` directory under `developer/src/` is published as an npm
+package. Use them as a library when you're already in a Node.js tool
+and want structured results instead of shelling out.
+
+Notable packages and their roles:
+
+| Package | What it does |
+|---|---|
+| `@keymanapp/kmc` | The unified CLI (drives the others) |
+| `@keymanapp/kmc-kmn` | Compile `.kmn` → `.kmx` (delegates to native kmcmplib) |
+| `@keymanapp/kmc-ldml` | Compile LDML XML → `.kmx+` (TypeScript-native) |
+| `@keymanapp/kmc-package` | Build `.kmp` package from `.kps` |
+| `@keymanapp/kmc-keyboard-info` | Validate / compile `.keyboard_info` |
+| `@keymanapp/kmc-analyze` | Static analysis / lint |
+| `@keymanapp/kmc-copy` | Duplicate a project under a new id |
+| `@keymanapp/kmc-generate` | Scaffold new projects |
+| `@keymanapp/kmc-model` | Compile lexical models |
+| `@keymanapp/kmc-model-info` | Validate `.model_info` metadata |
+| `@keymanapp/common-types` | TypeScript types for `.kpj`, `.keyboard_info`, etc., plus generated JSON-schema validators |
+
+Example: compiling a keyboard from a Node script.
+
+```ts
+import { KmnCompiler } from '@keymanapp/kmc-kmn';
+import { CompilerCallbacks, NodeCompilerCallbacks } from '@keymanapp/common-types';
+
+const callbacks: CompilerCallbacks = new NodeCompilerCallbacks();
+const compiler = new KmnCompiler();
+await compiler.init(callbacks);
+
+const result = await compiler.runCompiler('path/to/keyboard.kmn', /*infile_str=*/null, {
+ shouldAddCompilerVersion: true,
+});
+
+if (!result) {
+ // compile failure: read messages from callbacks
+ console.error(callbacks.messages);
+}
+// result.kmx is the compiled binary
+```
+
+The exact API may evolve; check each package's `src/` and exported `index.ts`
+for the current shape.
+
+## Cross-file consistency contract
+
+For tools generating or mutating a `.kmn`-based keyboard project (the
+non-LDML path), these values must stay aligned across the project files.
+Most validation failures and weird runtime behaviors come from breaking
+one of these.
+
+### Identity
+
+The **keyboard ID** is the most-replicated value. Treat it as the
+canonical key everywhere:
+
+| Where | How it appears |
+|---|---|
+| Directory name | `/` (convention) |
+| File names | `.kpj`, `.kmn`, `.kps`, `.kvks`, `.keyman-touch-layout`, `.ico`, `.keyboard_info` |
+| `.kpj` | The `` references files by relative path; the ID is implicit in the filenames |
+| `.keyboard_info` | The filename stem IS the ID; not repeated inside the JSON |
+| `.kps` | Lists output artifacts (`.kmx`, `.js`, `.kvk`) with `.` names |
+| Compiled `.kmx`/`.kmp` | Identity baked into the binaries; tools downstream key off it |
+
+Rules: lowercase, ASCII letters / digits / underscores. Must be unique
+across the [keymanapp/keyboards](https://github.com/keymanapp/keyboards)
+catalog if you plan to submit.
+
+### Display name
+
+The human-readable keyboard name appears in two places:
+
+| Where | Form |
+|---|---|
+| `.kmn` | `store(&NAME) 'Display Name'` |
+| `.kps` | `Display Name` |
+
+(`.keyboard_info` does *not* carry the name; consumers display the
+package metadata.)
+
+### Versions — three different things, don't confuse them
+
+This is the easiest place to mess up:
+
+| Concept | `.kmn` | `.keyboard_info` | `HISTORY.md` |
+|---|---|---|---|
+| Keyboard version (what you ship) | `store(&KEYBOARDVERSION) '1.2.3'` | (no field) | top-most heading: `1.2.3 (YYYY-MM-DD)` |
+| Min Keyman engine version | `store(&VERSION) '14.0'` | (no field) | n/a |
+| Schema/file format version | n/a | n/a (implicit in schema) | n/a |
+
+A bump to the keyboard version touches `.kmn` AND `HISTORY.md`. The
+engine min-version (`&VERSION`) only changes when you start using
+features that require a newer Keyman.
+
+### Copyright / Author / License
+
+| Where | Form |
+|---|---|
+| `.kmn` | `store(©RIGHT) '© Your Name'` |
+| `.kps` | `…Your Name` |
+| `.keyboard_info` | `"license": "mit"` (SPDX id) |
+| `LICENSE.md` | full license text |
+| `README.md` / `welcome.htm` | display only |
+
+The `.keyboard_info` license is an SPDX identifier (e.g. `"mit"`,
+`"apache-2.0"`) and is what triage uses. The `LICENSE.md` must be the
+matching text.
+
+### Language tags (BCP-47)
+
+| Where | Form |
+|---|---|
+| `.keyboard_info` | `"languages": ["en", "und-Latn", "am"]` (array of BCP-47) |
+| `.kmn` (optional) | `store(&BCP47CODE) 'en'` for the primary language |
+
+The `.keyboard_info` array is the authoritative list. Each entry must
+be a valid BCP-47 tag — use `@keymanapp/langtags` or `kmc analyze` to
+validate.
+
+### Resource references inside `.kmn`
+
+Each "embedded" resource needs the file to exist alongside the `.kmn`:
+
+| Store | Points at | Required if |
+|---|---|---|
+| `store(&BITMAP)` | `.ico` (or `.bmp`) | Always (or omit if no icon) |
+| `store(&VISUALKEYBOARD)` | `.kvks` | Desktop OSK / mobile touch support |
+| `store(&LAYOUTFILE)` | `.keyman-touch-layout` | Mobile touch (newer touch layout format) |
+
+If a store points at a missing file, `kmc build` fails. If a `.kvks`
+exists but isn't referenced, the OSK is silently omitted from the
+package — the failure mode that bites AI generators hardest.
+
+### Targets / platforms
+
+| Where | Form | Notes |
+|---|---|---|
+| `.kmn` | `store(&TARGETS) 'any'` or `'desktop mobile'` etc. | Space-separated list |
+| `.keyboard_info` (optional) | `"platformSupport": { "windows": "full", ... }` | More granular per-platform support |
+
+If `.kmn` says `'mobile'` but there's no `.kvks` / `.keyman-touch-layout`,
+`kmc build` warns or errors depending on flags.
+
+### Quick check: does the generated project compile + validate?
+
+A scaffolded keyboard from a healthy generator passes all of:
+
+```bash
+kmc build path/to/.kpj # exit 0 = compile clean
+kmc analyze path/to/.kpj # exit 0 = lint clean
+# JSON Schema validate .keyboard_info
+ajv validate -s common/schemas/keyboard_info/keyboard_info.schema.json \
+ -d path/to/.keyboard_info
+```
+
+Treat all three as required checks in your AI extension's
+generate-keyboard pipeline.
+
+## Validating metadata with JSON Schemas
+
+For triage tools that just want to *validate* keyboard submissions
+without compiling, the schemas live at:
+
+```
+common/schemas/keyboard_info/keyboard_info.schema.json # .keyboard_info
+common/schemas/keyboard-info-source/ # source-side
+common/schemas/displaymap/displaymap.schema.json # display maps
+common/schemas/kvk/kvk.schema.json # visual keyboards
+common/schemas/package/package.schema.json # package metadata
+common/schemas/kpj/kpj.schema.json # project files
+```
+
+Pre-generated validators (ajv-style) and TypeScript types are at:
+
+```
+common/web/types/obj/schemas/*.schema.validator.{cjs,mjs} # runtime validators
+common/web/types/src/schemas/*.schema.ts # TS types
+```
+
+If your tool is Node-based, install `@keymanapp/common-types` and
+import:
+
+```ts
+import { KeymanFileTypes, validateKeyboardInfoFile } from '@keymanapp/common-types';
+const { valid, errors } = await validateKeyboardInfoFile('path/to/x.keyboard_info');
+```
+
+If your tool isn't Node-based, point any JSON Schema validator (ajv-cli,
+jsonschema for Python, etc.) at the `.schema.json` files directly.
+
+## Triage criteria for keymanapp/keyboards submissions
+
+The [keymanapp/keyboards](https://github.com/keymanapp/keyboards) repo
+expects each submitted keyboard to clear these gates. A triage bot
+should check them in order:
+
+1. **Compiles.** `kmc build path/to/keyboard.kpj` returns exit 0.
+2. **Metadata is schema-valid.** `.keyboard_info` validates against the
+ schema; `.kps` validates against the package schema.
+3. **Identifiers match.** The `id` in `.keyboard_info`, the filename of
+ the `.kmn`/`.xml`, and the `id` attribute in `.kpj` are consistent.
+4. **License is recognized.** `license` in `.keyboard_info` is one of
+ the accepted SPDX identifiers (MIT, Apache-2.0, etc.).
+5. **Target language tags are valid BCP-47.** Each `bcp47` in
+ `.keyboard_info` is a valid tag (use `kmc analyze` or
+ `@keymanapp/langtags`).
+6. **Tests pass** (when present). If `kmc analyze` reports test
+ failures, the keyboard isn't ready.
+7. **No bundled binaries the source doesn't justify.** The package
+ shouldn't ship a `.kmx` if there's no matching `.kmn`/`.xml` source.
+8. **Documentation present.** A non-empty `welcome.htm` (or
+ `welcome.md`) and a `HISTORY.md`.
+9. **Version monotonic.** If updating an existing keyboard, version in
+ `.keyboard_info` must increase.
+
+Most of these are programmatically checkable via the `@keymanapp/kmc-*`
+packages plus the JSON Schemas. Layered approach for a triage bot:
+
+```mermaid
+flowchart TD
+ pr[New PR / submission]
+ s1{Schemas valid?}
+ s2{kmc build OK?}
+ s3{kmc analyze OK?}
+ s4{Custom checks
license, ids, version?}
+ pass[Auto-approve / queue for human]
+ fail[Bot comment with specifics]
+
+ pr --> s1
+ s1 -- yes --> s2
+ s1 -- no --> fail
+ s2 -- yes --> s3
+ s2 -- no --> fail
+ s3 -- yes --> s4
+ s3 -- no --> fail
+ s4 -- yes --> pass
+ s4 -- no --> fail
+```
+
+## Forking the compiler or engine for instrumentation
+
+If your goal is to exhaustively test keyboards or compare behavior
+(e.g. desktop vs touch), the practical approach is to fork a `kmc-*`
+package or Keyman Core itself, add instrumentation, and run your
+keyboard corpus through it.
+
+### Instrumenting `kmc` for compile-time analysis
+
+* Fork `developer/src/kmc-kmn/` (for `.kmn` keyboards) or
+ `developer/src/kmc-ldml/` (for LDML).
+* Add hooks in the compiler's `CompilerCallbacks` interface — log every
+ rule, every grouping, every transform.
+* Build with `developer/build.sh build:kmc-kmn` (or `:kmc-ldml`); run
+ via your fork's CLI.
+* Keep the patched files small and isolated so you can rebase the fork
+ onto upstream periodically.
+
+### Instrumenting Keyman Core for runtime comparison
+
+For runtime behavior comparisons (e.g. does this keyboard produce the
+same output on desktop and touch?):
+
+* Fork `core/src/` and add logging or trace hooks where the
+ `km_core_state_process_event()` function dispatches events.
+* Build with `core/build.sh build:x64` (or `:wasm` for browser-side).
+* For desktop testing: link your instrumented `libkmcore` into a small
+ C++ harness that feeds simulated key events.
+* For touch testing: build to WASM, load in a Node.js harness with
+ `@keymanapp/keyman/engine/web/test` helpers, simulate touch input.
+
+The `core/tests/` directory has examples of how Keyman Core is exercised
+from C++ test harnesses — these are good starting points for your own
+harness.
+
+### Comparing desktop vs touch behavior
+
+The behavior difference between desktop and touch usually comes from:
+
+* The `.kvks` (OSK layout) — different keys / modifiers exposed
+* `&platform()` rules in `.kmn` (or LDML transforms scoped to platforms)
+* The host's event handling — what the OS / browser passes to the engine
+
+For exhaustive comparison, instrument both contexts using the same input
+corpus:
+
+```mermaid
+graph TD
+ corpus[Keyboard test corpus]
+ desk[Desktop harness
libkmcore + simulated kbd events]
+ touch[Touch harness
WASM kmcore + simulated tap events]
+ diff[Diff tool
compare action streams]
+
+ corpus --> desk
+ corpus --> touch
+ desk --> diff
+ touch --> diff
+ diff --> report[Comparison report
per-keyboard, per-input]
+```
+
+The `web/src/test/` directory has fixtures and helpers for the touch
+side; `core/tests/` has fixtures for the desktop side. Reusing the same
+keyboard files in both harnesses is the easy part — generating an
+equivalent simulated-input stream is the harder design problem.
+
+## Don't reinvent these
+
+Things the `kmc` / Keyman Core ecosystem already does — don't roll your
+own:
+
+* **`.kmn` parsing**: use `kmc-kmn`'s parser (or the C++ parser in
+ `kmcmplib` if you need native code).
+* **LDML XML parsing**: use `kmc-ldml`'s parser (it handles the
+ CLDR-specific quirks).
+* **`.keyboard_info` validation**: use the schemas + validators in
+ `common/schemas/` and `common/web/types/`.
+* **Language-tag handling**: use `@keymanapp/common-types` /
+ `@keymanapp/langtags`; the BCP-47 + IANA subtag stuff is non-trivial
+ and the team has invested in getting it right.
+
+## Pointers
+
+* [keyboard-anatomy.md](keyboard-anatomy.md) — file format reference
+* [repository-map.md](repository-map.md) — where things live inside Keyman
+* [migration-guide.md](migration-guide.md) — what's old, what's new,
+ what to bias toward
+* [help.keyman.com/developer/](https://help.keyman.com/developer/) —
+ authoritative user-facing developer docs (the Keyman language
+ reference, the touch-layout designer guide, etc.)
+* [common/test/keyboards/](../common/test/keyboards/) — small,
+ feature-focused example keyboards
+* [keymanapp/keyboards](https://github.com/keymanapp/keyboards) — the
+ production keyboard catalog; the data your triage tools will work with
+* [keymanapp/lexical-models](https://github.com/keymanapp/lexical-models)
+ — predictive text models (separate from keyboards but use the same
+ `kmc-*` ecosystem)
diff --git a/dev-docs/keyboard-anatomy.md b/dev-docs/keyboard-anatomy.md
new file mode 100644
index 00000000000..eaf8f92a421
--- /dev/null
+++ b/dev-docs/keyboard-anatomy.md
@@ -0,0 +1,244 @@
+# Keyboard Project Anatomy
+
+A reference for tools that read, write, validate, or generate Keyman
+keyboard projects. If you're building something that produces Keyman
+keyboards (a scaffolder, an AI-assisted authoring extension, a triage
+bot), start here.
+
+For the Keyman *runtime* — how a compiled keyboard actually runs on the
+user's machine — see [repository-map.md](repository-map.md). For *why*
+some of these formats exist (and which are being phased out), see
+[migration-guide.md](migration-guide.md).
+
+## File extensions at a glance
+
+| Extension | Role | Hand-edit? | Notes |
+|---|---|---|---|
+| `.kpj` | Project file (XML) | Mostly — Tike maintains it | Lists the source files; references metadata |
+| `.kmn` | Keyman keyboard source | Yes | The historical proprietary format |
+| `.xml` (LDML) | LDML keyboard source | Yes | The modern open-standard format ([CLDR keyboards](https://cldr.unicode.org/index/keyboard-workgroup)) |
+| `.kvks` | Visual / on-screen keyboard layout (XML) | Yes (or via Tike) | Touch + desktop OSK definitions |
+| `.kvk` | Compiled visual keyboard binary | No | Output of `.kvks` |
+| `.keyboard_info` | Keyboard metadata (JSON) | Yes | Schema-validated; published to keyman.com |
+| `.kps` | Package source (XML) | Mostly — Tike maintains | Lists what goes into the package |
+| `.kmp` | Package binary (ZIP) | No | The single deliverable: contains kmx, js, kvk, fonts, docs |
+| `.kmx` | Compiled keyboard binary | No | Output of `.kmn` compilation; runs in Keyman Core |
+| `.kmx+` | LDML-extended kmx (same `.kmx` filename) | No | Output of LDML compilation; runs in Keyman Core |
+| `.js` | KeymanWeb keyboard | No | Output of compilation; runs in browser |
+| `.html` | Welcome / help page | Yes (optional) | Shipped inside the `.kmp` |
+| `HISTORY.md` | Version history | Yes | Convention; rendered on keyman.com |
+| `README.md` | Documentation | Yes | Optional |
+| `fonts/*.ttf` `*.otf` | Embedded keyboard fonts | n/a | Shipped inside the `.kmp` |
+
+## Source → compiled outputs
+
+```mermaid
+flowchart LR
+ subgraph SRC[Source files]
+ kpj[.kpj
project]
+ kmn[.kmn
source]
+ ldml[LDML XML
source]
+ kvks[.kvks
OSK layout]
+ kps[.kps
package source]
+ info[.keyboard_info
metadata]
+ end
+ subgraph CC[kmc compiler]
+ kmc[kmc CLI
TypeScript]
+ kmnc[kmcmplib
C++]
+ end
+ subgraph OUT[Compiled outputs]
+ kmx[.kmx /
.kmx+]
+ js[.js]
+ kvk[.kvk]
+ kmp[.kmp
package]
+ end
+
+ kpj --> kmc
+ kmn --> kmnc
+ kmnc --> kmx
+ kmn --> kmc
+ kmc --> js
+ ldml --> kmc
+ kvks --> kmc
+ kvks --> kvk
+ kps --> kmc
+ info --> kmc
+
+ kmx --> kmp
+ js --> kmp
+ kvk --> kmp
+ info --> kmp
+
+ style SRC fill:#46c,color:#fff
+ style CC fill:#284,color:#fff
+ style OUT fill:#a52,color:#fff
+```
+
+## File-by-file deep dive
+
+### `.kpj` — Keyman project file
+
+XML, root element ``. Lists every file in the
+project: source `.kmn` / LDML XML files, the `.kvks` OSK layout, the
+package source `.kps`, the metadata `.keyboard_info`, the documentation
+pages, the fonts.
+
+Hand-editing is sometimes necessary (e.g. adding a file Tike didn't pick
+up); for generated projects, follow the structure of one of the
+canonical examples (see [Examples in the repo](#examples-in-the-repo)).
+
+### `.kmn` — Keyman source
+
+The historical Keyman keyboard source format. Defines rules in a
+domain-specific syntax:
+
+```
+store(&NAME) 'My Keyboard'
+store(&VERSION) '1.0'
+begin Unicode > use(main)
+group(main) using keys
+'a' > 'A'
+```
+
+Reference: . New keyboards
+should be authored in LDML XML instead where possible — see § below and
+[migration-guide.md § LDML](migration-guide.md#migration-2-ldml-keyboards).
+`.kmn` is the today-required deliverable for touch keyboards because
+Keyman's LDML read side isn't yet complete for touch (the CLDR spec
+itself is — the gap is purely on Keyman's side).
+
+### LDML XML — Keyman keyboard authored in CLDR LDML
+
+**The future-canonical keyboard source format.** LDML is the Unicode CLDR
+open standard for keyboards. The spec covers everything Keyman needs
+including full touch markup (layers/forms/flicks/longpress/multi-tap),
+so a spec-correct LDML keyboard is a complete authoring artifact —
+hand-editable XML, no parallel files needed.
+
+File extension is `.xml`. Compiles to `.kmx+` (a `.kmx` with extra
+LDML-specific sections inside). References:
+* [Unicode CLDR keyboard workgroup](https://cldr.unicode.org/index/keyboard-workgroup)
+* Keyman compiler: `developer/src/kmc-ldml/`
+* Keyman runtime: `core/src/ldml/`
+
+**What works today**: desktop keyboards compile and run end-to-end from
+LDML.
+
+**What doesn't work today**: touch keyboards authored in LDML compile,
+but Keyman doesn't yet read the LDML touch markup through to the OSK on
+mobile/web — the runtime still drives off the legacy `.keyman-touch-layout`
+JSON. See [migration-guide.md § LDML touch gap](migration-guide.md#what-s-not-working-yet-the-touch-gap-all-layers)
+for details.
+
+**Practical guidance**:
+* For desktop-only keyboards, prefer LDML XML. No consistency contract.
+* For touch keyboards, generate LDML *and* a parallel `.kmn` + `.kvks` +
+ `.keyman-touch-layout` bundle. Treat LDML as the source of truth;
+ treat the bundle as transitional until Keyman closes the read gap.
+ See [external-tooling.md § Recommended generation strategy](external-tooling.md#recommended-generation-strategy).
+
+### `.kvks` — Visual keyboard source
+
+XML, defines the layout of the on-screen keyboard for *both* desktop OSK
+and mobile touch keyboards. Each key is positioned, sized, and bound to
+a `.kmn`/LDML rule via key codes or named keys. Reference:
+.
+
+Compiles to the binary `.kvk` shipped inside the `.kmp`.
+
+### `.keyboard_info` — Keyboard metadata
+
+JSON document describing the keyboard for the keyboards.keyman.com
+catalog and the Keyman app stores. Required fields include name, id,
+license, version, supported platforms, supported languages.
+
+**Schema** (validate against this):
+[`common/schemas/keyboard_info/keyboard_info.schema.json`](../common/schemas/keyboard_info/keyboard_info.schema.json).
+
+Auto-validators (CommonJS + ESM) are generated and live under
+`common/web/types/obj/schemas/keyboard_info.schema.validator.{cjs,mjs}`.
+TypeScript types live under
+`common/web/types/src/schemas/keyboard_info.schema.ts`.
+
+### `.kps` — Package source
+
+XML describing what goes into the final `.kmp` package: keyboards,
+visual keyboards, documentation files, fonts, install behavior.
+Reference: .
+
+### `.kmp` — Package (deliverable)
+
+ZIP file containing the compiled `.kmx`/`.kmx+` (one per keyboard),
+`.js` (for KeymanWeb), `.kvk` (compiled OSK), `kmp.json`, fonts, and
+optional `welcome.htm` / `readme.htm`. This is the single artifact a user
+or store installs.
+
+Inspection: any zip tool works. The package's identity, version, and
+contents are described in `kmp.json` at the root of the archive.
+
+### `.kmx`, `.kmx+`, `.js`
+
+Binary compiled outputs. Don't hand-edit. Documented for reference:
+* `.kmx` format: [docs/file-formats/kmx-file-format.md](../docs/file-formats/kmx-file-format.md)
+* `.kmx+` (LDML extensions): [docs/file-formats/kmx-plus-file-format.md](../docs/file-formats/kmx-plus-file-format.md)
+* `.js` (KeymanWeb): not documented in repo; generated by `kmc-kmn` /
+ `kmc-ldml` to be loaded by KeymanWeb at runtime.
+
+## Schemas
+
+Programmatically validating keyboard metadata / project structure uses
+the JSON Schemas under `common/schemas/`:
+
+| Schema | Validates | Path |
+|---|---|---|
+| `keyboard_info.schema.json` | `.keyboard_info` files | `common/schemas/keyboard_info/keyboard_info.schema.json` |
+| `package.schema.json` | `.kps` package metadata | `common/schemas/package/` |
+| `kpj.schema.json` (where applicable) | `.kpj` project files | `common/schemas/kpj/` |
+| `displaymap.schema.json`, `kvk.schema.json`, etc. | Various subformats | `common/schemas/` |
+
+The TypeScript-generated validators (used by `kmc` internally) live
+under `common/web/types/`. External tools can either:
+* call the JSON Schemas directly with their own validator (ajv, etc.), or
+* import the `@keymanapp/common-types` npm package for the
+ pre-generated validators and TS types.
+
+## Examples in the repo
+
+The most useful starting points for tooling work:
+
+| Location | What it is |
+|---|---|
+| `common/test/keyboards/baseline/` | A directory full of small `.kmn` keyboards, each illustrating one Keyman language feature (deadkeys, virtual keys, options, etc.) |
+| `common/test/keyboards/text_selection_tests_keyboard_9073/` | A complete project with `.kpj`, `.kmn`, `.keyboard_info`, `build.sh` |
+| `common/test/keyboards/platform-rules/` | Demonstrates platform-specific rule behavior (desktop vs touch) |
+| `web/src/test/manual/web/keyboards/diacritic_rota/` | A KeymanWeb test keyboard with full `.keyboard_info` |
+
+To see a *complete*, real-world example, point at the
+[keymanapp/keyboards](https://github.com/keymanapp/keyboards) repo —
+that's where production keyboards live and where AI submission-triage
+tools will spend most of their time. Layout: one keyboard per
+subdirectory under `release///`.
+
+## Minimum viable keyboard project
+
+For a generation tool, this is the smallest set of files that compiles
+to a valid `.kmp`:
+
+```
+my_keyboard/
+├── my_keyboard.kpj XML
+├── my_keyboard.kmn store(&NAME), begin Unicode > use(main),
+│ group(main) using keys, at least one rule
+├── my_keyboard.keyboard_info JSON validated against keyboard_info.schema.json
+└── my_keyboard.kps XML referencing the above
+```
+
+Plus optional but normal:
+* `my_keyboard.kvks` — touch / OSK layout
+* `welcome.htm` — first-run welcome page
+* `HISTORY.md` — version history
+* `fonts/*.ttf` — embedded fonts
+
+The `kmc generate` subcommand (see
+[external-tooling.md](external-tooling.md#scaffolding-a-new-keyboard))
+will produce a working scaffold of this set.
diff --git a/dev-docs/migration-guide.md b/dev-docs/migration-guide.md
new file mode 100644
index 00000000000..f6345f509ed
--- /dev/null
+++ b/dev-docs/migration-guide.md
@@ -0,0 +1,396 @@
+# Migration Guide: The Modernization in Flight
+
+Keyman is in the middle of several long-running migrations. As a contributor,
+knowing where each migration is on the timeline is the difference between
+modifying code that ships for another decade and modifying code that is
+weeks away from deletion. This guide is the map.
+
+Three things to understand before you write any non-trivial patch:
+
+1. **The Delphi removal** — replacing Delphi components with C++ and TypeScript.
+2. **LDML keyboards** — replacing the proprietary `.kmn`/`.kmx` keyboard
+ format with the open Unicode CLDR LDML keyboard standard.
+3. **KeymanWeb modernization** — folding the historical KeymanWeb codebase
+ onto the shared Keyman Core (C++ compiled to WebAssembly) and a modern
+ TypeScript host.
+
+These are tracked across several long-lived branches and a multi-year
+roadmap. The [Keyman roadmap blog](https://blog.keyman.com/category/roadmap/)
+is the authoritative high-level source; this document summarizes the state
+as understood from the codebase and links into specific issues and source
+trees you can read for yourself.
+
+## Migration 1: Delphi removal
+
+### Why
+
+The original Keyman Developer IDE and the Windows Engine were written in
+Delphi starting in the 1990s. Delphi was a practical choice then — strong
+Win32 integration, a mature VCL UI toolkit. Today the licensing situation
+is hostile to open-source contribution: the only freely-available tier
+(Community Edition) was crippled in 10.4+ to remove command-line
+compilation, leaving contributors stuck on the no-longer-distributed 10.3
+CE or on a paid Professional license.
+
+The Keyman team has been [tracking removal](https://github.com/keymanapp/keyman/issues/4599)
+since 2022. Progress has been steady but slow as team capacity has
+shrunk, so the long tail keeps stretching.
+
+### Scorecard (as of 2026 mid-year)
+
+```mermaid
+graph LR
+ subgraph DONE[Migrated]
+ kmcomp[kmcomp.exe
.kmn compiler]
+ kmanalyze_old[kmanalyze
analysis tool]
+ kmconvert_old[kmconvert
keyboard converter]
+ kmdecomp_old[kmdecomp]
+ runtime[Keyboard runtime
processing]
+ lang[Language code utilities]
+ end
+ subgraph DELPHI[Still Delphi]
+ tike[TIKE
Keyman Developer IDE]
+ kmshell[kmshell
Keyman Desktop UI]
+ engine[Windows Engine
keyman.exe, kmcomapi.dll, ...]
+ setup_dev[Keyman Developer setup]
+ codegen[Build/codegen tools
devtools, build_standards_data, ...]
+ end
+ subgraph NEW[New stack]
+ kmcmplib[kmcmplib
C++]
+ kmcli[kmc & kmc-*
TypeScript]
+ core[Keyman Core
C++]
+ end
+
+ kmcomp -.->|replaced by| kmcmplib
+ kmcomp -.->|wrapped by| kmcli
+ kmanalyze_old -.->|replaced by| kmcli
+ kmconvert_old -.->|replaced by| kmcli
+ runtime -.->|replaced by| core
+ lang -.->|replaced by| kmcli
+
+ style DONE fill:#284,color:#fff
+ style DELPHI fill:#a52,color:#fff
+ style NEW fill:#46c,color:#fff
+```
+
+**Migrated away from Delphi:**
+
+| Old (Delphi) | New stack | Where to find the new code |
+|---|---|---|
+| `kmcomp.exe` — the `.kmn` keyboard compiler | C++ (`kmcmplib`) + TS wrapper (`kmc-kmn`) | `developer/src/kmcmplib/`, `developer/src/kmc-kmn/` |
+| `kmanalyze` — analysis tool | `kmc-analyze` (TS) | `developer/src/kmc-analyze/` |
+| `kmconvert` — generation/conversion | `kmc-generate`, `kmc-copy`, `kmc-keyboard-info` (TS) | `developer/src/kmc-{generate,copy,keyboard-info}/` |
+| `kmdecomp` — decompiler | (none — marked Unsupported) | — |
+| Keyboard runtime engine | Keyman Core (C++) | `core/` |
+| Build-time language tag utilities | `kmc` family + `Keyman.System.LanguageCodeUtils` shim | `developer/src/kmc*/` |
+
+**Still Delphi (no replacement yet):**
+
+| Component | Path | Why still Delphi |
+|---|---|---|
+| Tike — the Keyman Developer IDE | `developer/src/Tike/` | Largest VCL app in the project; replacement is a major design exercise |
+| Keyman for Windows shell | `windows/src/desktop/kmshell/` | Deeply integrated with Windows TSF / shell |
+| Windows Engine | `windows/src/engine/{keyman,kmcomapi,insthelper,tsysinfo,tsysinfox64}/` | COM API + TSF text service plumbing |
+| Desktop helpers | `windows/src/desktop/{kmbrowserhost,kmconfig,setup,insthelp}/` | UI / installer infrastructure |
+| Developer installer | `developer/src/setup/` | WiX-driven Delphi setup wizard |
+| Build tools | `common/windows/delphi/tools/{devtools,build_standards_data,sentrytool,certificates,test-klog,buildunidata}/` | Codegen used by the Delphi build cascade |
+
+**About the "C# migration"** — there is no Delphi → C# migration in flight.
+The repo has 6 `.cs` files in a tiny test sandbox (`windows/src/support/NetInputBoxTest/`)
+and nothing else. The actual modernization targets are **C++** (Keyman
+Core, kmcmplib, Windows C++ engine pieces like `keyman32.dll`/`kmtip.dll`)
+and **TypeScript** (the `kmc-*` compiler suite, KeymanWeb).
+
+### Working with code that's mid-migration
+
+Two practical rules for new contributors:
+
+* **Prefer the new stack for new functionality.** If you're adding a new
+ compiler feature, add it to `kmc-*` (TypeScript) or `kmcmplib` (C++),
+ not to Tike or kmconvert.
+* **Resist the urge to refactor Delphi.** Touching `developer/src/Tike/`
+ or `windows/src/engine/keyman/` to "clean up" is often wasted effort
+ because the long-term plan is to retire the code. Fix the immediate bug,
+ flag the deeper issue in a comment + issue, move on.
+
+### Building Delphi components today
+
+If you must work in the Delphi tree, see the build doc that matches your
+Delphi version:
+
+* [docs/build/windows.md](../docs/build/windows.md) — the canonical flow
+ (Delphi 10.3 / Pro)
+* [docs/build/windows-d12.md](../docs/build/windows-d12.md) — the
+ workaround for Delphi 12 Community Edition contributors
+
+## Migration 2: LDML keyboards
+
+### What's changing
+
+Keyman's historical keyboard formats — `.kmn` (source) and `.kmx`
+(compiled binary) — predate the Unicode CLDR project's LDML keyboard
+standard. The team is adding first-class support for LDML XML keyboards
+so that:
+
+* Keyboards authored for LDML work natively in Keyman everywhere
+* Keyman authors can publish keyboards to the CLDR repository
+* Tooling between Keyman and other CLDR-aware projects becomes shared
+
+### Where it lives
+
+* **Compiler**: `developer/src/kmc-ldml/` — TypeScript module that compiles
+ LDML XML directly to a `.kmx` (the existing binary format that Keyman
+ Core runs).
+* **Core runtime**: `core/src/ldml/` and `core/include/ldml/` — the C++
+ LDML processor inside Keyman Core. Same C API as `.kmx` keyboards; the
+ binary format `kmx+` ([docs/file-formats/kmx-plus-file-format.md](../docs/file-formats/kmx-plus-file-format.md))
+ carries the LDML data through to runtime.
+* **Tests**: `core/tests/unit/ldml/` (runtime) and
+ `developer/src/kmc-ldml/test/` (compiler).
+
+### Status — what works today vs what's still in flight
+
+LDML support is shipping in pieces. The
+[March 2026 roadmap](https://blog.keyman.com/2026/03/keyman-roadmap-march-2026/)
+lays out the next several versions:
+
+* **v19 (current)**: "web-core" — bring LDML keyboard support to mobile,
+ tablet, and embedded web. This is where most LDML work landed in
+ shipped releases.
+* **v20**: an **LDML keyboard visual editor** (built as a VSCode plugin —
+ see [epic-ldml-editor scaffolding, issue #12798](https://github.com/keymanapp/keyman/issues/12798)),
+ plus the Delphi-to-C++ Windows-UI migration. The visual editor is the
+ long-term replacement for hand-editing LDML XML.
+* **v21**: rebuild the on-screen keyboard rendering using web technologies
+ so the touch/OSK experience is consistent across platforms.
+* **v22**: deprecate `.js` keyboards in favor of LDML everywhere.
+
+#### What works today (master)
+
+* `.kmn` → `.kmx`: fully working via `kmcmplib`
+* LDML XML → `.kmx+`: working for the **desktop** keyboard layer.
+ Compile via `kmc build path/to/keyboard.xml`.
+* Keyman Core runtime executes both `.kmx` and `.kmx+` keyboards.
+* `core/src/ldml/` handles transforms, markers, deadkeys, syllable-based
+ scripts (with some known edge cases under
+ [active LDML-runtime work](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-ldml+is%3Aopen)).
+
+#### What's NOT working yet (the touch gap, all layers)
+
+**The CLDR LDML keyboard spec itself is complete for touch** — layers
+(hardware and touch), forms, flicks, longpress, multi-tap, transforms,
+markers, all defined. The gap is entirely on **Keyman's read side**: an
+LDML keyboard with full touch layer markup is a valid spec-compliant
+document today, but Keyman doesn't yet consume that touch markup
+end-to-end.
+
+A consequence for tool builders: an AI extension *can* generate
+spec-correct LDML touch keyboards today, and those keyboards will
+become natively runnable on mobile/web when Keyman finishes the read
+side — no source rewriting needed at that point. Generating to the
+LDML spec is a forward-compatible bet even though runtime delivery
+today still requires a parallel `.kmn` + `.kvks` set.
+
+That said, fully consuming the LDML touch markup end-to-end on mobile
+and the browser through the kmx+ pipeline requires **four** layers of
+implementation, not just the compiler:
+
+| Layer | State | Where to look |
+|---|---|---|
+| kmc-ldml writing LAYR data | **Partial** | TODOs in `developer/src/kmc-ldml/src/compiler/` (see table below) |
+| kmx+ binary carrying LAYR | Done | Format defined; runtime parser at `core/src/kmx/kmx_plus.cpp:659+` |
+| Keyman Core C API exposing layout to hosts | **Missing** | No `km_core_keyboard_get_layer` / `get_form` in `core/include/` |
+| KeymanWeb / Mobile OSK consuming kmx+ layout | **Missing** | KeymanWeb OSK still drives off the legacy `.keyman-touch-layout` JSON spec; mobile engines the same |
+
+The interim today: kmc-ldml's
+`visual-keyboard-compiler.ts` emits a legacy `.kvk` from the LDML source
+so a touch-LDML keyboard *can* render through the existing OSK path. But
+that's a stopgap — it's not "LDML XML drives the touch experience
+natively through kmx+ on mobile and web", which is what the design aims
+for and what the roadmap's v21 OSK rebuild promises to deliver
+cross-platform.
+
+#### Compiler-side gaps (kmc-ldml)
+
+The specific TODOs in current master, all in
+`developer/src/kmc-ldml/src/compiler/`:
+
+| File | TODO | Effect |
+|---|---|---|
+| `metadata-compiler.ts:52` | `dpString: 'desktop'` hardcoded | `&TARGETS` store always says `'desktop'` even if the source declares touch layers |
+| `keys.ts:240` | "do nothing if only touch layers" | A touch-only LDML keyboard produces no keymap |
+| `keys.ts:148` | "TODO-LDML: } else { touch?" | Touch layer validation skipped |
+| `layr.ts:60` | "does not validate touch layers yet" | Layer count gate doesn't enforce touch |
+| `visual-keyboard-compiler.ts:7` | "This is an interim solution until Keyman Core supports interrogation of the KMX+ data for OSK" | Today's stopgap is to emit a legacy `.kvk` from the LDML source rather than carry touch through in kmx+ |
+
+Tracking issue: **[#7238 — support touch layouts in kmc-ldml](https://github.com/keymanapp/keyman/issues/7238)**
+(labelled `epic-ldml`, milestone A19S1).
+
+For the broader LDML editor / authoring story, see the
+[`epic-ldml-editor` label](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-ldml-editor)
+and the umbrella scaffolding issue [#12798](https://github.com/keymanapp/keyman/issues/12798).
+
+#### Cheapest contributions if you want to accelerate
+
+In order of leverage (smallest to largest):
+
+1. **`metadata-compiler.ts:52` one-liner** — emit `'desktop touch'`
+ when the source declares touch layers instead of the current
+ hardcoded `'desktop'`. Doesn't fix runtime, but lets correctly-built
+ LDML keyboards declare their actual platform support. Probably a
+ 1-line PR + tests.
+2. **Finish `keys.ts` touch handling** — the `keys.ts:148, 240` TODOs.
+ A few hundred lines; lands "touch-aware LDML compilation" without
+ needing runtime changes.
+3. **Finish `layr.ts:60` validation** — modest PR.
+
+Steps 1–3 close the compiler-side gap. The runtime gap (Core C API +
+KeymanWeb/Mobile OSK refactors) is the v21-scale effort; not a small
+PR. If a contributor wanted to make LDML touch keyboards *compile
+correctly today* (with the legacy `.kvk` runtime stopgap still doing
+the actual touch rendering), the three items above are the path.
+
+### What this means for you as an intern
+
+* **For desktop-only keyboards**: emit LDML XML. No consistency
+ contract; works today end-to-end. Bias new test keyboards toward
+ LDML.
+* **For keyboards that need to run on touch today** (mobile, web):
+ emit LDML XML as the canonical source AND a derived `.kmn` + `.kvks`
+ + `.keyman-touch-layout` bundle. Treat the LDML as authoritative; the
+ bundle is transitional until Keyman closes the read gap. See
+ [external-tooling.md § Recommended generation strategy](external-tooling.md#recommended-generation-strategy).
+* The CLDR LDML spec is complete for touch — your generated LDML is
+ forward-compatible. When Keyman finishes the read side, you delete
+ the transitional bundle and the same LDML just works.
+* If you encounter an LDML-related bug at runtime, look in `core/src/ldml/`
+ (the C++ runtime); at compile time, look in `developer/src/kmc-ldml/`.
+* `kmx+` files are still `.kmx` files on disk — same Keyman Core entry
+ point. The "+" refers to additional LDML-specific binary sections.
+
+## Migration 3: KeymanWeb modernization
+
+### What changed
+
+Original KeymanWeb was a large standalone JavaScript engine — a parallel
+implementation of the Keyman keyboard runtime in JavaScript, separate
+from the desktop/mobile engines. Two engines meant two divergent feature
+sets, two bug surfaces, two test matrices.
+
+The modernization replaces the JS-side keyboard engine with **Keyman Core
+compiled to WebAssembly**. The TypeScript code in `web/` is now a host
+that integrates with browser inputs (DOM events, OSK rendering, language
+switcher) and delegates *all* keyboard logic to Keyman Core via the WASM
+binding.
+
+```mermaid
+graph LR
+ subgraph WEB[Modern KeymanWeb / web/]
+ host[TypeScript host
DOM, OSK, UI]
+ wasm[Keyman Core
compiled to WASM]
+ end
+ user[Browser keyboard event]
+ page[Page text input]
+
+ user --> host
+ host --> wasm
+ wasm --> host
+ host --> page
+
+ style WEB fill:#83a,color:#fff
+```
+
+### Where it lives
+
+* **Host**: `web/src/` — TypeScript organized into:
+ * `web/src/engine/` — the WASM-binding glue and runtime helpers
+ * `web/src/app/` — browser entrypoints (KeymanWeb embedded library,
+ keyman.com app integration)
+ * `web/src/test/` — Karma + Mocha test suites
+* **WASM core**: built from `core/` via `core/build.sh build:wasm` and
+ consumed by `web/`'s build at link time. Lives in `web/build/engine/`
+ after build.
+* **Build**: `web/build.sh build` is the single entry point. Internally
+ it manages the emsdk toolchain, node packages, and the wasm dependency.
+
+### Status
+
+The Core-via-WASM model is shipping in 17+ release lines. Active work
+focuses on feature parity edge cases (touch-keyboard interactions,
+predictive text, IME interaction) and removing remaining legacy paths
+inherited from the original JS engine. Search the issue tracker for
+`web/` and `kmw` for current work.
+
+### What this means for you
+
+* When you debug a KeymanWeb keyboard bug, ask first: is the bug in the
+ keyboard logic (Keyman Core / WASM) or in the host (DOM event handling,
+ rendering)? The split is more useful than the old "look anywhere in the
+ KeymanWeb monolith."
+* If a behavior differs between desktop Keyman and KeymanWeb, the bug is
+ *probably* in the web host (`web/src/`), not in Keyman Core — Core is
+ shared.
+
+## Long-running migration branches
+
+The three migrations above don't always land directly on `master`. Some
+larger work happens on long-lived branches that periodically merge back.
+Conventional name prefixes you'll see in `git branch -r`:
+
+* `feat/core/...` — runtime features in `core/`, often coordinated with
+ LDML or platform-engine work
+* `feat/developer/...` — compiler / Developer-side features (kmc-* etc.)
+* `feat/web/...` — KeymanWeb modernization work
+* `chore/windows/...` — incremental cleanups in the Windows / Delphi tree
+* `auto/...` — automated branches (version bumps, etc.); not for human
+ contribution
+
+Before starting a non-trivial feature, search the open issues by `epic-*`
+label first, then `git branch -r | grep -i ` against
+`upstream/` to see if a long-running branch already covers it.
+
+## Roadmap and decision context
+
+The Keyman team publishes high-level direction on the
+[Keyman roadmap blog](https://blog.keyman.com/category/roadmap/). The
+current public roadmap is the
+[**March 2026 update**](https://blog.keyman.com/2026/03/keyman-roadmap-march-2026/) —
+read it before making bets on what to invest effort in. Headlines:
+
+| Version | Theme | Highlights |
+|---|---|---|
+| **19** (current) | LDML / web-core | CLDR keyboards on mobile, tablet, embedded web |
+| **20** | Compiler & UI modernization | **Delphi → C++ for Windows UI**, LDML visual editor (VSCode plugin, [#12798](https://github.com/keymanapp/keyman/issues/12798)), compiler rewrite |
+| **21** | Touch/OSK rebuild | On-screen keyboard rebuilt with web tech for cross-platform consistency |
+| **22** | Distribution & deprecation | KeymanWeb on npm; deprecate `.js` keyboards in favor of LDML; CLDR predictive text wordlists |
+
+All version targets are funding-dependent (the roadmap is explicit about
+this).
+
+**Umbrella tracking issues to watch:**
+
+* [#4599 — Delphi removal](https://github.com/keymanapp/keyman/issues/4599)
+ (filed 2022, umbrella)
+* [#7238 — LDML touch layout compilation](https://github.com/keymanapp/keyman/issues/7238)
+ (epic-ldml, see § Migration 2 above)
+* [#12798 — VSCode plugin scaffolding](https://github.com/keymanapp/keyman/issues/12798)
+ (epic-ldml-editor, milestone 20.0)
+* [`epic-ldml` label](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-ldml)
+ — open work on the LDML keyboard format support
+* [`epic-ldml-editor` label](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-ldml-editor)
+ — open work on LDML authoring tooling
+* [`epic-keyman-developer` label](https://github.com/keymanapp/keyman/issues?q=label%3Aepic-keyman-developer)
+ — broader Developer-side work
+
+## TL;DR for new contributors
+
+* **New features**: write them in the new stack (C++ for runtime, TS for
+ tooling). Don't add to Delphi unless you specifically have to.
+* **Bug fixes**: fix them where the bug is. If it's in Delphi-bound code,
+ patch Delphi. Don't refactor surrounding code.
+* **LDML**: it's the future format. Bias new test keyboards toward LDML
+ XML, not `.kmn`.
+* **KeymanWeb**: the engine *is* Keyman Core. The host is TypeScript.
+ Debug accordingly.
+* **When in doubt**: check the roadmap blog, search issues, ask in the
+ [SIL/Keyman forum](https://community.software.sil.org/c/keyman).
diff --git a/dev-docs/repository-map.md b/dev-docs/repository-map.md
new file mode 100644
index 00000000000..09379c1a888
--- /dev/null
+++ b/dev-docs/repository-map.md
@@ -0,0 +1,344 @@
+# Repository Map
+
+This is a tour of the Keyman monorepo. By the end of it you should know
+where to find each major component, what tech stack it uses, which
+platforms it targets, and how the components connect to each other.
+
+If you only read one section, read [Top-level layout](#top-level-layout) and
+[The four big subsystems](#the-four-big-subsystems).
+
+## What Keyman is, in three sentences
+
+Keyman is an open-source cross-platform input system that lets people type in
+any of the world's languages. It runs on Windows, macOS, Linux, iOS, Android,
+and in web browsers — same keyboard files everywhere. The repo contains the
+runtime engines for each platform, the developer tooling for authoring
+keyboards, the Keyman Developer IDE, the KeymanWeb JavaScript engine, and a
+shared C++ keyboard-processing core.
+
+## Top-level layout
+
+```mermaid
+graph TD
+ root[keyman/]
+ root --> core[core/
Keyman Core
C++ shared runtime]
+ root --> common[common/
Shared code & tools]
+ root --> developer[developer/
Keyman Developer
compiler + IDE]
+ root --> web[web/
KeymanWeb
browser engine]
+ root --> windows[windows/
Windows engine + desktop]
+ root --> mac[mac/
macOS]
+ root --> linux[linux/
Linux]
+ root --> ios[ios/
iOS]
+ root --> android[android/
Android]
+ root --> oem[oem/
OEM builds
e.g. FirstVoices]
+ root --> docs[docs/
Build & format docs]
+ root --> resources[resources/
Build scripts & data]
+ root --> devdocs[dev-docs/
You are here]
+
+ style root fill:#444,color:#fff
+ style devdocs fill:#28a,color:#fff
+ style core fill:#284,color:#fff
+```
+
+## The four big subsystems
+
+```mermaid
+graph LR
+ subgraph SC[Shared C++]
+ kmcore[core/
Keyman Core]
+ end
+ subgraph DV[Developer tooling]
+ kmcmplib[kmcmplib
.kmn compiler library]
+ kmc[kmc & kmc-*
CLI compiler suite]
+ tike[Tike
Developer IDE]
+ end
+ subgraph RT[Runtime engines]
+ eng_win[Windows Engine
Delphi + C++]
+ eng_mac[macOS Engine
Swift + ObjC]
+ eng_lin[Linux Engine
C++]
+ eng_ios[iOS Engine
Swift]
+ eng_and[Android Engine
Java/Kotlin + C++]
+ end
+ subgraph WB[KeymanWeb]
+ kmweb[KeymanWeb
TypeScript + WASM]
+ end
+
+ kmcore -.shared lib.-> eng_win
+ kmcore -.shared lib.-> eng_mac
+ kmcore -.shared lib.-> eng_lin
+ kmcore -.shared lib.-> eng_ios
+ kmcore -.shared lib.-> eng_and
+ kmcore -.wasm.-> kmweb
+
+ kmcmplib --> kmc
+ kmc --> tike
+ kmcmplib -.compiles .kmn.-> tike
+
+ style SC fill:#284,color:#fff
+ style DV fill:#46c,color:#fff
+ style RT fill:#a52,color:#fff
+ style WB fill:#83a,color:#fff
+```
+
+* **Shared C++** — `core/` is a single shared keyboard-processing engine
+ used by *every* runtime. When you press a key, the platform-specific
+ engine translates that into Keyman Core API calls; Core runs the keyboard
+ logic and returns the resulting text actions; the engine then injects
+ text into the focused application using platform APIs.
+
+* **Developer tooling** — under `developer/src/`. The historical Delphi
+ IDE (Tike) and the modern TypeScript CLI compiler family (`kmc` and the
+ ten `kmc-*` modules) live side by side. The `.kmn` keyboard compiler
+ itself is C++ (`kmcmplib`).
+
+* **Runtime engines** — one per platform under `windows/`, `mac/`,
+ `linux/`, `ios/`, `android/`. Each is a thin shell over Keyman Core that
+ handles the platform's keyboard hooks, IME registration, OS integration,
+ and configuration UI.
+
+* **KeymanWeb** — under `web/`. The same Keyman Core code compiled to
+ WebAssembly via Emscripten, plus a TypeScript host that integrates with
+ HTML inputs and embeds in Keyman web apps and `keyman.com`.
+
+## Tech stack per directory
+
+| Directory | Primary language(s) | Build system | Platforms |
+|---|---|---|---|
+| `core/` | C++ | Meson + Ninja | Cross (Win, Mac, Linux, WASM) |
+| `common/` | Mixed (C++, TS, Delphi, data) | Various | Shared |
+| `common/web/` | TypeScript | npm + esbuild | Cross |
+| `common/windows/delphi/` | Delphi | build.sh / Delphi IDE | Windows only |
+| `developer/src/kmcmplib/` | C++ | Meson | Cross |
+| `developer/src/kmc*/` | TypeScript | npm + esbuild | Cross |
+| `developer/src/Tike/` | Delphi (VCL) | build.sh + Delphi IDE | Windows only |
+| `developer/src/kmconvert/`, `setup/` | Delphi | build.sh + Delphi IDE | Windows only |
+| `web/` | TypeScript + WASM | build.sh + npm | Browsers |
+| `windows/src/engine/` | Delphi + C++ | build.sh + Delphi/VS | Windows |
+| `windows/src/desktop/` | Delphi | build.sh + Delphi IDE | Windows |
+| `mac/` | Swift, Objective-C | Xcode | macOS |
+| `linux/` | C++, Python | meson, debhelper | Linux |
+| `ios/` | Swift | Xcode | iOS |
+| `android/` | Java, Kotlin, C++ | Gradle | Android |
+| `oem/firstvoices/` | Platform-specific | Per-platform | Cross |
+| `resources/` | Bash, data files | Plain | Build infra |
+
+## How a keystroke flows (runtime)
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant OS as OS Input Layer
+ participant Engine as Platform Engine
(e.g. windows/src/engine)
+ participant Core as Keyman Core
(core/, C++)
+ participant App as Target App
+
+ User->>OS: Press key
+ OS->>Engine: Keyboard hook callback
+ Engine->>Core: km_core_state_process_event()
+ Core->>Core: Run active keyboard rules
(.kmx or LDML)
+ Core-->>Engine: List of TActionItem
(emit char, backspace, ...)
+ Engine->>OS: SendInput / TSF actions
+ OS->>App: Text typed
+```
+
+The platform engine never implements keyboard logic itself; it always
+delegates to Keyman Core. That's why a single keyboard file works across
+Windows, macOS, Linux, iOS, Android, and the web.
+
+## How a keyboard is built (developer flow)
+
+```mermaid
+flowchart LR
+ src[Source
.kmn or LDML XML]
+ kmc[kmc
TypeScript CLI]
+ kmcmplib[kmcmplib
C++ compiler]
+ kmx[.kmx
Keyman binary]
+ js[.js
KeymanWeb bundle]
+ kmp[.kmp
Package zip]
+
+ src --> kmc
+ kmc -.delegates .kmn.-> kmcmplib
+ kmcmplib --> kmx
+ kmc --> js
+ kmc --> kmp
+ kmx --> kmp
+ js --> kmp
+```
+
+The TypeScript `kmc` CLI is the modern entry point. For `.kmn` source it
+delegates to the C++ `kmcmplib`. For LDML XML source, `kmc-ldml` handles
+compilation directly in TypeScript. The single `.kmp` package contains
+the platform binaries plus metadata (`keyboard_info`, fonts, docs).
+
+## Subsystem deep-dives
+
+### `core/` — Keyman Core
+
+The cross-platform shared C++ runtime. Implements the keyboard-rule
+processor in a host-agnostic way. Exposed through a stable C API
+(`km_core_*` functions) so every platform engine and KeymanWeb can call
+the same logic.
+
+* **Build**: `core/build.sh build:x64` / `:x86` / `:wasm` / per-platform
+ targets. Output goes to `core/build///src/libkmcore-*.{dll,so,dylib}`.
+* **Tests**: `core/tests/` with `kmnkbd`, `ldml`, `kmx`, `kmx_plus`
+ unit-test directories.
+* **API**: see `core/docs/api.md` and the public headers in `core/include/`.
+
+### `developer/src/` — Keyman Developer
+
+The keyboard authoring side. Two halves:
+
+* **Modern compiler stack** (TypeScript): `kmc/` is the unified CLI;
+ `kmc-kmn/` delegates `.kmn` to `kmcmplib`; `kmc-ldml/` handles LDML
+ keyboards; `kmc-package/`, `kmc-keyboard-info/`, `kmc-analyze/`,
+ `kmc-copy/`, `kmc-generate/`, `kmc-model/`, `kmc-model-info/` cover the
+ other authoring tasks.
+* **Legacy Delphi IDE**: `Tike/Tike.dproj` is the visual authoring tool,
+ built with VCL. `setup/` is the Developer installer. `kmconvert/` is a
+ legacy conversion tool — marked Legacy and being phased out in favor of
+ the kmc-* family. See [migration-guide.md](migration-guide.md).
+
+### `web/` — KeymanWeb
+
+The browser-side keyboard engine. Same Keyman Core, compiled to
+WebAssembly via Emscripten and wrapped in a TypeScript host. Used by
+keyman.com web apps, by sites that embed the keyman.js script, and by
+Keyman Developer's preview mode.
+
+* **Build**: `web/build.sh build` (needs node + emsdk; sets up both).
+* **Output**: `web/build/app/`, `web/build/engine/`, `web/build/publish/`
+ for the release bundles.
+* **Tests**: `web/build/test/` after `build`, runnable via Karma.
+
+### `windows/src/engine/` — Windows runtime engine
+
+A mix of Delphi (the COM API, configuration helpers, TSF text service
+plumbing) and C++ (the actual input hook, `keyman32.dll`, the TSF text
+processor `kmtip.dll`). For a tour of which dproj does what, see
+`windows/src/engine/engine.groupproj` — the five Delphi projects build
+together as a group; the C++ pieces (`keyman32`, `kmtip`, `keymanhp`,
+`kmrefresh`, `mcompile`, `testhost`) build standalone via msbuild.
+
+### `windows/src/desktop/` — Keyman for Windows (the user-facing app)
+
+Delphi VCL apps: `kmshell` is the configuration tray app; `kmconfig` is
+the legacy settings UI; `kmbrowserhost` embeds CEF for the
+in-app browser; `setup` is the installer; `insthelp` is the install helper.
+
+### `mac/`, `linux/`, `ios/`, `android/`
+
+Each runs Keyman Core under the hood, with a platform-native shell:
+
+* **macOS** (`mac/Keyman4MacIM/`): Objective-C / Swift Input Method bundle.
+* **Linux** (`linux/ibus-keyman/`): IBus engine module written in C++,
+ packaged via the `keyman-config` Python helpers.
+* **iOS** (`ios/keyman/`): Swift app + KeymanEngine framework.
+* **Android** (`android/KMAPro/`): Kotlin/Java app + KMEA library;
+ Keyman Core compiled for NDK is loaded via JNI.
+
+### `common/` — Shared anything-and-everything
+
+Anything used by more than one platform lives here. Highlights:
+
+* `common/web/` — shared TypeScript helpers (`keyman-version`, `langtags`,
+ `types`, `utils`).
+* `common/windows/delphi/` — Delphi units shared by every Windows-Delphi
+ project (engine, desktop, developer). Plus bundled third-party Delphi
+ libraries: JCL, JVCL, mbcolor, dcpcrypt, CEF4Delphi.
+* `common/windows/cpp/` — Windows-specific C++ headers.
+* `common/include/`, `common/test/` — cross-platform shared includes and
+ test fixtures.
+* `common/tools/` — small cross-platform helpers like `hextobin`.
+
+### `resources/`, `build/`, `docs/`
+
+* `resources/build/` — the shared bash-builder framework (`builder-full.inc.sh`
+ etc.) that every project's `build.sh` sources. Read this once when you
+ start writing a new `build.sh`.
+* `resources/standards-data/` — language tag and ISO data used to generate
+ the BCP-47 Pascal modules.
+* `docs/` — build instructions, file-format references, history.
+
+## Build orchestration
+
+Every top-level directory has a `build.sh` that obeys the same conventions.
+A typical invocation is:
+
+```bash
+.//build.sh [:] [options...]
+```
+
+Where:
+* `` is `clean`, `configure`, `build`, `test`, `publish`, `install`,
+ `api`, or `edit`.
+* `` is component-specific (e.g. `./web/build.sh build:engine`).
+
+Read `/build.sh --help` to see available targets. Most scripts inherit
+the cross-platform builder framework from `resources/build/builder-*.inc.sh`,
+so they all have the same look-and-feel.
+
+```mermaid
+graph TD
+ root_build[./build.sh
top-level orchestrator]
+ root_build --> core_b[core/build.sh]
+ root_build --> dev_b[developer/build.sh]
+ root_build --> web_b[web/build.sh]
+ root_build --> win_b[windows/src/build.sh]
+ root_build --> linux_b[linux/build.sh]
+ root_build --> mac_b[mac/build.sh]
+ root_build --> ios_b[ios/build.sh]
+ root_build --> and_b[android/build.sh]
+
+ dev_b --> kmc_b[developer/src/kmc/build.sh]
+ dev_b --> kmcmplib_b[developer/src/kmcmplib/build.sh]
+ dev_b --> tike_b[developer/src/Tike/build.sh]
+ dev_b --> setup_b[developer/src/setup/build.sh]
+
+ win_b --> eng_b[windows/src/engine/build.sh]
+ win_b --> desk_b[windows/src/desktop/build.sh]
+ eng_b --> k32_b[engine/keyman32/build.sh]
+ eng_b --> kmtip_b[engine/kmtip/build.sh]
+ eng_b --> ihelp_b[engine/insthelper/build.sh]
+
+ style root_build fill:#444,color:#fff
+```
+
+## Cross-platform matrix
+
+Which directories actually build on which OS:
+
+| Directory | Win 11 | macOS | Linux |
+|---|---|---|---|
+| `core/` | ✓ (x86, x64, ARM64, WASM) | ✓ (universal) | ✓ |
+| `common/` | ✓ | ✓ | ✓ |
+| `developer/src/kmcmplib/` | ✓ | ✓ | ✓ |
+| `developer/src/kmc*/` | ✓ | ✓ | ✓ |
+| `developer/src/Tike/`, `setup/`, `kmconvert/` | Delphi only | — | — |
+| `web/` | ✓ | ✓ | ✓ |
+| `windows/` | ✓ | — | — |
+| `linux/` | — | — | ✓ |
+| `mac/`, `ios/` | — | ✓ | — |
+| `android/` | ✓ (Java/Kotlin/NDK) | ✓ | ✓ |
+
+When the `docs/build/windows.md` doc says "the following projects cannot be
+built on Windows: Keyman for Linux / Keyman for macOS / Keyman for iOS" —
+this matrix is what it's reflecting.
+
+## Where to start as an intern
+
+Depending on what you're working on:
+
+* **Keyboard runtime work**: start with `core/` and one of the platform
+ engines (most likely `windows/src/engine/` or `linux/ibus-keyman/`).
+* **Compiler / authoring work**: start with `developer/src/kmc/` and
+ follow the dependency from there into the `kmc-*` modules.
+* **Web work**: start with `web/` and the TypeScript app under
+ `web/src/app/`.
+* **Mobile**: `android/KMAPro/` or `ios/keyman/`.
+* **Delphi UI work (Tike, kmshell)**: read [migration-guide.md](migration-guide.md)
+ *first* — the long-term direction matters before you invest time in
+ Delphi changes.
+
+Whatever you touch first, read its `build.sh --help` and the `README.md`
+in that directory (most components have one).
diff --git a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas
index f73b1722b09..eb405224249 100644
--- a/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas
+++ b/developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas
@@ -937,11 +937,15 @@ function TJclCriticalSectionEx.TryEnter: Boolean;
//== { TJclEvent } ===========================================================
+// Keyman local patch: Delphi 11/12 compat (vendored JCL). Explicit BOOL() casts
+// added below to satisfy Delphi 12's stricter implicit-Boolean->BOOL conversion.
+// Backwards-compatible with Delphi 10.3 (BOOL is an ordinal-preserving typecast).
+// On JCL refresh from upstream: re-apply if upstream hasn't picked up the cast.
constructor TJclEvent.Create(SecAttr: PSecurityAttributes; Manual, Signaled: Boolean; const Name: string);
begin
inherited Create;
FName := Name;
- FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, Manual, Signaled, PChar(FName));
+ FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(FName));
if FHandle = 0 then
raise EJclEventError.CreateRes(@RsSynchCreateEvent);
FExisted := GetLastError = ERROR_ALREADY_EXISTS;
@@ -952,7 +956,7 @@ constructor TJclEvent.Open(Access: Cardinal; Inheritable: Boolean;
begin
FName := Name;
FExisted := True;
- FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, Inheritable, PChar(Name));
+ FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenEvent(Access, BOOL(Inheritable), PChar(Name));
if FHandle = 0 then
raise EJclEventError.CreateRes(@RsSynchOpenEvent);
end;
@@ -980,7 +984,7 @@ constructor TJclWaitableTimer.Create(SecAttr: PSecurityAttributes;
begin
FName := Name;
FResume := False;
- FHandle := CreateWaitableTimer(SecAttr, Manual, PChar(Name));
+ FHandle := CreateWaitableTimer(SecAttr, BOOL(Manual), PChar(Name));
if FHandle = 0 then
raise EJclWaitableTimerError.CreateRes(@RsSynchCreateWaitableTimer);
FExisted := GetLastError = ERROR_ALREADY_EXISTS;
@@ -1000,7 +1004,7 @@ constructor TJclWaitableTimer.Open(Access: Cardinal; Inheritable: Boolean;
FExisted := True;
FName := Name;
FResume := False;
- FHandle := OpenWaitableTimer(Access, Inheritable, PChar(Name));
+ FHandle := OpenWaitableTimer(Access, BOOL(Inheritable), PChar(Name));
if FHandle = 0 then
raise EJclWaitableTimerError.CreateRes(@RsSynchOpenWaitableTimer);
end;
@@ -1048,7 +1052,7 @@ constructor TJclSemaphore.Open(Access: Cardinal; Inheritable: Boolean;
begin
FName := Name;
FExisted := True;
- FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, Inheritable, PChar(Name));
+ FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenSemaphore(Access, BOOL(Inheritable), PChar(Name));
if FHandle = 0 then
raise EJclSemaphoreError.CreateRes(@RsSynchOpenSemaphore);
end;
@@ -1075,7 +1079,7 @@ constructor TJclMutex.Create(SecAttr: PSecurityAttributes; InitialOwner: Boolean
begin
inherited Create;
FName := Name;
- FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name));
+ FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(SecAttr, BOOL(InitialOwner), PChar(Name));
if FHandle = 0 then
raise EJclMutexError.CreateRes(@RsSynchCreateMutex);
FExisted := GetLastError = ERROR_ALREADY_EXISTS;
@@ -1086,7 +1090,7 @@ constructor TJclMutex.Open(Access: Cardinal; Inheritable: Boolean; const Name: s
inherited Create;
FName := Name;
FExisted := True;
- FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, Inheritable, PChar(Name));
+ FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.OpenMutex(Access, BOOL(Inheritable), PChar(Name));
if FHandle = 0 then
raise EJclMutexError.CreateRes(@RsSynchOpenMutex);
end;
diff --git a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
index 1877ee48efe..fab8609e2c8 100644
--- a/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
+++ b/developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas
@@ -123,8 +123,16 @@ constructor TJvForm.Create(AOwner: TComponent);
finally
Exclude(FFormState, fsCreating);
end;
+ // Keyman local patch: Delphi 11/12 compat (vendored JVCL). The
+ // OldCreateOrder property was removed in Delphi 11; the modern semantics
+ // are equivalent to OldCreateOrder=True, so always call DoCreate on
+ // VER350+. On JVCL refresh from upstream: re-apply if not yet upstreamed.
+{$IF Defined(VER350) or Defined(VER360)}
+ DoCreate;
+{$ELSE}
if OldCreateOrder then
DoCreate;
+{$IFEND}
end;
finally
GlobalNameSpace.EndWrite;
diff --git a/developer/src/ext/mbcolor/mxs.inc b/developer/src/ext/mbcolor/mxs.inc
index 41b82ad96e5..75ca0b34e73 100644
--- a/developer/src/ext/mbcolor/mxs.inc
+++ b/developer/src/ext/mbcolor/mxs.inc
@@ -7,6 +7,28 @@
{$define DELPHI_10_UP}
{$endif}
+ // Keyman local patch: Delphi 11/12 compat (vendored mbcolor). Without
+ // these blocks DELPHI_*_UP flags are never defined on VER350/VER360,
+ // which causes HTMLColors.pas to drop "uses Variants" and fail to
+ // resolve the Null constant. On mbcolor refresh: re-apply if needed.
+ {$ifdef VER350}
+ {$define DELPHI_5_UP}
+ {$define DELPHI_6_UP}
+ {$define DELPHI_7_UP}
+ {$define DELPHI_8_UP}
+ {$define DELPHI_9_UP}
+ {$define DELPHI_10_UP}
+ {$endif}
+
+ {$ifdef VER360}
+ {$define DELPHI_5_UP}
+ {$define DELPHI_6_UP}
+ {$define DELPHI_7_UP}
+ {$define DELPHI_8_UP}
+ {$define DELPHI_9_UP}
+ {$define DELPHI_10_UP}
+ {$endif}
+
{$ifdef VER330}
{$define DELPHI_5_UP}
{$define DELPHI_6_UP}
diff --git a/docs/build/windows-d12.md b/docs/build/windows-d12.md
new file mode 100644
index 00000000000..b7e740c6b0e
--- /dev/null
+++ b/docs/build/windows-d12.md
@@ -0,0 +1,933 @@
+# Build Keyman on Windows with Delphi 12 Community Edition
+
+> [!WARNING]
+> This is a workaround guide for developers using **Delphi 12 Athens Community
+> Edition (CE)**. Delphi CE 11+ deliberately blocks command-line `dcc32`
+> compilation:
+>
+> ```
+> This version of the product does not support command line compiling
+> ```
+>
+> Because of that, the repo's `build.sh` chain cannot drive Delphi builds
+> end-to-end on CE. Everything Delphi must be opened and built in the IDE by
+> hand, and the codegen / resource-compilation steps that `build.sh` would
+> normally perform must be run manually first. This guide documents that
+> workflow plus the local source patches needed to compile against Delphi 12.
+>
+> If you have a Delphi 10.3 / 10.4 Professional or Enterprise license, follow
+> the canonical [windows.md](windows.md) instead; none of the workarounds in
+> this file are needed.
+
+## Scope
+
+This guide covers:
+
+1. Prerequisites (Windows + Delphi 12 CE specific notes)
+2. One-time environment setup
+3. Local-only source patches required for Delphi 12
+4. Build order and the implicit cross-project dependencies
+5. Helper scripts that paper over what `build.sh` would normally do
+6. Running and debugging the dev build alongside an installed Keyman 19
+7. Troubleshooting common error messages
+
+The canonical reference for the standard (non-CE) flow is
+[windows.md](windows.md). Refer to it for repository layout, the
+`KEYMAN_ROOT` / `KEYMAN_CEF4DELPHI_ROOT` / `EMSCRIPTEN_BASE` env vars,
+Chocolatey package list, Visual Studio workloads, and Android dependencies.
+This document only describes the deltas.
+
+> [!IMPORTANT]
+> All patches and registry edits described here are **local-only** and must be
+> reverted before opening a pull request. The repo's CI runs against
+> Delphi 10.3 and will not accept any of these workarounds upstream.
+
+## 1. Prerequisites
+
+Beyond the [Base](windows.md#base-dependencies) and
+[Web](windows.md#web-dependencies) dependencies in `windows.md`, Delphi 12 CE
+needs the following:
+
+* **Delphi 12 Athens Community Edition**
+ * Download from
+ https://www.embarcadero.com/products/delphi/starter/free-download
+ * Installs to `C:\Program Files (x86)\Embarcadero\Studio\23.0\` -- note the
+ `23.0` version dir, not `20.0` (10.3) or `21.0` (10.4).
+ * During the install wizard, select **DUnit Unit Testing Frameworks**
+ (required by Keyman test projects).
+ * Launch the IDE once after install to complete registration. The first
+ launch populates the per-user `BDS\23.0` registry hive used in step 4.
+* **Visual Studio 2022 Community** with the C++ native desktop workload, plus
+ Windows 10 SDK (10.0.19041.0) and Windows 11 SDK (10.0.26100). See
+ [Windows Platform Dependencies](windows.md#windows-platform-dependencies)
+ for the full winget command.
+* **Keyman 19 (official release)** installed from
+ https://keyman.com/desktop. This is *not* optional for Delphi 12 CE
+ developers -- the dev kmshell.exe runtime-discovers its install path via
+ `TKeymanPaths.KeymanDesktopInstallPath()`, which is hardcoded to
+ `C:\Program Files (x86)\Keyman\Keyman Desktop\`. Without the install,
+ kmshell crashes at startup with `SKApplicationTitle has had a fatal
+ error...` before its main form appears. See section 7 (Running).
+* **Keyman Developer (official release)** is recommended in parallel for TIKE
+ overlay debugging -- TIKE loads templates and language data from the
+ installed Developer support files.
+* **7-Zip** (`choco install 7zip`) for extracting the CEF libcef payload.
+
+## 2. One-time setup
+
+### 2.1 Clone the repos
+
+Use the layout from [windows.md](windows.md#repository-paths). This guide
+assumes the keyman checkout is at `C:\Projects\keyman\keyman\` (matches the local
+machine). Use the same parent directory for the CEF and emsdk checkouts so
+the `KEYMAN_CEF4DELPHI_ROOT` / `EMSCRIPTEN_BASE` paths below line up.
+
+```bash
+# Git Bash -- use forward slashes via msys
+mkdir -p /c/Projects/keyman
+cd /c/Projects/keyman
+git clone https://github.com/keymanapp/keyman
+git clone https://github.com/keymanapp/CEF4Delphi_Binary
+git clone https://github.com/emscripten-core/emsdk
+```
+
+### 2.2 Environment variables
+
+```cmd
+:: Standard Windows cmd / PowerShell -- backslash paths
+SETX KEYMAN_ROOT "C:\Projects\keyman\keyman"
+SETX KEYMAN_CEF4DELPHI_ROOT "C:\Projects\keyman\CEF4Delphi_Binary"
+SETX EMSCRIPTEN_BASE "C:\Projects\keyman\emsdk\upstream\emscripten"
+```
+
+`SETX` is persistent but does not affect the current shell -- open a fresh
+shell to pick them up. Also add `\windows\lib` to `PATH` so
+Delphi's design-time packages can resolve.
+
+### 2.3 Emscripten
+
+```bash
+# Git Bash
+cd /c/Projects/keyman/emsdk
+emsdk install 3.1.58
+emsdk activate 3.1.58
+cd upstream/emscripten
+npm install
+```
+
+### 2.4 node.js via nvm-windows
+
+```cmd
+nvm install 20.16.0
+nvm use 20.16.0
+```
+
+### 2.5 CEF4Delphi_Binary -- switch to the pinned tag and extract libcef
+
+`common/windows/cef-checkout.sh` normally does this, but if you're driving
+Delphi from the IDE you'll want to do it once by hand. Some payload files
+exceed GitHub's 100 MB limit and ship as `.zip` files inside the repo; they
+must be extracted in place.
+
+```powershell
+$cefVer = (Get-Content C:\Projects\keyman\keyman\common\windows\CEF_VERSION.md).Trim()
+# e.g. 89.0.18
+
+Push-Location $env:KEYMAN_CEF4DELPHI_ROOT
+git reset --hard
+git clean -fd
+git fetch origin "v$cefVer"
+git switch "v$cefVer"
+
+if (-not (Test-Path .\libcef.dll)) {
+ Get-ChildItem *.zip | ForEach-Object {
+ & 'C:\Program Files\7-Zip\7z.exe' x -y $_.FullName
+ Remove-Item $_.FullName
+ }
+}
+Pop-Location
+```
+
+### 2.6 Install Keyman 19 official
+
+Install Keyman 19.0 from https://keyman.com/desktop. After install verify
+`C:\Program Files (x86)\Keyman\Keyman Desktop\kmshell.exe` exists and that
+`HKLM\SOFTWARE\WOW6432Node\Keyman\Keyman Engine` is populated. The overlay
+workflow in section 7 depends on these.
+
+### 2.7 Delphi 12 IDE Library Search Paths
+
+`build.sh` injects `dcc32 -I/-U/-R` flags that the IDE does not see. Without
+them, opening any of the Keyman .dproj files in Delphi triggers `F1026 File
+not found: jvcl.inc` / `jedi.inc` / `jcl.inc` / etc.
+
+These paths live in the per-user registry under
+`HKCU\Software\Embarcadero\BDS\23.0\Library\Win32` (and `...\Win64`), in the
+`Search Path` value, semicolon-separated. **Close Delphi first**; the IDE
+caches the value at startup.
+
+```powershell
+# Elevated PowerShell -- back up first
+reg export 'HKCU\Software\Embarcadero\BDS\23.0\Library' `
+ C:\Projects\keyman\keyman\delphi-library-paths.backup.reg /y
+
+$key32 = 'HKCU:\Software\Embarcadero\BDS\23.0\Library\Win32'
+$key64 = 'HKCU:\Software\Embarcadero\BDS\23.0\Library\Win64'
+
+$paths = @(
+ # Keyman common includes (mirrors DELPHIINCLUDES in delphi_flags.inc.sh)
+ 'C:\Projects\keyman\keyman\common\windows\delphi\ext\cef4delphi\source',
+ 'C:\Projects\keyman\keyman\common\windows\delphi\ext\dcpcrypt',
+ 'C:\Projects\keyman\keyman\common\windows\delphi\ext\jwa\Win32API',
+ 'C:\Projects\keyman\keyman\common\windows\delphi\ext\sentry',
+ 'C:\Projects\keyman\keyman\developer\src\ext\mbcolor',
+ 'C:\Projects\keyman\keyman\developer\src\ext\scfontcombobox',
+ # JCL / JVCL source roots (needed for TIKE)
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jcl\jcl\source\common',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jcl\jcl\source\prototypes',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jcl\jcl\source\vcl',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jcl\jcl\source\windows',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jcl\jcl\source\include', # jcl.inc
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jvcl\jvcl\design',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jvcl\jvcl\run',
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jvcl\jvcl\common', # jvcl.inc
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jvcl\jvcl\resources', # JvConsts.res
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi\jedi', # jedi.inc
+ 'C:\Projects\keyman\keyman\developer\src\ext\jedi' # parent for {$I jedi\jedi.inc}
+)
+
+foreach ($key in @($key32, $key64)) {
+ $cur = (Get-ItemProperty -Path $key -Name 'Search Path' `
+ -ErrorAction SilentlyContinue).'Search Path'
+ $merged = (@($cur -split ';') + $paths |
+ Where-Object { $_ } |
+ Select-Object -Unique) -join ';'
+ Set-ItemProperty -Path $key -Name 'Search Path' -Value $merged
+}
+```
+
+Restore via `reg import C:\Projects\keyman\keyman\delphi-library-paths.backup.reg`.
+
+Note: only the **Search Path** value was set, not Browsing Path or Debug DCU
+Path. Ctrl+click navigation to JCL/JVCL source and step-into-source debugging
+may need the same paths added there manually via the IDE
+(Tools > Options > Language > Delphi > Library).
+
+## 3. Local-only source patches
+
+These patches are required to compile against Delphi 12 (VER360) but are
+**not yet upstreamable** -- they coexist with Delphi 10.3 (VER340) by
+extending IFDEF chains rather than replacing the older guard. The patches
+must be reverted before any PR.
+
+> [!NOTE]
+> In some branches `windows/src/engine/engine.groupproj` carries a stale
+> reference to `inst\insthelper\insthelper.dproj` -- a path that doesn't
+> resolve because the real file is at
+> `windows\src\engine\insthelper\insthelper.dproj` (no `inst\` parent).
+> If your tree has the broken reference, either patch `engine.groupproj`
+> to drop the `inst\` segment or move the file. See section 3.3 and the
+> `[[delphi-12-local-patches]]` memory.
+
+### 3.1 Build-script: select Delphi version
+
+`KEYMAN_DELPHI_VERSION` selects which Delphi installation `build.sh` and the
+builder tool-detection look for. It defaults to `20.0` (Delphi 10.3, the CI
+target), so unset == existing CI behavior.
+
+For any Delphi 12 installation (Professional or CE), set it to `23.0`:
+
+```powershell
+# PowerShell (persistent for your user)
+[Environment]::SetEnvironmentVariable('KEYMAN_DELPHI_VERSION', '23.0', 'User')
+```
+
+```bash
+# Git Bash (per-shell)
+export KEYMAN_DELPHI_VERSION=23.0
+```
+
+Without this, `build.sh` looks for
+`C:\Program Files (x86)\Embarcadero\Studio\20.0\bin\rsvars.bat` and the builder
+platform system looks for `Studio\20.0\bin\dcc32.exe` to decide whether to include
+`win,delphi`-gated targets -- both fail silently on a Delphi-12-only machine.
+
+**Delphi 12 Professional** — setting `KEYMAN_DELPHI_VERSION=23.0` is sufficient.
+CLI compilation works normally; `build.sh` targets run end-to-end.
+
+**Delphi 12 Community Edition** — additionally set `KEYMAN_DELPHI_CE=1`. CE blocks
+CLI compilation, so `delphi_msbuild` switches to interactive mode: instead of
+invoking `msbuild.exe`, the script pauses and prompts:
+
+```
+Delphi CE: CLI compilation is not available.
+Please build in the Delphi IDE now, then press Enter to continue.
+```
+
+```powershell
+[Environment]::SetEnvironmentVariable('KEYMAN_DELPHI_CE', '1', 'User')
+```
+
+```bash
+export KEYMAN_DELPHI_CE=1
+```
+
+### 3.2 keyman.exe uiAccess strip (LOCAL ONLY, not committed)
+
+This patch is **deliberately not committed** to this branch — it would
+change release-signed-binary semantics in a way upstream CI doesn't want.
+Apply it manually as a working-tree-only edit if you need to overlay an
+unsigned dev `keyman.exe`. Run after a fresh checkout, before building
+keyman.dproj:
+
+```powershell
+$f = 'C:\Projects\keyman\keyman\windows\src\engine\keyman\manifest.in'
+(Get-Content $f) -replace 'uiAccess="true"', 'uiAccess="false"' | Set-Content $f -Encoding UTF8
+```
+
+Then re-run `build.sh build` for `windows/src/engine/keyman` (the pre-build
+`build_manifest.res` step regenerates manifest.xml / manifest.res before prompting
+for the IDE build), do a **Clean + Build** of keyman.dproj in Delphi IDE
+(see [[delphi-incremental-build-res-cache]] — Build alone won't re-link the
+new manifest.res), and run `windows/src/engine/build.sh install` elevated (section 5.3).
+
+**Why:** Windows refuses to launch any unsigned binary that declares
+`uiAccess="true"`, returning `Access is denied` (error 8235). The overlay
+workflow (section 7) replaces the signed installed keyman.exe with an
+unsigned dev build, so uiAccess must be turned off for the overlaid dev
+binary to launch. Trade-off: keyboard injection into elevated apps stops
+working until uiAccess is restored and the binary is signed via Keyman's
+test-cert pipeline.
+
+To revert (keep your tree CI-clean): `git checkout -- windows/src/engine/keyman/manifest.in`.
+
+### 3.3 engine.groupproj / insthelper path ambiguity
+
+There is a real path ambiguity in the engine tree:
+
+* The actual `insthelper.dproj` lives at
+ `windows\src\engine\insthelper\insthelper.dproj`.
+* Historical `build.sh` plumbing (and at least one variant of
+ `engine.groupproj` carried in some branches) refers to it as
+ `inst\insthelper\insthelper.dproj` -- i.e. with an extra `inst\`
+ segment that does not resolve.
+
+In the current tree, `engine.groupproj` does not include an `insthelper`
+entry at all (the group only builds keyman, kmcomapi, tsysinfo,
+tsysinfox64), so "Build All Projects" on the group works -- but
+`insthelper.dproj` must then be opened and built on its own. If you encounter
+a tree where the groupproj does carry the broken `inst\insthelper\...` path,
+the two clean workarounds are:
+
+(a) Patch `engine.groupproj` to drop the `inst\` segment -- change every
+ `inst\insthelper\insthelper.dproj` to `insthelper\insthelper.dproj` and
+ update the `` MSBuild attribute the same way.
+
+(b) Move the directory: `git mv windows/src/engine/insthelper
+ windows/src/engine/inst/insthelper` so the existing groupproj path
+ resolves. This is invasive and not recommended.
+
+In either case, also add `insthelper` to the `Build` / `Clean` / `Make`
+`CallTarget` lists if you want "Build All Projects" to pick it up. See the
+GAP note at the top of section 3 about `[[delphi-12-local-patches]]`.
+
+### 3.4 JCL Boolean -> BOOL casts, plus JclWin32 -> Winapi.Windows on CreateMutex
+
+`developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas`
+
+Delphi 12 dropped the implicit `Boolean` -> `BOOL` conversion. Every Win32
+sync API call must be wrapped:
+
+```pascal
+// - Result := CreateEvent(SecAttr, Manual, Signaled, PChar(Name));
+// + Result := CreateEvent(SecAttr, BOOL(Manual), BOOL(Signaled), PChar(Name));
+```
+
+Call sites that need the cast: `CreateEvent`, `OpenEvent`,
+`CreateWaitableTimer`, `OpenWaitableTimer`, `OpenSemaphore`, `CreateMutex`,
+`OpenMutex`. Other JCL units likely need the same treatment -- patch
+reactively.
+
+The `CreateMutex` site additionally needs a namespace switch -- the BOOL
+cast alone is not sufficient. Per `local-delphi-12-patches.patch`:
+
+```pascal
+// - FHandle := JclWin32.CreateMutex(SecAttr, InitialOwner, PChar(Name));
+// + FHandle := {$IFDEF HAS_UNITSCOPE}Winapi.{$ENDIF}Windows.CreateMutex(
+// + SecAttr, BOOL(InitialOwner), PChar(Name));
+```
+
+The reason is that `JclWin32.CreateMutex` is a `JclWin32`-namespaced
+re-declaration that Delphi 12 resolves ambiguously against
+`Winapi.Windows.CreateMutex`, producing a "Cardinal vs LongBool" type
+mismatch that the BOOL cast can't fix. Routing the call through
+`Winapi.Windows.CreateMutex` directly bypasses the ambiguity. The
+`{$IFDEF HAS_UNITSCOPE}` guard preserves compatibility with older Delphis
+that still use the un-namespaced `Windows` unit.
+
+### 3.5 JVCL `OldCreateOrder` removed in Delphi 11
+
+`developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas` (line ~126):
+
+```pascal
+// - if OldCreateOrder then
+// - DoCreate;
+// + {$IF Defined(VER350) or Defined(VER360)}
+// + DoCreate;
+// + {$ELSE}
+// + if OldCreateOrder then DoCreate;
+// + {$IFEND}
+```
+
+`TCustomForm.OldCreateOrder` was removed in Delphi 11; modern Delphi behaves
+as if it were always `True`.
+
+### 3.6 mbcolor: teach it about VER350/VER360
+
+`developer/src/ext/mbcolor/mxs.inc`
+
+Add two new IFDEF blocks mirroring the existing VER340 block:
+
+```pascal
+{$ifdef VER350} // Delphi 11
+{$define DELPHI_5_UP}
+{$define DELPHI_6_UP}
+{$define DELPHI_7_UP}
+{$define DELPHI_8_UP}
+{$define DELPHI_9_UP}
+{$define DELPHI_10_UP}
+{$endif}
+
+{$ifdef VER360} // Delphi 12
+{$define DELPHI_5_UP}
+{$define DELPHI_6_UP}
+{$define DELPHI_7_UP}
+{$define DELPHI_8_UP}
+{$define DELPHI_9_UP}
+{$define DELPHI_10_UP}
+{$endif}
+```
+
+Without this, mbcolor's `{$IFDEF DELPHI_6_UP}, Variants{$ENDIF}` silently
+drops `Variants` from the `uses` clause, causing `E2003 Undeclared
+identifier: 'null'` at `HTMLColors.pas:290`.
+
+### 3.7 devtools: map VER350 / VER360 to Studio version
+
+`common/windows/delphi/tools/devtools/SourceRootPath.pas`
+
+The constant is named `DelphiMajorVersion` (not `DelphiVersion`). The existing
+baseline (not patched by us) maps VER310->'18.0', VER320->'19.0',
+VER330->'20.0', VER340->'21.0'. The patch adds the two newer compilers:
+
+Before (baseline, abridged):
+
+```pascal
+{$IFDEF VER310} const DelphiMajorVersion = '18.0';
+{$ELSE}{$IFDEF VER320} const DelphiMajorVersion = '19.0';
+{$ELSE}{$IFDEF VER330} const DelphiMajorVersion = '20.0';
+{$ELSE}{$IFDEF VER340} const DelphiMajorVersion = '21.0';
+{$ELSE}
+ERROR: must define Delphi version
+{$ENDIF}{$ENDIF}{$ENDIF}{$ENDIF}
+```
+
+After (with VER350 / VER360 inserted before the `ERROR:` fallback):
+
+```pascal
+{$IFDEF VER340} const DelphiMajorVersion = '21.0';
+{$ELSE}{$IFDEF VER350} const DelphiMajorVersion = '22.0'; // 11 (NEW)
+{$ELSE}{$IFDEF VER360} const DelphiMajorVersion = '23.0'; // 12 (NEW)
+{$ELSE}
+ERROR: must define Delphi version
+{$ENDIF}{$ENDIF}{$ENDIF}
+```
+
+Note that the VER340->'21.0' baseline reflects upstream's choice to bump the
+Studio dir per CE-license rev, not the more conventional 10.3->'20.0'
+mapping. Don't "fix" the existing rows -- only append the two new ones.
+
+Without one of these defined, the `$ELSE` branch hits the `ERROR: must
+define Delphi version` literal which manifests as `E2029 Declaration
+expected`.
+
+### 3.8 Indy UTF-8 URL tripwire
+
+`common/windows/delphi/web/Keyman.System.HttpServer.Base.pas`
+
+Wrap the `ERROR! Check if this is still needed with Delphi update` tripwire
+in nested `{$IFNDEF VER340}{$IFNDEF VER350}{$IFNDEF VER360}`. The Indy URL
+encoding workaround is still required on 12; just extending the guard
+preserves the workaround.
+
+### 3.9 FixedTrackbar tripwire
+
+`common/windows/delphi/components/FixedTrackbar.pas`
+
+Same pattern -- extend the inner `{$MESSAGE ERROR}` guard with
+`{$IFNDEF VER350}{$IFNDEF VER360}`.
+
+### 3.10 CleartypeDrawCharacter EnumFontFamiliesEx return type
+
+`common/windows/delphi/general/CleartypeDrawCharacter.pas` (line ~594)
+
+`Winapi.Windows.EnumFontFamiliesEx` returns `Integer` starting with Delphi
+10.4. The existing guard only covered VER340; extend it:
+
+```pascal
+// - {$IFDEF VER340}
+// + {$IF Defined(VER340) or Defined(VER350) or Defined(VER360)}
+ if EnumFontFamiliesEx(...) <> 0 then
+// - {$ELSE}
+// + {$ELSE}
+ if EnumFontFamiliesEx(...) then
+// - {$ENDIF}
+// + {$IFEND}
+```
+
+### 3.11 JsonUtil ToChars signature change
+
+`common/windows/delphi/general/JsonUtil.pas` (line ~55)
+
+Delphi 11 added a required `Options: TJSONOutputOptions` parameter to
+`System.JSON.TJSONAncestor.ToChars`:
+
+```pascal
+{$IF Defined(VER350) or Defined(VER360)}
+ obj.ToChars(builder, []);
+{$ELSE}
+ obj.ToChars(builder);
+{$IFEND}
+```
+
+### 3.12 Generated `.res` files
+
+The local working tree will also show binary diffs to many `.res` files
+under `developer/src/`, `windows/src/desktop/`, `windows/src/engine/`. These
+are produced by `rc.exe` during preflight (icons / manifest / version) --
+they are *not* patches but should be excluded from any PR. See section 5.
+
+## 4. Build order
+
+Keyman's repo has several cross-project dependencies that are **not**
+expressed in `.dproj` or `.groupproj` files -- they're implicit because the
+producer is a Delphi tool that emits code consumed by another Delphi
+project, or because a Delphi `.res` embeds the output of another Delphi
+`.exe`. Doing things in the wrong order produces fatal `dcc32` errors with
+non-obvious messages.
+
+### CLI-buildable on Delphi 12 CE
+
+These do not need the IDE:
+
+* **Keyman Core** -- `./core/build.sh` (Rust / C++ via meson+ninja)
+* **kmcmplib** -- `./developer/src/kmcmplib/build.sh build` (Rust / C++)
+* **TypeScript modules** -- `kmc`, `kmc-kmn`, `kmc-ldml`, `kmc-package`,
+ `kmc-keyboard-info`, `kmc-analyze`, `kmc-copy`, `kmc-generate`,
+ `kmc-model`, `kmc-model-info` (each has its own `build.sh`)
+* **KeymanWeb** -- `./web/build.sh` (TypeScript / Emscripten)
+* **C++ engine pieces** -- `windows/src/engine/{keyman32,kmtip,keymanhp,
+ kmrefresh,mcompile,testhost}` (msbuild)
+
+Build these via the normal `build.sh` chain. They have no Delphi codegen
+dependency.
+
+> [!CAUTION]
+> `build.sh` cascades dependencies via `--builder-dep-parent`, so even a
+> "non-Delphi" CLI build can pull a Delphi tool in -- which then fails on
+> CE with `This version of the product does not support command line
+> compiling`. Build the tool by hand in the IDE first, or pass `--no-deps`.
+
+### Delphi-IDE-only on CE
+
+* **All of `windows/src/engine/`** -- `keyman.dproj`, `kmcomapi.dproj`,
+ `insthelper.dproj`, `tsysinfo.dproj`, `tsysinfox64.dproj`
+ (`engine.groupproj`)
+* **All of `windows/src/desktop/`** -- kmshell, kmbrowserhost, kmconfig,
+ insthelp, setup (`desktop.groupproj`)
+* **All of `developer/src/`** -- TIKE, kmconvert, setup
+ (`developer.groupproj`)
+* **All Delphi tools in `common/windows/delphi/tools/`** -- devtools,
+ build_standards_data, certificates, sentrytool, verify_signatures
+
+### The cross-project dependency graph
+
+The non-obvious dependencies, in the order they must be satisfied:
+
+#### (a) devtools.dproj BEFORE keyman.dproj / setup.dproj / kmshell.dproj
+
+`keyman.dpr` (engine) and `kmshell.dpr` (desktop) both depend on
+`MessageIdentifierConsts.pas`, a ~1500-line file generated from
+`windows/src/desktop/kmshell/xml/strings.xml` via
+`devtools.exe -buildmessageconstants`. The file is `.gitignored` -- it does
+not exist in a fresh checkout. Failure mode:
+
+```
+F1026 File not found: '...\windows\src\global\delphi\cust\MessageIdentifierConsts.pas'
+```
+
+`setup.dpr` depends on ~32 `Keyman.Setup.System.Locale..pas` files
+generated from `windows/src/desktop/setup/locale/*.xml` via
+`devtools.exe -buildsetupstrings`.
+
+#### (b) build_standards_data.dproj BEFORE TIKE.dproj
+
+`Keyman.System.LanguageCodeUtils` and friends reference five generated
+BCP-47 registry units in `common/windows/delphi/standards/`. The biggest is
+`Keyman.System.Standards.LangTagsRegistry.pas` (~2.4 MB). All five are
+`.gitignored`. Failure mode:
+
+```
+F1026 File not found: '...\Keyman.System.Standards.BCP47SubtagRegistry.pas'
+```
+
+#### (c) tsysinfox64.dproj BEFORE tsysinfo.dproj
+
+`tsysinfo.dproj` is Win32 but embeds the Win64 `tsysinfox64.exe` as a
+binary resource (`tsysinfo_x64.res`) so it can spawn the 64-bit helper
+process at runtime. The default Build All order in `engine.groupproj` is
+**keyman, kmcomapi, tsysinfo, tsysinfox64, insthelper** -- so tsysinfo is
+attempted before the .exe it embeds exists. Failure mode:
+
+```
+F1026 File not found: 'tsysinfo_x64.res'
+```
+
+**Right order:**
+
+1. Build `tsysinfox64.dproj` in the IDE (Win64 / Debug).
+2. Copy: `Copy-Item windows/src/engine/tsysinfox64/bin/Win64/Debug/tsysinfox64.exe windows/src/engine/tsysinfo/tsysinfox64.bin`
+3. From `windows/src/engine/tsysinfo/`: `rc /nologo tsysinfo_x64.rc`
+4. Now build `tsysinfo.dproj`.
+
+#### (d) kmcmplib (CLI build) BEFORE TIKE.dproj at runtime
+
+TIKE links to `kmcmplib-19.dll` (the .kmn keyboard compiler). It builds via
+`./developer/src/kmcmplib/build.sh build` -- no IDE needed. TIKE compiles
+fine without it, but throws "kmcmplib-19.dll not found" at runtime on any
+compile action.
+
+#### (e) keyman.dproj BEFORE kmshell.dproj runtime
+
+`kmshell.exe` is the Desktop UI; the actual engine work happens in
+`keyman.exe`, which kmshell spawns via the COM `IKeymanController` CLSID.
+kmshell compiles without keyman.exe -- the failure is at runtime: the
+dev kmshell shows "Keyman failed to start" when enabling a keyboard.
+
+#### (f) regsvr32 kmcomapi.dll BEFORE running kmshell
+
+`kmcomapi.dll` exposes the COM objects kmshell instantiates at startup.
+Without registration, `CoCreateInstance` returns `REGDB_E_CLASSNOTREG`
+(`Class not registered`) and kmshell dies before its main form appears.
+`windows/src/engine/build.sh install` handles this automatically (section 5.3).
+
+### The canonical build order
+
+For a clean checkout, this is the order that actually works:
+
+1. CLI build everything CLI-buildable: `./core/build.sh build`,
+ `./developer/src/kmcmplib/build.sh build`, TypeScript modules,
+ KeymanWeb. The C++ engine pieces can wait.
+2. Build devtools: `cd common/windows/delphi/tools/devtools && ./build.sh build`
+ (prompted to build `devtools.dproj` in IDE; press Enter when done).
+3. Build build_standards_data: `cd common/windows/delphi/tools/build_standards_data && ./build.sh build`
+ (prompted to build `build_standards_data.dproj` in IDE; after Enter, the script
+ automatically runs all five codegen invocations).
+4. Build engine components: `cd windows/src/engine && ./build.sh build`
+ (prompted for each Delphi project in dependency order; `tsysinfo.dproj` is
+ prompted after `tsysinfox64.dproj` because `build.sh` handles the dependency).
+5. Build desktop components: `cd windows/src/desktop && ./build.sh build`
+ (prompted for each desktop Delphi project).
+6. Build developer components: `cd developer/src && ./build.sh build`
+ (prompted for TIKE, kmconvert, and developer setup).
+7. Overlay dev builds (elevated Git Bash): `cd windows/src/engine && ./build.sh install`
+ then `cd windows/src/desktop && ./build.sh install` (section 5.3).
+
+## 5. Shell-based CE workflow
+
+With `KEYMAN_DELPHI_CE=1` set (section 3.1), the existing `build.sh` scripts
+handle the CE constraint interactively. At each step that would normally invoke
+`msbuild` through Delphi, the script pauses and prints:
+
+```
+Delphi CE: CLI compilation is not available.
+Please build in the Delphi IDE now, then press Enter to continue (or Ctrl-C to abort).
+```
+
+Pre-build steps (rc compilation, manifest generation) and post-build steps
+(binary copying, codegen) run automatically around your IDE builds.
+
+### 5.1 Pre-build resource compilation
+
+`build_version.res()` and `build_manifest.res()` use `rc.exe` from the Visual
+Studio environment -- they work on CE without modification. They run inside each
+project's `build.sh build` before the IDE prompt appears.
+
+> [!IMPORTANT]
+> After building in the IDE following a resource change, use **right-click →
+> Clean, then Build** in Delphi. An incremental Build silently embeds the stale
+> cached `.res`.
+
+### 5.2 Codegen (devtools.exe, build_standards_data.exe)
+
+Build the tools first via their `build.sh` scripts, which prompt for the IDE build
+then run codegen automatically:
+
+```bash
+# Git Bash (KEYMAN_DELPHI_CE=1 and KEYMAN_DELPHI_VERSION=23.0 must be exported)
+
+# Build devtools: prompted to build devtools.dproj in IDE, then exits
+cd "$KEYMAN_ROOT/common/windows/delphi/tools/devtools"
+./build.sh build
+
+# Build build_standards_data: prompted for IDE build, then auto-runs all five
+# build_standards_data.exe invocations to generate the registry .pas files
+cd "$KEYMAN_ROOT/common/windows/delphi/tools/build_standards_data"
+./build.sh build
+```
+
+The `-buildmessageconstants`, `-buildsetupstrings`, and `-buildlocaleindex` codegen
+steps run automatically when you build their dependent projects
+(`windows/src/global/delphi`, `windows/src/desktop/setup`, `windows/src/desktop/kmshell`).
+
+All generated `.pas` files are `.gitignored` and must be regenerated after `git clean -fdx`.
+
+### 5.3 Overlay dev builds
+
+From an **elevated** Git Bash (right-click Git Bash → Run as administrator):
+
+```bash
+cd "$KEYMAN_ROOT/windows/src/engine"
+./build.sh install
+
+cd "$KEYMAN_ROOT/windows/src/desktop"
+./build.sh install
+```
+
+`windows/src/engine/kmcomapi/build.sh install` re-registers `kmcomapi.dll` via
+`regsvr32` automatically.
+
+After overlay, launch Keyman via the Start Menu (or the installed `kmshell.exe`
+path) -- not from the dev tree -- so `TKeymanPaths.KeymanDesktopInstallPath()`
+finds the support files.
+
+> [!NOTE]
+> An alternative workflow -- keep `uiAccess="true"` in `manifest.in` (i.e.
+> skip the section 3.2 patch) and skip overlaying `keyman.exe` -- was
+> tried and abandoned. With `uiAccess="true"`, the unsigned dev
+> `keyman.exe` is refused by Windows (`error=8235`) and the engine never
+> starts. The patch + overlay path documented here is the only flow that
+> works without a signing cert.
+
+## 6. Running Keyman from the dev tree
+
+Because of `TKeymanPaths.KeymanDesktopInstallPath()`, the dev workflow is:
+
+1. Install Keyman 19 official from keyman.com (once).
+2. Build dev binaries via `build.sh build` with `KEYMAN_DELPHI_CE=1` (per section 4).
+3. From an elevated Git Bash: `cd windows/src/engine && ./build.sh install`, then
+ `cd windows/src/desktop && ./build.sh install` (section 5.3).
+4. Launch Keyman from the Start Menu.
+
+If launching keyman.exe (the engine) directly from the dev `bin\` folder
+returns `Could not find keyman.exe (error=8235)`, check that section 3.2's
+uiAccess patch was applied and that the project was Clean + Built (not just
+Built) after patching `manifest.in`.
+
+## 7. Debugging
+
+### 7.1 Run With Host Application
+
+For DLLs (kmcomapi, keymanhp) and projects that don't have a main entry
+point: in Delphi, **Run > Parameters > Host Application** =
+`C:\Program Files (x86)\Keyman\Keyman Desktop\kmshell.exe` (or
+`...keyman.exe`). Set breakpoints in the DLL source, then Run (F9).
+
+### 7.2 Attach to Process
+
+For long-running processes (an already-launched kmshell, the TSF text
+service): **Run > Attach to Process**, pick the process, then set
+breakpoints. The dev tree's `.dproj` projects emit `.dcu` files with full
+debug info into `bin\Win32\Debug\` and `bin\Win64\Debug\`, so as long as
+you've overlaid the binary you'll get proper symbols.
+
+### 7.3 What you get -- and don't get -- from a vanilla install
+
+The installed Keyman 19 from keyman.com ships **stripped** PDBs at best;
+practically, you'll see only export-table symbols when attaching. The
+overlay workflow replaces the installed binaries with dev builds whose
+debug info lives next to the source -- after overlay you can step through
+Keyman source in the IDE. Without overlay, the IDE will load the source
+file when you breakpoint but the line numbers will not match the running
+binary.
+
+For C++ pieces (keyman32, kmtip, mcompile), use Visual Studio's Attach to
+Process and load the `.pdb` from `windows/bin/`.
+
+## 8. Troubleshooting
+
+### `This version of the product does not support command line compiling`
+
+You're hitting the CE block on `dcc32`. Don't try to drive Delphi from
+`build.sh`; build the relevant `.dproj` in the IDE by hand. If `build.sh`
+pulled Delphi in as a transitive dep, pass `--no-deps` or build the
+upstream Delphi project first.
+
+### `F1026 File not found: 'MessageIdentifierConsts.pas'`
+
+Codegen wasn't run. Build `devtools.dproj` in the IDE, then run section 5.2
+step (a).
+
+### `F1026 File not found: 'Keyman.Setup.System.Locale..pas'` (one of ~32)
+
+Codegen wasn't run. Build `devtools.dproj` then run section 5.2 step (b).
+Trailing backslashes on the two args matter.
+
+### `F1026 File not found: 'Keyman.System.Standards.BCP47SubtagRegistry.pas'`
+
+`build_standards_data.exe` wasn't run. Build the `.dproj` then run section
+5.2 (BSD block).
+
+### `F1026 File not found: 'tsysinfo_x64.res'`
+
+Group build order issue. Build tsysinfox64 first, copy the exe to
+tsysinfo's source dir as `tsysinfox64.bin`, run `rc /nologo tsysinfo_x64.rc`,
+then build tsysinfo. See section 4 (c).
+
+### `F1026 File not found: 'version.res'` or `'manifest.res'`
+
+Pre-build resource compilation didn't run. Re-run `build.sh build` for the
+project (which calls `build_version.res` / `build_manifest.res` before the IDE
+prompt), or invoke `rc.exe` manually from the project directory.
+
+### `F1026 File not found: 'jvcl.inc'` / `'jedi/jedi.inc'` / `'jcl.inc'`
+
+Library Search Paths not in the registry. See section 2.7. Close and
+reopen Delphi after editing the registry; the IDE caches the value at
+startup.
+
+### `E2010 Incompatible types: 'Cardinal' and 'Boolean'` (JCL)
+
+JCL Boolean -> BOOL casts missing. See section 3.4.
+
+### `E2003 Undeclared identifier: 'OldCreateOrder'` (JVCL)
+
+JvComponent.pas not patched. See section 3.5.
+
+### `E2003 Undeclared identifier: 'null'` (HTMLColors)
+
+mbcolor's `mxs.inc` not patched for VER350/VER360 -- the `Variants` unit
+was silently dropped from the `uses` clause. See section 3.6.
+
+### `E2029 Declaration expected` near SourceRootPath.pas
+
+devtools/SourceRootPath.pas hit the `{$ELSE} {$MESSAGE ERROR}` fallback.
+See section 3.7.
+
+### `E2012 Type of expression must be BOOLEAN` near EnumFontFamiliesEx
+
+CleartypeDrawCharacter.pas guard not extended for VER350/VER360. See
+section 3.10.
+
+### `F2613 Unit 'JvComponentBase' not found`
+
+JVCL `run/` (or `design/`) is missing from Library Search Path. See
+section 2.7.
+
+### Delphi pops up an "Unsupported CEF version" dialog
+
+The CEF4Delphi_Binary checkout isn't on the tag in `CEF_VERSION.md`. See
+section 2.5. Don't ignore the dialog -- the CEF4Delphi Pascal binding and
+the libcef binary must match exactly.
+
+### `SKApplicationTitle has had a fatal error...` on kmshell launch
+
+kmshell can't find its strings.xml / locale files because
+`KeymanDesktopInstallPath()` is hardcoded to `Program Files`, and Keyman
+19 official isn't installed (or its install support files were removed by
+an aborted dev experiment). Install Keyman 19 official from keyman.com.
+
+### `Class not registered ClassID {CF46549D-...}` on kmshell launch
+
+`kmcomapi.dll` isn't registered. From an elevated Git Bash:
+
+```bash
+cd "$KEYMAN_ROOT/windows/src/engine/kmcomapi"
+./build.sh install
+```
+
+This re-registers the DLL automatically. If you want to run `regsvr32` directly:
+
+```powershell
+& regsvr32 /s 'C:\Program Files (x86)\Common Files\Keyman\Keyman Engine\kmcomapi.dll'
+```
+
+### `Could not find keyman.exe (error=8235)`
+
+Windows blocked launch of an unsigned `uiAccess="true"` binary. Patch
+`windows/src/engine/keyman/manifest.in` per section 3.2, Clean + rebuild
+`keyman.dproj`, then re-run `windows/src/engine/build.sh install` elevated.
+
+---
+
+## Upstream candidates
+
+If you'd like to PR some of these changes back upstream, the patches split
+into two groups based on whether they touch Keyman-owned code or vendored
+third-party code.
+
+**Upstreamable (Keyman-owned, additive, CI-safe with `KEYMAN_DELPHI_VERSION`
+unset):**
+
+* `resources/build/win/configure_environment.inc.sh` + `delphi_environment.inc.sh`
+ -- `KEYMAN_DELPHI_VERSION` env-var override (default preserves 10.3 behavior).
+* `resources/build/win/environment.inc.sh` -- `KEYMAN_DELPHI_CE=1` interactive
+ mode for `delphi_msbuild` (no-op when unset).
+* `common/windows/delphi/tools/devtools/SourceRootPath.pas` -- VER350/VER360
+ cases for `DelphiMajorVersion`.
+* `common/windows/delphi/web/Keyman.System.HttpServer.Base.pas` -- extend
+ VER tripwire to accept VER340/VER350/VER360.
+* `common/windows/delphi/components/FixedTrackbar.pas` -- same tripwire
+ pattern.
+* `common/windows/delphi/general/CleartypeDrawCharacter.pas` -- integer-return
+ comparison for `EnumFontFamiliesEx` on VER340+.
+* `common/windows/delphi/general/JsonUtil.pas` -- two-arg `ToChars` on
+ VER350/VER360 (Delphi 11+ RTL change).
+* `windows/src/engine/engine.groupproj` -- drop the broken
+ `inst\insthelper\insthelper.dproj` reference.
+
+These could ship as a single "Support compiling under Delphi 11/12" PR
+without conflicting with the broader Delphi-removal effort
+([keymanapp/keyman#4599](https://github.com/keymanapp/keyman/issues/4599)).
+
+**Local-only (vendored third-party):**
+
+* `developer/src/ext/jedi/jcl/jcl/source/common/JclSynch.pas` -- JCL Boolean
+ -> BOOL casts. The right fix is to refresh the bundled JCL from upstream
+ (project-jedi.org) when JCL releases a Delphi 12-compatible version.
+* `developer/src/ext/jedi/jvcl/jvcl/run/JvComponent.pas` -- JVCL
+ OldCreateOrder removal. Same -- pull from upstream JVCL when available.
+* `developer/src/ext/mbcolor/mxs.inc` -- third-party mbcolor library.
+ Upstream maintenance status unclear.
+
+These should *not* be PR'd upstream-Keyman as-is; the bundled third-party
+copies should be refreshed from their respective maintainers instead.
+
+## Reverting before a PR
+
+Before opening a PR, revert all section 3 patches and remove the section
+2.7 registry entries. The simplest check:
+
+```bash
+git status # should show only intentional changes
+git diff -- '*.res' # should be empty (preflight .res are local-only)
+```
+
+If `git diff` shows any of the files mentioned in section 3, revert them
+with `git checkout -- ` before pushing.
+
+Restore the original Library Search Path values:
+
+```powershell
+reg import C:\Projects\keyman\keyman\delphi-library-paths.backup.reg
+```
diff --git a/docs/build/windows.md b/docs/build/windows.md
index edc756340ff..a595a29f633 100644
--- a/docs/build/windows.md
+++ b/docs/build/windows.md
@@ -257,6 +257,11 @@ of appropriate node versions during builds.
for a short time. (We are actively working to remove Delphi dependencies
given the licensing issues with using it.)
+ * If you only have access to **Delphi 12 Community Edition**, see
+ [windows-d12.md](windows-d12.md) for an IDE-based workflow that papers over
+ the missing command-line compiler. The patches and helper scripts described
+ there are local-only and not required for the standard 10.3 / Pro flow.
+
Start Delphi IDE once after installation as it will create various environment
files and take you through required registration.
diff --git a/resources/build/win/configure_environment.inc.sh b/resources/build/win/configure_environment.inc.sh
index d0bce72a812..1d274c67be1 100644
--- a/resources/build/win/configure_environment.inc.sh
+++ b/resources/build/win/configure_environment.inc.sh
@@ -65,9 +65,11 @@ _build_vs_environment() {
_locate_rsvars() {
#
- # Delphi Compiler Configuration - Delphi 10.3.2
+ # Delphi Compiler Configuration - defaults to Delphi 10.3 (BDS 20.0).
+ # Override via the KEYMAN_DELPHI_VERSION environment variable to build with
+ # a newer Delphi installation (e.g., KEYMAN_DELPHI_VERSION=23.0 for Delphi 12).
#
- DELPHI_VERSION=20.0
+ DELPHI_VERSION="${KEYMAN_DELPHI_VERSION:-20.0}"
DCC32PATH="$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$DELPHI_VERSION\\bin")"
RSVars_path="$DCC32PATH/rsvars.bat"
}
diff --git a/resources/build/win/delphi_environment.inc.sh b/resources/build/win/delphi_environment.inc.sh
index 0465bcb6930..a68b08421c4 100644
--- a/resources/build/win/delphi_environment.inc.sh
+++ b/resources/build/win/delphi_environment.inc.sh
@@ -13,7 +13,10 @@
DELPHIWARNINGS=(-W-MESSAGE_DIRECTIVE -W-IMPLICIT_STRING_CAST -W-IMPLICIT_STRING_CAST_LOSS -W-EXPLICIT_STRING_CAST -W-EXPLICIT_STRING_CAST_LOSS -W-CVT_WCHAR_TO_ACHAR -W-CVT_NARROWING_STRING_LOST -W-CVT_ACHAR_TO_WCHAR -W-CVT_WIDENING_STRING_LOST -W-UNICODE_TO_LOCALE -W-LOCALE_TO_UNICODE -W-IMPLICIT_VARIANTS -W-IMPLICIT_INTEGER_CAST_LOSS -W-IMPLICIT_CONVERSION_LOSS -W-COMBINING_SIGNED_UNSIGNED64 -W-COMBINING_SIGNED_UNSIGNED64)
# !ENDIF
-DELPHI_VERSION=20.0
+# DELPHI_VERSION defaults to Delphi 10.3 (BDS 20.0). Override via the
+# KEYMAN_DELPHI_VERSION environment variable to build with a newer Delphi
+# installation (e.g., KEYMAN_DELPHI_VERSION=23.0 for Delphi 12).
+DELPHI_VERSION="${KEYMAN_DELPHI_VERSION:-20.0}"
DCC32PATH="$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$DELPHI_VERSION\\bin")"
source "$KEYMAN_ROOT/resources/build/win/delphi_environment_generated.inc.sh"
diff --git a/resources/build/win/environment.inc.sh b/resources/build/win/environment.inc.sh
index 9408c6660ef..eed75080f28 100644
--- a/resources/build/win/environment.inc.sh
+++ b/resources/build/win/environment.inc.sh
@@ -100,6 +100,15 @@ tds2dbg() {
}
delphi_msbuild() {
+ # When KEYMAN_DELPHI_CE=1 the Delphi Community Edition blocks CLI
+ # compilation. Pause and prompt the developer to build in the IDE.
+ if [[ "${KEYMAN_DELPHI_CE:-}" == "1" ]]; then
+ local project="${1:-}"
+ builder_echo warning "Delphi CE: CLI compilation is not available."
+ builder_echo warning "Please build ${project} in the Delphi IDE now, then press Enter to continue (or Ctrl-C to abort)."
+ read -r _
+ return 0
+ fi
run_in_delphi_env msbuild.exe "$@" "$DELPHI_MSBUILD_FLAG_DEBUG"
}
diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh
index 6ec2fbcebff..7e9ea36a0e4 100755
--- a/resources/builder.inc.sh
+++ b/resources/builder.inc.sh
@@ -2323,7 +2323,8 @@ builder_describe_platform() {
# Detect delphi compiler (see also delphi_environment.inc.sh)
if builder_is_windows; then
local ProgramFilesx86="$(cygpath -w -F 42)"
- if [[ -x "$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\20.0\\bin\\dcc32.exe")" ]]; then
+ local _delphi_version="${KEYMAN_DELPHI_VERSION:-20.0}"
+ if [[ -x "$(cygpath -u "$ProgramFilesx86\\Embarcadero\\Studio\\$_delphi_version\\bin\\dcc32.exe")" ]]; then
builder_installed_tools+=(delphi)
fi
fi
diff --git a/windows/src/engine/engine.groupproj b/windows/src/engine/engine.groupproj
index bb2548b6103..150ea34d9cf 100644
--- a/windows/src/engine/engine.groupproj
+++ b/windows/src/engine/engine.groupproj
@@ -15,9 +15,6 @@
-
-
-
Default.Personality.12
@@ -62,23 +59,14 @@
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+