A Multilingual Help Center Without Empty Pages

Product

A Kookee project could have its help center in English and German, and its hosted portal would still show every page in English. The German articles were there, and the API would have returned them. The portal never asked for them.

The language picker in the portal's header made it look worse. It was Kookee's own interface picker, with the six languages Kookee's buttons come in. Picking Deutsch translated the menus and left every article in English. A project written in Italian got a picker with no Italian in it.

This came up while I was moving the portal's announcement banner into @kookee/react, and I fixed it in the same release, Kookee 1.29.0.

Each language gets its own address

A reader who sends the German page to a colleague needs a German address to send. I put the language in the path and left the default language without one. On the Lumen demo portal, which has English and German, it looks like this:

text
lumen.kookee.dev/help/getting-started/invite-your-team       English, the default
lumen.kookee.dev/de/help/erste-schritte/dein-team-einladen   German
lumen.kookee.dev/en/help/getting-started/invite-your-team    → /help/getting-started/invite-your-team
lumen.kookee.dev/fr/help/getting-started/invite-your-team    → /help/getting-started/invite-your-team
lumen.kookee.dev/de/help/getting-started/invite-your-team    → /de/help/erste-schritte/dein-team-einladen

The default language has no segment because every portal link that existed before meant the default language: links in emails, in old posts, in Kookee's MCP answers and webhook payloads. They keep working and keep meaning the same thing, and Kookee still builds them that way.

The redirects keep one address per page. /en/ on a project whose default is English goes to the address without a segment. So does a language the project doesn't have, or one it removed later: a /fr/ link shared before French was removed lands on the English page instead of a 404. And an English slug under /de/, the address you get by typing /de in front of an English link, goes to the German article's own slug.

A missing translation falls back to the default language

Lumen's help center has 12 articles in English. Three of them, the Getting started ones, also exist in German. A German reader opens lumen.kookee.dev/de/help.

It could hide the other nine. The German help center then looks finished: three articles, all German. The reader who needs the billing article doesn't learn it exists, and writes to support about something that was answered in English all along.

It could show them with a "not translated yet" note. I considered that and dropped it: the reader can see the article is in English.

Or it could show the English article at the German address, with no note, as Kookee's API and the site widget already did. The portal does that now. The category cards on /de/help show Erste Schritte, the German version of Getting started, next to Dashboards, Billing and Guides, which exist only in English:

Help categories on lumen.kookee.dev/de/help: the German Erste Schritte next to Dashboards and Billing, which exist only in English

An English-only article opened from there keeps its /de/ address: the picker still says Deutsch, and the next link the reader follows stays under /de/.

The English help article Build a retention chart opened at its German address, with Deutsch in the language picker

The article itself carries lang="en", so a screen reader reads it with an English voice instead of pronouncing English words by German rules. For Arabic and Hebrew it also sets the right-to-left direction.

The same rule runs through the rest of the portal. Blog, changelog and pages lists show the reader's language where a translation exists and the default language where it doesn't. Help categories are matched across languages, so an untranslated article opens inside the German version of its category. Search looks in the page's language first, then in the default one.

Why kookee.dev hides untranslated posts instead

My own blog does the opposite. kookee.dev/de/blog lists only posts that exist in German, and a German link to an English-only post redirects to the English page.

The difference is search engines. kookee.dev is indexed, and an English article served at a German address is a second URL with the same text. I'd rather give Google one address per text. Portals send X-Robots-Tag: noindex, so search engines leave portal pages out, and for readers the fallback wins.

The language picker lists only your languages

The picker now lists the project's languages, each under its own name: Deutsch, Italiano, 日本語. Picking one goes to the same page in that language: the article's translation if there is one, or the same article under the new segment if there isn't.

The portal header on lumen.kookee.dev/de/help with the language picker open, listing English and Deutsch

A project with one language shows no picker at all. Projects get their languages under Settings → Project, in Supported Locales, and the picker appears in the header, the mobile menu and the footer once there are two.

I removed Kookee's own interface picker from portals entirely. A portal belongs to the project, and a reader who sees six languages there will expect the articles in six. Buttons and menus still follow the reader's last choice or their browser, else English. Picking a content language Kookee's interface also has (English, Russian, Spanish, French, Portuguese or German) switches the buttons too. Picking Italian leaves them as they were.

There's no redirect by browser language. A reader whose browser is set to German lands on lumen.kookee.dev/help and gets English, with the picker in the header.

What else changed on the portal

  • The site widget on the portal reads help and answers in the page's language. It used to follow the interface language, so it could show German help next to an English page.

  • The announcement banner shows announcements in the page's language.

  • The portal's home page counts an article and its translations once. It counted each language separately before.

  • Report this page on a translated page now files the report against the article. It used to fall back to the project.

Not changed: blog tags and hreflang

A tag's count on the portal blog still spans every language. Portals get no hreflang links or sitemaps, since search engines don't index them.

Using the public portal covers the portal's addresses, picker and redirects, and Multi-language content and locale fallback the same fallback through the SDK, for a help center on your own site.

Continue reading