Skip to content

Repository files navigation

langfence-mark-large

langfence

A client-side language guardrail for AI output in the browser. Detects when text has drifted from the language your backend told the model to use, and rewrites it — using Chrome's built-in Translator and Language Detector APIs (on-device, Gemini Nano). No server round-trip, no manual i18n dictionary.

The problem

Your backend prompts an LLM to always respond in, say, pt-BR. Most of the time it does. Occasionally it doesn't — a stray English phrase, a leaked technical term in another language, a full response in the wrong language entirely. The instruction was correct; the model still drifted.

langfence catches this on the client: it checks the language of text you're about to render, and only pays the cost of a rewrite when the text actually needs one. It's also useful for dynamic API responses you don't want to (or can't) map into a static i18n JSON file — it keeps output language-consistent without a translation dictionary.

Try it live: jhonatangalves.github.io/langfence-playground — every GuardrailOptions field as an interactive control, runs entirely in your own browser.

Install

npm install langfence

Quick start

import { normalize } from 'langfence'

const text = await normalize(apiResponseText, {
  expectedLanguage: 'pt-BR',
})

render(text)

That's it for the common case. normalize() only calls the Translator API when the text actually needs rewriting — if it's already confidently in pt-BR, you get the original string back untouched, with no extra API call.

Showing loading state

import { normalize, type GuardrailStatus } from 'langfence'

const text = await normalize(apiResponseText, {
  expectedLanguage: 'pt-BR',
  onStatusChange: (status: GuardrailStatus, error) => {
    // 'idle' | 'detecting' | 'translating' | 'done' | 'unsupported' | 'error'
    setGuardrailStatus(status)
    if (status === 'error') console.warn('langfence:', error)
  },
})

Showing a download progress bar

The first time a given language configuration is used, Chrome may need to download the underlying model, which can take a while. onDownloadProgress reports that separately from onStatusChange, since it's continuous (0–1) rather than a discrete state:

const text = await normalize(apiResponseText, {
  expectedLanguage: 'pt-BR',
  onDownloadProgress: ({ stage, loaded }) => {
    // stage: 'detecting' | 'translating'
    setDownloadProgress(Math.round(loaded * 100))
  },
})

It only fires on a cold start for that language configuration — once the model is on disk, later calls skip straight to 'detecting'/'translating' with no download step.

API

normalize(text, options)

function normalize(text: string, options: GuardrailOptions): Promise<string>

Always resolves to a string and never throws: on any native API failure, or when this browser doesn't support the APIs at all, the original text is returned unchanged (fail-open). Use onStatusChange to find out what actually happened.

Pipeline:

  1. Empty text → returned immediately, no API calls.
  2. Feature detection (Translator/LanguageDetector on globalThis) → if missing, status: 'unsupported', original text returned.
  3. LanguageDetector runs on the original text.
  4. If the top detected language equals expectedLanguage and its confidence is >= minConfidence, and forceRewrite isn't set, the original text is returned — Translator is never called.
  5. Otherwise, Translator rewrites the text into expectedLanguage, using the top detected language as the source. The text is translated line by line (splitting on \n, preserving blank lines and leading indentation) rather than as one blob, since Translator.translate() has no obligation to keep whitespace structure intact across a multi-line string.

GuardrailOptions

Option Type Default Description
expectedLanguage string — (required) BCP 47 tag the text should be in, e.g. 'pt-BR'.
minConfidence number 0.9 Minimum detector confidence to skip rewriting.
forceRewrite boolean false Always rewrite via Translator, skipping the confidence check.
onStatusChange (status, error?) => void undefined Called on every status transition; error is set only when status === 'error'.
onDownloadProgress (progress) => void undefined Called with { stage, loaded } while a model is downloading. Only fires on a cold start for that language configuration.

GuardrailStatus

'idle' | 'detecting' | 'translating' | 'done' | 'unsupported' | 'error'

GuardrailError

interface GuardrailError {
  stage: 'detecting' | 'translating'
  message: string
  cause: unknown // the original value thrown by the native API
}

Delivered exclusively through onStatusChangenormalize()'s return value is always just the resulting string.

GuardrailDownloadProgress

interface GuardrailDownloadProgress {
  stage: 'detecting' | 'translating'
  loaded: number // fraction between 0 and 1
}

Delivered exclusively through onDownloadProgress.

Caching

langfence caches native instances — one Translator per source/target language pair, one LanguageDetector per set of expected input languages — so repeated calls with the same language configuration reuse the same instance instead of paying model-creation cost again.

Limitations

  • Chrome/Edge only, desktop only. Translator requires Chrome 138+/Edge 148+; LanguageDetector the same. Neither is available on Firefox, Safari, or mobile browsers.
  • Requires a secure context (HTTPS).
  • On first use for a given language pair, the underlying model may need to download — this can take noticeable time and disk space. See developer.chrome.com/docs/ai for current hardware/storage requirements.
  • LanguageDetector is restricted to top-level windows and same-origin iframes by default; cross-origin iframes need a language-detector Permission Policy.
  • These are experimental browser APIs that have changed shape before (e.g. moving from window.ai.translator to window.Translator) and may change again. langfence targets the current global-namespace shape; breaking browser changes will be called out in the CHANGELOG.

Contributing

pnpm install
pnpm test:coverage   # 100% coverage required
pnpm lint
pnpm typecheck
pnpm build

Changes are versioned with Changesets: run pnpm changeset alongside your PR to describe the change for the changelog.

License

MIT

About

Client-side language guardrail for AI output — detects and fixes language drift using Chrome's built-in Translator and Language Detector APIs. No server, no API keys.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages