Skip to content

feat(vue-extractor): add reactivity transform support for props destructuring#2414

Merged
andrii-bodnar merged 11 commits intolingui:nextfrom
mister-what:vue-extractor-reactivity-transform
Feb 10, 2026
Merged

feat(vue-extractor): add reactivity transform support for props destructuring#2414
andrii-bodnar merged 11 commits intolingui:nextfrom
mister-what:vue-extractor-reactivity-transform

Conversation

@mister-what
Copy link

@mister-what mister-what commented Jan 26, 2026

Description

This PR adds support for Vue's Reactivity Transform in the vue-extractor to fix message ID mismatches when using destructured props from defineProps().

Problem

When using destructured props with Lingui macros like plural() or select(), the extractor and the runtime macro see different code representations:

<script setup>
const { selectedCount } = defineProps({ selectedCount: Number });

const text = plural(selectedCount, {
  one: "# item",
  other: "# items",
});
</script>

Extractor sees: plural(selectedCount, ...) and generates message ID for {selectedCount, plural, ...}
Runtime macro sees: plural(__props.selectedCount, ...) and generates message ID for {0, plural, ...}

This mismatch causes the translations to fail at runtime.

Solution

The extractor now runs Vue’s compileScript() when the reactivityTransform option is enabled.
This applies the same reactivity transform that Vue uses internally, so both the extractor and the runtime macro operate on identical code (i.e. __props.x instead of destructured bindings). As a result, message IDs are generated consistently and no longer mismatch at runtime.

The feature is opt-in (reactivityTransform: false by default) to avoid breaking existing setups that rely on the previous behavior.

To support this configuration cleanly, the extractor API was changed to a factory-based approach. A preconfigured extractor instance is still exported for backwards compatibility, but it is now marked as deprecated in favor of createVueExtractor().

Usage

// lingui.config.js
import { createVueExtractor } from "@lingui/extractor-vue";

export default defineConfig({
  // ... other config
  extractors: [createVueExtractor({ reactivityTransform: true })],
});

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Examples update

Checklist

@vercel
Copy link

vercel bot commented Jan 26, 2026

@mister-what is attempting to deploy a commit to the Crowdin Team on Vercel.

A member of the Team first needs to authorize it.

@timofei-iatsenko
Copy link
Collaborator

Hey, you also can mitigate that by using a named placeholder, which is going to be recommended way when we finish with V6 release

const { selectedCount } = defineProps({ selectedCount: Number });

// {selectedCount} shortcut to {selectedCount: selectedCount}, which is {label: value}
const text = plural({selectedCount}, {
  one: "# item",
  other: "# items",
});

@timofei-iatsenko
Copy link
Collaborator

@mister-what we currently have a code-freeze for the main branch and development goes in the next, your contribution is valuable and we happily accept it, please change the target to the next and resolve conflicts if any.

This fix would be included in upcoming v6 release.

@mister-what mister-what changed the base branch from main to next January 30, 2026 16:21
@mister-what mister-what force-pushed the vue-extractor-reactivity-transform branch from a22b982 to a9f3764 Compare January 30, 2026 16:38
@mister-what
Copy link
Author

@timofei-iatsenko alright 👍 I changed the target branch to next and resolved all conflicts.

@andrii-bodnar andrii-bodnar added this to the v6 milestone Feb 2, 2026
@vercel
Copy link

vercel bot commented Feb 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
js-lingui Ready Ready Preview Feb 10, 2026 9:03am

Request Review

@mister-what mister-what force-pushed the vue-extractor-reactivity-transform branch from 3ac8bd5 to 4dab83c Compare February 3, 2026 12:24
@mister-what
Copy link
Author

I fixed the failing snapshot tests and annotated some type-only imports.

@codecov
Copy link

codecov bot commented Feb 3, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.75%. Comparing base (dd43fb0) to head (206211e).
⚠️ Report is 305 commits behind head on next.

Files with missing lines Patch % Lines
packages/extractor-vue/src/compile-script-setup.ts 83.33% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             next    #2414       +/-   ##
===========================================
+ Coverage   76.66%   88.75%   +12.08%     
===========================================
  Files          81      118       +37     
  Lines        2083     3300     +1217     
  Branches      532      975      +443     
===========================================
+ Hits         1597     2929     +1332     
+ Misses        375      333       -42     
+ Partials      111       38       -73     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrii-bodnar
Copy link
Contributor

andrii-bodnar commented Feb 6, 2026

Thanks for the contribution! Is this something only relevant to the experimental (dependency tree) extractor and not the regular extractor?

It introduces the framework-specific configuration in the conf package. What about making it a configuration of the Vue Extractor? Something like:

// lingui.config.js
import { createVueExtractor } from '@lingui/extractor-vue'

export default {
  extractors: [
    createVueExtractor({
      reactivityTransform: false,
      // ...
    })
  ]
}

Although it's more complicated, it's more future-proof as we can easily add more options if needed. @mister-what @timofei-iatsenko what do you think?

@timofei-iatsenko
Copy link
Collaborator

This would be the best option - but a breaking change for existing users. If that would be implemented for the vue extractor the same should be done for babel extractor for consistency.

So instead of object both vue and babel would export a factory function from the package similarly to formatters.

@mister-what
Copy link
Author

I could make it non breaking by exposing the factory and exposing the configured extractor. The latter could then be deprecated.

Copy link
Collaborator

@timofei-iatsenko timofei-iatsenko left a comment

Choose a reason for hiding this comment

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

LGTM for me. Added one minor suggestion.

mister-what and others added 2 commits February 9, 2026 10:32
Co-authored-by: Timofei Iatsenko <1586852+timofei-iatsenko@users.noreply.github.com>
@andrii-bodnar
Copy link
Contributor

@mister-what please update the PR description for consistency. Thanks!

@mister-what
Copy link
Author

@andrii-bodnar description is updated 👍

@andrii-bodnar andrii-bodnar merged commit fb10a9e into lingui:next Feb 10, 2026
11 checks passed
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.

3 participants