[{"data":1,"prerenderedAt":677},["ShallowReactive",2],{"blog-llm-translation-pipeline-i18n-en":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":26,"sources":36,"cover":69,"og":70,"expertise":71,"locales":72,"lang":73,"title":76,"description":77,"coverAlt":78,"metaTitle":79,"takeaways":80,"faq":86,"toc":102,"blocks":136,"others":427},"llm-translation-pipeline-i18n","2026-10-17",9,"web",[9,10,11,12,13],"Translation","i18n","LLM","Vue","Localization",[15,16,17,18,19,20,21,22,23,24,25],"llm translation pipeline","icu messageformat placeholders","vue i18n llm translation","translation memory llm","glossary enforcement translation","german informal sie du","hungarian translation quality llm","structured output json translation","machine translation post-editing","locale file validation","cldr plural categories",[27,30,33],{"name":28,"url":29},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",{"name":31,"url":32},"ICU MessageFormat","https:\u002F\u002Funicode-org.github.io\u002Ficu\u002Fuserguide\u002Fformat_parse\u002Fmessages\u002F",{"name":34,"url":35},"Vue I18n","https:\u002F\u002Fvue-i18n.intlify.dev\u002F",[37,39,42,45,48,51,54,57,60,63,66],{"title":38,"url":32},"Unicode ICU User Guide: Formatting Messages",{"title":40,"url":41},"Vue I18n: Message Format Syntax","https:\u002F\u002Fvue-i18n.intlify.dev\u002Fguide\u002Fessentials\u002Fsyntax",{"title":43,"url":44},"Nuxt i18n: Getting started","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fgetting-started",{"title":46,"url":47},"Unicode CLDR: Plural rules","https:\u002F\u002Fcldr.unicode.org\u002Findex\u002Fcldr-spec\u002Fplural-rules",{"title":49,"url":50},"OpenAI: Structured outputs","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",{"title":52,"url":53},"DeepL API: Glossaries","https:\u002F\u002Fdevelopers.deepl.com\u002Fdocs\u002Fapi-reference\u002Fglossaries",{"title":55,"url":56},"W3C: Localization vs. internationalization","https:\u002F\u002Fwww.w3.org\u002FInternational\u002Fquestions\u002Fqa-i18n",{"title":58,"url":59},"GNU gettext manual","https:\u002F\u002Fwww.gnu.org\u002Fsoftware\u002Fgettext\u002Fmanual\u002Fgettext.html",{"title":61,"url":62},"RFC 5646: Tags for Identifying Languages","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5646",{"title":64,"url":65},"Rat für deutsche Rechtschreibung","https:\u002F\u002Fwww.rechtschreibrat.com\u002F",{"title":67,"url":68},"Microsoft Writing Style Guide","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fstyle-guide\u002Fwelcome\u002F","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fog.jpg","vue-nuxt-developer",[73,74,75],"en","de","hu","An LLM translation pipeline for i18n: glossary, placeholders and review","How to translate product and website strings with LLMs without breaking placeholders, plurals or tone: glossary, ICU messages, QA checks and review gates.","Three source cards, one translated card with a glossary ribbon, then a checked clipboard.","LLM translation pipeline for i18n · Balázs Csorba",[81,82,83,84,85],"Protect placeholders, tags and ICU syntax before the model sees the string, and verify them afterwards; that check catches most real breakage.","Translate values, never keys, and keep the file shape identical so the build stays diffable and reviewable.","A glossary plus a do-not-translate list does more for consistency than prompt wording, because it is enforced mechanically.","German and Hungarian need decisions up front: formal or informal address, compound handling, and quote style.","Let the model translate, let scripts verify, and let humans review only the strings where a mistake is expensive.",[87,90,93,96,99],{"q":88,"a":89},"Do I still need a translation management system?","Not necessarily. A JSON-per-locale layout with a review gate in the repository covers most product sites. A TMS becomes worth its price when you have external translators, many locales, or release branches to reconcile.",{"q":91,"a":92},"How large should one translation batch be?","Small enough that a failure is cheap to retry, large enough to give context. In practice 20 to 60 strings per request, grouped by page or component, works well and keeps costs predictable.",{"q":94,"a":95},"Can the model see the whole file for context?","Yes, as read-only context. Send the source file with the strings to translate marked, and let the response contain only the translated values. That keeps the prompt small and the output schema simple.",{"q":97,"a":98},"What about languages with no plural form in English?","ICU handles this, but only if the source string uses ICU too. Move plurals and counts into ICU messages first, then translate the message, not the concatenated sentence.",{"q":100,"a":101},"Is machine translation post-editing still needed?","For high-visibility copy, yes. For long-tail error messages and settings labels, a good glossary and automated checks usually beat a rushed human pass.",[103,106,109,112,115,118,121,124,127,130,133],{"id":104,"title":105},"why-translation-breaks","What actually breaks",{"id":107,"title":108},"pipeline-stages","The pipeline",{"id":110,"title":111},"protect-placeholders","Protect placeholders and tags first",{"id":113,"title":114},"icu-messages","Plurals and selects belong to ICU",{"id":116,"title":117},"structured-output","Translate values, never keys",{"id":119,"title":120},"glossary-and-translation-memory","Glossary and translation memory",{"id":122,"title":123},"german-and-hungarian-notes","German and Hungarian specifics",{"id":125,"title":126},"qa-checks","Checks that catch almost everything",{"id":128,"title":129},"cost-and-throughput","Cost and throughput",{"id":131,"title":132},"review-gates","Where humans still matter",{"id":134,"title":135},"sources","Sources",[137,141,144,147,155,168,169,178,181,182,185,187,194,195,198,225,227,230,231,234,236,239,240,243,281,284,285,288,291,301,302,305,307,319,328,329,332,369,372,373,376,387,390,391],{"type":138,"content":139},"paragraph",[140],"Every product website ends up with a translation pipeline, whether you plan one or not. The usual version is a spreadsheet, a script somebody wrote once, and a rule that German copy has to be finished before the release branch is cut. It works until a placeholder is renamed, a plural form disappears, or the same button label is translated three different ways.",{"type":138,"content":142},[143],"This post describes the pipeline I use for Vue and Nuxt sites: strings live in JSON, a model translates values with a glossary and explicit context, scripts verify the result mechanically, and humans only look at the strings where a mistake is expensive. It is deliberately boring, and that is the point: translation should be a build step, not an event.",{"type":145,"level":146,"id":104,"text":105},"heading",2,{"type":138,"content":148},[149,150,154],"Machine translation of plain prose is a solved problem for European languages. What breaks in software is everything the sentence does not carry: the ",{"tag":151,"children":152},"code",[153],"{count}"," that was inside the string, the HTML tag around a link, the plural form that German and Hungarian both need, the product name that must stay in English.",{"type":156,"items":157},"list",[158,160,162,164,166],[159],"A dropped or renamed placeholder, which turns a working page into a runtime error or an empty space.",[161],"A broken ICU message, because plural categories differ per language and concatenation cannot express them.",[163],"Translated HTML or Markdown tags, which either break rendering or leak into the visible text.",[165],"Inconsistent terminology: the same feature called three names across one page.",[167],"Tone drift, where a formal German interface suddenly addresses the reader informally.",{"type":145,"level":146,"id":107,"text":108},{"type":170,"attrs":171,"inner":175,"caption":176},"diagram",{"viewBox":172,"role":173,"aria-labelledby":174},"0 0 720 170","img","tr1-t tr1-d","\u003Ctitle id=\"tr1-t\">Translation pipeline stages\u003C\u002Ftitle>\u003Cdesc id=\"tr1-d\">Source strings are extracted, translated with context and glossary, verified by scripts, reviewed by a human where it matters, and published with the site build.\u003C\u002Fdesc>\u003Cdefs>\u003Cmarker id=\"tr1-ah\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\u003Cpath d=\"M0 0L10 5L0 10z\" class=\"d-head\" \u002F>\u003C\u002Fmarker>\u003C\u002Fdefs>\u003Crect x=\"20\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"79\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Extract\u003C\u002Ftext>\u003Ctext x=\"79\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">locale JSON\u003C\u002Ftext>\u003Cpath d=\"M138 85H158\" class=\"d-line\" marker-end=\"url(#tr1-ah)\" \u002F>\u003Crect x=\"160\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"220\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Translate\u003C\u002Ftext>\u003Ctext x=\"220\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">context + glossary\u003C\u002Ftext>\u003Cpath d=\"M279 85H299\" class=\"d-line\" marker-end=\"url(#tr1-ah)\" \u002F>\u003Crect x=\"301\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"360\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Verify\u003C\u002Ftext>\u003Ctext x=\"360\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">scripts\u003C\u002Ftext>\u003Cpath d=\"M419 85H439\" class=\"d-line\" marker-end=\"url(#tr1-ah)\" \u002F>\u003Crect x=\"441\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"500\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Review\u003C\u002Ftext>\u003Ctext x=\"500\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">diff view\u003C\u002Ftext>\u003Cpath d=\"M560 85H580\" class=\"d-line\" marker-end=\"url(#tr1-ah)\" \u002F>\u003Crect x=\"582\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"641\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Publish\u003C\u002Ftext>\u003Ctext x=\"641\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">build\u003C\u002Ftext>",[177],"Five stages: extract, translate, verify, review, publish. Only two of them involve a model.",{"type":138,"content":179},[180],"The extract step is the only place that produces source strings. If a string is hard-coded in a component, it will never be translated, so the first useful check is a linter that fails on visible text literals in templates. The translate step is stateless: it reads the source file, the glossary and the previous translation for the same key, and writes new values.",{"type":145,"level":146,"id":110,"text":111},{"type":138,"content":183},[184],"A placeholder is a contract between the string and the code that renders it. The model must not translate, reorder or drop it, and the code must not assume it survived. The reliable pattern is to extract every placeholder before the request and compare the list after the response; a mismatch is a hard error, not a warning.",{"type":151,"code":186},"import re\n\nPLACEHOLDER = re.compile(r\"\\{[A-Za-z_][\\w]*(?:,[^}]*)?\\}|\\{[0-9]+\\}|%[sd]|\u003C\u002F?[a-z][^>]*>\")\n\ndef slots(text: str) -> list[str]:\n    \"Every placeholder and tag in the string, in order of appearance.\"\n    return PLACEHOLDER.findall(text)\n\ndef check(source: str, target: str) -> list[str]:\n    \"Returns the differences; empty list means the translation is safe to ship.\"\n    a, b = slots(source), slots(target)\n    return [f\"missing {s}\" for s in a if s not in b] + [f\"added {s}\" for s in b if s not in a]\n",{"type":138,"content":188},[189,190,193],"Tags deserve the same treatment. If the source contains ",{"tag":151,"children":191},[192],"\u003Cstrong>{name}\u003C\u002Fstrong>",", the translation may move the tag but must keep both halves and the placeholder. A simple ordered comparison catches the common cases, and an HTML parser catches the rest.",{"type":145,"level":146,"id":113,"text":114},{"type":138,"content":196},[197],"German and Hungarian both distinguish singular and plural, so an English-only plural feels harmless right up to the first count of zero. ICU MessageFormat solves this properly, and Vue I18n speaks ICU, which means the message can live in the locale file and stay translatable as a unit.",{"type":199,"head":200,"rows":206},"table",[201,203,204,205],[202],"String",[73],[74],[75],[207,216],[208,210,212,214],[209],"Cart count",[211],"{count, plural, one {# item} other {# items}}",[213],"{count, plural, one {# Artikel} other {# Artikel}}",[215],"{count, plural, one {# termék} other {# termék}}",[217,219,221,223],[218],"Storage size",[220],"{size} GB of {total} GB used",[222],"{size} GB von {total} GB belegt",[224],"{size} GB a {total} GB-ból",{"type":151,"code":226},"\u003Cp>{{ $t('cart.items', { count: items.length }, items.length) }}\u003C\u002Fp>\n\n\u002F\u002F en.json\n\"cart\": { \"items\": \"{count, plural, one {# item} other {# items}}\" }\n\n\u002F\u002F de.json\n\"cart\": { \"items\": \"{count, plural, one {# Artikel} other {# Artikel}}\" }\n\n\u002F\u002F hu.json\n\"cart\": { \"items\": \"{count, plural, one {# termék} other {# termék}}\" }\n",{"type":138,"content":228},[229],"Two habits keep this working. Never build a sentence by concatenating a translated fragment with a number or a name, because word order differs; keep the number inside the message. And never let the pipeline see a half-translated plural: translate the whole ICU message as one string, with the same category names, and let a check confirm that every category in the source exists in the target.",{"type":145,"level":146,"id":116,"text":117},{"type":138,"content":232},[233],"The translation step receives a JSON object and returns an object with the same shape. Keys are identifiers and stay untouched; only values change. With structured outputs this is enforceable at the API level, and even without a schema it is the easiest contract to check: the set of keys must be identical before and after.",{"type":151,"code":235},"{\n  \"page\": \"checkout\",                  \u002F\u002F context for the model, not translated\n  \"glossary\": [[\"cart\", \"Warenkorb\"], [\"checkout\", \"Kassa\"]],\n  \"strings\": {\n    \"cart.title\": \"Your cart\",\n    \"cart.empty\": \"Your cart is empty\",\n    \"cart.items\": \"{count, plural, one {# item} other {# items}}\"\n  }\n}",{"type":138,"content":237},[238],"Grouping matters more than size. Strings that share a page, a component or a tone belong in one request, because the model uses the neighbours as context. Twenty to sixty strings per request is a good range: a retry is cheap, and the context is still specific. Re-translate only what changed, by comparing a hash of the source value, and the daily cost stays close to zero after the first full pass.",{"type":145,"level":146,"id":119,"text":120},{"type":138,"content":241},[242],"A glossary is the cheapest consistency tool in the pipeline: a list of source terms with their approved target terms and a rule about casing and inflection. The model gets it as input, and a check enforces it afterwards. If a forbidden translation appears, the string is retried once with the violation quoted back; after that it goes to review instead of looping.",{"type":199,"head":244,"rows":253},[245,247,249,251],[246],"Source",[248],"German",[250],"Hungarian",[252],"Rule",[254,263,272],[255,257,259,261],[256],"cart",[258],"Warenkorb",[260],"kosár",[262],"noun, capitalised in German",[264,266,268,270],[265],"checkout",[267],"Kassa",[269],"fizetés",[271],"do not translate, do not inflect",[273,275,277,279],[274],"AI agent",[276],"AI-Agent",[278],"AI-ügynök",[280],"keep the English term, gloss it on first use",{"type":138,"content":282},[283],"Translation memory is the other half: exact matches from previous releases are reused without a model call, and fuzzy matches are passed as a suggestion. In a JSON-per-locale setup the memory is just a table of source strings and accepted targets, but the effect is the same: the model only sees what is genuinely new, and reviewers only see what changed since the last release.",{"type":145,"level":146,"id":122,"text":123},{"type":138,"content":286},[287],"German punishes inconsistency more than grammar errors. Pick one address form per product, document it, and put it in the style guide that ships with the glossary; a shop that writes \"Sie\" in the footer and \"du\" in the onboarding reads as assembled from parts. Nouns stay capitalised, compounds are written together, and quotes are the German ones: „so“, not \"so\".",{"type":138,"content":289},[290],"Hungarian is agglutinative and has no grammatical gender, which changes the pipeline in two practical ways. First, \"user\" or \"customer\" cannot be carried through with pronouns: the translation must either repeat the noun or be rewritten without it. Second, suffixes attach to nouns, so a placeholder inside a case suffix has to be avoided or the message restructured until the suffix sits next to the word.",{"type":156,"items":292},[293,295,297,299],[294],"Write a one-page tone note per locale and keep it next to the glossary in the repository.",[296],"Encode every recurring term as a glossary entry instead of repeating it in the prompt.",[298],"Let a native speaker fix the tone note once, then treat the fixes as lint rules.",[300],"Test the result in the layout, not in the JSON: German and Hungarian are usually longer than English.",{"type":145,"level":146,"id":125,"text":126},{"type":138,"content":303},[304],"Every failure mode from the first section has a mechanical check. Run them in the same commit as the translation, not in a cleanup sprint later; a pipeline that cannot fail loudly will ship broken strings quietly.",{"type":151,"code":306},"\u002F\u002F verify-locales.mjs - runs in CI, exits non-zero on the first real problem\nconst checks = [\n  ['placeholder parity', (src, tgt) => sameSlots(src, tgt)],\n  ['icu category parity', (src, tgt) => sameIcuCategories(src, tgt)],\n  ['no empty values', (src, tgt) => tgt.trim().length > 0],\n  ['glossary respected', (src, tgt) => glossaryViolations(src, tgt).length === 0],\n  ['length sanity', (src, tgt) => tgt.length \u003C src.length * 2.2],\n]\n",{"type":156,"items":308},[309,311,313,315,317],[310],"Placeholder and tag parity, per string and per ICU message.",[312],"Identical key sets between source and target locale files.",[314],"No empty or unchanged values where the source changed.",[316],"Glossary violations listed with the offending term, so the retry prompt can quote them.",[318],"Length ratio bounds, which flag truncation and runaway expansions before a designer sees them.",{"type":320,"variant":321,"title":322,"body":323},"callout","warn","Let scripts decide, not the model",[324,326],[325],"It is tempting to feed a failing check back to the model in a loop until it passes. Cap that loop at one retry and escalate to review on the second failure: a model that is told to satisfy a placeholder check will find ways to satisfy it, including inventing placeholder copies in the text.",[327],"The check owns the contract; the model only proposes values.",{"type":145,"level":146,"id":128,"text":129},{"type":138,"content":330},[331],"Translation is a small part of an LLM bill because the strings are short. As a rule of thumb one thousand strings of about twenty words each cost roughly 20k input tokens and 25k output tokens; with a mid-tier model priced at three dollars per million input tokens and fifteen dollars per million output tokens, that is about forty-five cents per thousand strings. Frontier models cost several times that and rarely pay off for this task.",{"type":199,"head":333,"rows":340},[334,336,338],[335],"Pass",[337],"What it sees",[339],"Cost driver",[341,348,355,362],[342,344,346],[343],"First full run",[345],"Every source string plus glossary",[347],"Source volume, once",[349,351,353],[350],"Release delta",[352],"Only keys whose source hash changed",[354],"Change rate",[356,358,360],[357],"Exact memory hit",[359],"Nothing, copied from memory",[361],"Zero",[363,365,367],[364],"Review",[366],"Diffs, flagged violations, samples",[368],"Human minutes",{"type":138,"content":370},[371],"The throughput question matters more than the price. A full pass over a few thousand strings takes minutes and can run in CI; the bottleneck is review, which is why the pipeline should present reviewers with a diff of changed keys and a list of flagged strings instead of a file to read from the top.",{"type":145,"level":146,"id":131,"text":132},{"type":138,"content":374},[375],"A review gate is worth its cost when a mistake is expensive: onboarding, pricing pages, legal text, anything a customer will quote back to you. For the long tail of error messages and settings labels, automated checks plus a glossary beat a rushed human pass, and pretending otherwise just shifts the work to the next release.",{"type":156,"ordered":377,"items":378},true,[379,381,383,385],[380],"The pipeline produces a per-locale diff and a list of flagged strings.",[382],"A reviewer approves or edits only the flagged and high-visibility entries.",[384],"Approved values are written back, which updates the translation memory.",[386],"The build publishes the site, and the checks run again on the published artefact.",{"type":138,"content":388},[389],"None of this is exotic. Source strings in JSON, a glossary next to them, a model that translates values, a script that checks contracts, and a human who looks at the diff. The pipeline is boring on purpose, because the interesting parts of a product should never be the translations.",{"type":145,"level":146,"id":134,"text":135},{"type":156,"ordered":377,"items":392},[393,397,400,403,406,409,412,415,418,421,424],[394],{"tag":395,"href":32,"children":396},"a",[38],[398],{"tag":395,"href":41,"children":399},[40],[401],{"tag":395,"href":44,"children":402},[43],[404],{"tag":395,"href":47,"children":405},[46],[407],{"tag":395,"href":50,"children":408},[49],[410],{"tag":395,"href":53,"children":411},[52],[413],{"tag":395,"href":56,"children":414},[55],[416],{"tag":395,"href":59,"children":417},[58],[419],{"tag":395,"href":62,"children":420},[61],[422],{"tag":395,"href":65,"children":423},[64],[425],{"tag":395,"href":68,"children":426},[67],[428,502,557,629],{"slug":429,"published":5,"minutes":430,"category":7,"tags":431,"keywords":437,"about":449,"sources":458,"cover":495,"og":496,"expertise":497,"locales":498,"lang":73,"title":499,"description":500,"coverAlt":501},"schema-org-structured-data-b2b-shop",10,[432,433,434,435,436],"Schema.org","Structured data","B2B e-commerce","Technical SEO","Nuxt",[438,439,440,441,442,443,444,445,446,447,448],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[450,452,455],{"name":432,"url":451},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":453,"url":454},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":456,"url":457},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[459,462,465,468,471,474,477,480,483,486,489,492],{"title":460,"url":461},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":463,"url":464},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":466,"url":467},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":469,"url":470},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":472,"url":473},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":475,"url":476},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":478,"url":479},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":481,"url":482},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":484,"url":485},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":487,"url":488},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":490,"url":491},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":493,"url":494},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg","b2b-ecommerce-developer",[73,74,75],"Schema.org structured data for B2B shops: prices behind logins, units and variants","Product markup for B2B shops: snippet or merchant listing, which price to publish, part numbers, unit pricing, variants, Organization and SSR in Nuxt.","A product rectangle with a framed outline and a small price slot hidden behind a low wall.",{"slug":503,"published":5,"minutes":6,"category":7,"tags":504,"keywords":509,"about":521,"sources":529,"cover":551,"og":552,"expertise":497,"locales":553,"lang":73,"title":554,"description":555,"coverAlt":556},"ai-assistant-b2b-shop-customer-support",[434,505,506,507,508],"AI assistant","Customer support","LLM agents","EU AI Act",[510,511,512,513,514,515,516,517,518,519,520],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[522,525,528],{"name":523,"url":524},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":526,"url":527},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":456,"url":457},[530,533,536,539,542,545,548],{"title":531,"url":532},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":534,"url":535},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":537,"url":538},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":540,"url":541},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":543,"url":544},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":546,"url":547},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":549,"url":550},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[73,74,75],"An AI assistant for B2B shop support: tools, permissions and the human handoff","How to build an AI support assistant for a B2B shop: which requests to automate, tools that run in the customer context, handoff, AI Act and metrics.","A tall wall with one narrow gap; an arrow passes through it into a boxed enclosure of three squares, and another leads down to a tray.",{"slug":558,"published":5,"minutes":559,"category":7,"tags":560,"keywords":564,"about":576,"sources":586,"cover":623,"og":624,"expertise":71,"locales":625,"lang":73,"title":626,"description":627,"coverAlt":628},"vue-ai-chat-ui-streaming-accessibility",11,[12,436,561,562,563],"Accessibility","Streaming","AI chat",[565,566,567,568,569,570,571,572,573,574,575],"vue ai chat ui","nuxt llm streaming","sse vs fetch streaming","aria-live streaming text","accessible chat interface","abort fetch stream vue","dompurify markdown llm","role log chat","reduced motion chat ui","wcag 2.2 chat","screen reader streaming",[577,580,583],{"name":578,"url":579},"Web accessibility","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWeb_accessibility",{"name":581,"url":582},"Server-sent events","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",{"name":584,"url":585},"Vue.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVue.js",[587,590,593,596,599,602,605,608,611,614,617,620],{"title":588,"url":589},"MDN: Using server-sent events","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events\u002FUsing_server-sent_events",{"title":591,"url":592},"MDN: AbortController","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",{"title":594,"url":595},"MDN: ReadableStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FReadableStream",{"title":597,"url":598},"MDN: TextDecoderStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoderStream",{"title":600,"url":601},"MDN: ARIA log role","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FReference\u002FRoles\u002Flog_role",{"title":603,"url":604},"W3C: Understanding SC 4.1.3 Status Messages","https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG22\u002FUnderstanding\u002Fstatus-messages.html",{"title":606,"url":607},"W3C WAI: What's new in WCAG 2.2","https:\u002F\u002Fwww.w3.org\u002FWAI\u002Fstandards-guidelines\u002Fwcag\u002Fnew-in-22\u002F",{"title":609,"url":610},"MDN: prefers-reduced-motion","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-reduced-motion",{"title":612,"url":613},"DOMPurify README","https:\u002F\u002Fgithub.com\u002Fcure53\u002FDOMPurify",{"title":615,"url":616},"markdown-it README","https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",{"title":618,"url":619},"AI SDK docs: useChat","https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-ui\u002Fchatbot",{"title":621,"url":622},"Vue docs: nextTick","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fgeneral.html#nexttick","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fog.jpg",[73,74,75],"Streaming AI chat in Vue and Nuxt: accessible by design","How to build a Vue or Nuxt AI chat UI: fetch streams, abort and retry, safe markdown rendering, aria-live announcements, keyboard support and reduced motion.","A message card with a careful caret, a dotted line opening into ripples and a check.",{"slug":630,"published":631,"minutes":632,"category":7,"tags":633,"keywords":637,"about":648,"sources":655,"cover":671,"og":672,"expertise":71,"locales":673,"lang":73,"title":674,"description":675,"coverAlt":676},"nuxt-i18n-multilingual-seo-hreflang","2026-10-16",8,[436,10,634,635,636],"SEO","hreflang","Multilingual",[638,639,640,641,642,643,644,645,646,647],"nuxt i18n seo","nuxt hreflang","useLocaleHead","nuxt i18n canonical","hreflang x-default","prefix_except_default","multilingual seo nuxt","nuxt i18n sitemap hreflang","hreflang self-referencing","nuxt i18n baseUrl",[649,651,654],{"name":436,"url":650},"https:\u002F\u002Fnuxt.com\u002F",{"name":652,"url":653},"Hreflang","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHreflang",{"name":28,"url":29},[656,659,662,665,668],{"title":657,"url":658},"Nuxt i18n docs: SEO (useLocaleHead)","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide\u002Fseo",{"title":660,"url":661},"Nuxt i18n docs: Routing strategies","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide",{"title":663,"url":664},"Google Search Central: Tell Google about localized versions of your page","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Flocalized-versions",{"title":666,"url":667},"Google Search Central: Managing multi-regional and multilingual sites","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Fmanaging-multi-regional-sites",{"title":669,"url":670},"Google Search Central: How to specify a canonical URL","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fconsolidate-duplicate-urls","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fog.jpg",[73,74,75],"Multilingual SEO with Nuxt i18n: URL strategy, hreflang, canonicals and sitemaps","How to set up Nuxt i18n so search engines index every language: routing strategy, useLocaleHead, hreflang rules, partial translations and sitemaps.","A large sheet linked by three horizontal connectors to a column of three smaller sheets.",1792245345528]