Skip to content

i18n and copy

A developer or copy editor who needs to change a text on the landing page, publish or hide the Uzbek version, or add a language. It also explains to a translator why a “simple” wording change can fail the build.

  • Every visible string lives in src/i18n/dictionaries/{en,ru,uz}.ts. en.ts defines the Dictionary type; ru and uz must match it key for key. No hard-coded strings in components.
  • npm test rejects forbidden words, any digits other than 12, 72 and 3D, wrong apostrophes in Uzbek, a breakable space before a dash, and the wrong form of address.
  • Each language is written as an original, not a translation. Russian addresses the organiser as «вы» on the page and the guest as «ты» inside product mock-ups; Uzbek is «siz» everywhere.
  • Uzbek is published (uzPublished: true) but never the default: no browser language leads to /uz.
  • A longer text can fail npm run audit: the H1 may take at most 3 lines at 1280 and 1920 and 4 lines at 360 and 768, and header elements must have identical geometry in all three languages.
  • The Uzbek dictionary has not been proofread by a native speaker yet. See Known gaps.
File Role
src/i18n/dictionaries/en.ts English. Its shape is the Dictionary type
src/i18n/dictionaries/ru.ts Russian, typed Dictionary
src/i18n/dictionaries/uz.ts Uzbek in Latin script, typed Dictionary
src/i18n/index.ts export type Dictionary = typeof en and getDictionary(locale)
src/i18n/dictionaries.test.ts The rules below
src/content/site.ts Things that are not words and read the same in every language: studio name and link, the partner agency credit and its on/off switch, the year, the uzPublished flag
Key Covers
meta page title and description (also used for Open Graph)
nav, header, localeSwitch, footer chrome
hero H1, subtitle, CTA, scene caption, stepper labels, one sentence per scene step
scene product mock-ups: kiosk screens, bot messages, the group chat in “Why”
showcase, stickers showcase texts, people descriptions, the 12 sticker labels
how, why, organizer, data, setup, faq page sections
form labels, placeholders, error messages, consent line, “sent” screen

scene is the only key with a different voice. Everything outside it talks to the event organiser. scene is the product talking to a guest.

  1. Find the key in en.ts. Change the string in all three files. Each language is an original: rewrite it so it reads naturally, do not translate word for word.
  2. If you add a key, add it to en.ts first. TypeScript then reports the missing key in ru.ts and uz.ts.
  3. If you add an array item, add it to all three. The type does not check array lengths; the test does.
  4. Put a no-break space (written   in the source) before every em dash. See the dash rule below.
  5. Pass the string to components through props. A client component receives only its slice: Pick<Dictionary, "form">, not the whole dictionary.
  6. Run the checks:
Terminal window
npx tsc --noEmit
npm test
npm run shots -- --only=360,1280
npm run audit

shots and audit need a running server. See Local setup — landing page.

Never write a string literal in a component. The only texts drawn by code are things that are not copy: the clock time inside Telegram mock-ups and the 3-2-1 countdown on the kiosk.

Area Russian Uzbek English
The whole page: hero, sections, FAQ, form, “sent” screen «вы», to the organiser; the guest is spoken of in the third person «siz» direct; “you” is the organiser
Product mock-ups (every key under scene): the bot and kiosk talking to a guest «ты», as the real product does «siz» —

Further tone rules, from the repository’s CLAUDE.md:

  • Short sentences, active voice, concrete statements.
  • The page says “we”. It never names the company. The partner agency appears only in the footer credit, which comes from SITE.partner and is switched off with partner.enabled.
  • The reader is the organiser who gets a turnkey service. Do not write that they “run” or “manage” the station.
  • Mock-ups are in the page’s language, including the pack title.
  • The sticker style is described in words (“chunky cartoon”, «объёмные мультяшные», «hajmli multfilm»). Its internal name never appears.
May be said Must not be said or named
QR without an app, entry through Telegram prices
guest consent before the photo any figure except “12 stickers”, “a couple of minutes” and “72 hours”
choosing an outfit on the tablet reviews and client logos
12 chunky cartoon stickers with the guest’s face other messengers
the pack in Telegram in a couple of minutes printing stickers
the station is free immediately, it does not wait for generation “works without internet”
one pack per Telegram account “100 % likeness”
photos deleted automatically within 72 hours names of AI models and providers
monitoring and technical support during the event a backup or fallback AI provider
the station in the colours and with the logos of the event branded outfits or props
setup for the event with a rehearsal brands of past events

npm test runs these against all three dictionaries.

  • ru and uz have exactly the same key paths and array lengths as en.
  • No string is empty or whitespace only.

The whole dictionary is serialised to JSON, lower-cased and searched for substrings. Two consequences:

  • Key names count too, not only values.
  • Substrings match. A harmless word that contains a forbidden one fails. For example, any English word containing print or magic, or any word containing the letters gpt in sequence.
Category Blocked substrings
Another messenger one messenger brand name
Printing stickers печать стикер, печатаем, print, chop etish
Working without internet без интернета, offline, internetsiz
Guaranteed likeness 100%, 100 %
AI models and providers three provider and model names
Brands of past events two brand names
A backup provider резервн, запасн, fallback, backup, zaxira
Hype революц, revolution, inqilob, новый уровень, next level, all-in-one, магия, magic, sehr
The style’s internal name stylized
The partner agency its short name

The brand names are deliberately not repeated here. The exact list is the forbidden array in the test file.

Every run of digits, optionally followed by one letter, is checked. Only three tokens pass:

Token Meaning
12 stickers in a pack
72 hours until photos are deleted
3D allowed as a token

One exception: in the keys form.phone.placeholder and form.errors.phoneInvalid a sample phone number starting with +998 is removed before the check, because it shows the format to type and promises nothing.

So “24/7”, “5 minutes”, “2026”, “1st” and “No. 1” all fail. Write numbers as words if you must mention them, and check first whether you may promise them at all.

Letter Required code point Rejected
oʻ, gʻ (also capital) U+02BB ʻ (modifier letter turned comma) U+02BC after o or g
tutuq belgisi, as in maʼlumot U+02BC ʼ (modifier letter apostrophe) U+02BB after any letter other than o or g
— — ASCII ', typographic ‘ and ’, backtick, anywhere in uz

Most keyboards and translation tools produce the rejected characters. Paste the correct ones from an existing string. The fonts load only the latin and cyrillic subsets, and both code points are in latin; this is also why the monospace font is IBM Plex Mono, which has ʻ.

A dash must never start a line. The test rejects a normal space followed by an em dash ( —) in any string. Write a no-break space (U+00A0) before the dash. The dictionaries use the escape   so the character is visible in review:

title: "AI Sticker Pack — personal stickers for every guest at your event",

The test each part keeps one form of address checks whole words, case-insensitively:

Dictionary part Must not contain
ru, every key not under scene ты, тебя, тебе, тобой, твой and its forms
ru, every key under scene вы, вас, вам, вами, ваш and its forms
uz, every key sen, seni, senga, sening, sendan, senda

English has no such check.

The test file also pins the routing functions:

  • Accept-Language resolution for eleven sample headers;
  • six Uzbek headers that must never resolve to uz;
  • publishedLocales and languageAlternates for both values of the flag;
  • that the site’s own setting is currently true.

Defined in src/proxy.ts and src/i18n/config.ts. In Next.js 16 the file is called proxy.ts; it plays the role earlier versions called middleware.

flowchart TD
    A["Request path"] --> B{"Starts with /ru, /uz or /en?"}
    B -- yes --> C["Serve the page"]
    B -- no --> D{"NEXT_LOCALE cookie is a published locale?"}
    D -- yes --> E["Redirect to the cookie's locale"]
    D -- no --> F{"Accept-Language, by quality"}
    F -- "first match is a CIS language" --> G["Redirect to /ru"]
    F -- "first match is English" --> H["Redirect to /en"]
    F -- "only other languages" --> H
    F -- "missing, empty or a wildcard" --> G
Rule Detail
Locales ru, uz, en, in the order the switch shows them
Default ru, used when the browser says nothing useful
CIS languages mapped to /ru ru, uz, uk, be, kk, ky, tg, tk, az, hy — by primary subtag, so uz-Latn-UZ and kk resolve alike
Quality values honoured: en;q=0.3, kk;q=0.9 gives /ru
Uzbek browser /ru. The Uzbek page is chosen by the visitor, never detected
Cookie NEXT_LOCALE, written by the language switch, one year, SameSite=Lax. It records only the chosen language
Paths left alone anything under /_next, /api, and any path containing a dot (static files)
Other unprefixed paths redirected to /<locale><path>
hreflang one entry per published locale, plus x-default pointing at /en
Language switch swaps the first path segment, keeps the hash and the scroll position, cross-fades with a view transition

One boolean in src/content/site.ts:

locales: {
uzPublished: true,
},
Effect on true (current) false
/uz by direct link opens opens
Search indexing of /uz indexable robots: noindex, nofollow in the page metadata
Language switch on /ru and /en RU / UZ / EN RU / EN
Language switch on /uz itself RU / UZ / EN RU / UZ / EN — the draft still shows itself lit, for the proofreader
hreflang alternates ru, uz, en, x-default ru, en, x-default
sitemap.xml lists /uz omits /uz
Cookie NEXT_LOCALE=uz on / redirects to /uz ignored; Accept-Language decides
Browser language Uzbek on / /ru /ru

How the checks follow the flag:

  • npm run audit reads the flag from site.ts and expects the running build to behave accordingly. npm run audit -- --only=uz runs just the language and theme groups, for trying the other value on a second build.
  • npm test checks the routing functions for both values. It also asserts that the committed value is true (test the site's own setting is what the defaults follow — and it is published). If you set the flag to false, update that assertion in the same commit, or npm test fails.
  • The flag is read at build time. Changing it needs a new build and deploy.

Layout constraints from the longest language

Section titled “Layout constraints from the longest language”

Text length is a layout input here. Two different languages are “the longest”, depending on the element.

Element Longest language Constraint Enforced by
Overall copy Uzbek it defines the hero text column (44 %), the header slots and the CTA width convention
H1 Russian the top of the H1 size clamp, 2.35rem, was chosen for the Russian headline audit
H1 line count all three ≤ 4 lines at 360 and 768; ≤ 3 lines at 1280 and 1920 audit
H1 line count through the font swap all three identical in the fallback font and in Unbounded audit
Hero subtitle all three ≤ 4 lines at 360 and 768; ≤ 2 lines at 1280 and 1920 audit
Header CTA and hero CTA all three identical x, width and height in every language at 1280 and 1920; text not clipped audit
Navigation links published locales centre drifts at most 1 px between languages audit
Kiosk title in the hero tablet all three one line at 768, 1280 and 1920 audit
Hero CTA on a phone all three visible without scrolling at 360×780 audit
Sentence under the stepper per locale every step has a distinct sentence audit (on /ru)

The H1 font size is clamp(1.55rem, 1rem + 2vw, 2.35rem). If a new headline does not fit, shorten the headline. Do not raise a line limit to make a text pass.

  1. Config. In src/i18n/config.ts: add the code to locales; extend the hard-coded comparison in isLocale; add an entry to OG_LOCALE; decide how Accept-Language should treat the language (today the function returns only ru or en) and whether the new locale needs its own published flag like uzPublished.
  2. Dictionary. Create src/i18n/dictionaries/<code>.ts typed as Dictionary and register it in src/i18n/index.ts.
  3. Switch label. Add localeSwitch.<code> to every dictionary; the switch renders dict.localeSwitch[code].
  4. Switch component. In src/components/site/LocaleSwitch.tsx extend the ORDER array and the path regular expression /^\/(ru|uz|en)/.
  5. Fonts. Check that Unbounded, Onest and IBM Plex Mono cover the script with the loaded subsets (latin, cyrillic). Adding a subset changes font weight on the first screen; re-measure.
  6. Tests. In dictionaries.test.ts add the dictionary to ALL, add the language’s forbidden words and its form-of-address rule, and update the expected arrays in the publishedLocales and languageAlternates tests.
  7. Scripts. Update the hard-coded locale lists: LOCALES in scripts/audit.mjs and scripts/shots.mjs (and the file-name pattern (ru|uz|en) in shots.mjs), the loop in scripts/js-weight.mjs, the default runs in scripts/lighthouse.mjs, the list in scripts/scroll-film.mjs. The audit’s expected switch contents (ru,uz,en) change too.
  8. Layout. Run npm run shots and npm run audit. A new language may be the new longest one; the header geometry and H1 limits apply to it from the first run.
  9. Leads server. The server accepts locale values ru, uz, en and stores anything else as en (LOCALES in the product repository’s bot/services/leads.py). Add the code there, or requests from the new page are recorded as English.
  10. Things that follow automatically because they iterate locales or publishedLocales(): the redirect in proxy.ts, generateStaticParams, the sitemap, the hreflang alternates, the Open Graph image route.
  11. Product language. The real bot and kiosk speak Russian only. A landing page in a new language shows mock-ups in that language; the product has to follow before the first event in it.
  • The Uzbek dictionary was written as a draft. Its header comment and one comment in the form section (К вычитке, “to be proofread”) say a native speaker’s review is pending.
  • The page is nevertheless published and indexable (uzPublished: true).
  • Treat every Uzbek string as unreviewed. When the review happens, re-check the three typographic rules above, because reviewers’ tools tend to replace ʻ and ʼ with ordinary apostrophes.

The gap is tracked on Known gaps.

  • Landing quality tooling — the audit checks that constrain text length.
  • i18n across the system — languages in the bot, the kiosk and the landing page together.
  • Showcase packs — outfit names, people descriptions and sticker labels are dictionary keys too.
  • Known gaps — the pending Uzbek proofread and other open items.
  • Client FAQ — what sales may promise, in line with the “may be said” list.

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.