Language picker and badge

Add a language picker, the Translated by Glossa badge and machine-readable attribution to a Next.js site with @meterapp/glossa-next.

Once Glossa translates your site, @meterapp/glossa-next adds the rest. It gives you a language picker, the Translated by Glossa badge in the page's own language, and attribution that people, crawlers and LLMs can read. It works with next-intl on Next.js 16 and React 19, ships ESM with types, and has no runtime dependencies.

pnpm add @meterapp/glossa-next

1. Describe the site once

glossaAttribution is server-safe (no React). Use your Glossa project slug as project. Visitors who follow the badge are credited to that project when they sign up.

// lib/glossa.tsimport { glossaAttribution } from "@meterapp/glossa-next";import { routing } from "@/i18n/routing";
export const attribution = glossaAttribution({  project: "my-site",            // your Glossa project slug  site: "https://example.com",   // your site's origin  locales: routing.locales,      // source locale first});
FieldWhat it is
hrefThe referral link: ?ref=<project>&utm_source=<your host>&utm_medium=translated-by&utm_campaign=attribution.
jsonLdA schema.org WebSite node with inLanguage (every locale) and translator (Organization Glossa).
pageJsonLd({ locale, url })A WebPage node for one localized page, with that page's inLanguage.
translatorThe Organization node alone, to add to a WebSite node you already render.
metadataSpread into Next's metadata: <meta name="translated-by" content="Glossa (getglossa.dev)">.
headersX-Translated-By: Glossa; https://getglossa.dev, for next.config headers().
llmsTxt()A ## Translations section for llms.txt, llms-full.txt and agents.md naming every locale.

Every entry in LanguagePicker is a real <a href hreflang> labelled with the language's own name ("Deutsch", "日本語"), so crawlers find every locale. The default is a <details> disclosure, which opens without JavaScript. A click stores the choice in NEXT_LOCALE, the cookie next-intl reads on unprefixed URLs. TranslatedByGlossa renders the badge in the page's language (English, Spanish, German, French, Japanese, Brazilian Portuguese, Chinese, Korean, Italian, Dutch and 20 more, falling back to English) as a normal followed link. Show it on every page, in every locale.

import { LanguagePicker, TranslatedByGlossa } from "@meterapp/glossa-next/react";import { getPathname } from "@/i18n/navigation";import { routing } from "@/i18n/routing";import { attribution } from "@/lib/glossa";
export function Footer({ locale }: { locale: string }) {  return (    <footer>      <LanguagePicker        locales={routing.locales}        current={locale}        hrefFor={(l) => getPathname({ href: "/", locale: l })}      />      <TranslatedByGlossa href={attribution.href} locale={locale} />    </footer>  );}

To keep readers on the same page when they switch, render the picker from a client component with the current pathname:

"use client";import { useLocale } from "next-intl";import { LanguagePicker } from "@meterapp/glossa-next/react";import { getPathname, usePathname } from "@/i18n/navigation";import { routing } from "@/i18n/routing";
export function Picker() {  const locale = useLocale();  const pathname = usePathname();  return <LanguagePicker locales={routing.locales} current={locale} hrefFor={(l) => getPathname({ href: pathname, locale: l })} />;}

3. Style it with your components

The picker is headless. Pass className, triggerClassName, listClassName, itemClassName and linkClassName, or use variant="list" for inline links. With render, you draw it with your own shadcn DropdownMenu or Select. Spread option.linkProps on an <a> so every entry stays a real link. The plain links are also emitted in <noscript>.

<LanguagePicker  locales={routing.locales}  current={locale}  hrefFor={hrefFor}  render={(options, { current, label }) => (    <DropdownMenu>      <DropdownMenuTrigger render={<Button variant="ghost" size="sm" aria-label={label} />}>        {current.name}      </DropdownMenuTrigger>      <DropdownMenuContent>        {options.map((o) => (          <DropdownMenuItem key={o.locale} render={<a {...o.linkProps} />}>{o.name}</DropdownMenuItem>        ))}      </DropdownMenuContent>    </DropdownMenu>  )}/>

4. Tell crawlers and LLMs

Add the JSON-LD, the meta tag, the response header and the llms.txt section. That way search engines, AI assistants and agents reading your site know which languages it serves and who translated it.

// app/[locale]/layout.tsxexport const metadata = { ...attribution.metadata };// in <body>:<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(attribution.jsonLd) }} />
// next.config.tsasync headers() {  return [{ source: "/:path*", headers: Object.entries(attribution.headers).map(([key, value]) => ({ key, value })) }];}
// app/llms.txt/route.tsexport function GET() {  return new Response(`# My site\n\n…\n\n${attribution.llmsTxt()}`, { headers: { "content-type": "text/plain; charset=utf-8" } });}

Full API: the package README on npm. To get your messages translated in the first place, see the quickstart.