feat(vue): add lightweight Vue runtime - #2012
Conversation
size-limit report 📦
|
1 similar comment
1 similar comment
archie-mckenzie
left a comment
There was a problem hiding this comment.
This is a strong first cut — the id-numbering scheme is a faithful port of react-core's addGTIdentifier (pinned by hand-computed fixtures that hash correctly against core's hashSource), the plugin state is genuinely SSR-isolated with tested latest-wins semantics, the clone strategy in translateVueChildren is honestly commented about why it bypasses cloneVNode, and reusing JsxChildren + hashSource + the new gt-i18n/internal/string helpers instead of reimplementing the wire format is exactly the right modularity call. JSDoc quality is excellent throughout.
That said, I think four things need to land before the first npm release, because they sit on surfaces that calcify (public API shape and persisted catalog hashes):
- Scoped slots crash
<T>— serialization invokes every component's slots with zero args; any headless-UI-stylev-slot="{ ... }"child throws and takes down the whole subtree (see inline). - Branch serialization leaks
class/style/event listeners into hashes — including function source text, which differs between dev and minified prod, so hashes become unreproducible by extraction tooling. Greenfield is the moment to fix this; these hashes become persisted catalog keys (see inline). - The child-only formatting API silently produces wrong output —
parseFloat('1,234.5') === 1, and<DateTime>can't accept aDateor epoch at all because Vue slot interpolation stringifies everything. This is the one place where "lightweight" costs correctness rather than just features, and it's the most expensive API in the PR to change later. I'd add a typedvalueprop now (see inline). gt-i18nmust be a real dependency — as a devDependency +alwaysBundle, changesets will never republish gt-vue when the encode/decode format changes; the wire format gets frozen into the published dist while every other runtime tracks gt-i18n. gt-react already demonstrates the right pattern (see inline).
Two cross-cutting notes on our values:
Modularity: the serialized-JSX contract (numbering rules, branch renumbering, _gt_<name>_<id> variable naming, fragment/comment/text coalescing) now lives in two implementations with known intentional divergences (Vue flattens fragments without consuming a counter slot, merges adjacent strings) and zero shared verification. The failure mode is a silent hash miss — translations just "never found." Before the Vue CLI extraction PR (stack 2/3) lands, I'd like a golden-fixture corpus in core (source tree in → exact serialized JsxChildren + hash out) that react-core, gt-vue, and the extraction tooling all assert against, with the Vue divergences documented as spec. Same story in miniature for the STRING hash, which is now derived independently in msgString, gt-vue's translateString, and the CLI (see inline). Longer-term (not this PR): the load/cache/dedupe/latest-wins catalog store is framework-agnostic and could live in gt-i18n so gt-vue keeps only the ref/inject adapter.
Speed: the runtime does avoidable work on hot paths — serializeNodes computed and discarded when a compile-time _hash exists, serialize+SHA-256 on every render for the default locale where the lookup is a guaranteed miss, per-call hashing in useGT with no compiler to inject $_hash for Vue yet, and an unconditional revision bump that makes background preloads rerender the whole app. Each has a small, behavior-preserving fix (see inline).
Smaller checklist items with no inline anchor:
.size-limit.cjs: add an entry for the newgt-i18n/internal/stringentry point (i18n('gt-i18n/internal/string', 'internal-string')).packages/vue/package.json: missingrelease/release:alpha/release:beta/release:latestscripts that gt-react/gt-i18n/gt-tanstack-start define —turbo release:alpha --filter=gt-vueis currently a silent no-op, and alpha releases are the likeliest flow for a package the README labels unstable.pnpm-workspace.yaml: addgt-vuetominimumReleaseAgeExcludeor future in-repo example apps can't install a fresh release for 48h.- gt-vue was left out of the changesets fixed group (
react-core/gt-next/gt-react/...). Reasonable while 0.x, but let's record that decision and revisit once it stabilizes — the gt-react/gt-react-native parity rule exists precisely to stop sibling runtimes drifting. - The reliance on undocumented VNode internals in
cloneWithChildren(ctx,slotScopeIds,h(vnode)-as-type) is well-commented and I'm fine accepting it, but only alongside the peer-range/CI-matrix fix flagged inline on package.json.
The test suite is unusually thorough for a first cut (concurrent-SSR isolation, locale races, directive preservation), which makes the specific gaps flagged inline stand out more — most notably that the README's primary documented API, the context prop on <T>, is never exercised by any test.
2 similar comments
1b955ae to
e9980bf
Compare
1 similar comment
## Summary - Add a dedicated Changesets prerelease workflow for pushes to the `iris` branch. - Keep `iris` permanently in Changesets prerelease mode, parallel to `main`. - Create or update a `[ci] iris release` PR whenever changesets land on `iris`. - Publish npm prereleases without touching stable CLI binary aliases, R2 uploads, or PyPI releases. ## Testing - `bash -n scripts/version-packages-iris.sh` — passed - Parsed `.github/workflows/release.yml` with the repository's YAML parser and verified the permanent Iris branch release job — passed - `pnpm exec oxfmt --check .github/workflows/release.yml .changeset/pre.json` — passed - `git diff --check` — passed - Disposable permanent-branch simulation — Changesets automatically registered packages added after prerelease mode began and generated the expected `*-iris.0` versions - Versioned Vue stack build/pack simulation — produced correct Iris dependency links; 10 package builds passed ## Notes - Changesets state: `.changeset/pre.json` keeps the branch in `iris` prerelease mode. - Base: `iris`. - Merge this foundation PR before #2012. New workspace packages such as `gt-vue` and the Vue extractor are added to the prerelease baseline automatically when their changesets are versioned. <!-- greptile_comment --> <h3>Greptile Summary</h3> The PR adds a permanent Changesets prerelease pipeline for the `iris` branch while keeping stable release artifacts isolated to `main`. - Adds the Iris prerelease baseline and tag state. - Adds an Iris-only release job that creates release PRs or publishes npm prereleases. - Temporarily targets Changesets version calculation at `iris` while restoring the committed configuration afterward. - Removes the superseded Odysseus versioning script. <details open><summary><h3>Confidence Score: 5/5</h3></summary> The PR appears safe to merge. No blocking failure remains. </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | .github/workflows/release.yml | Adds the Iris trigger and an Iris-only npm prerelease job while retaining stable artifact publishing behind the main-only job guard. | | scripts/version-packages-iris.sh | Temporarily rewrites the Changesets base branch and changelog generator for Iris versioning, then restores the committed configuration on every exit. | | .changeset/pre.json | Establishes permanent Iris prerelease mode with initial versions matching the current workspace package versions. | | scripts/version-packages-odysseus.sh | Removes the superseded Odysseus-specific Changesets configuration script. | </details> <details><summary><h3>Flowchart</h3></summary> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD Push{Push branch} -->|main| Stable[Main release job] Push -->|iris| Iris[Iris prerelease job] Stable --> StableNpm[Stable npm release] Stable --> Binaries[CLI binaries and aliases] Stable --> R2[R2 uploads] Stable --> PyPI[PyPI release] Iris --> Verify[Verify pre.json mode and iris tag] Verify --> Version[Temporarily version against iris] Version --> ReleasePR{Pending version changes?} ReleasePR -->|Yes| PR[Create or update iris release PR] ReleasePR -->|No / version PR merged| PreNpm[Publish npm iris prereleases] ``` </details> <sub>Reviews (2): Last reviewed commit: ["ci: keep Iris in prerelease mode"](4aad45c) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=50913640)</sub> <!-- /greptile_comment -->
df9865e to
f2a1ebe
Compare
163973a to
8184c2f
Compare
Summary
gt-vueruntime withT, child-onlyVar, typed-valueNum/DateTime/Currency,Plural,Branch,useGT,useMessages, andmsg.$contextis the only public metadata option; all content is literal STRING data with no ICU interpolation, formatting fields, or development hot reload.createGT()instances, async per-locale catalog caching, reactive locale changes, latest-wins concurrency, retryable loader diagnostics, and source-locale fast paths for client and SSR use.gt-i18n/internal/string;gt-i18nis a real external runtime dependency in both ESM and CJS.Testing
gt-vue: full runtime suite passed, including SSR/hydration, keyed VNode reconciliation, opaque slots, Suspense, and concurrencygt-i18n: 33 files / 314 tests@generaltranslation/react-core: 12 files / 50 tests, including shared hand-pinned wire-format fixturesgt-vue7.56 kB Brotli;gt-i18n/internal/string2.97 kB)vue-tsc, Vite production builds, and real Chromium locale/formatting loops with zero console warnings/errorsgt-vue@0.1.0correctly depends ongt-i18n@1.1.0Notes
gt-vueis explicitly documented as unstable and independently versioned during 0.x.gt-vuerelease and the literal STRING contract update ingt-i18n.main. Next: feat(cli): extract gt-vue messages #2013, test(vue): add Vite integration example #2014, then test(vue): add Vite SSR example #2033.Greptile Summary
This PR introduces
gt-vue, a lightweight Vue 3 i18n runtime, alongside shared contract additions togt-i18n(STRING hash/registration, browser cookie helpers, cookie name re-exports fromreact-core).createGT()plugin: Reactive locale state backed by a per-locale catalog cache with latest-wins concurrency, retryable failed loads, and an explicitrevisionref that drives re-renders without making theMapitself reactive.T(rich VNode translation),Var/Num/DateTime/Currency(typed value formatters),Plural/Branch(plural-rules and arbitrary branch selection), all withinheritAttrs: falseand Fragment roots for correct SSR hydration boundaries.useLocale(reactive read-only ref viatoRef(getter)),useSetLocale,useGT,useMessages, plusmsg()for static string registration — string-only, no ICU interpolation.gt-i18nadditions:hashStringMessage,msgString,browserCookiesshared with the Vue runtime;react-corecookie names now re-exported fromgt-i18nto eliminate duplication.Confidence Score: 5/5
packages/i18n/src/utils/browserCookies.tsandpackages/vue/src/runtime/localeCookie.tsare worth a second read if cross-session locale persistence becomes a requirement.Important Files Changed
loadcorrectly guards the default locale via an empty pre-seeded catalog, deduplicates concurrent fetches, retries after failure, and only bumpsrevisionwhen the active locale changes.Sequence Diagram
sequenceDiagram participant App participant GTPlugin as createGT() Plugin participant CatalogCache as Catalog Cache (Map) participant Loader as loadTranslations(locale) participant VueReactivity as Vue Reactivity (revision ref) participant TComponent as T / Composables App->>GTPlugin: app.use(gt) GTPlugin->>CatalogCache: "seed defaultLocale → {}" GTPlugin->>Loader: load(initialLocale) [async, fire-and-forget] Loader-->>CatalogCache: catalogs.set(locale, catalog) CatalogCache-->>VueReactivity: "revision.value += 1 (if locale matches)" VueReactivity-->>TComponent: re-render with translated catalog App->>GTPlugin: setLocale('fr') GTPlugin->>Loader: load('fr') [deduped] Loader-->>CatalogCache: catalogs.set('fr', catalog) GTPlugin->>CatalogCache: localeAccessor.setLocale('fr') GTPlugin->>VueReactivity: "revision.value += 1" VueReactivity-->>TComponent: re-render with 'fr' catalog TComponent->>CatalogCache: getCatalog() → catalogs.get(getLocale()) CatalogCache-->>TComponent: translation entries (hash → JsxChildren) TComponent->>TComponent: translateVueChildren / translateStringReviews (14): Last reviewed commit: "fix(vue): persist locale in browser cook..." | Re-trigger Greptile