Skip to content

compiler: i18n translations can be generated in Spanish when using a custom OpenAI-compatible model #2041

Open

Description

@visoar

Description

We hit a translation correctness issue in @lingo.dev/compiler while building a Next.js app with sourceLocale: "en" and targetLocales: ["zh-Hans"].

In our setup, newly generated zh-Hans translations were sometimes written in Spanish instead of Simplified Chinese.

For example, these strings were generated into src/.lingo/cache/zh-Hans.json and .next/zh-Hans.json as:

  • Uploaded -> Subido
  • Drag, drop, or browse -> Arrastre, suelte o explore
  • Queued -> En cola
  • Needs attention -> Necesita atención
  • Add images -> Agregar imágenes
  • Canceled -> Cancelado

The target locale in the request was definitely zh-Hans, not es.

Environment

  • @lingo.dev/compiler: 0.4.0
  • Next.js: 16.1.6
  • React: 19
  • App type: Next.js App Router
  • Build mode: translate
  • Target locale: zh-Hans

Configuration

lingo.config.ts:

import { SOURCE_LOCALE, TARGET_LOCALES } from "./src/lib/config/i18n";

export const LINGO_DEFAULT_MODEL = "openai:LongCat-Flash-Chat";

export const LINGO_MODEL_MAP: Record<string, string> = {
  "*:*": LINGO_DEFAULT_MODEL,
};

export const LINGO_PLURALIZATION_MODEL = LINGO_DEFAULT_MODEL;

Environment:

OPENAI_BASE_URL=https://api.longcat.chat/openai/v1
OPENAI_API_KEY=***

So this is using the openai: provider with an OpenAI-compatible custom base URL.

Steps to Reproduce

  1. Configure Lingo compiler with:
    • sourceLocale: "en"
    • targetLocales: ["zh-Hans"]
  2. Use a custom OpenAI-compatible model via:
    • models: { "*:*": "openai:LongCat-Flash-Chat" }
    • OPENAI_BASE_URL=https://api.longcat.chat/openai/v1
  3. Add a few new English UI strings that are not yet present in .lingo/cache/zh-Hans.json
  4. Run production build with translation generation enabled
  5. 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:

  1. using a custom OpenAI-compatible model/backend
  2. 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:

  1. Make the few-shot examples target-locale aware instead of always using Spanish
  2. Add a validation step that rejects obviously wrong-language output for the requested locale
  3. Allow disabling bundled few-shot examples for custom model setups
  4. 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.

Activity

  1. adityasharmawork commented on Jul 19, 2026

    @adityasharmawork
    Contributor

    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!

  2. adityasharmawork commented on Jul 19, 2026

    @adityasharmawork
    Contributor

    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.

  3. visoar commented on Jul 19, 2026

    @visoar
    Author

    Great! It's finally possible to solve this problem. Happy.

    thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions