[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-languages":166},[4,38,57,65,114,138,157],{"id":5,"group":6,"pages":7},"get-started","Get started",[8,13,18,23,28,33],{"path":9,"title":10,"description":11,"navTitle":10,"group":6,"order":12},"\u002Fdocs\u002Fgetting-started","Getting started","Create your app from the kit, run it locally with no credentials, make yourself superadmin and find where your own code goes.",1,{"path":14,"title":15,"description":16,"navTitle":15,"group":6,"order":17},"\u002Fdocs\u002Flayout","Project structure","Where code lives by where it runs, and which server\u002F directories turn a new file into a route, a job or a provider.",2,{"path":19,"title":20,"description":21,"navTitle":20,"group":6,"order":22},"\u002Fdocs\u002Fconfiguration","Configuration","starter.config.ts switches each part of the kit and its providers, environment variables hold the secrets: every key and what it changes.",3,{"path":24,"title":25,"description":26,"navTitle":25,"group":6,"order":27},"\u002Fdocs\u002Ftemplate","Updating from the kit","Keep the kit as a git remote and take each update as one commit you review, with pnpm template:update, then resolve what conflicts.",4,{"path":29,"title":30,"description":31,"navTitle":30,"group":6,"order":32},"\u002Fdocs\u002Fdeployment","Deployment","Ship the app as one Cloudflare Worker: create the resources, set the secrets, deploy main from GitHub Actions or your machine.",5,{"path":34,"title":35,"description":36,"navTitle":35,"group":6,"order":37},"\u002Fdocs\u002Fmcp","MCP server","Connect Claude Code, Cursor or VS Code to https:\u002F\u002Fnuxter.app\u002Fmcp, so your agent searches and reads these docs and their design notes.",6,{"id":39,"group":40,"pages":41},"accounts","Accounts",[42,47,52],{"path":43,"title":44,"description":45,"navTitle":44,"group":40,"order":46},"\u002Fdocs\u002Fauth","Auth","Pick the sign-in methods, add an OAuth provider, protect a route, and handle sessions, second factors and account deletion.",7,{"path":48,"title":49,"description":50,"navTitle":49,"group":40,"order":51},"\u002Fdocs\u002Ftenancy","Organizations","Scope routes and pages to an organization, and work with its members, roles, seats, invitations and a second level such as branches.",8,{"path":53,"title":54,"description":55,"navTitle":54,"group":40,"order":56},"\u002Fdocs\u002Fpermissions","Roles and permissions","Routes check permissions that an account’s role or its organization’s plan grants: checking one, adding a role, and the staff tools.",9,{"id":58,"group":59,"pages":60},"payments","Payments",[61],{"path":62,"title":59,"description":63,"navTitle":59,"group":59,"order":64},"\u002Fdocs\u002Fpayments","Sell plans through Stripe, Polar, Lemon Squeezy or Creem: define plans, connect a provider, gate features, trials, coupons and refunds.",10,{"id":66,"group":67,"pages":68},"building-blocks","Building blocks",[69,74,79,84,89,94,99,104,109],{"path":70,"title":71,"description":72,"navTitle":71,"group":67,"order":73},"\u002Fdocs\u002Fdatabase","Database","Postgres through Drizzle, on Hyperdrive in production and PGlite locally: add a table, query, transactions, migrations and seed data.",11,{"path":75,"title":76,"description":77,"navTitle":76,"group":67,"order":78},"\u002Fdocs\u002Fbackground-jobs","Background jobs","Run work after the response on Cloudflare Queues with retries: add and enqueue a job, run jobs locally, and schedule cron tasks.",12,{"path":80,"title":81,"description":82,"navTitle":81,"group":67,"order":83},"\u002Fdocs\u002Fevents","Events","React to what the kit does by adding a file to server\u002Flisteners\u002F, fire events of your own, and look up every event’s payload.",13,{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"order":88},"\u002Fdocs\u002Femail","Email","Set up sending, send your own mail with deliverEmail, change the templates and wording, handle unsubscribes and add a provider.",14,{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"order":93},"\u002Fdocs\u002Fcontact","Contact","A \u002Fcontact page that mails you a message or shows your address after a Turnstile check: choose the channels, set the address, know the limits.",15,{"path":95,"title":96,"description":97,"navTitle":96,"group":67,"order":98},"\u002Fdocs\u002Fnotifications","Notifications","The dashboard bell: add a notification type, send one with notify(), and how categories, opt-outs and retention apply.",16,{"path":100,"title":101,"description":102,"navTitle":101,"group":67,"order":103},"\u002Fdocs\u002Fstorage","Storage","Uploads in R2 with one policy per purpose: add a purpose, accept an upload, replace or delete a file, and serve it to its owner.",17,{"path":105,"title":106,"description":107,"navTitle":106,"group":67,"order":108},"\u002Fdocs\u002Ffeature-flags","Feature flags","Declare typed flags in starter.config.ts, read them with flag() or useFlag(), roll them out by rule or percentage, and add a provider.",18,{"path":110,"title":111,"description":112,"navTitle":111,"group":67,"order":113},"\u002Fdocs\u002Fai","AI","Call models from server code by role: text, images, speech, transcription, embeddings and video, with provider fallbacks.",19,{"id":115,"group":116,"pages":117},"web","Web",[118,123,128,133],{"path":119,"title":120,"description":121,"navTitle":120,"group":116,"order":122},"\u002Fdocs\u002Fsecurity-headers","Security headers","The Content-Security-Policy and hardening headers on every response: allow another origin, keep tokens out of Referer, accept cross-site posts.",20,{"path":124,"title":125,"description":126,"navTitle":125,"group":116,"order":127},"\u002Fdocs\u002Frate-limiting","Rate limiting","Cap how often a caller hits a route with requireRateLimit, count only failures, space out sends, and protect the zone.",21,{"path":129,"title":130,"description":131,"navTitle":130,"group":116,"order":132},"\u002Fdocs\u002Fanalytics","Analytics","Product analytics and ad conversions sent from the server with no browser script: add an event, connect a provider, and handle consent.",22,{"path":134,"title":135,"description":136,"navTitle":135,"group":116,"order":137},"\u002Fdocs\u002Fobservability","Observability","Find a failed request by its id, send faults to Sentry or Better Stack, report an error you caught, and change the error page.",23,{"id":139,"group":140,"pages":141},"content-and-look","Content and look",[142,147,152],{"path":143,"title":144,"description":145,"navTitle":144,"group":140,"order":146},"\u002Fdocs\u002Fblog","Blog","Markdown posts with an RSS feed per language, and workflows that draft posts with an AI model and open each one as a pull request.",24,{"path":148,"title":149,"description":150,"navTitle":149,"group":140,"order":151},"\u002Fdocs\u002Flanguages","Languages","Add or remove a language, write copy in the locale files, and write code, refusals and links that follow the reader’s language.",25,{"path":153,"title":154,"description":155,"navTitle":154,"group":140,"order":156},"\u002Fdocs\u002Ftheme","Theme","Change the brand colour and font, and build dashboard sections, list pages, loading placeholders and charts that match the kit.",26,{"id":158,"group":159,"pages":160},"quality","Quality",[161],{"path":162,"title":163,"description":164,"navTitle":163,"group":159,"order":165},"\u002Fdocs\u002Ftesting","Testing","Unit tests in Node, component tests in a real Nuxt app and Playwright end to end: test a route or a component, and what CI runs.",27,{"page":167,"surround":1429,"linksToKit":767},{"path":148,"title":149,"description":150,"navTitle":149,"group":140,"seo":168,"body":170},{"title":169,"description":150},"Nuxt i18n: add languages and translate copy",{"type":171,"value":172,"toc":1418},"minimark",[173,177,198,203,285,289,295,423,455,458,546,556,560,588,607,626,630,698,715,719,733,841,916,920,931,1049,1055,1096,1111,1115,1129,1157,1160,1208,1211,1326,1329,1333,1414],[174,175,149],"h1",{"id":176},"languages",[178,179,180,181,185,186,189,190,193,194,197],"p",{},"The app ships in English and Serbian (Latin script). The default language is\nserved at ",[182,183,184],"code",{},"\u002F",", and every other language gets a path prefix: ",[182,187,188],{},"\u002Fsr",", ",[182,191,192],{},"\u002Fsr\u002Fblog",",\n",[182,195,196],{},"\u002Fsr\u002Fdashboard",". You work here when you add or remove a language, or write copy\nand code that has to follow the reader's language.",[199,200,202],"h2",{"id":201},"how-it-works","How it works",[204,205,206,236,259,272,282],"ol",{},[207,208,209,212,213,223,224,231,232,235],"li",{},[182,210,211],{},"i18n"," in ",[214,215,220],"a",{"href":216,"rel":217,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fstarter.config.ts",[218],"nofollow","Licensees only",[182,221,222],{},"starter.config.ts"," lists the languages.\n",[214,225,228],{"href":226,"rel":227,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fnuxt.config.ts",[218],[182,229,230],{},"nuxt.config.ts"," passes the list to @nuxtjs\u002Fi18n with\nthe ",[182,233,234],{},"prefix_except_default"," strategy.",[207,237,238,239,246,247,250,251,254,255,258],{},"Copy lives in ",[214,240,243],{"href":241,"rel":242,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Ftree\u002Fmain\u002Fi18n\u002Flocales",[218],[182,244,245],{},"i18n\u002Flocales\u002F",", one JSON file per\nlanguage. Templates call ",[182,248,249],{},"$t('key')",", scripts call ",[182,252,253],{},"t('key')"," from\n",[182,256,257],{},"useI18n()",".",[207,260,261,262,212,265,258],{},"Nuxt UI's own strings, such as aria-labels and \"No data\", come from\n",[182,263,264],{},"UI_LOCALES",[214,266,269],{"href":267,"rel":268,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Futils\u002Fui-locales.ts",[218],[182,270,271],{},"app\u002Futils\u002Fui-locales.ts",[207,273,274,275,277,278,281],{},"On a visitor's first request to ",[182,276,184],{},", the browser's language picks the\nlocale, and the ",[182,279,280],{},"i18n_redirected"," cookie remembers it. The server reads the\nsame cookie to choose the language of mail and redirects.",[207,283,284],{},"A missing key renders the default language's string. An untranslated blog\npost or legal page is served from the default language's content at the\nprefixed URL.",[199,286,288],{"id":287},"add-a-language","Add a language",[178,290,291,292,294],{},"Add the entry to ",[182,293,222],{},":",[296,297,302],"pre",{"className":298,"code":299,"language":300,"meta":301,"style":301},"language-ts shiki shiki-themes github-light github-light github-dark","i18n: {\n  defaultLocale: 'en',\n  locales: [\n    { code: 'en', language: 'en-US', name: 'English', file: 'en.json' },\n    { code: 'sr', language: 'sr-Latn-RS', name: 'Srpski', file: 'sr.json' },\n    { code: 'de', language: 'de-DE', name: 'Deutsch', file: 'de.json' },\n  ],\n},\n","ts","",[182,303,304,315,329,337,365,389,413,418],{"__ignoreMap":301},[305,306,308,311],"span",{"class":307,"line":12},"line",[305,309,211],{"class":310},"snPdu",[305,312,314],{"class":313},"sxrX7",": {\n",[305,316,317,320,323,327],{"class":307,"line":17},[305,318,319],{"class":310},"  defaultLocale",[305,321,322],{"class":313},": ",[305,324,326],{"class":325},"sIIMD","'en'",[305,328,193],{"class":313},[305,330,331,334],{"class":307,"line":22},[305,332,333],{"class":310},"  locales",[305,335,336],{"class":313},": [\n",[305,338,339,342,344,347,350,353,356,359,362],{"class":307,"line":27},[305,340,341],{"class":313},"    { code: ",[305,343,326],{"class":325},[305,345,346],{"class":313},", language: ",[305,348,349],{"class":325},"'en-US'",[305,351,352],{"class":313},", name: ",[305,354,355],{"class":325},"'English'",[305,357,358],{"class":313},", file: ",[305,360,361],{"class":325},"'en.json'",[305,363,364],{"class":313}," },\n",[305,366,367,369,372,374,377,379,382,384,387],{"class":307,"line":32},[305,368,341],{"class":313},[305,370,371],{"class":325},"'sr'",[305,373,346],{"class":313},[305,375,376],{"class":325},"'sr-Latn-RS'",[305,378,352],{"class":313},[305,380,381],{"class":325},"'Srpski'",[305,383,358],{"class":313},[305,385,386],{"class":325},"'sr.json'",[305,388,364],{"class":313},[305,390,391,393,396,398,401,403,406,408,411],{"class":307,"line":37},[305,392,341],{"class":313},[305,394,395],{"class":325},"'de'",[305,397,346],{"class":313},[305,399,400],{"class":325},"'de-DE'",[305,402,352],{"class":313},[305,404,405],{"class":325},"'Deutsch'",[305,407,358],{"class":313},[305,409,410],{"class":325},"'de.json'",[305,412,364],{"class":313},[305,414,415],{"class":307,"line":46},[305,416,417],{"class":313},"  ],\n",[305,419,420],{"class":307,"line":51},[305,421,422],{"class":313},"},\n",[424,425,426,431,449],"ul",{},[207,427,428,430],{},[182,429,182],{}," is the URL prefix and the file's stem.",[207,432,433,436,437,440,441,444,445,448],{},[182,434,435],{},"language"," is a BCP 47 tag. It is the hreflang value, the name of the\nlanguage's child sitemap, and the locale every ",[182,438,439],{},"Intl"," call gets. Pick the tag\nfor the script you write in: ",[182,442,443],{},"sr"," formats dates in Cyrillic, ",[182,446,447],{},"sr-Latn-RS"," in\nLatin.",[207,450,451,454],{},[182,452,453],{},"name"," is what the language switcher shows, written in the language it\nnames.",[178,456,457],{},"Then:",[204,459,460,471,540],{},[207,461,462,463,466,467,470],{},"Copy ",[182,464,465],{},"i18n\u002Flocales\u002Fen.json"," to ",[182,468,469],{},"de.json"," and translate every value.",[207,472,473,474,477,478],{},"Add a ",[182,475,476],{},"de"," entry to the tables that don't read the config:\n",[424,479,480,497,509,527],{},[207,481,482,485,486,489,490],{},[182,483,484],{},"EMAIL_COPY"," and ",[182,487,488],{},"UNSUBSCRIBE_COPY"," in\n",[214,491,494],{"href":492,"rel":493,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Femail\u002Fcopy.ts",[218],[182,495,496],{},"server\u002Femail\u002Fcopy.ts",[207,498,499,212,502],{},[182,500,501],{},"BLOG_COLLECTIONS",[214,503,506],{"href":504,"rel":505,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Futils\u002Ffeed.ts",[218],[182,507,508],{},"server\u002Futils\u002Ffeed.ts",[207,510,511,212,513,515,516,518,519,522,523,526],{},[182,512,264],{},[182,514,271],{},". Nuxt UI ships German, so import\n",[182,517,476],{}," from ",[182,520,521],{},"@nuxt\u002Fui\u002Flocale",". For a language it lacks, write one with\n",[182,524,525],{},"defineLocale",", as the file does for Serbian.",[207,528,529,489,532,539],{},[182,530,531],{},"locales",[214,533,536],{"href":534,"rel":535,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fscripts\u002Fblog\u002Fblog.config.mjs",[218],[182,537,538],{},"scripts\u002Fblog\u002Fblog.config.mjs",", if the\nAI blog workflow should write German too",[207,541,542,543,258],{},"Translate the legal pages into ",[182,544,545],{},"content\u002Fde\u002Flegal\u002F",[178,547,548,549,551,552,555],{},"The build refuses a declared language whose JSON file is missing, and the\ntests fail for each of the first four tables until it has the new code. The\nblog's ",[182,550,531],{}," is not checked. Route rules, sitemap exclusions, analytics\npath folding, the switcher and the content collections all read\n",[182,553,554],{},"i18n.locales",", so they need no edit.",[199,557,559],{"id":558},"remove-a-language","Remove a language",[178,561,562,563,565,566,569,570,573,574,577,578,580,581,584,585,258],{},"Delete its entry in ",[182,564,554],{},", its ",[182,567,568],{},"i18n\u002Flocales\u002F\u003Ccode>.json",", its\n",[182,571,572],{},"content\u002F\u003Ccode>\u002F",", and its entries in the tables above. Tests that exercise\nthat language by name go too: for Serbian, ",[182,575,576],{},"test\u002Fnuxt\u002Fserbian-locale.test.ts","\nand the ",[182,579,443],{}," cases in ",[182,582,583],{},"test\u002Funit\u002Funsubscribe-route.test.ts"," and\n",[182,586,587],{},"test\u002Funit\u002Fblog-feed.test.ts",[178,589,590,591,594,595,598,599,602,603,606],{},"With one language left, the app switches itself to ",[182,592,593],{},"no_prefix",": no browser\nlanguage redirect, no hreflang alternates, one flat ",[182,596,597],{},"\u002Fsitemap.xml",", and a\nswitcher that renders nothing. ",[182,600,601],{},"vitest.config.ts"," skips the four component\nsuites that need a second language. ",[182,604,605],{},"$t()"," keeps working, so adding a language\nback is the config entry above.",[178,608,609,610,613,614,617,618,621,622,625],{},"A later ",[182,611,612],{},"pnpm template:update"," that changes a file you deleted, usually\n",[182,615,616],{},"sr.json",", stops on a modify\u002Fdelete conflict. ",[182,619,620],{},"git rm"," the file again and\ncontinue (",[214,623,624],{"href":24},"Template",").",[199,627,629],{"id":628},"write-copy","Write copy",[424,631,632,647,665,695],{},[207,633,634,635,638,639,642,643,646],{},"Keys are nested objects. ",[182,636,637],{},"\"auth\": { \"title\": … }"," is reachable as\n",[182,640,641],{},"auth.title","; a literal ",[182,644,645],{},"\"auth.title\""," key is not.",[207,648,649,650,653,654,657,658,660,661,664],{},"Write a literal ",[182,651,652],{},"@"," as ",[182,655,656],{},"{'@'}",". An unescaped ",[182,659,652],{}," breaks the whole file, and\nevery ",[182,662,663],{},"t()"," in the app then renders its key path.",[207,666,667,668,489,671,294,678],{},"Write plurals with as many branches as the language has. English has two;\nSerbian has three, picked by ",[182,669,670],{},"serbianPlural",[214,672,675],{"href":673,"rel":674,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fi18n\u002Fi18n.config.ts",[218],[182,676,677],{},"i18n\u002Fi18n.config.ts",[296,679,683],{"className":680,"code":681,"language":682,"meta":301,"style":301},"language-json shiki shiki-themes github-light github-light github-dark","\"locked\": \"Pokušajte ponovo za {minutes} minut. | Pokušajte ponovo za {minutes} minuta. | Pokušajte ponovo za {minutes} minuta.\"\n","json",[182,684,685],{"__ignoreMap":301},[305,686,687,690,692],{"class":307,"line":12},[305,688,689],{"class":325},"\"locked\"",[305,691,322],{"class":313},[305,693,694],{"class":325},"\"Pokušajte ponovo za {minutes} minut. | Pokušajte ponovo za {minutes} minuta. | Pokušajte ponovo za {minutes} minuta.\"\n",[207,696,697],{},"An empty string is a value. Fallback doesn't replace it, and the tests\nrefuse it.",[178,699,700,707,708,711,712,714],{},[214,701,704],{"href":702,"rel":703,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Fi18n-locales.test.ts",[218],[182,705,706],{},"test\u002Funit\u002Fi18n-locales.test.ts"," holds\nevery file to ",[182,709,710],{},"en.json",": the same keys, the same interpolations, the right\nnumber of plural branches, no unescaped ",[182,713,652],{}," and nothing blank. It also checks\nthat every declared language has a file and every file is declared.",[199,716,718],{"id":717},"write-code-that-follows-the-language","Write code that follows the language",[178,720,721,722,725,726,728,729,732],{},"Anything holding a translated string is a ",[182,723,724],{},"computed"," or a getter. ",[182,727,663],{},"\nresolves once, when it is called, so a module-scope schema or a plain string in\n",[182,730,731],{},"useSeoMeta"," stays in the language the page first loaded in:",[296,734,736],{"className":298,"code":735,"language":300,"meta":301,"style":301},"const { t } = useI18n()\n\nconst schema = computed(() => z.object({ email: emailSchema(t) }))\n\nuseSeoMeta({\n  title: () => t('seo.dashboard.settings.activity.title'),\n})\n",[182,737,738,763,769,803,807,814,836],{"__ignoreMap":301},[305,739,740,744,747,751,754,757,760],{"class":307,"line":12},[305,741,743],{"class":742},"s8jYJ","const",[305,745,746],{"class":313}," { ",[305,748,750],{"class":749},"sBjJW","t",[305,752,753],{"class":313}," } ",[305,755,756],{"class":742},"=",[305,758,759],{"class":310}," useI18n",[305,761,762],{"class":313},"()\n",[305,764,765],{"class":307,"line":17},[305,766,768],{"emptyLinePlaceholder":767},true,"\n",[305,770,771,773,776,779,782,785,788,791,794,797,800],{"class":307,"line":22},[305,772,743],{"class":742},[305,774,775],{"class":749}," schema",[305,777,778],{"class":742}," =",[305,780,781],{"class":310}," computed",[305,783,784],{"class":313},"(() ",[305,786,787],{"class":742},"=>",[305,789,790],{"class":313}," z.",[305,792,793],{"class":310},"object",[305,795,796],{"class":313},"({ email: ",[305,798,799],{"class":310},"emailSchema",[305,801,802],{"class":313},"(t) }))\n",[305,804,805],{"class":307,"line":27},[305,806,768],{"emptyLinePlaceholder":767},[305,808,809,811],{"class":307,"line":32},[305,810,731],{"class":310},[305,812,813],{"class":313},"({\n",[305,815,816,819,822,824,827,830,833],{"class":307,"line":37},[305,817,818],{"class":310},"  title",[305,820,821],{"class":313},": () ",[305,823,787],{"class":742},[305,825,826],{"class":310}," t",[305,828,829],{"class":313},"(",[305,831,832],{"class":325},"'seo.dashboard.settings.activity.title'",[305,834,835],{"class":313},"),\n",[305,837,838],{"class":307,"line":46},[305,839,840],{"class":313},"})\n",[424,842,843,859,888,901],{},[207,844,845,846,849,850,852,853,855,856,258],{},"Build internal links with ",[182,847,848],{},"useLocalePath()",". It is idempotent, so passing\n",[182,851,196],{}," returns ",[182,854,196],{},". It returns an empty string for a\nroute that doesn't exist, so a link to a page you may delete needs a\nfallback: ",[182,857,858],{},"localePath('\u002Fpricing') || '\u002Fpricing'",[207,860,861,862,865,866,869,870,873,874,877,878,885,886,258],{},"A path already produced by ",[182,863,864],{},"localePath()"," or ",[182,867,868],{},"switchLocalePath()"," goes to\n",[182,871,872],{},"ULink"," or a menu item with ",[182,875,876],{},"locale: false",", as\n",[214,879,882],{"href":880,"rel":881,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomposables\u002FuseLanguageSwitcher.ts",[218],[182,883,884],{},"useLanguageSwitcher"," does.\nOtherwise it is localised twice and the English link points at ",[182,887,188],{},[207,889,890,891,894,895,897,898,258],{},"Format dates and numbers with ",[182,892,893],{},"useLocaleTag()",", which returns the\n",[182,896,435],{}," tag rather than the code:\n",[182,899,900],{},"new Intl.NumberFormat(useLocaleTag().value)",[207,902,903,904,907,908,911,912,915],{},"Query content by a path rebuilt from the route's ",[182,905,906],{},"slug"," param, never by\n",[182,909,910],{},"route.path",", which carries the prefix. Give ",[182,913,914],{},"useAsyncData"," a getter key that\nincludes the locale, because switching language reuses the page component.",[199,917,919],{"id":918},"word-a-server-refusal","Word a server refusal",[178,921,922,923,926,927,930],{},"A route's ",[182,924,925],{},"statusMessage"," stays English: it is what logs and API clients read.\nEvery refusal a person can see also carries a ",[182,928,929],{},"data.reason",", and the browser\nwords it from the locale files. Adding one takes four edits:",[204,932,933,946,1025,1042],{},[207,934,935,936,489,939,258],{},"Add the reason to ",[182,937,938],{},"REFUSAL_REASONS",[214,940,943],{"href":941,"rel":942,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Frefusals\u002Freasons.ts",[218],[182,944,945],{},"shared\u002Frefusals\u002Freasons.ts",[207,947,948,949],{},"Throw it:",[296,950,952],{"className":298,"code":951,"language":300,"meta":301,"style":301},"import type { RefusalReason } from '#shared\u002Frefusals\u002Freasons'\n\nthrow createError({\n  statusCode: 413,\n  statusMessage: 'The report is too large to export.',\n  data: { reason: 'report-too-large' satisfies RefusalReason },\n})\n",[182,953,954,971,975,985,995,1005,1021],{"__ignoreMap":301},[305,955,956,959,962,965,968],{"class":307,"line":12},[305,957,958],{"class":742},"import",[305,960,961],{"class":742}," type",[305,963,964],{"class":313}," { RefusalReason } ",[305,966,967],{"class":742},"from",[305,969,970],{"class":325}," '#shared\u002Frefusals\u002Freasons'\n",[305,972,973],{"class":307,"line":17},[305,974,768],{"emptyLinePlaceholder":767},[305,976,977,980,983],{"class":307,"line":22},[305,978,979],{"class":742},"throw",[305,981,982],{"class":310}," createError",[305,984,813],{"class":313},[305,986,987,990,993],{"class":307,"line":27},[305,988,989],{"class":313},"  statusCode: ",[305,991,992],{"class":749},"413",[305,994,193],{"class":313},[305,996,997,1000,1003],{"class":307,"line":32},[305,998,999],{"class":313},"  statusMessage: ",[305,1001,1002],{"class":325},"'The report is too large to export.'",[305,1004,193],{"class":313},[305,1006,1007,1010,1013,1016,1019],{"class":307,"line":37},[305,1008,1009],{"class":313},"  data: { reason: ",[305,1011,1012],{"class":325},"'report-too-large'",[305,1014,1015],{"class":742}," satisfies",[305,1017,1018],{"class":310}," RefusalReason",[305,1020,364],{"class":313},[305,1022,1023],{"class":307,"line":46},[305,1024,840],{"class":313},[207,1026,1027,1028,489,1031,1038,1039,258],{},"Map it to a key in ",[182,1029,1030],{},"REFUSAL_COPY_KEYS",[214,1032,1035],{"href":1033,"rel":1034,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Futils\u002Frefusal-copy.ts",[218],[182,1036,1037],{},"app\u002Futils\u002Frefusal-copy.ts",", for example\n",[182,1040,1041],{},"'report-too-large': 'refusals.reportTooLarge'",[207,1043,1044,1045,1048],{},"Add ",[182,1046,1047],{},"refusals.reportTooLarge"," to every locale file.",[178,1050,1051,1054],{},[182,1052,1053],{},"useAuthErrorToast()"," then shows the translated copy under your own title:",[296,1056,1058],{"className":298,"code":1057,"language":300,"meta":301,"style":301},"const addAuthError = useAuthErrorToast()\n\naddAuthError(error, { title: t('dashboard.settings.privacy.saveFailed') })\n",[182,1059,1060,1074,1078],{"__ignoreMap":301},[305,1061,1062,1064,1067,1069,1072],{"class":307,"line":12},[305,1063,743],{"class":742},[305,1065,1066],{"class":749}," addAuthError",[305,1068,778],{"class":742},[305,1070,1071],{"class":310}," useAuthErrorToast",[305,1073,762],{"class":313},[305,1075,1076],{"class":307,"line":17},[305,1077,768],{"emptyLinePlaceholder":767},[305,1079,1080,1083,1086,1088,1090,1093],{"class":307,"line":22},[305,1081,1082],{"class":310},"addAuthError",[305,1084,1085],{"class":313},"(error, { title: ",[305,1087,750],{"class":310},[305,1089,829],{"class":313},[305,1091,1092],{"class":325},"'dashboard.settings.privacy.saveFailed'",[305,1094,1095],{"class":313},") })\n",[178,1097,1098,1099,1102,1103,1110],{},"A ",[182,1100,1101],{},"description"," you pass wins over the reason's copy. A refusal whose reason\nthe catalogue doesn't know is worded by its status code, never with the\nserver's English.\n",[214,1104,1107],{"href":1105,"rel":1106,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Frefusal-copy.test.ts",[218],[182,1108,1109],{},"test\u002Funit\u002Frefusal-copy.test.ts"," fails on\na reason without copy, on copy no reason uses, and on a declared reason nothing\non the server sends.",[199,1112,1114],{"id":1113},"build-a-localised-link-on-the-server","Build a localised link on the server",[178,1116,1117,1118,1120,1121,1128],{},"The server has no ",[182,1119,848],{},".\n",[214,1122,1125],{"href":1123,"rel":1124,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Futils\u002Fi18n.ts",[218],[182,1126,1127],{},"server\u002Futils\u002Fi18n.ts"," stands in for it:",[424,1130,1131,1137,1151],{},[207,1132,1133,1136],{},[182,1134,1135],{},"requestLocale(event)"," is the cookie's locale when it names a configured\nlanguage, and the default otherwise.",[207,1138,1139,1142,1143,1146,1147,1150],{},[182,1140,1141],{},"localeRedirect(event, path, locale?)"," prefixes ",[182,1144,1145],{},"path"," with the visitor's\nlocale, or with ",[182,1148,1149],{},"locale",". The default language stays unprefixed, and a\nlocale segment already on the path is replaced rather than doubled.",[207,1152,1153,1156],{},[182,1154,1155],{},"localeCodes(event)"," lists the configured codes. Check a path segment against\nit before using it as a locale.",[178,1158,1159],{},"This is how the password reset mail builds its link:",[296,1161,1163],{"className":298,"code":1162,"language":300,"meta":301,"style":301},"return `${base}${localeRedirect(event, '\u002Freset-password', locale)}?token=${token}`\n",[182,1164,1165],{"__ignoreMap":301},[305,1166,1167,1170,1173,1176,1179,1182,1184,1187,1189,1192,1194,1196,1199,1202,1205],{"class":307,"line":12},[305,1168,1169],{"class":742},"return",[305,1171,1172],{"class":325}," `${",[305,1174,1175],{"class":313},"base",[305,1177,1178],{"class":325},"}${",[305,1180,1181],{"class":310},"localeRedirect",[305,1183,829],{"class":325},[305,1185,1186],{"class":313},"event",[305,1188,189],{"class":325},[305,1190,1191],{"class":325},"'\u002Freset-password'",[305,1193,189],{"class":325},[305,1195,1149],{"class":313},[305,1197,1198],{"class":325},")",[305,1200,1201],{"class":325},"}?token=${",[305,1203,1204],{"class":313},"token",[305,1206,1207],{"class":325},"}`\n",[199,1209,20],{"id":1210},"configuration",[1212,1213,1214,1233],"table",{},[1215,1216,1217],"thead",{},[1218,1219,1220,1224,1227,1230],"tr",{},[1221,1222,1223],"th",{},"Key",[1221,1225,1226],{},"Where",[1221,1228,1229],{},"Default",[1221,1231,1232],{},"What it does",[1234,1235,1236,1258,1285,1307],"tbody",{},[1218,1237,1238,1244,1248,1252],{},[1239,1240,1241],"td",{},[182,1242,1243],{},"i18n.defaultLocale",[1239,1245,1246],{},[182,1247,222],{},[1239,1249,1250],{},[182,1251,326],{},[1239,1253,1254,1255,1257],{},"Served at ",[182,1256,184],{},". Fills missing keys and untranslated documents",[1218,1259,1260,1264,1268,1282],{},[1239,1261,1262],{},[182,1263,554],{},[1239,1265,1266],{},[182,1267,222],{},[1239,1269,1270,1273,1274,1277,1278,1273,1280,1198],{},[182,1271,1272],{},"en"," (",[182,1275,1276],{},"en-US","), ",[182,1279,443],{},[182,1281,447],{},[1239,1283,1284],{},"The languages. One entry turns prefixes, detection and the switcher off",[1218,1286,1287,1292,1296,1302],{},[1239,1288,1289],{},[182,1290,1291],{},"i18n.locales[].file",[1239,1293,1294],{},[182,1295,222],{},[1239,1297,1298,189,1300],{},[182,1299,710],{},[182,1301,616],{},[1239,1303,1304,1305],{},"The file under ",[182,1306,245],{},[1218,1308,1309,1313,1317,1323],{},[1239,1310,1311],{},[182,1312,531],{},[1239,1314,1315],{},[182,1316,538],{},[1239,1318,1319,189,1321],{},[182,1320,1272],{},[182,1322,443],{},[1239,1324,1325],{},"Languages the AI blog workflow writes and translates",[178,1327,1328],{},"There are no environment variables.",[199,1330,1332],{"id":1331},"good-to-know","Good to know",[424,1334,1335,1355,1365,1391,1404],{},[207,1336,1337,1338,212,1341,1343,1344,1346,1347,1350,1351,1354],{},"Nitro matches route rules against the raw path, which knows nothing of the\nprefixes. Wrap a new rule in ",[182,1339,1340],{},"perLocale()",[182,1342,230],{},", or\n",[182,1345,196],{}," misses the ",[182,1348,1349],{},"ssr: false"," that ",[182,1352,1353],{},"\u002Fdashboard"," has.",[207,1356,1357,1358,258],{},"An untranslated legal page is served under the other language as that\nlanguage's terms. To answer 404 instead, remove the fallback query in\n",[214,1359,1362],{"href":1360,"rel":1361,"title":219},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fpages\u002Flegal\u002F%5B...slug%5D.vue",[218],[182,1363,1364],{},"app\u002Fpages\u002Flegal\u002F[...slug].vue",[207,1366,1367,189,1370,485,1373,1376,1377,189,1380,189,1383,865,1385,1388,1389,625],{},[182,1368,1369],{},"brand.name",[182,1371,1372],{},"blog.title",[182,1374,1375],{},"blog.description"," are printed by the RSS\nfeed without vue-i18n. Keep them plain text: a ",[182,1378,1379],{},"{",[182,1381,1382],{},"}",[182,1384,652],{},[182,1386,1387],{},"|"," fails\nthe build (",[214,1390,144],{"href":143},[207,1392,1393,1396,1397,1400,1401,1403],{},[182,1394,1395],{},"nuxtSiteConfig.name"," is ",[182,1398,1399],{},"@:brand.name",", the one linked message. It names the\nsite in the structured data on every page, so rename the product in\n",[182,1402,1369],{}," and leave the link alone.",[207,1405,1406,1407,1409,1410,1413],{},"The browser's language is only consulted at ",[182,1408,184],{},". A visitor who opens\n",[182,1411,1412],{},"\u002Fpricing"," directly gets English, whatever their browser prefers, because the\nURL names the language.",[1415,1416,1417],"style",{},"html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":301,"searchDepth":17,"depth":17,"links":1419},[1420,1421,1422,1423,1424,1425,1426,1427,1428],{"id":201,"depth":17,"text":202},{"id":287,"depth":17,"text":288},{"id":558,"depth":17,"text":559},{"id":628,"depth":17,"text":629},{"id":717,"depth":17,"text":718},{"id":918,"depth":17,"text":919},{"id":1113,"depth":17,"text":1114},{"id":1210,"depth":17,"text":20},{"id":1331,"depth":17,"text":1332},[1430,1431],{"path":143,"title":144,"description":145,"navTitle":144,"group":140,"order":146},{"path":153,"title":154,"description":155,"navTitle":154,"group":140,"order":156}]