今日已更新 133 条资讯 | 累计 37382 条内容
关于我们

标签:#i18n

找到 9 篇相关文章

AI 资讯

Why your hreflang tags are being ignored

Originally published on the WeLocale blog . Most SEO work is a matter of degree. You improve a title, you gain a little. hreflang is not like that. It either forms a valid set that search engines act on, or it does nothing at all, and the failure is completely silent. No warning, no penalty, no message in Search Console telling you the tags you carefully added are being discarded. We build a translation widget, which means we generate hreflang tags for other people's sites. This post is what we have learned about why they get ignored, including the parts where our own approach has real limits. The rule that breaks most setups hreflang is not a property of a page. It is a property of a set of pages, and every page in that set has to agree. If your English page says the German version is at /de/ , the German page has to say the English version is at / . If it does not, the declaration is one-way, and one-way declarations get dropped. Google calls these return links and treats their absence as a reason to distrust the whole set. This is why hreflang fails in a way that feels unfair. Every individual page looks correct when you inspect it. The problem only exists in the relationship between pages, which is exactly the thing you cannot see by viewing source on one URL. The corollary catches people too: each page must list itself . A German page whose tags mention English and French but not German is an incomplete set. Incomplete sets get dropped. The other four failure modes en-UK. The language code comes from ISO 639-1 and the region code from ISO 3166-1. In ISO 3166-1 the United Kingdom is GB. There is no UK. The tag is silently invalid, and it is easily the most common hreflang error on the web. Same class of mistake: lowercase regions, uppercase languages, and a region with no language at all. URLs that redirect. hreflang has to point at the final URL. If it points at http and you redirect to https, or it omits a trailing slash your server adds, the target is a redir

2026-08-25 原文 →
AI 资讯

Building a 9-Language Fan Site with Next.js 15 and next-intl (No Middleware)

I recently built a multilingual fan site for The Duskbloods , an upcoming FromSoftware game. The challenge: 9 languages (English, Japanese, Korean, Chinese, Spanish, French, German, Italian, Portuguese), static generation , and no middleware — all deployed on Cloudflare Workers. Here's how I did it and what I learned. The Architecture The site uses Next.js 15 App Router with next-intl v4 for internationalization. The key constraint: I wanted to avoid middleware to keep Cloudflare Worker costs down. src/ ├── app/ │ ├── (root)/ # English at / │ │ ├── gameplay/ │ │ ├── characters/ │ │ └── ... │ └── [locale]/ # Other languages at /zh, /ja, /ko... │ ├── gameplay/ │ ├── characters/ │ └── ... ├── messages/ # Translation files │ ├── en.json │ ├── ja.json │ ├── zh.json │ └── ... └── components/ # Shared components └── views/ Route Groups for Language Separation Instead of using middleware to detect locale, I use route groups : (root) — English content at the root path / [locale] — Other languages at /zh , /ja , /ko , etc. This means English gets clean URLs ( /gameplay ) while other languages get prefixed URLs ( /zh/gameplay ). Good for SEO — English is the default, and other languages have clear URL signals. Why No Middleware? Cloudflare Workers charge per request. Middleware runs on every request. For a static site with 9 languages, that's 9x the middleware invocations for every page load. By handling locale in the route, I skip middleware entirely. // src/app/[locale]/layout.tsx export async function generateStaticParams () { return [ ' ja ' , ' zh ' , ' ko ' , ' es ' , ' fr ' , ' de ' , ' it ' , ' pt ' ]. map ( locale => ({ locale })); } This pre-generates all locale variants at build time. Zero runtime locale detection. The Translation System Message Files Each locale has a JSON message file: // src/messages/zh.json { "gameplay" : { "intro" : { "eyebrow" : "玩法介绍" , "title" : "游戏机制" , "lead" : "深入了黄昏征讨的核心机制。" }, "virtue" : { "title" : "美德" , "types" : [ { "title" : "讨伐之美德

2026-08-22 原文 →
AI 资讯

I set the font to the largest size and found the same bug eleven times

I was cleaning up the UI on a side-project iOS app and did one thing: set Dynamic Type to XXXL and screenshot every screen. Reading the code had turned up nothing. The screenshots showed problems immediately. Eleven of them, in the end. All the same cause. Here's the conclusion first. "A parent that pins things side by side" × "text that grows" is not a bug, it's a pattern. And in Japanese it breaks reliably worse than in English. English truncates. Japanese stacks vertically. Same layout, different failure depending on language. English wraps at word boundaries and, failing that, ends in … . You can't read it, but you can tell something was cut. Japanese doesn't do that. It can break between almost any two characters, so once a column is squeezed to one character wide, you get one character per line, stacked downward. Actual output: Rendered Intended Paire / d / Macs Paired Macs ( broken mid-word ) Claud / e / Code Claude Code One character per line "実行中のセッション" (Running sessions) Three step labels stacked vertically A three-step horizontal stepper De / mo , turning the capsule into a circle A "Demo" badge Paire / d / Macs is English breaking mid-word. Once the column has only a few characters left, even English gets there. Japanese gets there much earlier. The cause had the same shape every time Nearly all eleven were this: HStack { Image ( systemName : icon ) . frame ( width : 44 ) // fixed Text ( label ) Spacer () Text ( value ) // pinned right } The 44pt icon and the trailing value claim their width first, leaving the label column a few characters. The fix: rows that carry a value drop the value to the line below — but affordances like chevrons and toggles stay on the right. That row component was shared across the whole settings tree, so fixing one place fixed the entire settings screen. Which also means one decision inside a shared component was breaking eleven screens. ViewThatFits is not a general answer I used ViewThatFits to switch to a stacked layout. It

2026-08-16 原文 →
AI 资讯

I stopped hardcoding locales, and adding a language became a one-line change

Most i18n tutorials stop at "put your strings in a JSON file". That covers labels. It does not cover the thing that actually breaks: URLs. I run a personality test site in five locales (French, English, Brazilian Portuguese, European Portuguese, Spanish). Same site, five language trees, roughly 96 articles each. The first version had the shape every project seems to grow by accident: $prefix = $locale === 'en' ? '/en' : '' ; That line, or a cousin of it, spread across controllers, views and helpers. Every one of them was correct when written and wrong the moment a third locale showed up. The bug it produces is the worst kind: nothing throws, the page renders, and the Spanish version quietly links to French URLs. The rule that fixed it One rule, enforced by a test: no locale literal anywhere outside the config file. Not in controllers, not in views, not in helpers. If code needs to know something about a locale, it asks the config. Adding a locale then becomes: content files, plus one entry in config/locales.php . Nothing structural. 'default' => 'fr' , 'available' => [ 'fr' , 'en' , 'pt-br' , 'pt-pt' , 'es' ], Three helpers cover essentially every call site: locale_prefix () // '' for the default locale, '/es' otherwise locale_url ( '/disc' ) // prefixed, canonical, ready to print locale_slug ( 'groupe' ) // the localized route segment The part nobody warns you about: slugs are data Labels translate. Slugs are a different problem, because a slug is simultaneously a URL, a cache key, an SEO asset and a foreign key into your own content. The decision that saved me: one locale is canonical, always. French, in my case. Every slug in every other language resolves back to a French slug before anything else happens. $slugs = app ( SlugService :: class ); $slugs -> toLocale ( 'quatre-tendances' , 'es' ); // 'cuatro-tendencias' $slugs -> resolveToCanonical ( 'cuatro-tendencias' ); // 'quatre-tendances' Without that pivot you get N-to-N translation tables and, eventually, two

2026-08-08 原文 →
开发者

«es» no es un mercado: el bug de i18n que nos costó reescribir una campaña entera

Nota: en Lumora construimos libros infantiles personalizados con IA, y escribimos en diez idiomas. Este artículo cuenta un problema de i18n con el que chocamos de frente y cómo lo resolvimos. Lo contamos desde dentro, con nuestro nombre, porque el error nos costó reescribir una campaña entera. Casi todos los equipos tratamos el idioma como si fuera el país. Ponemos es en el selector, sacamos las cadenas a un JSON y damos el problema por cerrado. Funciona hasta el día en que tu producto tiene algo que ver con una fecha. Y entonces descubres que es no es un mercado, son varios calendarios distintos que comparten vocabulario. El día en que el regalo no llega el 25 Teníamos una campaña de regalos escrita en español. Correcta gramaticalmente, revisada, sin errores de traducción. Y aun así estaba mal para una parte grande de quien la leía: En España , el gran momento de regalo infantil no es el 25 de diciembre: son los Reyes Magos, el 6 de enero . Un mensaje que dice "pídelo antes del 24" le está dando a una familia española una fecha límite equivocada por casi dos semanas. En México conviven las dos cosas: Navidad y Reyes, con la rosca del 6 de enero. En Argentina, Chile o Uruguay , la Navidad es el 25 de diciembre… en pleno verano . A 30 grados. Con las vacaciones escolares largas empezando, no terminando. Fíjate en lo incómodo del asunto: los tres casos hablan español. Los tres pasan el mismo test de traducción. Y los tres necesitan un mensaje distinto, una imagen distinta y una fecha límite distinta. El mismo golpe existe en portugués. En Brasil la Navidad también cae en pleno verano, enero es mes de vacaciones, y el día en que de verdad se reinicia la rutina familiar no es el 1 de enero: es la volta às aulas , en febrero. Si tu calendario de contenidos asume "año nuevo, hábitos nuevos" en enero, en Brasil llegas un mes antes de que a nadie le importe. Por qué el código de locale no te salva La respuesta obvia es "usa es-ES y es-AR ". Es correcta y casi nunca es sufic

2026-08-07 原文 →
开发者

Use Google Sheets as a Translation Database for Your Web App (Apps Script + Next.js)

Every i18n setup I've seen has the same three-way standoff. Developers want type-safe JSON in the repo. Translators want a familiar tool, not a pull request. Product wants to fix a typo without a deploy. So you either pay $50–$500/month for a localization SaaS, or you copy-paste strings between a translator's spreadsheet and your JSON files until something silently breaks. For projects under ~1,000 keys, there's a better middle: the spreadsheet is the database. Translators edit a Google Sheet; an Apps Script endpoint serves it as clean locale JSON; your app pulls that at build time. Here's the whole pattern, with the code. Why a sheet beats a translation service for small projects A localization SaaS earns its price at scale — dozens of translators, thousands of keys, screenshots and review workflows. A 300-key marketing site doesn't have that problem; it has a coordination problem. A Sheet solves coordination for free: translators already know it, it has revision history and suggested edits built in, and product can change a string in ten seconds. You only add the two things a raw sheet lacks — a clean JSON API and a fallback for missing translations. The schema: one tab, one row per key A strings tab, with the key in column A and one column per locale: key en tr es fr hero.title Welcome Hoş geldiniz Bienvenido Bienvenue hero.cta Get started Başla Empezar Commencer Use dot-notation keys ( hero.title ) so the JSON nests naturally in your i18n library. Keep a tiny meta tab too: B1 = default locale ( en ), B3 = version ( 1.0.0 ). The Apps Script endpoint Deploy this as a Web App (same mechanics as any Apps Script webhook ). doGet serves one locale — or all of them — as JSON, and the fallback lives right in the query: an empty cell resolves to the default locale, so a half-translated key never ships blank. // Code.gs const SHEET_ID = ' your-sheet-id ' ; function doGet ( e ) { const locale = ( e . parameter . locale || ' all ' ). toLowerCase (); const result = buildLoca

2026-07-31 原文 →
AI 资讯

Docusaurus i18n: How to keep translations in sync (manual vs Crowdin vs GitHub Action)

If you maintain a Docusaurus site in more than one language, you already know the actual problem isn't translation — it's staying in sync . Someone updates three paragraphs in the English docs, and six months later the Chinese (or Spanish, or whatever) version is quietly wrong, and nobody notices until a user files an issue about it. I went looking at how teams actually solve this, and it mostly comes down to three approaches. Writing this down mostly for my own reference, but sharing in case it saves someone else the research. Approach 1: Just do it manually This is what most small-to-mid docs sites do, at least at first. A maintainer (or a translator on Slack) watches for doc PRs and manually updates the other language folders. It works fine until it doesn't. The failure mode is always the same: it's invisible. Nobody gets paged when a translated page goes stale — it just sits there, slightly wrong, until a reader notices the code sample doesn't match anymore. For a project with a handful of docs and one contributor doing translations, this is honestly fine. Past ~50-100 pages or more than one language, it stops scaling — not because the translation work is hard, but because tracking what changed becomes a full-time job nobody signed up for. Approach 2: A translation management platform (Crowdin, Lokalise, etc.) These are built for exactly this problem and they're genuinely good at it — string extraction, translator workflows, in-context editing, the works. If you have a dedicated localization team or professional translators involved, this is probably still the right call. The tradeoff for a docs-only, engineering-driven project: they're built around the assumption that there's a human translator (or a review pipeline) doing the actual translating, plus a separate sync step to pull translations back into your repo. That's the right tool when translation quality and nuance matter enormously (marketing copy, legal text) or when you have translators who aren't devel

2026-07-28 原文 →
AI 资讯

4 ways canvas text rendering breaks in multilingual apps (that en/ja testing will never catch)

I run a large fleet of "preview it, then download it as PNG" web tools — name tags, certificate generators, price cards, badges — in five languages: Japanese, English, Spanish, French, Portuguese. Canvas 2D text rendering looks correct as long as you only test Japanese and English. It breaks when you run es/fr/pt through it. After stepping on these repeatedly, the failures collapse into four patterns. The premise: Latin languages run 1.4–2× longer than Japanese Design data first. The same label, measured across five locales: Example ja en es fr pt Tool name 22 chars 35 62 48 50 "Standard" button 4 8 10 20 12 Rule of thumb: fr/pt come out 1.4–1.7× longer than ja; es can balloon to nearly 3×. A font size and maxWidth tuned to fit Japanese will not fit the Latin locales. All four failure patterns grow from this. Pattern 1: hand-rolled wrapping via text.split(/\s+/) collapses on CJK The classic snippet — split on spaces, wrap word by word — does nothing for Japanese or Chinese, where words aren't space-delimited. An entire sentence becomes one unbreakable token and clips at the canvas edge. Test with real Japanese input and check that the final line renders to its last character. "Most of it showed up" is not a pass. Pattern 2: an ASCII-only tokenizer splits words at accented characters Fix pattern 1 with a character-class tokenizer like [A-Za-z0-9'\-_] and you've traded one regression for another: ç é ã ó ñ aren't in that class, so produção fragments into produ / ç / ão mid-word. An English test will never catch this. Generate actual PNGs with fr/es/pt samples and eyeball the area around accented characters. I never found another detection method — string-comparison tests can't see a rendering-level split. Pattern 3: the important word at the end vanishes into "…" Since fr/pt run 1.4–1.7× longer than the ja the layout was tuned for, text overflows its two lines and gets ellipsized. The cruel part: what disappears is the tail of the phrase — often the semantically criti

2026-07-23 原文 →
AI 资讯

Why Arabic text comes out backwards when you extract it from a PDF (and how to fix it)

If you've ever built a feature that extracts text from PDFs, an Arabic-speaking user has probably filed this bug: "the words come out in reverse order." Not the letters — the words . Every line reads last-word-first. I spent the better part of a year fixing this class of bugs while building Confileo , a free PDF toolkit with first-class Arabic support. Here's what's actually going on, because almost every explanation online is wrong or incomplete. The four distinct failure modes People say "Arabic breaks" as if it's one bug. It's four: 1. Visual vs logical order (the reversed-words bug) A PDF doesn't store text the way a Word file does — it stores positioned glyph runs : "paint these shapes at these coordinates." For left-to-right scripts, the paint order happens to match the reading order, so naive extraction works by accident. Arabic is right-to-left. Many PDF generators emit the glyph runs in visual order — the order they appear on screen, left to right. A naive extractor concatenates the runs as stored and produces every line word-reversed. The text was never "reversed" in the file; your extractor just assumed paint order == reading order. Fix: reconstruct logical order using glyph positions + the Unicode Bidirectional Algorithm (UAX #9), not the content-stream order. Libraries like PyMuPDF already return text in logical order — a common mistake is "fixing" that output by reversing it again, which is how you get double-reversed text. Rule of thumb: never reverse Arabic yourself. If it looks backwards, your rendering layer lacks bidi support; the data is usually fine. 2. Disconnected letters (the ransom-note bug) Arabic letters are contextual: ع renders differently in initial, medial, final and isolated positions, and letters join. That joining is applied at render time by a shaping engine (HarfBuzz being the standard). If any step of your pipeline round-trips text through a non-shaping renderer — a canvas library, a barebones PDF writer, an image caption filter

2026-07-04 原文 →