Skip to content

Add typed ICU localisation and compiled Svelte integration - #3

Draft
elucidsoft wants to merge 14 commits into
mainfrom
feat/i18n
Draft

elucidsoft wants to merge 14 commits into
mainfrom
feat/i18n

Conversation

@elucidsoft

Copy link
Copy Markdown
Contributor

Applications need one localisation API that works across browser UI and request-scoped server/email rendering. This adds optional @warpkit/i18n: consumer-owned locales and catalogues, typed ICU message parameters, strict catalogue validation, locale negotiation, and explicit timezone formatting.

The optional Svelte context updates text and formatting without navigation or remounting. Browser and server exports contain compiled JavaScript; consumers do not compile framework runes. Tests import the public exports and verify dirty input, focus, operation identity, provider isolation, cleanup and declaration errors. There are no product-specific messages, languages or policies in the runtime.

Build/publish registration and the public guide are included. Browser verification also found a test-helper compatibility defect: awaiting the renderer preserves the added WarpKit handle when the renderer is thenable. Its regression test and stale missing-route fixtures are corrected.

Validation:

  • Full build passed; typecheck reported 0 errors and 50 warnings.
  • 440 core tests and 526 package tests passed.
  • 30 affected Chromium tests passed; the final localisation/helper run passed after the final message-validation change.
  • Consumer browser tests are included in CI.

Catalogues are eager in this initial API. Translation quality, language selection, persistence, URL policy and lazy resource loading remain consumer responsibilities or future framework enhancements. No package has been published. The external review runtime was unavailable locally (scripts/tooling-python.sh is absent); these checks are not an independent-review result.

A legacy-alias redirect in StateConfig.redirects (e.g. /jobs -> /activity)
dropped the original request's query string and hash, because
runPipeline() built the recursive navigation's path from the bare
redirect target alone. A shared filtered link (/jobs?status=error) lost
its filter the moment the alias redirected.

Navigator.mergeRedirectTarget() reparses the redirect target and
recombines pathname+search+hash, letting a target-declared component win
over the original request's so ordering stays valid regardless of which
component the target itself carries. RouteMatcher.match()'s
pathname-only contract is untouched.

Regression: BUG-07-098 (cloudlayerio bug-tracker).
BUG-07-098's matrix covered single-hop redirects only. Each hop of
runPipeline() reparses the CURRENT request's path, so a second config
redirect chained onto the first needs its own pin proving the
already-merged query from hop one survives hop two, rather than the
carry only holding at the outermost call.

Found during BUG-07-098 Plan TPR (codex).
The two-hop chain pin added at BUG-07-098 Plan TPR Round 0 only
exercised the case where neither hop's target declares its own query.
Missing cell: hop one's target owns a query (winning per §03.4's
single-hop precedence), which then has to survive being reparsed as
hop two's own request rather than losing to the true original
request's query.

Found during BUG-07-098 Plan TPR Round 1 (opencode).
Round 1's hop-one-target-owned-query pin has no hash counterpart. The
merge treats search and hash symmetrically by construction, but the
matrix had no test proving it — a fix that only special-cased query
across hops would still pass every existing case.

Found during BUG-07-098 Plan TPR Round 2 (codex).
Two precedent single-hop cases each pinned one owned component in
isolation (query-only, hash-only). Neither proved a target owning
BOTH components at once carries both — a fix that checked only one
component before falling back to the original request could still
pass both isolated cases while dropping the other here.

Found during BUG-07-098 Plan TPR Round 3 (codex).
The JSDoc claimed StateConfig.redirects entries "are bare pathnames"
as a general fact. The type is Record<string, string> — nothing
forces bare pathnames structurally, and mergeRedirectTarget() itself
handles a target that declares its own query/hash. Scoped the claim
to what every current consumer declares.

Found during BUG-07-098 Plan TPR Round 4 (codex).
The single-hop both-components-owned case and the two-hop
single-component cases were each pinned separately, but nothing
combined them: hop one's target owning both query and hash still had
to survive being reparsed as hop two's own request.

Found during BUG-07-098 Plan TPR Round 4 (codex).
Every prior two-hop case gave hop two a bare target, so the merge's
precedence logic was only ever exercised fresh at hop one. This pins
that hop two's own declared query/hash wins over whatever hop one
produced, proving the merge re-evaluates precedence at each hop
rather than treating the prior hop's output as immutable.

Found during BUG-07-098 Plan TPR Round 5 (codex).
The Round 5 "hop-two owns both components" case passed even with
mergeRedirectTarget() reverted: a target owning both components is
indistinguishable from a no-merge implementation, since its own
literal redirect string already equals the expected output regardless
of merge logic. Redesigned to own only the hash (forcing the query to
still be carried from a bare hop one), verified failing without the
fix. Added the query-only mirror to complete the hop-two
partial-ownership cell.

Found during BUG-07-098 Plan TPR Round 6 (agy + codex).
The no-match-to-default branch already had a negative pin proving its
query-dropping behavior is unchanged. The state-mismatch-to-default
and beforeNavigate-hook redirect branches — the other two mechanisms
BUG-07-098's root cause analysis explicitly scoped out — had none,
leaving the scope boundary only two-thirds clamped.

Found during BUG-07-098 Plan TPR Round 8 (codex).
…e duplication

A live rebuild of this package's dist/ while a consumer's Vite dev server
holds it open via a symlink can serve two distinct module instances of
context.ts to different import chains in one page load. With a local
Symbol('warpkit-v2'), the two instances produced two different context
keys, so setContext in one and getContext in the other never matched —
surfacing as "usePage must be called within WarpKitProvider" even though
the component tree was correctly nested. Symbol.for resolves to the same
global registry entry regardless of how many times the module is
evaluated, closing this class of dev-time duplication bug.
…as core

Sibling instance of the WARPKIT_CONTEXT fix (f2509d0): a local Symbol()
context key breaks setContext/getContext matching across duplicate module
instances Vite's dev server can serve when this package's dist/ is
rebuilt live under a consumer app's already-open dev server.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant