JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 41
  • Score
    100M100P100Q78819F
  • License MIT

Tiny set of tools to implement internationalization for Astro

Package Exports

  • astro-nanointl
  • astro-nanointl/client
  • astro-nanointl/middleware
  • astro-nanointl/package.json
  • astro-nanointl/utils

Readme

Mask Group 9

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-nanointl

Or you can go ahead and give astro add a try:

pnpm astro add astro-nanointl

Plug

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 GetStaticPaths

All 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-Language response header. This will allow

    to specify the page's intended audience and can indicate that this is more than one language. (MDN)

    So the implementation will look something like that (considering that we are using extractLocale middleware):
    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 lang attribute to you html element in layout. This can be accomplished by either using extractLocale middleware or passing locale as a prop to layout.
  • Pass all available translation using l function to your client-side (UI Frameworks) components as prop and dynamically change them on the fly. This can be especially useful in output: '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 use nanostores/persistent to add some layer on the top of vanilla localStorage. 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 hreflang to 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 f object and its functions to format time, dates, numbers, currencies and more. It's built on the top of Intl and 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 the locales array, which should be considered the default
    • default - first element of locales array

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 - l function, 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 as defaultLocale
  • 👋 componentName - name of translations workspace
    • According to the following style:
    {
      "componentName": {
        "key": "value"
      }
    }

    Represents the componentName.

  • 👋 baseTranslation - the transaltion schema and translation object for defaultLocale
    • 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 defaultLocale is "ru" then
    useLocale(undefined, 'componentName', {
      "key": "Значение"
    })

    must be used.

Returns:

  • 👌 locale - defined locale or undefined in case if locale does not exist in locales array.
  • 👌 t - object containing translated values for specified locale or undefined in case if translation does not exist. If undefined make sure you added [expected_language].json file to your locales directory.
  • 👌 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 calling next().

See the usage above.


MIT License © 2023 e3stpavel