Portal modules, localization and keyboard shortcuts
Portal modules are statically imported application code, similar to VSCode contribution points. A deployment chooses an explicit module list at its composition root and rebuilds to change that list. There is no network code loading, marketplace, runtime installation, eval, mutable plugin registry, or implicit service discovery.
packages/portal/index.mjs provides compilePortalModules(modules), portalContributionsProvider(modules), the PortalContributions ngin Query, RunPortalCommand ngin Action and composePortalViews. The provider owns one immutable resource and has declared static deps = []. The Query projects the resource; it creates no parallel observer system. Compilation rejects duplicate module/contribution IDs, missing dependencies, cycles, unknown contribution kinds and invalid view/command/route definitions. Module dependencies have deterministic topological order. Contributions have stable order by explicit order, module ID and contribution ID.
A module exports {id, dependencies, contributions}. Contribution arrays include navigation, views, commands, settings, translations, contracts, routes, and keybindings. Navigation titles, command titles and settings titles are English source text passed to the host translator. Views supply factory(uiCapabilities), called once at composition. Its result renders the Query projection and bubbles native intent events. Factories receive the host's shared DOM and translation capabilities; they receive no engine, container or dispatch handle. The business layer never imports the UI.
Commands declare {id, title, deps, capability, execute(resources, detail, context)}. RunPortalCommand(registry, id, detail) leases declared dependencies and portalAccess: {capability, command} before its execute method can run. The host must provide the authorizer; absence or denial fails before execution. The command uses ngin's existing feed for domain events. This wrapper never dispatches a nested Action or creates a second command lifecycle. Backend route descriptors name an Action and capability, but are not automatically mounted HTTP routes: the server composition must explicitly wire authentication, CSRF, bounded argument parsing and a capability provider. Module route contributions do not turn the owner-authorized demonstration API into production APIs.
Publications sample
modules/publications/index.mjs is an opt-in sample with real create/list/update/delete operations, a navigation/view factory, commands, source-text translations, a reserved .example contract ID and backend route descriptors. It is not enabled by the default build. The pure view provides create/edit/delete controls that emit 3sln-intent with {kind: 'portal-command', command, detail}. A host must project Publications into the view's state.publications, pass its shared h and t, and route that intent to RunPortalCommand at composition. A module's presence alone does not imply a connected backend.
PublicationsProvider declares portalAccess and publicationStorage. It obtains an owner-scoped grant from the authorizer and returns a handle containing only the requested operation. A read handle cannot mutate. Update/delete require the current revision and cross-owner mutations fail. MemoryPublicationStorage is a demonstration backing provider; inject a durable implementation of its resource interface for a real deployment. Its interface is list(owner), create(owner, {title, body}), update(owner, id, {title, body, revision}), and remove(owner, id, revision). Human acceptance, durable storage, publication authority and production security approval remain unsigned.
Actual bab localization
packages/i18n/vendor/bab/index.js is the exact offline @3sln/bab 0.2.0 runtime, accompanied by its MIT license and provenance. Its runtime is dependency-free; the extraction tooling's dependencies are not browser dependencies. We verified the installed package metadata, README and actual source before integration.
The actual API is createTranslator({locale, catalogue, onMissing}). The source text is the key: tr('Hello {name}', {name}). It returns bab's Translation String object; plural selection uses tr('{#} messages', {count}).singular('{#} message'). ObjectCatalogue uses locale-scoped buckets, while ChainCatalogue layers module translations over core strings. Unknown messages fall back to readable source text, unknown parameters remain visible, and onMissing runs once per source/scope.
createLocalization({locale, catalogue, onMissing}) supplies an immutable UI capability {locale, direction, t, number, date}. t converts bab's String object into a primitive for the shared DOM library; t.plural and t.scope retain actual bab semantics. date accepts epoch milliseconds. English and Spanish core catalogs contain 441 source-text entries. Unsupported/invalid locale tags fall back to English. Both shipped languages have ltr direction. Language belongs to the browser-state Provider; persistence stores only the selected language. Format dates/numbers with that locale and pass changing values as {parameters}. Never translate message subjects, message bodies, handles, collection names, user-defined rule names, server secrets, resource URLs or other user-authored content. DOM rendering treats translated strings as text, never HTML.
Context-aware keybindings
packages/portal/keybindings.mjs contains pure parsing, compilation, resolution and inspection functions. Shortcut state, the pending chord, preference overrides and focus context belong to the browser Provider/Actions/Query projection. A single native key listener at composition performs a synchronous pure match to decide preventDefault, then signals the named domain Action. Views never install dispatch callbacks or keyboard registries.
Defaults and editable user overrides use {command, key, when}; chords use spaces, for example mod+k mod+c. mod means Command on Apple platforms and Control elsewhere. Up to three strokes, 128 user overrides and bounded clauses are accepted. A removal override uses {command: '-commandId'} and optionally limits the removed key/when. Resetting overrides restores the module defaults. Unknown commands, extra fields, malformed keys and malformed conditions are rejected, so a failed settings edit can preserve the previous configuration.
Context is {view, dialogOpen, textInputFocus, selectionCount, authenticated, offline} from portalContext(state, focus). Conditions accept known context identifiers, literal equality/inequality, !, &&, || and parentheses. The parser has length/token/depth limits; it supports no calls, property traversal, arbitrary JavaScript, eval or Function. Example: view == "inbox" && authenticated && !textInputFocus.
User bindings win over defaults. More specific conditions win within the same origin, then later declarations, then command ID. inspectKeybindings returns active rows, conflicts and the effective winner for settings UI. Pending chords expire after 1500 ms and a failed chord retries the current stroke as a fresh sequence. Text inputs retain all shortcuts unless a matching clause explicitly mentions textInputFocus; browser-reserved keys without an explicit context clause remain untouched. IME composition and previously handled events remain untouched. Shortcut configuration contains no credentials or message payloads and can persist locally alongside language; account preference-fragment synchronization is a future integration, not a claim of existing sync.
Validation
tests/portal.test.mjs, tests/portal-keybindings.test.mjs and tests/i18n.test.mjs verify deterministic contribution lookup, dependency errors/cycles, Query/provider integration, authorization before execution, owner-scoped CRUD and revision conflicts, real bab interpolation/plurals/fallbacks, supported locale formatting, declarative condition rejection, chords/timeouts, overrides/removals/conflicts, and typing/browser/IME safeguards. Browser composition additionally requires the application's DOM and browser checks.