@i18n-tiny/astro
v1.1.1
Published
Tiny, type-safe i18n library for Astro with automatic type inference and zero dependencies
Maintainers
Readme
@i18n-tiny/astro
A zero-dependency, type-safe, minimal internationalization library for Astro.
Works with both SSG and SSR. Configuration is completed by a single define function, with full auto-completion for key paths like messages.common.title.
Locale handling in getStaticPaths requires only minimal setup, avoiding repetitive per-page definitions.
SSR: Use middleware.create() for routing with automatic language detection.
SSG: Use integration.create() to copy default locale content to root at build time.
Features
- Type-safe: Full TypeScript support with automatic type inference - autocomplete for
messages.site.name,t('common.title'), and all nested keys - Zero dependencies: No external i18n libraries needed
- Server-first: Native Astro server-side rendering support
- Minimal SSG: Works seamlessly with Astro's static site generation
- Simple API: Single configuration, minimal boilerplate
- Small: Minimal bundle size
- Flexible: Use with Astro's built-in i18n or standalone middleware
Installation
npm install @i18n-tiny/astro
# or
pnpm add @i18n-tiny/astro
# or
yarn add @i18n-tiny/astroUsage
Project Structure
src/
├── pages/
│ └── [locale]/
│ ├── index.astro
│ └── about.astro
├── messages/
│ ├── en.ts
│ └── ja.ts
├── i18n.ts
└── middleware.tsMinimal Setup
Place this file anywhere in your project.
1. Create message files
// src/messages/en.ts
export default {
common: {
title: "My Site",
description: "Welcome to my site"
},
nav: {
home: "Home",
about: "About"
}
}// src/messages/ja.ts
export default {
common: {
title: "マイサイト",
description: "サイトへようこそ"
},
nav: {
home: "ホーム",
about: "概要"
}
}2. Define i18n instance
Place this file anywhere in your project (i18n.ts, lib/i18n/index.ts, etc.)
// src/i18n.ts
import { define } from '@i18n-tiny/astro'
import enMessages from './messages/en'
import jaMessages from './messages/ja'
// IMPORTANT: `as const` is required for type inference
export const locales = ['en', 'ja'] as const
export type Locale = (typeof locales)[number]
export const defaultLocale: Locale = 'en'
export const { getMessages, getTranslations } = define({
locales,
defaultLocale,
messages: { en: enMessages, ja: jaMessages }
})3. Setup middleware
// src/middleware.ts
import { defineMiddleware } from 'astro/middleware'
import { create } from '@i18n-tiny/astro/middleware'
import { locales, defaultLocale } from './i18n'
const middleware = create({
locales,
defaultLocale
})
export const onRequest = defineMiddleware(middleware)4. Use in pages
---
// src/pages/[locale]/index.astro
import { getMessages, getTranslations } from '../../i18n'
const { locale } = Astro.params
const messages = getMessages(locale)
const t = getTranslations(locale)
---
<html lang={locale}>
<head>
<title>{messages.common.title}</title>
{/* ^^^^^ Auto-complete */}
</head>
<body>
<h1>{messages.common.title}</h1>
<p>{t('common.description')}</p>
{/* ^^^^^^^^^^^^^^^^^^ Auto-complete */}
</body>
</html>That's it! Types are automatically inferred - no manual type annotations needed.
API Reference
@i18n-tiny/astro
define(config)
Defines an i18n instance with automatic type inference.
Parameters:
| Parameter | Type | Description |
| --------------- | -------------------------- | ------------------------------------------------------------- |
| locales | readonly string[] | Array of supported locales (optional, inferred from messages) |
| defaultLocale | string | Default locale (optional, uses first locale) |
| messages | Record<Locale, Messages> | Messages object keyed by locale |
Returns:
{
locales: readonly string[]
defaultLocale: string
getMessages: (locale: string | undefined) => Messages
getTranslations: (locale: string | undefined, namespace?: string) => TranslationFunction
}DefineConfig (type)
Type for the configuration object passed to define().
@i18n-tiny/astro/middleware (SSR only)
create(config)
Creates an Astro middleware handler for i18n routing.
Note: Middleware only works in SSR mode. For SSG, use the integration instead.
Parameters:
| Parameter | Type | Default | Description |
| ---------------- | ------------------- | --------------- | ----------------------------------------------------------------------- |
| locales | readonly string[] | - | Array of supported locales |
| defaultLocale | string | - | Default locale for redirects |
| fallbackLocale | string | defaultLocale | Fallback when detection fails |
| excludePaths | string[] | [] | Paths to exclude from i18n handling |
| prefixDefault | boolean | false | Whether to prefix default locale in URLs |
| detectLanguage | boolean | true | Whether to detect from Accept-Language |
| routing | 'rewrite' | - | SSR rewrite mode (mutually exclusive with prefixDefault/detectLanguage) |
Routing Behavior Matrix:
| prefixDefault | detectLanguage | / behavior |
| ------------- | -------------- | ------------------------------------------------ |
| false | false | Serves fallbackLocale, no detection |
| false | true | Detects, redirects non-default, rewrites default |
| true | false | Redirects to /[defaultLocale] |
| true | true | Detects and redirects to detected locale |
Examples:
// Default: detect language, redirect non-default, rewrite default
export const onRequest = defineMiddleware(
create({
locales: ['en', 'ja'],
defaultLocale: 'en'
})
)
// Always prefix all locales (including default)
export const onRequest = defineMiddleware(
create({
locales: ['en', 'ja'],
defaultLocale: 'en',
prefixDefault: true
})
)
// No detection, always use fallback
export const onRequest = defineMiddleware(
create({
locales: ['en', 'ja'],
defaultLocale: 'en',
detectLanguage: false
})
)
// SSR rewrite mode (locale in Astro.locals)
export const onRequest = defineMiddleware(
create({
locales: ['en', 'ja'],
defaultLocale: 'en',
routing: 'rewrite'
})
)SSR Rewrite Mode:
When using routing: 'rewrite', the locale is stored in Astro.locals.locale:
---
// src/pages/index.astro (no [locale] folder needed)
import { getMessages } from '../i18n'
const locale = Astro.locals.locale // 'en' or 'ja'
const messages = getMessages(locale)
---
<html lang={locale}>
<body>
<h1>{messages.common.title}</h1>
</body>
</html>MiddlewareConfig (type)
Type for the configuration object passed to create().
@i18n-tiny/astro/integration (SSG only)
create(config)
Creates an Astro integration for i18n static file generation.
Note: This integration is for SSG mode only. It copies the default locale's content to the root directory after build, enabling
/to serve the default locale without a prefix.
Parameters:
| Parameter | Type | Default | Description |
| --------------- | --------- | ------- | -------------------------------------------------------- |
| defaultLocale | string | - | Default locale - content from this locale is copied to root |
| prefixDefault | boolean | false | If true, skips copying (all locales remain prefixed) |
Example:
// astro.config.mjs
import { defineConfig } from 'astro/config'
import { create } from '@i18n-tiny/astro/integration'
export default defineConfig({
integrations: [
create({
defaultLocale: 'en'
})
]
})Build Output:
dist/
├── index.html ← copied from /en/index.html
├── about/index.html ← copied from /en/about/index.html
├── en/
│ ├── index.html
│ └── about/index.html
└── ja/
├── index.html
└── about/index.htmlImportant: In SSG mode, automatic language detection (
detectLanguage) does not work because there is no server to read theAccept-Languageheader. Users will always see the default locale first when visiting/.
IntegrationConfig (type)
Type for the configuration object passed to create().
@i18n-tiny/astro/router
Link Component
Localized Link component that auto-detects locale from current URL.
---
import Link from '@i18n-tiny/astro/router/Link.astro'
---
<!-- Auto-localized (maintains current URL pattern) -->
<Link href="/about">About</Link>
<!-- Explicit locale override -->
<Link href="/" locale="ja">日本語</Link>
<!-- Raw path (no localization) -->
<Link href="/" locale="">English</Link>@i18n-tiny/core
detectLocale(acceptLanguage, supportedLocales)
Detects the best matching locale from the Accept-Language header.
import { detectLocale } from '@i18n-tiny/core/middleware'
const acceptLanguage = request.headers.get('accept-language')
const locale = detectLocale(acceptLanguage, ['en', 'ja'])
// Returns: 'en' | 'ja' | null@i18n-tiny/core/router
getLocalizedPath(path, locale, defaultLocale, prefixDefault?)
Generate a localized path with locale prefix.
import { getLocalizedPath } from '@i18n-tiny/core/router'
getLocalizedPath('/about', 'ja', 'en') // '/ja/about'
getLocalizedPath('/about', 'en', 'en') // '/about'
getLocalizedPath('/about', 'en', 'en', true) // '/en/about'removeLocalePrefix(pathname, locales)
Remove locale prefix from pathname.
import { removeLocalePrefix } from '@i18n-tiny/core/router'
removeLocalePrefix('/ja/about', ['en', 'ja']) // '/about'
removeLocalePrefix('/ja', ['en', 'ja']) // '/'
removeLocalePrefix('/about', ['en', 'ja']) // '/about'Advanced Usage
Static Site Generation (SSG)
For static sites, use getStaticPaths and the integration:
1. Add the integration to astro.config.mjs:
import { defineConfig } from 'astro/config'
import { create } from '@i18n-tiny/astro/integration'
export default defineConfig({
integrations: [
create({ defaultLocale: 'en' })
]
})2. Use getStaticPaths in your pages:
---
// src/pages/[locale]/index.astro
import { locales, getMessages } from '../../i18n'
export function getStaticPaths() {
return locales.map((locale) => ({
params: { locale }
}))
}
const { locale } = Astro.params
const messages = getMessages(locale)
---
<html lang={locale}>
<body>
<h1>{messages.common.title}</h1>
</body>
</html>Note: The integration copies
/en/*to/*after build, so users can access the site at/without a locale prefix. Middleware is not needed for SSG.
Language Switcher
---
// src/components/LanguageSwitcher.astro
import Link from '@i18n-tiny/astro/router/Link.astro'
import { locales } from '../i18n'
const locale = Astro.params.locale ?? Astro.locals.locale
const localeNames: Record<string, string> = {
en: 'English',
ja: '日本語'
}
---
<nav>
{locales.map((loc) => (
<Link
href="/"
locale={loc}
style={loc === locale ? 'font-weight: bold;' : ''}
>
{localeNames[loc]}
</Link>
))}
</nav>With Astro's Built-in i18n
You can also use Astro's built-in i18n routing with @i18n-tiny/astro for translations only:
// astro.config.mjs
import { defineConfig } from 'astro/config'
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'ja'],
routing: {
prefixDefaultLocale: false
}
}
})---
// src/pages/[locale]/index.astro
import { getMessages } from '../../i18n'
// Astro.currentLocale is available in Astro 4.0+
const locale = Astro.currentLocale
const messages = getMessages(locale)
---
<html lang={locale}>
<body>
<h1>{messages.common.title}</h1>
</body>
</html>TypeScript
Astro.locals Type Safety
For type-safe access to Astro.locals.locale, add the type reference to your project:
// src/env.d.ts
/// <reference types="astro/client" />
/// <reference types="@i18n-tiny/astro/locals" />This provides types for:
Astro.locals.locale- Current localeAstro.locals.locales- Supported locales arrayAstro.locals.originalPathname- Original path (rewrite mode)
License
MIT
