You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
compiler: i18n translations can be generated in Spanish when using a custom OpenAI-compatible model #2041
Add a few new English UI strings that are not yet present in .lingo/cache/zh-Hans.json
Run production build with translation generation enabled
Inspect newly generated entries in:
.lingo/cache/zh-Hans.json
.next/zh-Hans.json
Expected Result
New translations for zh-Hans should always be generated in Simplified Chinese.
Actual Result
Some newly generated zh-Hans entries were generated in Spanish.
What We Verified
We checked the following locally:
The app only supports en and zh-Hans
targetLocales is correct
Lingo translation requests were sent to /translations/zh-Hans
The wrong-language output only affected newly generated cache misses
Existing cached Chinese entries remained correct
This suggests the locale routing is correct, but the final model output can drift into Spanish under this setup.
Possible Cause
This may be caused by a combination of:
using a custom OpenAI-compatible model/backend
the compiler's built-in few-shot examples being en -> es
In @lingo.dev/compiler@0.4.0, the bundled few-shot example in build/translators/lingo/shots.mjs is Spanish-based (locale: "es"). A weaker or less instruction-following OpenAI-compatible model may get biased by that example and respond in Spanish even when the target locale is zh-Hans.
Suggested Improvements
Any of these would likely help:
Make the few-shot examples target-locale aware instead of always using Spanish
Add a validation step that rejects obviously wrong-language output for the requested locale
Allow disabling bundled few-shot examples for custom model setups
Document that some weaker OpenAI-compatible models may produce cross-locale drift
Additional Notes
We manually corrected the wrong zh-Hans cache entries and confirmed the app then rendered correctly.
If helpful, I can provide a minimal reproduction config based on Next.js 16 + @lingo.dev/compiler@0.4.0.
Hey @vrcprl and @maxprilutskiy and other maintainers!
This seems interesting to me and I have already started working towards the fix, can you please assign it to me? Thanks!
I spent some time digging into this and can confirm the behavior. The cause is the one suggested above: the few-shot example the compiler sends to the model.
In @lingo.dev/compiler, the built-in few-shot lives in packages/new-compiler/src/translators/lingo/shots.ts and is a single hard-coded English→Spanish pair. It gets injected into every request in translator.ts (the raw-LLM path used for custom / OpenAI-compatible models), just before the actual source dictionary. Because a few-shot pair is effectively a worked "here is the correct answer" example, sending a Spanish one while asking for zh-Hans hands the model two conflicting signals: the system prompt asks for the target locale (as a bare code), while the only concrete demonstration in the request is Spanish. Stronger models follow the instruction; weaker or less instruction-following backends tend to imitate the example and drift to Spanish. That also lines up with the observation that it only showed up on cache misses, since cached entries never reach the model.
Two things made it easier to hit: the prompt referenced the target only as a locale code rather than naming the language, and there is no post-generation language check, so the drifted output was accepted and cached.
What I changed, all under packages/new-compiler/src/translators/lingo/:
Replaced the static shots import with a getShots(targetLocale) selector that only returns an example whose language matches the requested target. The existing Spanish example is kept, and I added correct ones for a curated set (fr, de, it, pt, nl, ru, ja, ko, and Simplified/Traditional Chinese). Any locale without a bundled example gets none rather than a mismatched one, so the model is never shown a wrong-language demonstration. Chinese is matched script-aware, so a Simplified target never receives a Traditional example or vice versa.
Added the readable language name to the system prompt (for example Target language (locale code): zh-Hans (Simplified Chinese)) via Intl.DisplayNames, which gives even the un-bundled locales a clearer target signal and falls back to the plain code if a name can't be resolved.
Added unit tests for the selector and the prompt, plus a changeset.
On scope and safety: existing translations are unaffected because the cache is keyed by content hash rather than the prompt, so nothing gets re-translated; the default lingo.dev engine path doesn't use these examples and is untouched; and I intentionally left the deprecated @lingo.dev/_compiler package alone even though it carries the same pattern, since it's marked for removal. The bundled example strings are standard UI terms, but I'd welcome a second look from a native speaker on any of them, and any locale can be adjusted or dropped back to the no-example fallback with no risk.
This is my reading of the problem and the least-invasive fix I could find, but you know the codebase far better than I do. If the approach doesn't fit how you'd prefer to handle this, or you'd rather see a different shape (validating the output language, making the examples configurable, and so on), I'm glad to revise. I'm still getting familiar with the project and would rather get it right by your standards than land my first take as-is.
Activity
adityasharmawork commentedon Jul 19, 2026
Hey @vrcprl and @maxprilutskiy and other maintainers!
This seems interesting to me and I have already started working towards the fix, can you please assign it to me? Thanks!
adityasharmawork commentedon Jul 19, 2026
I spent some time digging into this and can confirm the behavior. The cause is the one suggested above: the few-shot example the compiler sends to the model.
In
@lingo.dev/compiler, the built-in few-shot lives inpackages/new-compiler/src/translators/lingo/shots.tsand is a single hard-coded English→Spanish pair. It gets injected into every request intranslator.ts(the raw-LLM path used for custom / OpenAI-compatible models), just before the actual source dictionary. Because a few-shot pair is effectively a worked "here is the correct answer" example, sending a Spanish one while asking forzh-Hanshands the model two conflicting signals: the system prompt asks for the target locale (as a bare code), while the only concrete demonstration in the request is Spanish. Stronger models follow the instruction; weaker or less instruction-following backends tend to imitate the example and drift to Spanish. That also lines up with the observation that it only showed up on cache misses, since cached entries never reach the model.Two things made it easier to hit: the prompt referenced the target only as a locale code rather than naming the language, and there is no post-generation language check, so the drifted output was accepted and cached.
What I changed, all under
packages/new-compiler/src/translators/lingo/:shotsimport with agetShots(targetLocale)selector that only returns an example whose language matches the requested target. The existing Spanish example is kept, and I added correct ones for a curated set (fr, de, it, pt, nl, ru, ja, ko, and Simplified/Traditional Chinese). Any locale without a bundled example gets none rather than a mismatched one, so the model is never shown a wrong-language demonstration. Chinese is matched script-aware, so a Simplified target never receives a Traditional example or vice versa.Target language (locale code): zh-Hans (Simplified Chinese)) viaIntl.DisplayNames, which gives even the un-bundled locales a clearer target signal and falls back to the plain code if a name can't be resolved.On scope and safety: existing translations are unaffected because the cache is keyed by content hash rather than the prompt, so nothing gets re-translated; the default
lingo.devengine path doesn't use these examples and is untouched; and I intentionally left the deprecated@lingo.dev/_compilerpackage alone even though it carries the same pattern, since it's marked for removal. The bundled example strings are standard UI terms, but I'd welcome a second look from a native speaker on any of them, and any locale can be adjusted or dropped back to the no-example fallback with no risk.This is my reading of the problem and the least-invasive fix I could find, but you know the codebase far better than I do. If the approach doesn't fit how you'd prefer to handle this, or you'd rather see a different shape (validating the output language, making the examples configurable, and so on), I'm glad to revise. I'm still getting familiar with the project and would rather get it right by your standards than land my first take as-is.
visoar commentedon Jul 19, 2026
Great! It's finally possible to solve this problem. Happy.
thanks.