astro-i18n
TypeScript icon, indicating that this package has built-in type declarations

2.2.4 • Public • Published
astro-i18n logo

astro-i18n

A TypeScript-first internationalization library for Astro.

Features

  • 🪶 Lightweight.
  • 🌏 Internationalized routing.
  • 🔒 Type-safety and autocompletion.
  • 🔧 Support for plurals, context, interpolations and formatters.
  • 🚀 Built for Astro and nothing else.
  • 💻 Astro island compatible.

Introduction

Most internationalization libraries rely on raw untyped strings to access their translations. This is equally possible with astro-i18n however it can generate project specific types to get type-safety and autocompletion for your translations.

Installation

# npm
npm install astro-i18n
# yarn
yarn add astro-i18n
# pnpm
pnpm add astro-i18n

Get started

Run the quick install command :

./node_modules/.bin/astro-i18n install
# serverless (no filesystem/node.js setup)
./node_modules/.bin/astro-i18n install --serverless

Verify that your middleware file at src/middleware.ts or src/middleware/index.ts uses the astro-i18n middleware. If the file was generated by the quick install command it should be fine.

// src/middleware.ts
import { sequence } from "astro/middleware"
import { useAstroI18n } from "astro-i18n"

const astroI18n = useAstroI18n(/* astro-i18n config, custom formatters */)

export const onRequest = sequence(astroI18n)

If you are using serverless all your initial data needs to be passed to the middleware, you should import your root astro-i18n config and pass it to the middleware.

When using node (SSR or SSG) you can either go for the same workflow as serverless or use the filesystem to store your config and translations.

If you used the quick install (with node) a config file for astro-i18n should have been generated, you will find it in your root directory (astro-i18n.config.(ts|js|cjs|mjs|json)). This is the same as passing it in the middleware parameters. Configure it as needed, you can set your primaryLocale and secondaryLocales there.

You can put your translations in four locations :

  • Inside the middleware config.
  • Inside the config file at the root.
  • In your src/pages folder (local page translations), see below.
  • In the src/i18n directory, see below.

astro-i18n works on the server-side and on the client-side so that it can work with client-side frameworks. Because of this translations are aggregated into groups:

  • The "common" group which is accesible on every page.
  • The current page group named after its route, such as "/about" or "/posts/[id]", as its name implies it's only accessible on the current page.
  • Any other custom group named by yourself, for example "admin". It's up to you to decide on which pages your custom group is accessible.

By default any translation accessible on the current page is visible on the client-side (you may change that in the config).

Filesystem translations screenshot

As shown in the screenshot above, local page translations can be either in src/pages or src/i18n/pages, nest them just like you do with astro file-based routing. All the following translation file formats are valid :

const regular = `${locale}.json` // fr.json
const private = `_${locale}.json` // _fr.json
const regularWithTranslation = `${locale}.${segmentTranslation}.json` // fr.translated-page-name.json
const privateWithTranslation = `_${locale}.${segmentTranslation}.json` // _fr.translated-page-name.json

These files can either be directly in the page directory or grouped in a special directory called i18n by default. Concerning custom groups, create a directory containing the translation files named after your group in src/i18n. To make them accessible in a page check the loadingRules.

To get type-safety and generate your translated routes run the following command npm run i18n:sync.

Once you have some translations you can use the main translation function t :

// src/pages/about/_i18n/en.json
{
	"my_nested": {
		"translation_key": "hello"
	}
}
// src/pages/about/_i18n/fr.a-propos.json
{
	"my_nested": {
		"translation_key": "bonjour"
	}
}
// src/pages/about/index.astro
import { astroI18n, t } from "astro-i18n"

astroI18n.locale // "en"
t("my_nested.translation_key") // hello

// src/pages/fr/a-propos/index.astro
import { astroI18n, t } from "astro-i18n"

astroI18n.locale // "fr"
t("my_nested.translation_key") // bonjour

If there's duplicate keys t will return the more specific one (custom group => local page => common group).

To translate a link use the l function, by default all route segment translations are visible on the client-side :

// src/pages/fr/a-propos/index.astro
import { astroI18n, l } from "astro-i18n"

astroI18n.locale // "en"
l("/about") // "about"

// src/pages/fr/a-propos/index.astro
import { astroI18n, l } from "astro-i18n"

astroI18n.locale // "fr"
l("/about") // "a-propos"

That's it for the basics, for more keep reading !

Configuration

primaryLocale

The default locale for your app.

secondaryLocales

All the other locales supported by your app.

fallbackLocale

If left undefined this will be equal to your primaryLocale, to disable it set it to an empty string. The locale to search a translation in when it's missing in another one.

showPrimaryLocale

Boolean deciding whether to show the default locale in the url or not.

trailingSlash

Possible values are "always" or "never" (default). When set to "always", generated routes will have a trailing slash.

run

Possible values are "server" or "client+server" (default). When set to "client+server" the available translations for the current page and all route translations will be serialized and sent to the client so that astro-i18n can work with client-side frameworks. You can disable this behaviour by setting run to "server".

translations

Your app translations in the following format :

{
	"common": {
		"en": {
			"hello": "Hello",
			"form": {
				// accessed with the "form.first_name" key :
				"first_name": "First name"
			}
		},
		"fr": {
			"hello": "Bonjour",
			"form": {
				"first_name": "Prénom"
			}
		}
	},
	"admin": {
		"en": {
			"how_are_you": "How are you ?"
		},
		"fr": {
			"how_are_you": "Comment allez vous ?"
		}
	},
	// routes are also a valid group, they will automatically load in the corresponding page :
	"/posts/[id]": {
		"en": {
			"bye": "Bye"
		},
		"fr": {
			"bye": "Au revoir"
		}
	}
}

translationLoadingRules

An array of rules specifying what group to load on which page :

[
	{
		"routes": ["^/admin.*"], // regex for all routes beginning with "/admin"
		"groups": ["^admin$"] // regex to load the "admin" group
	}
]

translationDirectory

Which name to use for the directories in src and in src/pages, by default it's "i18n" (/src/i18n and src/pages/i18n/src/pages/about/i18n).

{
	"i18n": "i18n",
	"pages": "i18n"
}

routes

Your route segment translations. This is where you can translate your routes, for example "/about" to "/a-propos".

{
	// only specify secondary locales
	"fr": {
		// "primary_locale_segment": "translated_segment"
		"about": "a-propos"
	}
}

Variants

Variants are the way that plurals and context are handled. You can set multiple variables, numbers will match the closest value however other types will only match exact matches. They are set in the translation key, here's some examples :

{
	"car": "There is a car.", // default value for "car" key
	"car{{ cars: 2 }}": "There are two cars.",
	"car{{ cars: 3 }}": "There are many cars.",
	"foo{{ multiple: [true, 'bar'] }}": "baz.",
	"context{{ weather: 'rain' }}": "It's rainy.",
	"context{{ weather: 'sun' }}": "It's sunny."
}
import { astroI18n, t } from "astro-i18n"

t("car") // "There is a car."
t("car", { cars: 0 }) // "There are two cars."
t("car", { cars: 1 }) // "There are two cars."
t("car", { cars: 2 }) // "There are two cars."
t("car", { cars: 3 }) // "There are many cars."
t("car", { cars: 18 }) // "There are many cars."
t("foo", { multiple: true }) // "baz."
t("foo", { multiple: "bar" }) // "baz."
t("context", { weather: "rain" }) // "It's rainy."
t("context", { weather: "sun" }) // "It's sunny."

Interpolations

Interpolations can be used to display dynamic data in your translation values, here's some examples :

{
	"interpolation_1": "{# variable #}",
	"interpolation_2": "{# variable(alias) #}",
	"interpolation_3": "{# variable(alias)>uppercase #}",
	"interpolation_4": "{# 15>intl_format_number({ style: 'currency', currency: currency }) #}",
	"interpolation_5": "{# { foo: 'bar'}(alias)>json(false)>uppercase #}"
}
import { astroI18n, t } from "astro-i18n"

astroI18n.locale // "en"

t("interpolation_1", { variable: "foo" }) // "foo"
t("interpolation_2", { alias: "bar" }) // "bar"
t("interpolation_3", { alias: "baz" }) // "BAZ"
t("interpolation_4", { currency: "EUR" }) // "€15.00"
t("interpolation_5") // '{"FOO":"BAR"}'
t("interpolation_5", { alias: { bar: "baz" } }) // '{"BAR":"BAZ"}'

As you can see there's multiple parts to an interpolation :

  • The interpolation value which is what the formatters apply on.
  • The interpolation alias which can either change the name of the value if it's a variable or make the value a default value replaceable by the alias variable.
  • The interpolation formatters which can take arguments, you can chain them and create your own. When creating a formatter, if you want it to work on the client side it must not use anything outside the function scope.

You can apply this to any value, even if it's nested or inside the formatters arguments.

Interpolation formatters

An interpolation formatter is a function that takes the interpolation value and transforms it, if there's multiple of them they will be chained. These are the default interpolation formatters :

  • upper() : value.toUpperCase().
  • uppercase() : alias for upper.
  • lower() : value.toLowerCase().
  • lowercase() : alias for lower.
  • capitalize() : Capitalizes the first character of value and lowers the others.
  • json(format = true) : JSON.stringify(value), if format is true it will format the JSON.
  • default_falsy(defaultValue) : When !value, defaultValue will be used instead.
  • default(defaultValue) : alias for default_falsy.
  • default_nullish(defaultValue) : When value == null, defaultValue will be used instead.
  • default_non_string(defaultValue) : When typeof value !== "string", defaultValue will be used instead.
  • intl_format_number(options, locale = astroI18n.locale) : Formats value with Intl.NumberFormat.
  • intl_format_date(options, locale = astroI18n.locale) : Formats value with Intl.DateTimeFormat.

If the formatter has no arguments you can write it without parenthesises, for example value>upper.

You can add your own custom formatters in the second argument of the middleware. If you want them to work on the client-side they must not use anything outside the function scope. Here's an example of a custom formatter :

export function xXify(value: unknown, repeats: unknown = 1) {
	if (typeof repeats !== "number") {
		throw new Error("repeats must be a number")
	}
	if (repeats <= 0) return value
	return `${"xX".repeat(repeats)}${value}${"Xx".repeat(repeats)}`
}

// example that wouldn't work client-side :
const error = new Error("repeats must be a number")
function xXify(value: unknown, repeats: unknown = 1) {
	if (typeof repeats !== "number") throw error // cannot use anything outside of scope
	// ...
}

Reference

defineAstroI18nConfig

function defineAstroI18nConfig(
	config: Partial<AstroI18nConfig>,
): Partial<AstroI18nConfig>

Utility function to get type safety when defining the config.

useAstroI18n

function useAstroI18n(
	config?: Partial<AstroI18nConfig> | string, // string if path to config file
	formatters?: {
		[name: string]: (value: unknown, ...args: unknown[]) => unknown
	},
): AstroMiddleware

The astro-i18n middleware, mandatory to make the library work.

createGetStaticPaths

function createGetStaticPaths(
	callback: (
		props: GetStaticPathsProps,
	) => GetStaticPathsItem[] | Promise<GetStaticPathsItem[]>,
): GetStaticPathsItem[] | Promise<GetStaticPathsItem[]>

You should use this function if you plan to use any translation features inside a getStaticPaths. This is to fix some Astro behaviour. Using the t function is not recommended inside getStaticPaths.

t

Alias for astroI18n.t.

l

Alias for astroI18n.l.

astroI18n.environment

environment: "node" | "none" | "browser"

The current detected environment.

astroI18n.route

route: string

The current route (without the locale, for example /fr/about will return /about).

astroI18n.pages

pages: string[]

An array of all the available pages. At the moment it only detects pages for which you have a translation (even if it's empty).

astroI18n.page

page: string

The corresponding page for the current route, for example /posts/my-cool-slug will be /posts/[slug]. Only gets detected if you have a translation for that page (even if it's empty).

astroI18n.locale

locale: string

The locale for the current page.

astroI18n.locales

locales: string[]

All the supported locales.

astroI18n.primaryLocale

primaryLocale: string

The configured primary locale.

astroI18n.secondaryLocales

secondaryLocales: string

The configured secondary locales.

astroI18n.fallbackLocale

fallbackLocale: string

The configured fallback locale.

astroI18n.isInitialized

isInitialized: boolean

True once the config has been loaded and the state initialized.

astroI18n.t

function t(
	key: string,
	properties?: Record<string, unknown>,
	options?: {
		route?: string
		locale?: string
		fallbackLocale?: string
	},
): string

The main translation function (t is an alias).

astroI18n.l

function l(
	route: string,
	parameters?: Record<string, unknown>,
	options?: {
		targetLocale?: string
		routeLocale?: string
		showPrimaryLocale?: boolean
		query?: Record<string, unknown>
	},
): string

The main routing function (l is an alias).

astroI18n.addTranslations

function addTranslations(translations: {
	[group: string]: {
		[locale: string]: DeepStringRecord
	}
}): AstroI18n

Adds new translations at runtime.

astroI18n.addFormatters

function addFormatters(formatters: {
	[name: string]: (value: unknown, ...args: unknown[]) => unknown
}): AstroI18n

Adds new formatters at runtime.

astroI18n.addTranslationLoadingRules

function addTranslationLoadingRules(
	translationLoadingRules: {
		groups: string[]
		routes: string[]
	}[],
): AstroI18n

Adds new translation loading rules at runtime.

astroI18n.addRoutes

function addRoutes(routes: {
	[secondaryLocale: string]: {
		[segment: string]: string
	}
}): AstroI18n

Adds new route segment translations at runtime.

astroI18n.extractRouteLocale

function extractRouteLocale(route: string): string

Utility function to parse one of the configured locales out of the given route.

astroI18n.redirect

function redirect(destination: string | URL, status = 301): void

Use this instead of Astro.redirect to redirect users.

Components

You can use these premade components in your project :

HrefLangs

SEO component, see hreflang.

---
import { astroI18n } from "astro-i18n"

const params: Record<string, string> = {}
for (const [key, value] of Object.entries(Astro.params)) {
	if (value === undefined) continue
	params[key] = String(value)
}

const hrefLangs = astroI18n.locales.map((locale) => ({
	href:
		Astro.url.origin +
		astroI18n.l(Astro.url.pathname, params, {
			targetLocale: locale,
		}),
	hreflang: locale,
}))
---

{
	hrefLangs.map(({ href, hreflang }) => (
		<link rel="alternate" href={href} hreflang={hreflang} />
	))
}

LocaleSwitcher

Simple component to switch locales.

---
import { astroI18n, l } from "astro-i18n"

interface Props {
	showCurrent: boolean
	labels: {
		[locale: string]: string
	}
}

const { showCurrent = true, labels = {} } = Astro.props

const params: Record<string, string> = {}
for (const [key, value] of Object.entries(Astro.params)) {
	if (value === undefined) continue
	params[key] = String(value)
}

let links = astroI18n.locales.map((locale) => ({
	locale,
	href: l(Astro.url.pathname, params, {
		targetLocale: locale,
	}),
	label: labels[locale] || locale.toUpperCase(),
}))

if (!showCurrent) {
	links = links.filter((link) => link.locale !== astroI18n.locale)
}
---

<nav>
	<ul>
		{
			links.map(({ href, label }) => (
				<li>
					<a href={href}>{label}</a>
				</li>
			))
		}
	</ul>
</nav>

CLI

astro-i18n install

Generates default files and add i18n commands to package.json.

astro-i18n generate:pages

Generates the pages corresponding to your current config and src/pages folder. You can use the --purge argument to clear old pages.

astro-i18n generate:types

Generates types in src/env.d.ts to give you type safety according to your config & translations.

astro-i18n extract:keys

Extract all the translation keys used in src/pages (as long as they are strings and not variables), and adds them to your i18n folder for every locale. This is useful if you use your keys as values.

Package Sidebar

Install

npm i astro-i18n

Weekly Downloads

237

Version

2.2.4

License

MIT

Unpacked Size

175 kB

Total Files

109

Last publish

Collaborators

  • alexandre-fernandez