@lunmark/vokativ-next-intl
v0.0.1
Published
Serbian vocative for next-intl: write <voc>{name}</voc> in a Serbian message and the name is addressed properly.
Downloads
425
Maintainers
Readme
@lunmark/vokativ-next-intl
The Serbian vocative for next-intl: mark where a Serbian message addresses someone, and the name is put in the vocative.
// messages/sr.json
"greeting": "Dobar dan, <voc>{name}</voc>!" // → "Dobar dan, Ivane!"
// messages/en.json, unchanged
"greeting": "Good afternoon, {name}!" // → "Good afternoon, Ivan!"t("greeting", { name: user.firstName }) // no change at any call siteIt works everywhere next-intl formats a message: server and client components, t(),
t.rich(), t.markup() and getTranslations(), with or without precompiled messages.
Setup
pnpm add @lunmark/vokativ-next-intlIn next.config.ts, use it in place of next-intl's plugin. next-intl's own setting goes
second, unchanged:
import createVokativNextIntlPlugin from "@lunmark/vokativ-next-intl/plugin"
const withNextIntl = createVokativNextIntlPlugin(
{ languages: { auto: true } },
"./i18n/request.ts"
)
export default withNextIntl(nextConfig)Already wrapping your config another way? withVokativ(withNextIntl(nextConfig)) does the
same after next-intl's plugin:
import { withVokativ } from "@lunmark/vokativ-next-intl/plugin"Then put <voc>…</voc> around the name in your Serbian messages. That's all.
Languages
languages decides which locales get the vocative, and in which script the name is written.
{ languages: { auto: true } } // the default
{ languages: { "sr-Latn": "latin", sr: "cyrillic" } } // explicit
{ languages: { auto: true, "sr-ME": false } } // all Serbian but oneauto: truecovers every Serbian locale (sr,sr-*). A locale code that names a script gets it (sr-Latn-RS→ Latin,sr-Cyrl→ Cyrillic); a baresrkeeps the name's own script.- A locale key covers that locale and the ones under it (
"sr-Latn"coverssr-Latn-RS); the most specific key wins."latin"or"cyrillic"writes the name in that script (Ivan → Иване on a Cyrillic page),truekeeps the name's own script,falseturns the vocative off. - A locale code that names its own script keeps it when the matching key doesn't name one:
with
{ sr: "cyrillic" },sr-Latnis still Latin. - Everywhere else,
<voc>leaves the name as it is, so a tag that slips into another language does no harm.
A misspelled setting ("cyrilic") stops the build with an error rather than doing nothing.
Gender
If the message also gets a gender value ("m"/"male" or "f"/"female"), which Serbian
messages often need anyway for "Dragi/Draga", surnames are declined for men:
"dear": "{gender, select, f {Draga <voc>{name}</voc>} other {Dragi <voc>{name}</voc>}}"
// { name: "Marko Janković", gender: "m" } → "Dragi Marko Jankoviću"
// { name: "Ana Janković", gender: "f" } → "Draga Ana Janković"Without one, only the first name is declined; a surname is never wrong left as written.
Tests (Vitest)
Vitest doesn't run next.config, so add the same swap there, or a message with <voc>
renders as its key:
// vitest.config.ts
import { vokativNextIntl } from "@lunmark/vokativ-next-intl/vitest"
export default defineConfig({
plugins: [vokativNextIntl({ languages: { auto: true } })],
})How it works
Every message next-intl formats goes through one function in use-intl,
use-intl/format-message. The plugin points that import at a small stand-in, the way
next-intl itself swaps it when it precompiles messages. The stand-in adds a voc handler and
calls the original formatter.
- A message without
<voc>goes straight through: about 20 ns extra. - A
vocyou pass yourself always wins. - Names longer than 100 characters are left as written.
- The declension is
@lunmark/vokativ, about 3 KB gzipped with this package.
Good to know
- Requirements: next-intl 4, and Next.js 15.3 or later, with Turbopack or webpack.
- Typed messages generated from your Serbian file make
t.rich()andt.markup()ask for avochandler, since the tag is in their type. Plaint()doesn't. Most apps type their messages from English, where there's no tag. t.markup()doesn't escape values, with or without<voc>; the name comes out exactly as{name}would.t()andt.rich()render through React, which escapes.use-intl/format-messageis a public entry point, but swapping it isn't a documented next-intl API. This package pins next-intl 4 and is tested against it.
