What the browser actually sends
Every request a browser makes carries an Accept-Language header. A French user's Firefox sends something like fr-FR,fr;q=0.9,en;q=0.8: French preferred, English acceptable. The server can read it. JavaScript can read the same preference list as navigator.languages. So the obvious design is: look at the header, send the visitor to the matching page. Plenty of sites do exactly that, and it is the one approach Google's documentation tells you not to use.
Two reasons. Googlebot crawls mostly from the United States with English preferences, so an automatic redirect means Google only ever sees the English site and the translations never rank in their own countries. And people share links. A Dutch colleague forwards the Dutch page to a German one; the German's browser redirects to the German version; the paragraph the Dutch colleague was pointing at is now on a different URL. Links that do not land where they point are a small betrayal every time.
Seven URLs, each telling the others apart
The approach that search engines want is boring and explicit. Each language lives at its own address, /support-plans-fr.php and so on, and every copy carries a set of hreflang links naming all the others, plus an x-default pointing at the English page for anyone whose language is not covered:
<link rel="alternate" hreflang="en" href="https://www.graith.co.uk/support-plans.php" />
<link rel="alternate" hreflang="fr" href="https://www.graith.co.uk/support-plans-fr.php" />
<link rel="alternate" hreflang="nl" href="https://www.graith.co.uk/support-plans-nl.php" />
...
<link rel="alternate" hreflang="x-default" href="https://www.graith.co.uk/support-plans.php" />
That block has to be identical on all seven pages, which is a classic source of mistakes when pages are edited by hand. Google ignores hreflang pairs that do not point back at each other. Each translated page also gets its own canonical tag, its own og:locale, an <html lang> attribute in the right language (screen readers switch pronunciation on it), and a row of language links under the main heading so a human can switch without any of this machinery. Every internal link on a French page points at the French version of the target where one exists, and at the English page otherwise.
Offer, do not redirect
With the pages in place, the header question comes back: what should happen when a French browser lands on an English page? The answer is a one-line bar at the top: Cette page existe en français, with a link to the French version and a "non merci" button. Nothing moves until the visitor clicks. Google sees the English page it asked for. Shared links land where they point. The script that does this is about forty lines:
- Read the page's own
hreflanglinks, so the script knows which translations exist without being told. The same file works on every page, and on pages with no translations it does nothing. - Walk
navigator.languagesin order and stop at the first language that has a translation. - If that language is not the one the page is already in, show the bar, worded in the offered language, not the page's language. A French speaker on an English page should be able to read the offer.
One refinement makes it feel like a feature rather than nagging. If a visitor has chosen a language before, later visits to a page in another language jump straight to their choice. Dismissing the bar is remembered too. Both need the site to remember something, which brings in the cookie.
The one cookie, and why it needs no consent
The UK rule on cookies is the Privacy and Electronic Communications Regulations, PECR, regulation 6. It applies to anything stored on or read from the visitor's device, whatever it is called, and it requires consent unless the storage is strictly necessary for a service explicitly requested by the user. The ICO's guidance on that exemption lists preferences the user has set themselves as a typical example. The question is never "is this cookie harmless" but "did the user ask for the thing this cookie does".
So the design is arranged around that test:
- Nothing is stored when the bar is merely shown. Arriving at the site sets no cookie.
- The cookie
graith_langis written only when the visitor clicks the link or the dismiss button. Its value is a two-letter language code. That is the whole of the data. - It is first-party,
SameSite=Lax,Secure, and nothing else reads it. - The privacy notice names it, says what it holds and how long it lasts.
A click on "show me the French version" is as explicit a request as a website gets, and remembering it is the service. The honest caveat: the European regulators' 2012 opinion that shaped this exemption said user-interface preferences like language should ideally be remembered for a session or a few hours. We chose a year, on the view that a returning visitor who has to be asked again on every visit is worse served, and that the notice discloses it. If a reader's lawyer prefers the stricter reading, the lifetime is one number in the script.
What would have broken this is the thing most sites do without thinking: bundling the language choice with analytics or marketing cookies and asking for consent to all of it with one banner. Then the visitor has to accept tracking to get their language remembered, which is exactly the coupling the regulation exists to prevent, and the site needs a consent manager, a cookie policy page and a way to withdraw consent. None of that is needed when the only cookie is one the visitor explicitly asked for.
Analytics without a banner either
The same week, the site gained usage analytics for the first time, and the choice was made on the same test. Google Analytics sets identifiers on the device for its own purposes, which is not something any visitor requested, so it needs a consent banner in the UK and an ad-blocker removes it for a third of visitors anyway. Cloudflare Web Analytics sets no cookie, stores nothing in the browser and does not fingerprint. It reports page views, referrers and countries in aggregate. For a site whose job is to show that it takes security seriously, loading a consent banner to justify a tracking script would have been the wrong first impression. The result is that a visitor can read every page, pick a language and send an enquiry, and the only thing ever written to their device is the preference they chose.
Keeping 36 pages honest
Translation is the easy part now. The hard part is month two, when the English support plans page gets a new paragraph and the six translations silently fall behind. The usual answer is a content management system with translation workflow, which this site, a few dozen hand-written PHP pages, does not have and does not need.
Instead the English page is the master and a small build script owns the rest. It reads each English page, extracts the translatable segments (every paragraph, heading, list item, meta description and image alt text, as HTML fragments so inline links and emphasis survive), and looks each one up in a JSON file per language. A translated copy is written with all the plumbing above applied automatically: hreflang set, canonical, locale, language row, rewritten links. Any segment without a translation stays English and is listed in the output, so an edit to the English page is followed by one command, a short list of sentences to translate, and a rebuild. The translations themselves were produced by Claude, one agent per language running in parallel, each told to keep tags, prices, product names and addresses untouched and to use the business register of its country, and each verified mechanically afterwards: every value has the same tag sequence and the same links as its source.
The one trap worth passing on from that build. A regular expression that matches an element and its content will, on a page of nested divs, match the outer element up to the first closing tag and swallow the inner ones. The price paragraph on one page sat inside a styled box and was invisible to the extractor for exactly that reason. The fix is to scan opening tags one at a time and find each one's own closing tag. If a translated page has one stubborn English paragraph, that is usually why.
What it took and what it is for
Six pages, six languages, hreflang, the banner, the cookie, the privacy notice update, analytics and the build script: one working day, most of it spent on the decisions in this article rather than on the words. Whether it brings a single enquiry from Helsinki is not known yet. What it has already done is make the question "can you set our site up in several languages properly" one we can answer from a site that is set up that way.
If you want this on your site
Multilingual pages with correct hreflang and no consent machinery is a well-bounded piece of work on most Drupal or plain PHP sites, and it is included where relevant in a site audit, which checks the hreflang reciprocity, the canonical tags and what the site writes to visitors' devices before anyone has clicked anything. Agencies can have it done for a client under Drupal for agencies. For anything else, get in touch, in any of seven languages.
