Yesterday's language work had a side effect I only noticed when I opened the admin panel today: the post list had tripled. Every post is now a real row per language under the hood, which is the right data model, but the admin list showed all of them. 34 posts, 102 rows, and the one I wanted to edit buried between its own Norwegian and Spanish twins. It looked like a mess because it was one.
The fix has two halves. The list now filters to one language by default (English), so each post shows up once, and every row carries a small chip strip showing which versions exist and their status. Green chip, published. Grey chip, still a draft. Click a chip and you land in that version's editor. A filter select lets me flip the whole list to Norwegian, Spanish, or the old everything view when I actually want it.

The second half is inside the editor. The little language badge in the corner became a dropdown. Pick a language that already exists and you jump straight to that version. Pick one that does not exist yet and it creates the prefilled draft on the spot and takes you there. The decision logic is small enough to test as a pure function:
export function resolveLanguageSwitch(
current: Language,
siblings: LanguageSwitchSibling[],
selected: Language
): LanguageSwitchResult {
if (selected === current) return { type: 'noop' };
const sibling = siblings.find((s) => s.language === selected);
return sibling ? { type: 'navigate', id: sibling.id } : { type: 'create' };
}There is one race hiding in that create path: if the version somehow got created between loading the page and picking the language, the create request answers 409. The first instinct is to show an error, but from the editor the click meant "take me to the Spanish version", so on a 409 the server action just looks up which row won and navigates there instead. The user asked to switch, so switch.
The chips needed a small backend addition too. The list endpoint had no idea about siblings, and asking per row would mean a query per post on every page load. Instead one batched query fetches every sibling across the whole page's translation groups at once and the handler folds them into each row. One extra query total, not thirty.
The review caught one real bug before merge: an unrecognized language in the URL (say a stale bookmark with ?language=de) correctly fell back to English for the data, but the filter dropdown was told the raw value and quietly desynced from what the list actually showed. One line, but exactly the kind of lie a UI should not tell.
Honest leftover: the editor still has the old translations panel lower down, now redundant next to the dropdown. It works, it is just duplicated, and it can go in a later cleanup. The list, though, is back to 34 rows, and the messy week where every post existed in triplicate is over.