Package Exports
- astro-nanointl
 - astro-nanointl/client
 - astro-nanointl/middleware
 - astro-nanointl/package.json
 - astro-nanointl/utils
 
Readme
Small, yet powerful set of tools to integrate internationalization/i18n/intl to your Astro project. Strategy-agnostic (supports both SSG and SSR) and inspired by nanostores/i18n and Next.js.
Prerequisites
Please do not use this tools for client side internationalization as they are not intended to use on the client and may load bunch of unnecessary JavaScript. If you want to implement i18n on the client please take a look at nanostores/i18n or pass translations as props.
Install
Use your favorite package manager to install astro-nanointl from npm. Below the pnpm example:
pnpm add -D astro-nanointlOr you can go ahead and give astro add a try:
pnpm astro add astro-nanointlPlug
Next, you should just add the integration into your astro.config:
import { defineConfig } from 'astro/config';
+import nanointl from "astro-nanointl";
export default defineConfig({
  integrations: [
+   nanointl({
+     locales: ["en", "ru"]
+   })
  ]
});TypeScript users
If you're using TypeScript consider adding types to your env.d.ts:
/// <reference types="astro/client" />
+/// <reference types="astro-nanointl/client" />TypeScript Wizards
If you want to be stricter with types you can go ahead and override UserDefinedIntl interface with your types in env.d.ts like so:
/// <reference types="astro/client" />
/// <reference types="astro-nanointl/client" />
declare module 'virtual:nanointl' {
  interface UserDefinedIntl {
    // here is your locales types
    locales: ['en', 'ru']
  }
}The result will be the following:
const { locales } = useLocales()
//      ^? const locales: ["en", "ru"]Declare your translations
Add your translations to whatever directory you want in project, but keep in mind that integration will scan for every JSON file that ends with locale specified in integration's locales property and is in locales or translations directory except public directory. The glob pattern is the following:
import.meta.glob([
  '/**/{locales,translations}/**/*.json',
  '!/public/**/{locales,translations}/**/*.json',
], { eager: true, import: 'default' })Translation files should be plain JSON files without deeply nested objects. Overall the structure is the following:
// locales/ru.json
//  or src/locales/ru.json
//  or locales/greetings/ru.json
//  or src/locales/greetings/ru.json
// all of the above are valid paths
//  but not:
//  public/locales/ru.json
{
  "greetings": { // 'greetings' is componentName
    "hello": "Привет" // this is a transaltion object
  }
}Play
Start playing around by using useLocale composable to get the t object containing translated values and useLocales composable which returns default locale and the list of all availbale locales declared by you earlier.
SSR
See the simpliest example: ```typescript
// src/pages/[lang]/index.astro -> using output: 'server'
import { useLocale } from 'astro-nanointl/utils'
// basically getting locale from url when SSR const { lang } = Astro.params const { locale, t } = useLocale(lang, 'greetings', { hello: 'Hello everyone!' })
{ t.hello }
Locale used: { locale }
```To avoid errors we can also add check for undefined and throw 404 (Not Found) or 406 (Not Acceptable) if locale or translation was not presented.
if (!locale || !t) {
  return new Response(null, {
    status: 404, 
    statusText: 'Not Found'
  })
  // or
  return Astro.redirect('/404')
}SSG
Let's create example for SSG: ```typescript
// src/pages/[locale]/index.astro -> using output: 'static' or output: 'hybrid'
import type { GetStaticPaths, InferGetStaticParamsType } from 'astro'
import { useLocales, useLocale } from 'astro-nanointl/utils'
export const getStaticPaths = (() => { const { locales } = useLocales()
return locales.map((locale) => ({ params: { locale } })) }) satisfies GetStaticPaths
type Params = InferGetStaticParamsType
const { locale } = Astro.params as Params const { t } = useLocale(locale, 'greetings', { hello: 'Hello from static page!' })
{ t.hello }
Locale used: { locale }
```Middleware
You can also consider using middleware to extract the locale from url automagically and persist it to Astro.locals global:
// src/middleware.ts
import { sequence } from 'astro/middleware'
import { extractLocale } from 'astro-nanointl/middleware'
export const onRequest = sequence(extractLocale)Then you'll be able to use Astro.locals to retrieve the locale.
// src/pages/[locale]/index.astro -> using `output: 'server'`
import { useLocale } from 'astro-nanointl/utils'
// with middleware added
const { locale } = Astro.locals
const { t } = useLocale(locale, 'greetings', {
  hello: 'Hello everyone!'
})
---
<h1>{ t.hello }</h1>
<p>Locale used: { locale }</p>Keep in mind! You still need to check if Astro.locals.locale was defined using if statement and return appropriate Response.
Default locale
You might also seen that mostly we use https://example.com for some sort of default locale and https://example.com/fr for language other than our default one. This behaviour can be easily implemented. See the example.
Firstly, you can specify the default locale (by default the first one in locales array will be considered as defaultLocale):
// astro.config.ts
integrations: [
  nanointl({
    locales: ["en", "ru"],
    // by default "en" is considered as `defaultLocale`
    //  but we can override it
    defaultLocale: "ru",
    //  now "ru" is default one
  })
]Then, while in SSG you can get the defaultLocale and check if current locale is the same as default one and set the undefined for param accordingly. NB! Make sure you use rest parameter for locale.
// src/pages/[...locale]/index.astro -> using `output: 'static'` or `output: 'hybrid'`
import type { GetStaticPaths } from 'astro'
import { useLocales } from 'astro-nanointl/utils'
export const getStaticPaths = (() => {
  const { locales, defaultLocale } = useLocales()
  return locales.map((locale) => ({
    params: {
      locale: locale === defaultLocale ? undefined : locale
    }
  }))
}) satisfies GetStaticPathsAll other steps are completely the same because you can safely pass undefined as a locale for useLocale composable (see API. useLocale below). As a result it will return you the actual value of a defaultLocale back.
While in SSR, you don't have to change anything except adding rest parameter for locale. All other steps will be exactly the same as already mentioned.
Slugs
Keep in mind! You can also use useLocale composable in getStaticPaths function which will let you add translated slugs to generated routes in SSG. For SSR, you can use regular if statement to check if slug matches the translation and then decide what to return: the actual page or 404 page.
Parameterization, pluralization and more
Out of the box you can use following transformers:
params- lets you add parameters to your translation, like so:
greet: params<{ name: string }>('Hello {name}!')
// then use
t.greet({ name: 'John' }) // prints `Hello, John!`count- lets you quickly introduce pluralization, like this:
stars: count({
  one: 'a star',
  many: '{count} stars'
})
// then use
t.stars(1) // prints `a star`
t.stars(2) // prints `2 stars`args- lets you specify arguments for translation, like:
candies: args<[string, string]>('The first candy name is %1, while second one is %2')
// then use
t.candies('Snickers', 'Bounty') // prints `The first candy name is Snickers, while second one is Bounty`Keep in mind! The translation in files should represent the input transformer string, meaning:
// with this base translation
{
  greet: params<{ name: string }>('Hello {name}!'),
  stars: count({
    one: 'a star',
    many: '{count} stars'
  }),
  candies: args<[string, string]>('The first candy name is %1, while second one is %2')
}// ru.json
// json translation should look like this
{
  "componentName": {
    "greet": "Привет, {name}!",
    "stars": {
      "one": "звезда",
      "few": "{count} звезды",
      "many": "{count} звёзд"
    },
    "candies": "Название первой конфеты - %1, в то время как вторая называется %2"
  }
}For more advanced use cases you can create your own transformer using @nanostores/i18n docs and by importing transform and strings helper functions from astro-nanointl/utils.
Ideas
- You can use Content Collections and this example to add localized content to your pages.
 -  You can add custom middleware to add the 
Content-Languageresponse header. This will allow
So the implementation will look something like that (considering that we are usingto specify the page's intended audience and can indicate that this is more than one language. (MDN)
extractLocalemiddleware):export const onRequest = defineMiddleware(async ({ locals }, next) => { const response = await next() response.headers.append('Content-Language', locals.locale) return response })
 -  Use translated slugs that will boost your SEO. You can use them in both SSG and SSR. It is simple as calling 
useLocale. Read this. -  Add 
langattribute to youhtmlelement in layout. This can be accomplished by either usingextractLocalemiddleware or passinglocaleas a prop to layout. -  Pass all available translation using 
lfunction to your client-side (UI Frameworks) components as prop and dynamically change them on the fly. This can be especially useful inoutput: 'static'mode when you need to localize some static page on the client like 404 page.const { l } = useLocales() const translations = l('greetings', { hello: 'Hello' }) console.log(translations.en.hello) // `Hello` console.log(translations.ru.hello) // `Привет`
 -  Keep the track of current locale and change it on the client using 
localStorage. The following script will let you to quickly persist the locale on the client and use it here and thereafter. Advice! You can also usenanostores/persistentto add some layer on the top of vanillalocalStorage. More advice! By adding this script to some layout will help you to update your locale on the client in every page that uses the layout.<script> const locale = document.documentElement.lang // vanilla version window.localStorage.setItem('locale', locale) // or nanostores import { $locale } from '~/stores/locale' $locale.set(locale) </script>
 -  Don't forget to add 
hreflangto your<a>links. -  Even thought it is not recommended (worse UX) but you can have different pages for different locales. It is quite simple because you can use conditional syntax to make Astro page templates differ from each other. See the example:
{ locale === 'ru' ? <p>Some different layout { t.here }</p> : <h1>Totally diferrent layout { t.here }</h1> }
 -  Use 
fobject and its functions to format time, dates, numbers, currencies and more. It's built on the top ofIntland provides easy to use API:const { f } = useLocale('en', ...) console.log(f.time(-2, 'days')) // prints `2 days ago`
 - Have some troubles? Don't hesitate to add the issue and ask about 😉
 
API
Params status table:
| Emoji | Status | 
|---|---|
| 👋 | Required | 
| 👌 | Optional | 
nanoIntlIntegration
Adds the virtual:nanointl module containing user defined props.
Package: astro-nanointl
Params:
- 👋 
locales- array of available locales, the order matters - 👌 
defaultLocale- locale from thelocalesarray, which should be considered the default- default - first element of 
localesarray 
 - default - first element of 
 
Returns:
AstroIntegration
useLocales
Returns the list of all available locales and defaultLocale.
Package: astro-nanointl/utils
Params: none
Returns:
- 👋 
locales- non empty array of all available locales, the order can be not the same as in integration setup - 👋 
defaultLocale- default locale - 👋 
l-lfunction, that returns the list of all translations in format{ '[locale]': { key: 'translation' } } 
useLocale
Return the locale and t function which is used for translation.
Package: astro-nanointl/utils
Params:
- 👋 
locale- locale to be used for translation- if 
undefined, then treated asdefaultLocale 
 - if 
 - 👋 
componentName- name of translations workspace- According to the following style:
 
{ "componentName": { "key": "value" } }
Represents the
componentName. - 👋 
baseTranslation- the transaltion schema and translation object fordefaultLocale- According to the following style:
 
{ "componentName": { "key": "value" } }
Represents the object
{ "key": "value" }.- In code must be declared in locale that is specified as default, i.e. if 
defaultLocaleis"ru"then 
useLocale(undefined, 'componentName', { "key": "Значение" })
must be used.
- Must not be deeply nested object.
 - Supports 
nanostores/i18ntransformers to implement pluralization, parameters and more. 
 
Returns:
- 👌 
locale- defined locale orundefinedin case if locale does not exist inlocalesarray. - 👌 
t- object containing translated values for specified locale orundefinedin case if translation does not exist. Ifundefinedmake sure you added[expected_language].jsonfile to yourlocalesdirectory. - 👌 
f- object containing function for formatting time, dates, numbers, currencies and more. Uses Intl API under the hood. 
extractLocale middleware
Extracts the current locale from Astro.params using locale or lang properties. Otherwise sets undefined to Astro.locals locale property.
Package: astro-nanointl/middleware
Params:
Returns:
Response: either directly, or by callingnext().
See the usage above.
MIT License © 2023 e3stpavel