Skip to content

feat(vue): add lightweight Vue runtime - #2012

Merged
eoinest merged 28 commits into
irisfrom
e/repo/add-gt-vue-runtime
Aug 7, 2026
Merged

feat(vue): add lightweight Vue runtime#2012
eoinest merged 28 commits into
irisfrom
e/repo/add-gt-vue-runtime

Conversation

@eoinest

@eoinest eoinest commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add an unstable, lightweight gt-vue runtime with T, child-only Var, typed-value Num / DateTime / Currency, Plural, Branch, useGT, useMessages, and msg.
  • Keep string translation deliberately narrow: $context is the only public metadata option; all content is literal STRING data with no ICU interpolation, formatting fields, or development hot reload.
  • Provide isolated 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.
  • Preserve Vue component props/listeners and scoped slots, keep rich-content IDs and plural selection compatible with react-core, exclude presentation/runtime attributes from persisted Branch hashes, and suppress unsupported fallthrough attrs on wrapperless value components.
  • Share the literal STRING hash/registration contract through gt-i18n/internal/string; gt-i18n is 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 concurrency
  • gt-i18n: 33 files / 314 tests
  • @generaltranslation/react-core: 12 files / 50 tests, including shared hand-pinned wire-format fixtures
  • Forced Turbo build/typecheck for the runtime dependency graph — passed
  • Root library-default, lint, and format gates — passed
  • Size limits — passed (gt-vue 7.56 kB Brotli; gt-i18n/internal/string 2.97 kB)
  • Packed consumers on Vue 3.3.13 and 3.5.40 — passed ESM, CJS, SSR, vue-tsc, Vite production builds, and real Chromium locale/formatting loops with zero console warnings/errors
  • Changesets simulation — gt-vue@0.1.0 correctly depends on gt-i18n@1.1.0

Notes

Greptile Summary

This PR introduces gt-vue, a lightweight Vue 3 i18n runtime, alongside shared contract additions to gt-i18n (STRING hash/registration, browser cookie helpers, cookie name re-exports from react-core).

  • createGT() plugin: Reactive locale state backed by a per-locale catalog cache with latest-wins concurrency, retryable failed loads, and an explicit revision ref that drives re-renders without making the Map itself reactive.
  • Component suite: T (rich VNode translation), Var/Num/DateTime/Currency (typed value formatters), Plural/Branch (plural-rules and arbitrary branch selection), all with inheritAttrs: false and Fragment roots for correct SSR hydration boundaries.
  • Composables: useLocale (reactive read-only ref via toRef(getter)), useSetLocale, useGT, useMessages, plus msg() for static string registration — string-only, no ICU interpolation.
  • gt-i18n additions: hashStringMessage, msgString, browserCookies shared with the Vue runtime; react-core cookie names now re-exported from gt-i18n to eliminate duplication.

Confidence Score: 5/5

  • The runtime logic is correct and well-tested; all findings are non-blocking style or design observations.
  • The plugin's concurrency model (latest-wins locale requests, catalog deduplication, revision-driven reactivity), VNode identity reconciliation, and SSR hydration guards are all implemented correctly. The only substantive point of note is that the locale cookie is a session cookie, which means user locale preference resets when the browser closes — this is intentional per the JSDoc and matches the React runtime contract, but is worth a deliberate decision. No logic errors, data races, or security issues were found.
  • No files require special attention. packages/i18n/src/utils/browserCookies.ts and packages/vue/src/runtime/localeCookie.ts are worth a second read if cross-session locale persistence becomes a requirement.

Important Files Changed

Filename Overview
packages/vue/src/runtime/state.ts Core plugin factory. Locale concurrency (latest-wins), catalog caching, and reactive revision counter are all implemented correctly. load correctly guards the default locale via an empty pre-seeded catalog, deduplicates concurrent fetches, retries after failure, and only bumps revision when the active locale changes.
packages/vue/src/runtime/localeCookie.ts Cookie-backed locale accessor. SSR guard (typeof document check) is correct. setBrowserCookieValue writes a session cookie with no max-age/expires, so locale preference resets on every browser session end. The docstring acknowledges "session cookie" intentionally, matching the React runtime contract.
packages/vue/src/rendering/translateVueChildren.ts Rich VNode translation engine. Source serialization, identity/key reconciliation, branch/plural rendering, and Suspense handling are carefully implemented. Uses undocumented Vue internals (ctx, slotScopeIds) in copyRenderMetadata — previously reviewed and justified as the only stable path for preserving scoped-CSS scope IDs.
packages/vue/src/components/T.ts Translation component. Correctly creates per-instance identity cache, handles $context/$_hash dual-path (compiler output and template-friendly), and delegates to translateVueChildren. inheritAttrs: false and Fragment root both intentional.
packages/vue/src/components/variables.ts Var/Num/DateTime/Currency components. Formatting logic, locale fallback chain, and _locale override for rich pipeline are all correct. Empty/null value guards and NaN checks are present. Currency props merge order is correct (explicit currency prop takes precedence over options.currency).
packages/i18n/src/utils/browserCookies.ts New shared cookie helper. getBrowserCookieValue delegates to getCookieValue which calls decodeURIComponent; setBrowserCookieValue writes the raw value without encodeURIComponent. For BCP 47 locale codes (all safe ASCII) this round-trips correctly. Cookie is a session cookie with no SameSite attribute.
packages/i18n/src/utils/hashStringMessage.ts Canonical STRING catalog key derivation shared across framework runtimes. Precomputed $_hash short-circuit is present; falls through to hashSource with STRING dataFormat, correctly separating the key space from JSX hashes.
packages/react-core/src/setup/cookieNames.ts Now re-exports cookie name constants from gt-i18n/internal/cookies instead of duplicating them. Clean de-duplication; public export paths are preserved.

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 / translateString
Loading

Reviews (14): Last reviewed commit: "fix(vue): persist locale in browser cook..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
generaltranslation 18.09 KB (0%)
generaltranslation/runtime 14.87 KB (0%)
generaltranslation/id 2.55 KB (0%)
generaltranslation/internal 7.36 KB (0%)
generaltranslation/types 115 B (0%)
generaltranslation/errors 81 B (0%)
@generaltranslation/format 9.5 KB (0%)
@generaltranslation/format/types 89 B (0%)
@generaltranslation/format/internal 880 B (0%)
gt-i18n 11.96 KB (+0.7% 🔺)
gt-i18n/types 13 B (0%)
gt-i18n/internal 22.4 KB (-0.25% 🔽)
gt-i18n/internal/types 13 B (0%)
@generaltranslation/react-core/pure 25.6 KB (+0.31% 🔺)
@generaltranslation/react-core/hooks 20.6 KB (+0.07% 🔺)
@generaltranslation/react-core/components 22.74 KB (+0.3% 🔺)
@generaltranslation/react-core/components-rsc 26.19 KB (+0.18% 🔺)
gt-react (client) 32.14 KB (+0.46% 🔺)
gt-react (rsc) 28.71 KB (+0.28% 🔺)
gt-react (server) 31.77 KB (+0.47% 🔺)
gt-react/macros 8.81 KB (+0.04% 🔺)
gt-next (client) 44.2 KB (+0.84% 🔺)
gt-next (rsc) 48.16 KB (+0.82% 🔺)
gt-next (server) 44.31 KB (+0.44% 🔺)
gt-next/config 270.25 KB (+0.1% 🔺)
gt-next/server 46.71 KB (+0.68% 🔺)
gt-next/middleware 36.82 KB (+0.82% 🔺)
gt-next/link 43.02 KB (+0.32% 🔺)
gt-next/internal/_dictionary 144 B (0%)
gt-next/internal/_load-translations 144 B (0%)
gt-next/internal/_load-dictionary 144 B (0%)
gt-next/internal/_getLocale 125 B (0%)
gt-next/internal/_getRegion 122 B (0%)
gt-node 23.61 KB (+0.47% 🔺)
gt-node/types 219 B (0%)
gt-node/internal 13.43 KB (-0.11% 🔽)
gt-tanstack-start (client) 31.68 KB (+0.21% 🔺)
gt-tanstack-start (server) 32.13 KB (+0.31% 🔺)
gt-tanstack-start/server 10.27 KB (+0.26% 🔺)
gt-react-native 30.11 KB (+0.15% 🔺)
gt-react-native/plugin 4.6 KB (0%)
gt-react-native/internal 746 B (0%)
gt-i18n/internal/cookies 228 B (+100% 🔺)
gt-i18n/internal/string 2.9 KB (+100% 🔺)
gt-vue 8.18 KB (+100% 🔺)

Comment thread packages/vue/src/rendering/translateVueChildren.ts
Comment thread packages/vue/src/components.ts Outdated
@eoinest

eoinest commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@eoinest

eoinest commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread packages/vue/src/rendering/translateVueChildren.ts
@eoinest

eoinest commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@eoinest

eoinest commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest eoinest left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@archie-mckenzie archie-mckenzie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. Scoped slots crash <T> — serialization invokes every component's slots with zero args; any headless-UI-style v-slot="{ ... }" child throws and takes down the whole subtree (see inline).
  2. 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).
  3. The child-only formatting API silently produces wrong outputparseFloat('1,234.5') === 1, and <DateTime> can't accept a Date or 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 typed value prop now (see inline).
  4. gt-i18n must 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 new gt-i18n/internal/string entry point (i18n('gt-i18n/internal/string', 'internal-string')).
  • packages/vue/package.json: missing release/release:alpha/release:beta/release:latest scripts that gt-react/gt-i18n/gt-tanstack-start define — turbo release:alpha --filter=gt-vue is currently a silent no-op, and alpha releases are the likeliest flow for a package the README labels unstable.
  • pnpm-workspace.yaml: add gt-vue to minimumReleaseAgeExclude or 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.

Comment thread packages/vue/src/rendering/translateVueChildren.ts Outdated
Comment thread packages/vue/src/rendering/translateVueChildren.ts
Comment thread packages/vue/src/rendering/translateVueChildren.ts Outdated
Comment thread packages/vue/src/rendering/translateVueChildren.ts Outdated
Comment thread packages/vue/src/rendering/translateVueChildren.ts Outdated
Comment thread packages/vue/src/__tests__/runtime.test.ts
Comment thread packages/vue/src/__tests__/package-layout.test.ts
Comment thread packages/i18n/src/translation-functions/msg/msg.ts
Comment thread packages/i18n/src/translation-functions/msg/msgString.ts Outdated
Comment thread packages/i18n/src/internal-string.ts Outdated
@eoinest

eoinest commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

2 similar comments
@eoinest

eoinest commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest

eoinest commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

archie-mckenzie
archie-mckenzie previously approved these changes Aug 6, 2026
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest
eoinest changed the base branch from main to iris August 6, 2026 18:57
@eoinest
eoinest changed the base branch from iris to e/release/add-iris-publishing August 6, 2026 19:25
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

Base automatically changed from e/release/add-iris-publishing to iris August 6, 2026 19:40
eoinest added a commit that referenced this pull request Aug 6, 2026
## 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 -->
@eoinest
eoinest force-pushed the e/repo/add-gt-vue-runtime branch from df9865e to f2a1ebe Compare August 6, 2026 19:40
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest
eoinest force-pushed the e/repo/add-gt-vue-runtime branch from 163973a to 8184c2f Compare August 7, 2026 00:43
@eoinest

eoinest commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest
eoinest merged commit 8d376e2 into iris Aug 7, 2026
31 checks passed
@eoinest
eoinest deleted the e/repo/add-gt-vue-runtime branch August 7, 2026 00:53
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.

2 participants