@@ -18,7 +18,7 @@ import {
1818} from "../translation-server" ;
1919import {
2020 cleanupExistingMetadata ,
21- getMetadataPath ,
21+ getMetadataPath as rawGetMetadataPath ,
2222 MetadataManager ,
2323} from "../metadata/manager" ;
2424import { createLingoConfig } from "../utils/config-factory" ;
@@ -50,14 +50,24 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
5050 // Won't work for webpack most likely. Use mode there to set correct environment in configs.
5151 const isDev = config . environment === "development" ;
5252 const startPort = config . dev . translationServerStartPort ;
53- const metadataFilePath = getMetadataPath ( config ) ;
53+
54+ // For webpack: store the actual mode and use it to compute the correct metadata path
55+ let webpackMode : "development" | "production" | undefined ;
56+ // Should be dynamic, because webpack only tells us the mode inside the plugin, not inside the config.
57+ const getMetadataPath = ( ) => {
58+ return rawGetMetadataPath (
59+ webpackMode ? { ...config , environment : webpackMode } : config ,
60+ ) ;
61+ } ;
5462
5563 return {
5664 name : PLUGIN_NAME ,
5765 enforce : "pre" , // Run before other plugins (especially before React plugin)
5866
5967 vite : {
6068 async buildStart ( ) {
69+ const metadataFilePath = getMetadataPath ( ) ;
70+
6171 cleanupExistingMetadata ( metadataFilePath ) ;
6272
6373 registerCleanupOnCurrentProcess ( {
@@ -86,12 +96,13 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
8696 } ,
8797
8898 async buildEnd ( ) {
99+ const metadataFilePath = getMetadataPath ( ) ;
89100 if ( ! isDev ) {
90101 try {
91102 await processBuildTranslations ( {
92103 config,
93104 publicOutputPath : "public/translations" ,
94- metadataFilePath : metadataFilePath ,
105+ metadataFilePath,
95106 } ) ;
96107 } catch ( error ) {
97108 logger . error ( "Build-time translation processing failed:" , error ) ;
@@ -104,13 +115,22 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
104115 // if (config.isEmbeddedIntoNext) {
105116 // return;
106117 // }
107- compiler . hooks . watchRun . tapPromise ( PLUGIN_NAME , async ( ) => {
118+
119+ webpackMode =
120+ compiler . options . mode === "development" ? "development" : "production" ;
121+ const metadataFilePath = getMetadataPath ( ) ;
122+ // Yes, this is dirty play, but webpack runs only for this plugin, and this way we save people from using wrong config
123+ config . environment = webpackMode ;
124+
125+ compiler . hooks . initialize . tap ( PLUGIN_NAME , ( ) => {
108126 cleanupExistingMetadata ( metadataFilePath ) ;
109127 registerCleanupOnCurrentProcess ( {
110128 cleanup : ( ) => cleanupExistingMetadata ( metadataFilePath ) ,
111129 } ) ;
130+ } ) ;
112131
113- if ( compiler . options . mode === "development" && ! translationServer ) {
132+ compiler . hooks . watchRun . tapPromise ( PLUGIN_NAME , async ( ) => {
133+ if ( webpackMode === "development" && ! translationServer ) {
114134 translationServer = await startTranslationServer ( {
115135 startPort,
116136 onError : ( err ) => {
@@ -121,7 +141,7 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
121141 `Translation server started successfully on port: ${ port } ` ,
122142 ) ;
123143 } ,
124- config : { ... config , environment : compiler . options . mode } ,
144+ config,
125145 } ) ;
126146 registerCleanupOnCurrentProcess ( {
127147 asyncCleanup : async ( ) => {
@@ -132,12 +152,12 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
132152 } ) ;
133153
134154 compiler . hooks . additionalPass . tapPromise ( PLUGIN_NAME , async ( ) => {
135- if ( compiler . options . mode === "production" ) {
155+ if ( webpackMode === "production" ) {
136156 try {
137157 await processBuildTranslations ( {
138- config : { ... config , environment : compiler . options . mode } ,
158+ config,
139159 publicOutputPath : "public/translations" ,
140- metadataFilePath : metadataFilePath ,
160+ metadataFilePath,
141161 } ) ;
142162 } catch ( error ) {
143163 logger . error ( "Build-time translation processing failed:" , error ) ;
@@ -146,7 +166,7 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
146166 }
147167 } ) ;
148168
149- // Duplicates process handlers, but won't hurt
169+ // Duplicates the cleanup process handlers does , but won't hurt since cleanup is idempotent.
150170 compiler . hooks . shutdown . tapPromise ( PLUGIN_NAME , async ( ) => {
151171 cleanupExistingMetadata ( metadataFilePath ) ;
152172 await translationServer ?. stop ( ) ;
@@ -189,43 +209,48 @@ export const lingoUnplugin = createUnplugin<LingoPluginOptions>((options) => {
189209 return null ;
190210 } ,
191211
192- load ( id ) {
193- logger . warn ( `ID: ${ id } ` ) ;
194- if ( id === "\0virtual:lingo-dev-config" ) {
195- const serverUrl =
196- translationServer ?. getUrl ( ) || `http://127.0.0.1:${ startPort } ` ;
197- const cacheDir = getCacheDir ( config ) ;
198-
199- return `export const serverUrl = ${ JSON . stringify ( serverUrl ) } ;
212+ load : {
213+ filter : {
214+ id : / v i r t u a l : / ,
215+ } ,
216+ handler ( id : string ) {
217+ logger . warn ( `ID: ${ id } ` ) ;
218+ if ( id === "\0virtual:lingo-dev-config" ) {
219+ const serverUrl =
220+ translationServer ?. getUrl ( ) || `http://127.0.0.1:${ startPort } ` ;
221+ const cacheDir = getCacheDir ( config ) ;
222+
223+ return `export const serverUrl = ${ JSON . stringify ( serverUrl ) } ;
200224export const cacheDir = ${ JSON . stringify ( cacheDir ) } ;` ;
201- }
225+ }
202226
203- // Server locale resolver - default implementation
204- if ( id === "\0virtual:locale-server" ) {
205- // For Next.js, generate server-side locale resolver using cookies
206- const implementation = generateServerLocaleCode ( config ) ;
207- return `
227+ // Server locale resolver - default implementation
228+ if ( id === "\0virtual:locale-server" ) {
229+ // For Next.js, generate server-side locale resolver using cookies
230+ const implementation = generateServerLocaleCode ( config ) ;
231+ return `
208232export async function getServerLocale() {${ implementation }
209233}
210234` ;
211- }
235+ }
212236
213- // Client locale resolver - default implementation
214- // Includes both getClientLocale() and persistLocale()
215- if ( id === "\0virtual:locale-client" ) {
216- const { getClientLocale, persistLocale } =
217- generateClientLocaleCode ( config ) ;
218- return `
237+ // Client locale resolver - default implementation
238+ // Includes both getClientLocale() and persistLocale()
239+ if ( id === "\0virtual:locale-client" ) {
240+ const { getClientLocale, persistLocale } =
241+ generateClientLocaleCode ( config ) ;
242+ return `
219243export function getClientLocale() {
220244 ${ getClientLocale }
221245}
222246
223247export function persistLocale(locale) {
224248 ${ persistLocale }
225249}` ;
226- }
250+ }
227251
228- return null ;
252+ return null ;
253+ } ,
229254 } ,
230255
231256 transform : {
@@ -239,8 +264,8 @@ export function persistLocale(locale) {
239264 code : config . useDirective ? useI18nRegex : undefined ,
240265 } ,
241266 handler : async ( code , id ) => {
267+ // TODO (AleksandrSl 13/12/2025): It's weird we don't have any this.getOptions() here
242268 try {
243- // TODO (AleksandrSl 13/12/2025): How do I get Webpack mode here?
244269 // Transform the component
245270 const result = transformComponent ( {
246271 code,
@@ -255,8 +280,7 @@ export function persistLocale(locale) {
255280 logger . debug ( `No transformation needed for ${ id } ` ) ;
256281 return null ;
257282 }
258-
259- const metadataManager = new MetadataManager ( metadataFilePath ) ;
283+ const metadataManager = new MetadataManager ( getMetadataPath ( ) ) ;
260284
261285 // Update metadata with new entries (thread-safe)
262286 if ( result . newEntries && result . newEntries . length > 0 ) {
0 commit comments