easy-web-i18n
Installation
Section titled “Installation”pnpm add @easy-web/i18nOverview
Section titled “Overview”@easy-web/i18n provides everything a bilingual (de/en) Astro site needs: path localisation helpers (localizedHref, getLocaleFromPath), SEO utilities for alternate links and canonical URLs, and locale-aware Intl formatters for dates, numbers, currency, and lists. All helpers use the Intl APIs built into modern runtimes — no external translation library is required.
The recommended entry point is the createI18n factory, which binds your site’s locale config once and returns a typed helper bundle.
createI18n
Section titled “createI18n”Factory function. Pass your site’s locale configuration and receive a pre-bound helper bundle. This is the recommended way to use the package.
Signature: function createI18n<L extends string>(cfg: I18nConfig<L>): I18nBundle<L>
import { createI18n } from '@easy-web/i18n';
export const i18n = createI18n({ locales: ['de', 'en'] as const, defaultLocale: 'de', baseUrl: 'https://example.com',});
// In an Astro page:const href = i18n.localizedHref('/about', 'en'); // '/en/about'const locale = i18n.getLocaleFromPath('/en/contact'); // 'en'const links = i18n.getAlternateLinks('/about');// [{ hreflang: 'de', href: 'https://example.com/about' },// { hreflang: 'en', href: 'https://example.com/en/about' },// { hreflang: 'x-default', href: 'https://example.com/about' }]The returned bundle also exposes a format namespace with all formatters pre-bound to accept a locale as the first argument:
i18n.format.date('de', new Date()); // '26.6.2026'i18n.format.currency('de', 9.99, 'EUR'); // '9,99 €'i18n.format.list('en', ['a', 'b', 'c']); // 'a, b, and c'I18nConfig
Section titled “I18nConfig”Configuration interface for createI18n.
Signature:
interface I18nConfig<L extends string> { locales: readonly L[]; defaultLocale: L; baseUrl: string;}localizedHref
Section titled “localizedHref”Returns a localised path. De-localises path before re-prefixing so the function is idempotent — calling it on an already-prefixed path never produces a double-prefix such as /en/en/foo. Query strings and hash fragments are preserved.
The default locale produces no prefix (e.g. de → /about); non-default locales are prefixed (e.g. en → /en/about).
Signature:
function localizedHref(opts: { path: string; locale: string; defaultLocale: string;}): stringimport { localizedHref } from '@easy-web/i18n';
localizedHref({ path: '/about', locale: 'de', defaultLocale: 'de' }); // '/about'localizedHref({ path: '/about', locale: 'en', defaultLocale: 'de' }); // '/en/about'
// Idempotent — safe on already-prefixed paths:localizedHref({ path: '/en/about', locale: 'en', defaultLocale: 'de' }); // '/en/about'
// Query strings and hashes are preserved:localizedHref({ path: '/about?ref=home#section', locale: 'en', defaultLocale: 'de' });// '/en/about?ref=home#section'getLocaleFromPath
Section titled “getLocaleFromPath”Extracts the active locale from a URL pathname by inspecting the first path segment.
Signature:
function getLocaleFromPath(opts: { pathname: string; locales: readonly string[]; defaultLocale: string;}): stringimport { getLocaleFromPath } from '@easy-web/i18n';
getLocaleFromPath({ pathname: '/en/about', locales: ['de', 'en'], defaultLocale: 'de' }); // 'en'getLocaleFromPath({ pathname: '/about', locales: ['de', 'en'], defaultLocale: 'de' }); // 'de'getLocaleFromPath({ pathname: '/', locales: ['de', 'en'], defaultLocale: 'de' }); // 'de'getAlternateLinks
Section titled “getAlternateLinks”Generates <link rel="alternate"> data for every locale plus an x-default entry. Use in your <head> for SEO.
Signature:
function getAlternateLinks(opts: { path: string; locales: readonly string[]; defaultLocale: string; baseUrl: string;}): AlternateLink[]---import { getAlternateLinks } from '@easy-web/i18n';
const links = getAlternateLinks({ path: Astro.url.pathname, locales: ['de', 'en'], defaultLocale: 'de', baseUrl: 'https://example.com',});---<head> {links.map(({ hreflang, href }) => ( <link rel="alternate" hreflang={hreflang} href={href} /> ))}</head>AlternateLink
Section titled “AlternateLink”Shape of each item returned by getAlternateLinks.
Signature: type AlternateLink = { hreflang: string; href: string }
getCanonicalUrl
Section titled “getCanonicalUrl”Returns the canonical URL for the current locale, stripping the locale prefix for the default locale.
Signature:
function getCanonicalUrl(opts: { path: string; locale: string; defaultLocale: string; baseUrl: string;}): stringimport { getCanonicalUrl } from '@easy-web/i18n';
getCanonicalUrl({ path: '/about', locale: 'de', defaultLocale: 'de', baseUrl: 'https://example.com' });// 'https://example.com/about'
getCanonicalUrl({ path: '/about', locale: 'en', defaultLocale: 'de', baseUrl: 'https://example.com' });// 'https://example.com/en/about'Formatters
Section titled “Formatters”All formatters cache their Intl instances internally — safe to call in hot render paths.
formatDate
Section titled “formatDate”Signature: function formatDate(locale: string, date: Date | number, opts?: Intl.DateTimeFormatOptions): string
import { formatDate } from '@easy-web/i18n';
formatDate('de', new Date('2026-06-26')); // '26.6.2026'formatDate('en', new Date('2026-06-26')); // '6/26/2026'formatDate('de', new Date(), { dateStyle: 'long' }); // '26. Juni 2026'formatNumber
Section titled “formatNumber”Signature: function formatNumber(locale: string, value: number, opts?: Intl.NumberFormatOptions): string
import { formatNumber } from '@easy-web/i18n';
formatNumber('de', 1234567.89); // '1.234.567,89'formatNumber('en', 1234567.89); // '1,234,567.89'formatCurrency
Section titled “formatCurrency”Signature: function formatCurrency(locale: string, value: number, currency: string, opts?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>): string
import { formatCurrency } from '@easy-web/i18n';
formatCurrency('de', 9.99, 'EUR'); // '9,99 €'formatCurrency('en', 9.99, 'USD'); // '$9.99'formatRelativeTime
Section titled “formatRelativeTime”Signature: function formatRelativeTime(locale: string, value: number, unit: Intl.RelativeTimeFormatUnit, opts?: Intl.RelativeTimeFormatOptions): string
import { formatRelativeTime } from '@easy-web/i18n';
formatRelativeTime('de', -3, 'day'); // 'vor 3 Tagen'formatRelativeTime('en', -3, 'day'); // '3 days ago'formatRelativeTime('en', 1, 'month'); // 'in 1 month'formatList
Section titled “formatList”Signature: function formatList(locale: string, items: string[], opts?: Intl.ListFormatOptions): string
import { formatList } from '@easy-web/i18n';
formatList('en', ['apples', 'oranges', 'bananas']); // 'apples, oranges, and bananas'formatList('de', ['Äpfel', 'Orangen', 'Bananen']); // 'Äpfel, Orangen und Bananen'