4 Commits
Author SHA1 Message Date
Juan Denis 78201eeefc Format dates and numbers in the panel's language
107 direct toLocaleString/DateString/TimeString calls followed the
browser's locale (or a hard-coded en-US/en-GB) instead of the language
the panel is set to, and built a formatter per call. They now go
through utils/intl (formatDate/Time/DateTime/Number), keeping each
site's components via parts. A missing date now renders the fallback
dash instead of "Invalid Date". Ratchet ceiling 115 -> 4.
2026-10-10 20:19:32 -04:00
Juan DenisandClaude Fable 5 02967b0547 cutover(git): serverkit-git leaves the tree (plan 52 Phase 6)
builtin-extensions/serverkit-git + the tracked frontend live copy removed;
the bundled registry entry flips to bundled:false at v1.1.0 (the version
that carries the Gitea backend) with the local zip's sha256; the gated
auto-install (_gitea_was_used) moves to REGISTRY_GATED_SLUGS, leaving
GATED_BUILTIN_SLUGS empty with the machinery intact.

i18n: en.json regenerated (the app.git/app.email/app.cloudflare* namespaces
ride with their extensions, which carry en+es bundles; other languages are
recoverable from git history), all locales pruned, intl-door ratchet
tightens 123 -> 115.

Expected red on this branch: the two registry-mirror tests, until the four
repos are published + signed + re-vendored (see docs/plans/83).

OPERATOR: publish jhd3197/serverkit-git, sign, sync-registry-index.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:53:27 -04:00
Juan DenisandClaude Opus 5 9c0ae9e957 feat(extensions): enforce the SDK gate at install; localize the GUI builtin
Follow-up audit of the extension surface, across all of them rather than one.

THE COMPATIBILITY HOLE. Two places documented an install-time `sdk_version`
check as existing -- app/utils/sdk.py's docstring ("checked at install (manifest
lint) and at load") and the runtime loader's comment, which named
`_assert_manifest_sdk_compatible` -- but `sdk_version_satisfies` had ZERO
production callers; only its own unit test. Consequences split by extension
kind: a runtime ESM extension installed cleanly and failed later at the load
gate, while a builtin/in-repo extension never passes that gate at all, so its
range went unchecked everywhere. The function now exists and runs at install,
beside the min/max_panel_version check, and refuses with what the operator
needs to act: what the extension wants, what this panel offers. Fail-open on a
missing range, matching the load gate's 'grace'. Proved both directions.

This matters now because SDK 1.3.0 added the localization surface and made
i18next/react-i18next shared singletons: an extension that externalizes them
needs a panel at 1.3.0+. Pinning `^1.3.0` is what turns a white screen on an
older panel into a readable message. The bump is additive, so anything pinned
to ^1.2.0 keeps installing and loading. Documented as a version table.

THE AUDIT. Measured, not assumed -- both censuses now take `--root` so they can
run against any extension repo (and refuse `--update` there, since the ceilings
belong to the panel):

  2,105 untranslated literals across 13 extension repos (WordPress alone 1,099
  in 44 files), plus 78 direct toLocale*/Intl calls that follow the BROWSER's
  locale (WordPress 56). None of it was visible to any check before, because
  every census stopped at frontend/src.

serverkit-gui -- the one builtin with real UI -- is converted as the worked
example, importing useTranslation/useFormat from 'serverkit-sdk' exactly as a
third-party extension would. That also closes its own toLocaleTimeString.
Ceiling 8055 -> 8034, intl door 124 -> 123.

The probe now asserts the claim the SDK surface rests on: an extension
resolving i18next through the shared vendor gets the HOST's INITIALISED
instance, and an extension-style t() returns Spanish. A second instance would
be uninitialised and every t() in that extension would render English forever
with nothing reporting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:31:42 -04:00
Juan DenisandClaude Opus 5 7050b24137 feat(i18n): one locale-aware format door; delete the timeAgo shim
Plan 79 §C + ratchet 5.

utils/intl.js owns dates, times, relative time, durations, numbers, percents
and lists. Three things the scattered calls could not do:

- A module-level locale published by LocaleContext, not read from a hook, so
  the API error mapper, log formatter and CSV export format in the panel's
  language too. useFormat() is a subscription over the same module, not a
  second door.
- Formatter instances cached per (locale, kind, options). The
  toLocaleDateString() calls this replaces built a fresh formatter per cell
  per render, and Intl construction is the expensive half.
- Relative time via Intl.RelativeTimeFormat rather than translation keys --
  the browser already carries every language's plural wording, and keying
  "{{count}} days ago" would mean maintaining that table by hand.

This fixes a locale bug that predates any translation work: 124 call sites
use toLocale*() with no argument, which follows the BROWSER's locale, so a
panel set to Spanish already rendered English chrome around Spanish dates.
formatNumber.js was worse -- it built Intl.NumberFormat('en') at module load,
hardcoding English AND making it impossible to follow a switch.

The three human-facing utils now delegate instead of reimplementing, so their
existing callers become locale-aware without being touched. formatBytes gains
the locale decimal separator ("1,5 KB" in es) by replacing a toFixed + regex
trim with maximumFractionDigits, which trims identically in en -- parity
proved case by case against the pre-migration examples.

utils/timeAgo.js is deleted rather than left as a shim; its 6 importers move
to utils/time. A door nobody is forced through is a door nobody uses.

One bug found by the tests: formatNumber(null) returned "0", because
Number(null) is 0 -- a missing metric would have rendered as a real zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:22:28 -04:00