├── .changeset
├── README.md
└── config.json
├── .github
├── FUNDING.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── biome.json
├── demo
├── .gitignore
├── .vscode
│ ├── extensions.json
│ └── launch.json
├── README.md
├── astro.config.ts
├── package.json
├── public
│ ├── astrolicious.png
│ └── favicon.svg
├── src
│ ├── components
│ │ ├── Footer.astro
│ │ ├── Header.astro
│ │ ├── HeroMessage.astro
│ │ └── LocaleSwitcher.astro
│ ├── content
│ │ ├── config.ts
│ │ └── posts
│ │ │ ├── en
│ │ │ └── hello-world.md
│ │ │ ├── fr
│ │ │ └── bonjour-le-monde.md
│ │ │ └── it
│ │ │ └── ciao-mondo.md
│ ├── env.d.ts
│ ├── layouts
│ │ └── Layout.astro
│ ├── locales
│ │ ├── en
│ │ │ └── common.json
│ │ ├── fr
│ │ │ └── common.json
│ │ └── it
│ │ │ └── common.json
│ ├── pages
│ │ ├── 404.astro
│ │ ├── api
│ │ │ └── hello.ts
│ │ ├── fr
│ │ │ └── test.astro
│ │ ├── it
│ │ │ └── test.astro
│ │ └── test.astro
│ └── routes
│ │ ├── about.astro
│ │ ├── blog.astro
│ │ ├── blog
│ │ └── [slug].astro
│ │ └── index.astro
├── tailwind.config.mjs
└── tsconfig.json
├── docs
├── .gitignore
├── .vscode
│ ├── extensions.json
│ └── launch.json
├── README.md
├── astro.config.ts
├── package.json
├── public
│ └── favicon.svg
├── src
│ ├── assets
│ │ ├── logo-dark.svg
│ │ └── logo-light.svg
│ ├── content
│ │ ├── config.ts
│ │ └── docs
│ │ │ ├── demo.mdx
│ │ │ ├── getting-started
│ │ │ ├── installation.mdx
│ │ │ ├── known-issues.mdx
│ │ │ ├── showcase.mdx
│ │ │ └── usage.mdx
│ │ │ ├── index.mdx
│ │ │ ├── recipes
│ │ │ ├── framework-components.mdx
│ │ │ ├── get-static-paths.mdx
│ │ │ ├── sitemap.mdx
│ │ │ └── translated-404-pages.mdx
│ │ │ ├── reference
│ │ │ ├── components
│ │ │ │ ├── i18n-client.mdx
│ │ │ │ └── i18n-head.mdx
│ │ │ ├── content-collections
│ │ │ │ ├── collection-filters.mdx
│ │ │ │ └── handle-i18n-slug.mdx
│ │ │ ├── types.mdx
│ │ │ └── utilities
│ │ │ │ ├── get-default-locale-placeholder.mdx
│ │ │ │ ├── get-default-locale.mdx
│ │ │ │ ├── get-html-attrs.mdx
│ │ │ │ ├── get-locale-path.mdx
│ │ │ │ ├── get-locale-placeholder.mdx
│ │ │ │ ├── get-locale.mdx
│ │ │ │ ├── get-locales-placeholder.mdx
│ │ │ │ ├── get-locales.mdx
│ │ │ │ ├── get-switcher-data.mdx
│ │ │ │ ├── set-dynamic-params.mdx
│ │ │ │ ├── switch-locale-path.mdx
│ │ │ │ └── t.mdx
│ │ │ └── usage
│ │ │ ├── client.mdx
│ │ │ ├── configuration.mdx
│ │ │ └── translations.mdx
│ ├── env.d.ts
│ └── style.css
├── tailwind.config.mjs
└── tsconfig.json
├── package.json
├── package
├── CHANGELOG.md
├── README.md
├── assets
│ ├── components
│ │ ├── I18nClient.astro
│ │ └── I18nHead.astro
│ ├── middleware.ts
│ └── stubs
│ │ ├── sitemap.d.ts
│ │ ├── virtual.d.ts
│ │ └── virtual.mjs
├── env.d.ts
├── package.json
├── src
│ ├── content-collections.ts
│ ├── i18next
│ │ ├── index.ts
│ │ ├── namespaces.ts
│ │ └── resources.ts
│ ├── index.ts
│ ├── integration.ts
│ ├── internal.ts
│ ├── options.ts
│ ├── routing
│ │ ├── hmr.ts
│ │ ├── index.ts
│ │ └── register.ts
│ ├── sitemap
│ │ ├── generate-sitemap.ts
│ │ ├── integration.ts
│ │ ├── options.ts
│ │ ├── route-config.ts
│ │ └── utils.ts
│ └── types.ts
├── tsconfig.json
└── tsup.config.ts
├── playground
├── .gitignore
├── .vscode
│ ├── extensions.json
│ └── launch.json
├── astro.config.mts
├── package.json
├── public
│ └── favicon.svg
├── src
│ ├── components
│ │ ├── Counter.tsx
│ │ └── LocaleSwitcher.astro
│ ├── env.d.ts
│ ├── layouts
│ │ └── Layout.astro
│ ├── locales
│ │ ├── en
│ │ │ ├── common.json
│ │ │ └── home.json
│ │ └── fr
│ │ │ ├── common.json
│ │ │ └── home.json
│ ├── pages
│ │ └── test.astro
│ └── routes
│ │ ├── TEST-UPPER.astro
│ │ ├── about.astro
│ │ ├── blog.astro
│ │ ├── blog
│ │ └── [slug].astro
│ │ ├── data.json.ts
│ │ ├── index.astro
│ │ ├── user.astro
│ │ └── user
│ │ └── [slug].astro
├── tailwind.config.mjs
└── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── scripts
└── release.mjs
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": ["playground", "docs", "demo"]
11 | }
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: florian-lefebvre
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v3
11 |
12 | - name: Setup PNPM
13 | run: corepack enable && pnpm -v
14 |
15 | - name: Setup Node
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: 18.19.0
19 | cache: pnpm
20 |
21 | - name: Install dependencies
22 | run: pnpm install --frozen-lockfile
23 |
24 | - name: Build
25 | run: pnpm --filter @astrolicious/i18n build
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "[mdx]": {
4 | "editor.defaultFormatter": "unifiedjs.vscode-mdx"
5 | },
6 | "[typescript]": {
7 | "editor.defaultFormatter": "biomejs.biome"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Florian Lefebvre
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > This integration is unmaintained due to lack of time. It should mostly work but do not expect fixes or new features.
3 |
4 | # @astrolicious/i18n
5 |
6 | Yet another i18n integration for [Astro](https://astro.build/) with server and client utilities, type safety and translations built-in.
7 |
8 | To see how to get started, check out the [package README](./package/README.md)
9 |
10 | ## Licensing
11 |
12 | [MIT Licensed](./LICENSE). Made with ❤️ by [Florian Lefebvre](https://github.com/florian-lefebvre).
13 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.5.2/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "linter": {
7 | "enabled": true,
8 | "rules": {
9 | "recommended": true,
10 | "suspicious": {
11 | "noExplicitAny": "warn"
12 | }
13 | }
14 | },
15 | "files": {
16 | "ignore": ["dist", ".astro", ".netlify"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
23 | .netlify
--------------------------------------------------------------------------------
/demo/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/demo/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | ## I18n for Astro Demo
2 |
3 |
4 | Welcome to the I18n for Astro demo site! This project showcases the capabilities of the I18n for Astro integration, providing a glimpse into the seamless multilingual experience you can create for your website.
5 |
6 |
7 | Please note that this is a demo site meant to highlight the features and possibilities of I18n for Astro. For more information on how to use I18n for Astro in your own projects, please refer to the main repository and documentation.
--------------------------------------------------------------------------------
/demo/astro.config.ts:
--------------------------------------------------------------------------------
1 | import react from "@astrojs/react";
2 | import tailwind from "@astrojs/tailwind";
3 | import i18n from "@astrolicious/i18n";
4 | import { defineConfig } from "astro/config";
5 |
6 | import netlify from "@astrojs/netlify";
7 |
8 | // https://astro.build/config
9 | export default defineConfig({
10 | site: "https://astro-i18n-demo.netlify.app",
11 | integrations: [
12 | i18n({
13 | defaultLocale: "en",
14 | locales: ["en", "fr", "it"],
15 | pages: {
16 | "/about": {
17 | fr: "/a-propos",
18 | it: "/chi-siamo",
19 | },
20 | "/blog": {
21 | fr: "/le-blog",
22 | it: "/blog",
23 | },
24 | "/blog/[slug]": {
25 | fr: "/le-blog/[slug]",
26 | },
27 | },
28 | client: {
29 | data: true,
30 | paths: true,
31 | },
32 | sitemap: true,
33 | }),
34 | tailwind(),
35 | react(),
36 | ],
37 | output: "hybrid",
38 | adapter: netlify({
39 | imageCDN: false,
40 | }),
41 | });
42 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "astro dev",
9 | "build": "astro check && astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/check": "^0.5.10",
15 | "@astrojs/netlify": "^5.2.0",
16 | "@astrojs/react": "^3.3.1",
17 | "@astrojs/tailwind": "^5.1.0",
18 | "@astrolicious/i18n": "workspace:*",
19 | "@tailwindcss/forms": "^0.5.7",
20 | "@types/react": "^18.3.1",
21 | "@types/react-dom": "^18.3.0",
22 | "astro": "^4.14.0",
23 | "i18next": "^23.11.3",
24 | "react": "^18.3.1",
25 | "react-dom": "^18.3.1",
26 | "tailwindcss": "^3.4.3",
27 | "typescript": "^5.4.5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/public/astrolicious.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/astrolicious/i18n/9048f5dd7e9ef8f320f2aa2e0af46da2a2f1ba78/demo/public/astrolicious.png
--------------------------------------------------------------------------------
/demo/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/src/components/Footer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | ---
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/src/components/Header.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocalePath, t } from "i18n:astro";
3 | import LocaleSwitcher from "./LocaleSwitcher.astro";
4 | ---
5 |
19 |
--------------------------------------------------------------------------------
/demo/src/components/HeroMessage.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 |
4 | const { message } = Astro.props;
5 | ---
6 |
7 |
{t(message)}
8 |
9 |
--------------------------------------------------------------------------------
/demo/src/components/LocaleSwitcher.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocale, getSwitcherData } from "i18n:astro";
3 |
4 | const locale = getLocale();
5 | const data = getSwitcherData();
6 | ---
7 |
8 |
9 | {
10 | data.map((e) => (
11 |
12 | {e.locale}
13 |
14 | ))
15 | }
16 |
--------------------------------------------------------------------------------
/demo/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, reference, z } from "astro:content";
2 |
3 | export const collections = {
4 | posts: defineCollection({
5 | type: "content",
6 | schema: z.object({
7 | title: z.string(),
8 | description: z.string(),
9 | author: z.string(),
10 | defaultLocaleVersion: reference("posts").optional(),
11 | }),
12 | }),
13 | };
14 |
--------------------------------------------------------------------------------
/demo/src/content/posts/en/hello-world.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Importance of Internationalization (i18n) in Software Development
3 | description: Discover why i18n is crucial for creating software that reaches a global audience and provides an inclusive user experience.
4 | author: Claude 3 Opus
5 | ---
6 | Internationalization, or i18n, is a critical aspect of software development that enables applications to adapt to different languages and cultural conventions. By implementing i18n best practices, developers can create software that reaches a wider global audience, improves user experience, and demonstrates a commitment to inclusivity. Embracing i18n not only helps businesses expand their market reach but also fosters a sense of belonging among users from diverse backgrounds.
7 |
--------------------------------------------------------------------------------
/demo/src/content/posts/fr/bonjour-le-monde.md:
--------------------------------------------------------------------------------
1 | ---
2 | defaultLocaleVersion: en/hello-world
3 | title: L'importance de l'internationalisation (i18n) dans le développement de logiciels
4 | description: Découvrez pourquoi l'i18n est essentielle pour créer des logiciels qui atteignent un public mondial et offrent une expérience utilisateur inclusive.
5 | author: Claude 3 Opus
6 | ---
7 |
8 | L'internationalisation, ou i18n, est un aspect essentiel du développement de logiciels qui permet aux applications de s'adapter à différentes langues et conventions culturelles. En mettant en œuvre les meilleures pratiques d'i18n, les développeurs peuvent créer des logiciels qui atteignent un public mondial plus large, améliorent l'expérience utilisateur et démontrent un engagement envers l'inclusivité. L'adoption de l'i18n aide non seulement les entreprises à élargir leur portée sur le marché, mais favorise également un sentiment d'appartenance parmi les utilisateurs de divers horizons.
9 |
--------------------------------------------------------------------------------
/demo/src/content/posts/it/ciao-mondo.md:
--------------------------------------------------------------------------------
1 | ---
2 | defaultLocaleVersion: en/hello-world
3 | title: L'importanza dell'internazionalizzazione (i18n) nello sviluppo del software
4 | description: Scopri perché l'i18n è fondamentale per creare software che raggiungano un pubblico globale e forniscano un'esperienza utente inclusiva.
5 | author: Claude 3 Opus
6 | ---
7 |
8 | L'internazionalizzazione, o i18n, è un aspetto critico dello sviluppo del software che consente alle applicazioni di adattarsi a diverse lingue e convenzioni culturali. Implementando le migliori pratiche di i18n, gli sviluppatori possono creare software che raggiungono un pubblico globale più ampio, migliorano l'esperienza utente e dimostrano un impegno verso l'inclusività. Abbracciare l'i18n non solo aiuta le aziende ad espandere la loro portata di mercato, ma promuove anche un senso di appartenenza tra gli utenti di background diversi.
--------------------------------------------------------------------------------
/demo/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
--------------------------------------------------------------------------------
/demo/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { ViewTransitions } from "astro:transitions";
3 | import { getHtmlAttrs } from "i18n:astro";
4 | import I18NClient from "@astrolicious/i18n/components/I18nClient.astro";
5 | import I18NHead from "@astrolicious/i18n/components/I18nHead.astro";
6 | import Footer from "../components/Footer.astro";
7 | import Header from "../components/Header.astro";
8 |
9 | interface Props {
10 | title: string;
11 | }
12 |
13 | const { title } = Astro.props;
14 | ---
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {title}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/demo/src/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "hero": "Welcome to I18n for Astro Demo",
4 | "footer": "Made with ❤️ by astrolicious",
5 | "about": "About",
6 | "aboutMessage": "This is a simple page",
7 | "opensource": "Did you know that this page is available on GitHub? No? Well, now you know!",
8 | "opensourceCTA": "Check it out!",
9 | "blog": "Blog"
10 | }
11 |
--------------------------------------------------------------------------------
/demo/src/locales/fr/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Accueil",
3 | "hero": "Bienvenue sur la démo d'I18n pour Astro",
4 | "footer": "Créé avec ❤️ par astrolicious",
5 | "about": "A propos",
6 | "aboutMessage": "Ceci est une page simple",
7 | "opensource": "Saviez-vous que cette page est disponible sur GitHub ? Non ? Eh bien, maintenant vous le savez !",
8 | "opensourceCTA": "Allez y jeter un œil !",
9 | "blog": "Le blog"
10 | }
11 |
--------------------------------------------------------------------------------
/demo/src/locales/it/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "hero": "Benvenuti alla demo di I18n per Astro",
4 | "footer": "Fatto con il ❤️ da astrolicious",
5 | "about": "Chi siamo",
6 | "aboutMessage": "Questa è una pagina semplice",
7 | "opensource": "Lo sapevi che questa pagina è disponibile su GitHub? No? Beh, ora lo sai!",
8 | "opensourceCTA": "Vai a dare un'occhiata!",
9 | "blog": "Blog"
10 | }
11 |
--------------------------------------------------------------------------------
/demo/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocale } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const locale = getLocale();
6 | const title = "Page not found";
7 |
8 | export const prerender = false;
9 | ---
10 |
11 |
12 | {title}
13 | Locale: {locale}
14 |
15 |
--------------------------------------------------------------------------------
/demo/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | import type { APIRoute } from "astro";
2 |
3 | export const GET: APIRoute = () => {
4 | return new Response("Hello world");
5 | };
6 |
--------------------------------------------------------------------------------
/demo/src/pages/fr/test.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = `Test ${t("about")}`;
6 | ---
7 |
8 |
9 | {title}
10 |
11 |
--------------------------------------------------------------------------------
/demo/src/pages/it/test.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = `Test ${t("about")}`;
6 | ---
7 |
8 |
9 | {title}
10 |
11 |
--------------------------------------------------------------------------------
/demo/src/pages/test.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = `Test ${t("about")}`;
6 | ---
7 |
8 |
9 | {title}
10 |
11 |
--------------------------------------------------------------------------------
/demo/src/routes/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import HeroMessage from "~/components/HeroMessage.astro";
4 | import Layout from "~/layouts/Layout.astro";
5 |
6 | const title = t("about");
7 | ---
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/src/routes/blog.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import { getLocale, getLocalePath, t } from "i18n:astro";
4 | import Layout from "~/layouts/Layout.astro";
5 |
6 | const title = t("blog");
7 | const posts = await getCollection(
8 | "posts",
9 | ({ slug }) => slug.split("/")[0] === getLocale(),
10 | );
11 | ---
12 |
13 |
14 | {title}
15 |
27 |
28 |
--------------------------------------------------------------------------------
/demo/src/routes/blog/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import {
4 | getDefaultLocalePlaceholder,
5 | getLocalePlaceholder,
6 | setDynamicParams,
7 | t,
8 | } from "i18n:astro";
9 | import sitemap from "i18n:astro/sitemap";
10 | import {
11 | collectionFilters,
12 | handleI18nSlug,
13 | } from "@astrolicious/i18n/content-collections";
14 | import type { GetStaticPaths } from "astro";
15 | import Layout from "~/layouts/Layout.astro";
16 |
17 | export const getStaticPaths = (async () => {
18 | const locale = getLocalePlaceholder();
19 | const defaultLocale = getDefaultLocalePlaceholder();
20 |
21 | const posts = await getCollection("posts", (post) =>
22 | collectionFilters.byLocale(post, { locale }),
23 | );
24 |
25 | return await Promise.all(
26 | posts.map(async (post) => {
27 | const equivalentPosts = await getCollection("posts", (p) =>
28 | collectionFilters.matchingEntries(p, {
29 | currentEntry: post,
30 | key: "defaultLocaleVersion",
31 | locale,
32 | defaultLocale,
33 | }),
34 | );
35 |
36 | const dynamicParams = equivalentPosts.map((entry) => {
37 | const { locale, slug } = handleI18nSlug(entry.slug);
38 |
39 | return {
40 | locale,
41 | params: {
42 | slug,
43 | },
44 | };
45 | });
46 |
47 | sitemap({
48 | dynamicParams,
49 | });
50 |
51 | return {
52 | params: {
53 | slug: handleI18nSlug(post.slug).slug,
54 | },
55 | props: {
56 | post,
57 | dynamicParams,
58 | },
59 | };
60 | }),
61 | );
62 | }) satisfies GetStaticPaths;
63 |
64 | const { post, dynamicParams } = Astro.props;
65 |
66 | setDynamicParams(dynamicParams);
67 |
68 | const title = `${t("blog")} - ${post.data.title}`;
69 | ---
70 |
71 |
72 | {title}
73 |
74 |
--------------------------------------------------------------------------------
/demo/src/routes/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import HeroMessage from "~/components/HeroMessage.astro";
4 | import Layout from "~/layouts/Layout.astro";
5 |
6 | const title = t("home");
7 | ---
8 |
9 |
10 |
11 |
12 |
13 | Sitemap
14 |
18 |
19 |
--------------------------------------------------------------------------------
/demo/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require("@tailwindcss/forms")],
8 | };
9 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"]
7 | },
8 | "jsx": "react-jsx",
9 | "jsxImportSource": "react"
10 | },
11 | "exclude": ["dist"]
12 | }
13 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/docs/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/docs/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Starlight Starter Kit: Basics
2 |
3 | [](https://starlight.astro.build)
4 |
5 | ```
6 | npm create astro@latest -- --template starlight
7 | ```
8 |
9 | [](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
10 | [](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
11 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
12 |
13 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro + Starlight project, you'll see the following folders and files:
18 |
19 | ```
20 | .
21 | ├── public/
22 | ├── src/
23 | │ ├── assets/
24 | │ ├── content/
25 | │ │ ├── docs/
26 | │ │ └── config.ts
27 | │ └── env.d.ts
28 | ├── astro.config.mjs
29 | ├── package.json
30 | └── tsconfig.json
31 | ```
32 |
33 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
34 |
35 | Images can be added to `src/assets/` and embedded in Markdown with a relative link.
36 |
37 | Static assets, like favicons, can be placed in the `public/` directory.
38 |
39 | ## 🧞 Commands
40 |
41 | All commands are run from the root of the project, from a terminal:
42 |
43 | | Command | Action |
44 | | :------------------------ | :----------------------------------------------- |
45 | | `npm install` | Installs dependencies |
46 | | `npm run dev` | Starts local dev server at `localhost:4321` |
47 | | `npm run build` | Build your production site to `./dist/` |
48 | | `npm run preview` | Preview your build locally, before deploying |
49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
50 | | `npm run astro -- --help` | Get help using the Astro CLI |
51 |
52 | ## 👀 Want to learn more?
53 |
54 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
55 |
--------------------------------------------------------------------------------
/docs/astro.config.ts:
--------------------------------------------------------------------------------
1 | import starlight from "@astrojs/starlight";
2 | import tailwind from "@astrojs/tailwind";
3 | import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
4 | import { defineConfig } from "astro/config";
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | site: "https://astro-i18n.netlify.app",
9 | integrations: [
10 | starlight({
11 | title: "I18n for Astro",
12 | logo: {
13 | light: "./src/assets/logo-dark.svg",
14 | dark: "./src/assets/logo-light.svg",
15 | },
16 | customCss: ["./src/style.css"],
17 | social: {
18 | github: "https://github.com/astrolicious/i18n",
19 | discord: "https://astro.build/chat",
20 | },
21 | head: [
22 | {
23 | tag: "link",
24 | attrs: {
25 | rel: "preconnect",
26 | href: "https://rsms.me/",
27 | },
28 | },
29 | {
30 | tag: "link",
31 | attrs: {
32 | rel: "stylesheet",
33 | href: "https://rsms.me/inter/inter.css",
34 | },
35 | },
36 | ],
37 | editLink: {
38 | baseUrl: "https://github.com/astrolicious/i18n/edit/main/docs/",
39 | },
40 | lastUpdated: true,
41 | expressiveCode: {
42 | themes: ["one-dark-pro"],
43 | plugins: [pluginLineNumbers()],
44 | defaultProps: {
45 | overridesByLang: {
46 | bash: {
47 | showLineNumbers: false,
48 | },
49 | },
50 | },
51 | },
52 | sidebar: [
53 | {
54 | label: "Home",
55 | link: "/",
56 | },
57 | {
58 | label: "Getting started",
59 | items: [
60 | { label: "Installation", link: "/getting-started/installation/" },
61 | { label: "Usage", link: "/getting-started/usage/" },
62 | { label: "Known issues", link: "/getting-started/known-issues/" },
63 | { label: "Showcase", link: "/getting-started/showcase/" },
64 | ],
65 | },
66 | {
67 | label: "Usage",
68 | items: [
69 | { label: "Configuration", link: "/usage/configuration/" },
70 | { label: "Translations", link: "/usage/translations/" },
71 | { label: "Client usage", link: "/usage/client/" },
72 | ],
73 | },
74 | { label: "Demo", link: "/demo/" },
75 | {
76 | label: "Recipes",
77 | autogenerate: {
78 | directory: "recipes",
79 | },
80 | },
81 | {
82 | label: "Components",
83 | autogenerate: {
84 | directory: "reference/components",
85 | },
86 | },
87 | {
88 | label: "Content collections",
89 | autogenerate: {
90 | directory: "reference/content-collections",
91 | },
92 | },
93 | {
94 | label: "Utilities",
95 | autogenerate: {
96 | directory: "reference/utilities",
97 | },
98 | },
99 | {
100 | label: "Types",
101 | link: "/reference/types/",
102 | },
103 | ],
104 | }),
105 | tailwind({ applyBaseStyles: false }),
106 | ],
107 | });
108 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "type": "module",
4 | "private": true,
5 | "version": "0.0.1",
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "astro dev",
9 | "build": "astro check && astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/check": "^0.5.10",
15 | "@astrojs/starlight": "^0.21.5",
16 | "@astrojs/starlight-tailwind": "^2.0.2",
17 | "@astrojs/tailwind": "^5.1.0",
18 | "@expressive-code/plugin-line-numbers": "^0.33.5",
19 | "astro": "^4.14.0",
20 | "sharp": "^0.33.3",
21 | "tailwind": "^4.0.0",
22 | "tailwindcss": "^3.4.3",
23 | "typescript": "^5.4.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
13 |
14 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/src/assets/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
13 |
14 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/src/assets/logo-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
13 |
14 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from "astro:content";
2 | import { docsSchema } from "@astrojs/starlight/schema";
3 | import { z } from "astro/zod";
4 |
5 | export const collections = {
6 | docs: defineCollection({
7 | schema: docsSchema({
8 | extend: z.object({
9 | // Add a default value to the built-in `banner` field.
10 | banner: z.object({ content: z.string() }).default({
11 | content: "This integration is unmaintained due to lack of time. It should mostly work but do not expect fixes or new features.",
12 | }),
13 | }),
14 | }),
15 | }),
16 | };
17 |
--------------------------------------------------------------------------------
/docs/src/content/docs/demo.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Demo
3 | ---
4 | import { LinkCard } from "@astrojs/starlight/components"
5 |
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/installation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | ---
4 |
5 | import { Tabs, TabItem } from '@astrojs/starlight/components';
6 |
7 | :::caution[Prerequisites]
8 | - `astro` must be at least on version `4.14.0` or `5.0.0`
9 | - `tsconfig.json` `compilerOptions.strict` should be set to `true`. If you're using Astro tsconfig presets, use either `astro/tsconfigs/strict` or `astro/tsconfigs/strictest`
10 | :::
11 |
12 | ## Automatic installation
13 |
14 | Run the following command in your terminal using your favorite package manager.
15 |
16 |
17 |
18 | ```bash
19 | pnpm astro add @astrolicious/i18n
20 | ```
21 |
22 |
23 | ```bash
24 | npm run astro add @astrolicious/i18n
25 | ```
26 |
27 |
28 | ```bash
29 | yarn astro add @astrolicious/i18n
30 | ```
31 |
32 |
33 |
34 | This will update your `astro.config.mjs` with the following content:
35 |
36 | ```js title="astro.config.mjs" ins={2,6}
37 | import { defineConfig } from "astro/config"
38 | import i18n from "@astrolicious/i18n"
39 |
40 | export default defineConfig({
41 | integrations: [
42 | i18n()
43 | ]
44 | })
45 | ```
46 |
47 | ## Manual installation
48 |
49 | Install the necessary dependencies:
50 |
51 |
52 |
53 | ```bash
54 | pnpm add @astrolicious/i18n i18next
55 | ```
56 |
57 |
58 | ```bash
59 | npm install @astrolicious/i18n i18next
60 | ```
61 |
62 |
63 | ```bash
64 | yarn add @astrolicious/i18n i18next
65 | ```
66 |
67 |
68 |
69 | Update your `astro.config.mjs` with the following content:
70 |
71 | ```js title="astro.config.mjs" ins={2,6}
72 | import { defineConfig } from "astro/config"
73 | import i18n from "@astrolicious/i18n"
74 |
75 | export default defineConfig({
76 | integrations: [
77 | i18n()
78 | ]
79 | })
80 | ```
81 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/known-issues.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Known issues
3 | ---
4 |
5 | Below are the currently known issues and limitations (but they're probably more!):
6 |
7 | ## Rest parameters
8 |
9 | Translating rest parameters in your integration config `pages` option and using it with
10 | `getLocalePath` and `switchLocalePath` **has not been tested yet**. Note that this is planned
11 | and will be tackled soon.
12 |
13 | ## Starlight
14 |
15 | Usage in **Starlight** has not been tested but is not recommended, as it has its own i18n system.
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/showcase.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase
3 | ---
4 |
5 | - [Legis Music](https://legismusic.com/)
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Usage
3 | ---
4 |
5 | import { LinkCard, FileTree, Steps } from '@astrojs/starlight/components';
6 |
7 | :::danger[Disclaimer]
8 | This integration is incompatible with the [native Astro internationalization features](https://docs.astro.build/en/guides/internationalization/).
9 | :::
10 |
11 | ## Update the integration configuration
12 |
13 | This package has a few required options:
14 |
15 | ```js title="astro.config.mjs" {7,8}
16 | import { defineConfig } from "astro/config"
17 | import i18n from "@astrolicious/i18n"
18 |
19 | export default defineConfig({
20 | integrations: [
21 | i18n({
22 | defaultLocale: "en",
23 | locales: ["en", "fr"] // must include the default locale
24 | })
25 | ]
26 | })
27 | ```
28 |
29 | There are many convenient options, check out the reference below to learn more.
30 |
31 |
35 |
36 | ## Structure
37 |
38 |
39 |
40 | - src/
41 | - locales/ Data used for translations
42 | - en/
43 | - common.json
44 | - fr/
45 | - common.json
46 | - pages/ Not managed by the package
47 | - custom.astro Accessible at /custom
48 | - api/
49 | - hello.ts Accessible at /api/hello
50 | - routes/ Managed by the package
51 | - index.astro Accessible at `/` and `/fr` by default
52 | - blog.astro Accessible at `/blog` and `/fr/blog` by default
53 |
54 |
55 |
56 | ## Import the utilities
57 |
58 | Utilities can be imported from `i18n:astro` inside `routes`, `pages` and middlewares:
59 |
60 | ```astro
61 | ---
62 | import { getLocale } from "i18n:astro"
63 |
64 | const locale = getLocale()
65 | ---
66 | ```
67 |
68 | Have a look at the reference to see what you can do!
69 |
70 | ## Client usage
71 |
72 | Using utilities from `i18n:astro` on the client is opt-in. You need 2 things:
73 |
74 |
75 |
76 | 1. Import the ` ` component, likely in your layout:
77 |
78 | ```astro title="src/layouts/Layout.astro" ins={2,10}
79 | ---
80 | import I18nClient from "@astrolicious/i18n/components/I18nClient.astro"
81 | ---
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | ```
96 |
97 | 2. Enable client features in the integration configuration:
98 |
99 | ```js title="astro.config.mjs" ins={9-13}
100 | import { defineConfig } from "astro/config"
101 | import i18n from "@astrolicious/i18n"
102 |
103 | export default defineConfig({
104 | integrations: [
105 | i18n({
106 | defaultLocale: "en",
107 | locales: ["en", "fr"],
108 | client: {
109 | data: true,
110 | // paths: true,
111 | // translations
112 | }
113 | })
114 | ]
115 | })
116 | ```
117 |
118 |
119 |
120 | Learn more about it works below!
121 |
122 |
126 |
--------------------------------------------------------------------------------
/docs/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: I18n for Astro
3 | description: Yet another i18n integration for Astro with server and client utilities, type safety and translations built-in.
4 | ---
5 |
6 | import { Card, CardGrid } from '@astrojs/starlight/components';
7 |
8 | Yet another i18n integration for Astro with server and client utilities, type safety and translations built-in. _A project under the Astrolicious umbrella._
9 |
10 |
11 |
12 | Get locales, localized paths and more directly in your Astro components.
13 |
14 |
15 | Handle translated paths with different slugs per locale.
16 |
17 |
18 | Let your code editor help you with relevant auto-completions.
19 |
20 |
21 | Translate your website easily using `i18next`.
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/src/content/docs/recipes/framework-components.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Framework components
3 | ---
4 |
5 | Framework components are a bit tricky to work with. **Prefer using this package in Astro components**.
6 | It mainly depends on if you use a [client directive](https://docs.astro.build/en/reference/directives-reference/#client-directives)
7 | and which one.
8 |
9 | :::caution
10 | This has not been tested but _should_ be right! If you encounter weird cases, feel free to open
11 | an issue on GitHub or click on "Edit page" below"
12 | :::
13 |
14 | ## Cases
15 |
16 | ### No client directive
17 |
18 | You don't need to do anything! It will be rendered on the server so you can even call `setDynamicParams`.
19 |
20 | ### `load`, `idle`, `visible` and `media`
21 |
22 | If you're using `client:load`, `client:idle`, `client:visible` or `client:media`, you need to use the
23 | utilities within your component. This requires enabling client features.
24 |
25 | ```tsx title="src/components/Test.tsx" del={3} ins={6}
26 | import { getLocale } from "i18n:astro"
27 |
28 | const locale = getLocale()
29 |
30 | export default function Test() {
31 | const locale = getLocale()
32 |
33 | return {locale}
34 | }
35 | ```
36 |
37 | ### `client:only`
38 |
39 | If you're using `client:only`, `client:idle`, `client:visible` or `client:media`, you can use the
40 | utilities anywhere in the file. This requires enabling client features.
41 |
42 | ```tsx title="src/components/Test.tsx"
43 | import { getLocale } from "i18n:astro"
44 |
45 | const locale = getLocale() // valid
46 |
47 | export default function Test() {
48 | const locale = getLocale() // valid
49 |
50 | return {locale}
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/src/content/docs/recipes/get-static-paths.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getStaticPaths
3 | ---
4 |
5 | `getStaticPaths` is a bit special in Astro. On one hand it's absolutely necessary for dynamic SSG
6 | routes but it runs before everything, even middlewares.
7 |
8 | Under the hood, the package uses a middleware so we can't access any of the standard utilities inside
9 | of `getStaticPaths`. Instead, we provide placeholder functions that are build time macros: `getLocalePlaceholder`,
10 | `getLocalesPlaceholder` and `getDefaultLocalePlaceholder`.
11 | That means it will be replaced by it's literal value. For instance:
12 |
13 | ```astro
14 | ---
15 | import { getLocalePlaceholder, getLocalesPlaceholder, getDefaultLocalePlaceholder } from "i18n:astro"
16 |
17 | export const getStaticPaths = () => {
18 | const locale = getLocalePlaceholder()
19 | const defaultLocale = getDefaultLocalePlaceholder()
20 | const locales = getLocalesPlaceholder()
21 |
22 | return []
23 | }
24 | ---
25 | ```
26 |
27 | Will be replaced by the following, no matter the context:
28 |
29 | ```astro {5}
30 | ---
31 | import { getLocalePlaceholder, getLocalesPlaceholder, getDefaultLocalePlaceholder } from "i18n:astro"
32 |
33 | export const getStaticPaths = () => {
34 | const locale = "en"
35 | const defaultLocale = "en"
36 | const locales = ["en", "fr"]
37 |
38 | return []
39 | }
40 | ---
41 | ```
42 |
43 | Be careful not to use these functions in interpolations or it could result in invalid code.
--------------------------------------------------------------------------------
/docs/src/content/docs/recipes/sitemap.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Sitemap
3 | ---
4 |
5 | To enable the sitemap, set the `sitemap` option of the integration config to `true` or an object:
6 |
7 | ```ts title="astro.config.mjs" ins={3}
8 | i18n({
9 | // ...
10 | sitemap: true // or {}
11 | })
12 | ```
13 |
14 | After you run `astro sync`, you'll be able to import a new function from `i18n:astro/sitemap` on your Astro pages:
15 |
16 | ```astro
17 | ---
18 | import sitemap from "i18n:astro/sitemap"
19 | ---
20 | ```
21 |
22 | This allows you to set specific params at the route level. It's really interesting to generate i18n friendly sitemaps (with alternates):
23 |
24 | ```ts
25 | import { getCollection } from "astro:content";
26 | import {
27 | getLocalePlaceholder,
28 | getDefaultLocalePlaceholder,
29 | setDynamicParams,
30 | } from "i18n:astro";
31 | import sitemap from "i18n:astro/sitemap";
32 | import {
33 | collectionFilters,
34 | generateDynamicParams,
35 | handleI18nSlug,
36 | } from "@astrolicious/i18n/content-collections";
37 | import type { GetStaticPaths } from "astro";
38 |
39 | export const getStaticPaths = (async () => {
40 | const locale = getLocalePlaceholder();
41 | const defaultLocale = getDefaultLocalePlaceholder();
42 |
43 | const posts = await getCollection("posts", (post) =>
44 | collectionFilters.byLocale(post, { locale }),
45 | );
46 |
47 | return await Promise.all(
48 | posts.map(async (post) => {
49 | const equivalentPosts = await getCollection("posts", (p) =>
50 | collectionFilters.matchingEntries(p, {
51 | currentEntry: post,
52 | key: "defaultLocaleVersion",
53 | locale,
54 | defaultLocale,
55 | }),
56 | );
57 |
58 | const dynamicParams = equivalentPosts.map((entry) => {
59 | const { locale, slug } = handleI18nSlug(entry.slug);
60 |
61 | return {
62 | locale,
63 | params: {
64 | slug,
65 | },
66 | };
67 | });
68 |
69 | sitemap({
70 | dynamicParams,
71 | });
72 |
73 | return {
74 | params: {
75 | slug: handleI18nSlug(post.slug).slug,
76 | },
77 | props: {
78 | post,
79 | dynamicParams,
80 | },
81 | };
82 | }),
83 | );
84 | }) satisfies GetStaticPaths;
85 |
86 | const { post, dynamicParams } = Astro.props;
87 |
88 | setDynamicParams(dynamicParams);
89 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/recipes/translated-404-pages.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Translated 404 pages
3 | ---
4 |
5 | import { Tabs, TabItem } from '@astrojs/starlight/components';
6 |
7 | When having a translated website, it makes sense to have your 404 pages translated as well! This
8 | can be achieved fairly easily using this package.
9 |
10 | ## Hybrid or server
11 |
12 | This method is actually the easiest. We are going to create a server rendered 404 page that will
13 | allow to show the content in the current locale dynamically!
14 |
15 | First, create a new page at `src/pages/404.astro`. It's important to put in the `pages` directory
16 | and not the `routes` directory to prevent the integration from generating one per locale.
17 |
18 | The important part here is to make sure the page is not prerendered! If you're in hybrid mode, make sure
19 | to add `export const prerender = false`.
20 |
21 | You can then use any utility from this package without any issue! For example:
22 |
23 | ```astro title="src/pages/404.astro" {5}
24 | ---
25 | import Layout from "~/layouts/Layout.astro";
26 | import { getLocale, t } from "i18n:astro"
27 |
28 | export const prerender = false;
29 |
30 | const locale = getLocale()
31 | const title = t("404:pageNotFound");
32 | ---
33 |
34 |
35 | {title}
36 | Locale: {locale}
37 |
38 | ```
39 |
40 | ## Static
41 |
42 | If you want to keep your site fully static, you'll want to generate a 404 page per locale
43 | and rewrite paths with your hosting provider. Below we'll have a look with Vercel and Netlify.
44 |
45 | ### Creating the page
46 |
47 | First, create a new page at `src/routes/404.astro`. You can use any utility from this package
48 | without any issue! For example:
49 |
50 | ```astro title="src/pages/404.astro"
51 | ---
52 | import Layout from "~/layouts/Layout.astro";
53 | import { getLocale, t } from "i18n:astro"
54 |
55 | const locale = getLocale()
56 | const title = t("404:pageNotFound");
57 | ---
58 |
59 |
60 | {title}
61 | Locale: {locale}
62 |
63 | ```
64 |
65 | ### Creating rewrites
66 |
67 | Now that we have 404 pages available (eg. at `/404` and `/fr/404`), we need to tell the host to
68 | rewrite the path to the right locale.
69 |
70 |
71 |
72 | ```json title="vercel.json" ins={4-8}
73 | {
74 | "$schema": "https://openapi.vercel.sh/vercel.json",
75 | "rewrites": [
76 | {
77 | "source": "/(?[^/]*)/(.*)",
78 | "destination": "/$lang/404/"
79 | }
80 | ]
81 | }
82 | ```
83 |
84 |
85 | ```text title="public/_redirects" ins={1}
86 | /:lang/* /:lang/404/ 404
87 | ```
88 |
89 |
90 |
91 | :::note
92 | Do you know how to do it for other adapters? Send a PR!
93 | :::
94 |
95 | ### Handling the `"prefix"` strategy
96 |
97 | If you `strategy: "prefix"` in your integration config, we need to make an adjustement.
98 | Following the previous steps, the following 404 are available: `/en/404` and `/fr/404`.
99 | What happens if someone visits `/test`? It will show the default host 404, not ideal.
100 |
101 | What you can do instead is to duplicate your `src/routes/404.astro` to `src/pages/404.astro`
102 | to handle this case. No other action is required!
103 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/components/i18n-client.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: I18nClient
3 | ---
4 |
5 | This component is required if you want to use client features. It allows passing data to the client (always the minimum) for you:
6 |
7 | ```astro title="src/layouts/Layout.astro" ins={2,10}
8 | ---
9 | import I18nClient from "@astrolicious/i18n/components/I18nClient.astro"
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/components/i18n-head.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: I18nHead
3 | ---
4 |
5 | This component automatically generates alternate links to improve SEO.
6 |
7 | ```astro title="src/layouts/Layout.astro" ins={2,10}
8 | ---
9 | import I18nHead from "@astrolicious/i18n/components/I18nHead.astro"
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/content-collections/collection-filters.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Collection filters
3 | ---
4 |
5 | We expose a few utilities to help you handle i18n collections in `getStaticPaths`, through `collectionFilters`.
6 |
7 | ```ts showLineNumbers=false
8 | import { collectionFilters } from "@astrolicious/i18n/content-collections"
9 | ```
10 |
11 | ## `byLocale`
12 |
13 | Allows you to filter by locale, assuming your entries slugs (or ids) follow the `[locale]/[...parts]` pattern:
14 |
15 | ```ts "collectionFilters.byLocale"
16 | import { getCollection } from "astro:content";
17 | import { getLocalePlaceholder } from "i18n:astro";
18 | import { collectionFilters } from "@astrolicious/i18n/content-collections"
19 |
20 | export const getStaticPaths = (async () => {
21 | // ...
22 | const locale = getLocalePlaceholder();
23 | const posts = await getCollection("posts", (post) =>
24 | collectionFilters.byLocale(post, { locale }),
25 | );
26 | // ...
27 | })
28 | ```
29 |
30 | ## `matchingEntries`
31 |
32 | Allows you to get all entries that match an entry you pass. The usecase is to generate dynamic params:
33 |
34 | ```ts {24-29}
35 | import { getCollection } from "astro:content";
36 | import {
37 | getDefaultLocalePlaceholder,
38 | getLocalePlaceholder,
39 | } from "i18n:astro";
40 | import {
41 | collectionFilters,
42 | generateDynamicParams,
43 | handleI18nSlug,
44 | } from "@astrolicious/i18n/content-collections";
45 | import type { GetStaticPaths } from "astro";
46 |
47 | export const getStaticPaths = (async () => {
48 | const locale = getLocalePlaceholder();
49 | const defaultLocale = getDefaultLocalePlaceholder();
50 |
51 | const posts = await getCollection("posts", (post) =>
52 | collectionFilters.byLocale(post, { locale }),
53 | );
54 |
55 | return await Promise.all(
56 | posts.map(async (post) => {
57 | const equivalentPosts = await getCollection("posts", (p) =>
58 | collectionFilters.matchingEntries(p, {
59 | currentEntry: post,
60 | key: "defaultLocaleVersion",
61 | locale,
62 | defaultLocale,
63 | }),
64 | );
65 |
66 | const dynamicParams = equivalentPosts.map((entry) => {
67 | const { locale, slug } = handleI18nSlug(entry.slug);
68 |
69 | return {
70 | locale,
71 | params: {
72 | slug,
73 | },
74 | };
75 | });
76 |
77 | return {
78 | params: {
79 | slug: handleI18nSlug(post.slug).slug,
80 | },
81 | props: {
82 | post,
83 | dynamicParams,
84 | },
85 | };
86 | }),
87 | );
88 | }) satisfies GetStaticPaths;
89 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/content-collections/handle-i18n-slug.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Handle i18n slug
3 | ---
4 |
5 | Allows you to retrieve the locale and the remaining slug, assuming your slug (or id) follow the `[locale]/[...parts]` pattern:
6 |
7 | ```ts
8 | import { handleI18nSlug } from "@astrolicious/i18n/content-collections";
9 |
10 | const { locale, slug } = handleI18nSlug(entry.slug);
11 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/types.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Types
3 | ---
4 |
5 | The `i18n:astro` module exports a few types:
6 |
7 | ```ts
8 | export type Locale = "en" | "fr";
9 |
10 | export type LocalePathParams = {
11 | "/about": never;
12 | "/blog": never;
13 | "/": never;
14 | "/blog/[slug]": {
15 | "slug": string;
16 | };
17 | };
18 |
19 | export type LocalePath = keyof LocalePathParams;
20 | ```
21 |
22 | ## `Locale`
23 |
24 | Generated from your integration config `locales` Array. Example:
25 |
26 | ```ts showLineNumbers=false
27 | export type Locale = "en" | "fr";
28 | ```
29 |
30 | ## `LocalePathParams`
31 |
32 | Generated from the files in `src/routes`. Example:
33 |
34 | ```ts showLineNumbers=false
35 | export type LocalePathParams = {
36 | "/about": never;
37 | "/blog": never;
38 | "/": never;
39 | "/blog/[slug]": {
40 | "slug": string;
41 | };
42 | };
43 | ```
44 |
45 | ## `LocalePath`
46 |
47 | ```ts showLineNumbers=false
48 | export type LocalePath = keyof LocalePathParams;
49 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-default-locale-placeholder.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getDefaultLocalePlaceholder
3 | ---
4 |
5 | Allows you to get the default locale inside of `getStaticPaths`. It's a build time macro.
6 | That means it will be replaced by it's literal value. For instance:
7 |
8 | ```astro
9 | ---
10 | import { getDefaultLocalePlaceholder } from "i18n:astro"
11 |
12 | export const getStaticPaths = () => {
13 | const defaultLocale = getDefaultLocalePlaceholder()
14 |
15 | return []
16 | }
17 | ---
18 | ```
19 |
20 | Will be replaced by the following, no matter the context:
21 |
22 | ```astro {5}
23 | ---
24 | import { getDefaultLocalePlaceholder } from "i18n:astro"
25 |
26 | export const getStaticPaths = () => {
27 | const defaultLocale = "en"
28 |
29 | return []
30 | }
31 | ---
32 | ```
33 |
34 | Be careful not to use `getDefaultLocalePlaceholder` in interpolations or it could result in invalid code.
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-default-locale.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getDefaultLocale
3 | ---
4 |
5 | Allows you to retrieve the default locale, specified in the integration configuration.
6 |
7 | ```astro
8 | ---
9 | import { getDefaultLocale } from "i18n:astro"
10 |
11 | const locale = getDefaultLocale()
12 | ---
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-html-attrs.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getHtmlAttrs
3 | ---
4 |
5 | Having a website in a given locale require a few attributes on the `html` element. You can get those
6 | automatically through the `getHtmlAttrs` utility!
7 |
8 | ```astro title="src/layouts/Layout.astro" ins={2,4}
9 | ---
10 | import { getHtmlAttrs } from "i18n:astro"
11 | ---
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-locale-path.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getLocalePath
3 | ---
4 |
5 | Allows you get the right url for the current locale. It's strictly typed and even requires you to pass parameters as needed.
6 |
7 | :::note
8 | `getLocalePath` does not take your `trailingSlash` configuration into account and always returns without a trailing slash.
9 | :::
10 |
11 | ```astro
12 | ---
13 | // Assuming:
14 | // 1. stragegy is set to "prefixExceptDefault"
15 | // 2. The request page is /fr/my-page
16 | import { getLocalePath } from "i18n:astro"
17 |
18 | // Routes defined in the integration config
19 | /* /fr */
20 | getLocalePath("/")
21 | /* /fr/a-propos */
22 | getLocalePath("/about")
23 | /* /fr/le-blog/hello-world */
24 | getLocalePath("/blog/[slug]", { slug: "hello-world" })
25 | /* /about */
26 | getLocalePath("/about", null, "en")
27 |
28 | // Routes not defined in the integration config
29 | /* /fr/abc */
30 | getLocalePath("/abc")
31 | /* /def */
32 | getLocalePath("/def", null, "en")
33 | ---
34 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-locale-placeholder.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getLocalePlaceholder
3 | ---
4 |
5 | Allows you to get the current locale inside of `getStaticPaths`. It's a build time macro.
6 | That means it will be replaced by it's literal value. For instance:
7 |
8 | ```astro
9 | ---
10 | import { getLocalePlaceholder } from "i18n:astro"
11 |
12 | export const getStaticPaths = () => {
13 | const locale = getLocalePlaceholder()
14 |
15 | return []
16 | }
17 | ---
18 | ```
19 |
20 | Will be replaced by the following, no matter the context:
21 |
22 | ```astro {5}
23 | ---
24 | import { getLocalePlaceholder } from "i18n:astro"
25 |
26 | export const getStaticPaths = () => {
27 | const locale = "en"
28 |
29 | return []
30 | }
31 | ---
32 | ```
33 |
34 | Be careful not to use `getLocalePlaceholder` in interpolations or it could result in invalid code.
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-locale.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getLocale
3 | ---
4 |
5 | Allows you to retrieve the current locale.
6 |
7 | ```astro
8 | ---
9 | import { getLocale } from "i18n:astro"
10 |
11 | const locale = getLocale()
12 | ---
13 | ```
14 |
15 | :::caution
16 | If you want to access the current locale in `getStaticPaths`, have a look at `getLocalePlaceholder`.
17 | :::
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-locales-placeholder.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getLocalesPlaceholder
3 | ---
4 |
5 | Allows you to get the locales inside of `getStaticPaths`. It's a build time macro.
6 | That means it will be replaced by it's literal value. For instance:
7 |
8 | ```astro
9 | ---
10 | import { getLocalesPlaceholder } from "i18n:astro"
11 |
12 | export const getStaticPaths = () => {
13 | const locales = getLocalesPlaceholder()
14 |
15 | return []
16 | }
17 | ---
18 | ```
19 |
20 | Will be replaced by the following, no matter the context:
21 |
22 | ```astro {5}
23 | ---
24 | import { getLocalesPlaceholder } from "i18n:astro"
25 |
26 | export const getStaticPaths = () => {
27 | const locales = ["en", "fr"]
28 |
29 | return []
30 | }
31 | ---
32 | ```
33 |
34 | Be careful not to use `getLocalesPlaceholder` in interpolations or it could result in invalid code.
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-locales.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getLocales
3 | ---
4 |
5 | Allows you to retrieve the locales from your integration config.
6 |
7 | ```astro
8 | ---
9 | import { getLocales } from "i18n:astro"
10 |
11 | const locales = getLocales()
12 | ---
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/get-switcher-data.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: getSwitcherData
3 | ---
4 |
5 | Generates useful data for a locale switcher component. Here is a basic implementation:
6 |
7 | ```astro "getSwitcherData"
8 | ---
9 | import { getSwitcherData, getLocale } from "i18n:astro";
10 |
11 | const locale = getLocale();
12 | const data = getSwitcherData();
13 | ---
14 |
15 |
16 | {
17 | data.map((e) => (
18 |
19 | {e.locale}
20 |
21 | ))
22 | }
23 |
24 |
25 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/set-dynamic-params.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: setDynamicParams
3 | ---
4 |
5 | import { FileTree } from '@astrojs/starlight/components';
6 |
7 | When having dynamic params in your routes (like `src/routes/blog/[slug]`), you need to manually specify the
8 | equivalent params in other locales. That will then be passed to `switchLocalePath` and `getSwitcherData`.
9 | This is particularly useful if you have different corresponding slugs per locale. For example if you want:
10 |
11 |
12 |
13 | - blog/
14 | - hello-world
15 | - fr/
16 | - le-blog/
17 | - bonjour-le-monde
18 |
19 |
20 |
21 | ```astro title="src/routes/blog/[slug].astro" ins={3,29-31}
22 | ---
23 | import type { GetStaticPaths } from "astro";
24 | import { setDynamicParams, getLocalePlaceholder, t } from "i18n:astro";
25 |
26 | export const getStaticPaths = (() => {
27 | const locale = getLocalePlaceholder();
28 |
29 | const slugs = [
30 | {
31 | en: "hello-world",
32 | fr: "bonjour-le-monde",
33 | },
34 | // ...
35 | ];
36 |
37 | return slugs.map((slug) => ({
38 | params: {
39 | slug: slug[locale],
40 | },
41 | props: {
42 | slugs: slug,
43 | },
44 | }));
45 | }) satisfies GetStaticPaths;
46 |
47 | const { slug } = Astro.params;
48 | const { slugs } = Astro.props;
49 |
50 | setDynamicParams(
51 | Object.entries(slugs).map(([locale, slug]) => ({ locale, params: { slug } }))
52 | );
53 | ---
54 | ```
55 |
56 | ## Data shapes
57 |
58 | It accepts a single parameter with 2 shapes: object or array.
59 |
60 | ### Object
61 |
62 | ```ts
63 | setDynamicParams({
64 | en: {
65 | slug: 'hello-world'
66 | },
67 | fr: {
68 | slug: 'bonjour-le-monde'
69 | }
70 | })
71 | ```
72 |
73 | ### Array
74 |
75 | More convenient when working with data programmatically
76 |
77 | ```ts
78 | setDynamicParams([
79 | {
80 | locale: "en",
81 | params: {
82 | slug: "hello-world"
83 | }
84 | },
85 | {
86 | locale: "fr",
87 | params: {
88 | slug: "bonjour-le-monde"
89 | }
90 | }
91 | ])
92 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/switch-locale-path.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: switchLocalePath
3 | ---
4 |
5 | Allows you to get the equivalent path of the current route in any locale.
6 |
7 |
8 | ```astro
9 | ---
10 | import { switchLocalePath } from "i18n:astro"
11 |
12 | const enPath = switchLocalePath("en") // /about
13 | const frPath = switchLocalePath("fr") // /fr/a-propos
14 | ---
15 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/reference/utilities/t.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: t
3 | ---
4 |
5 | The `t` function is re-exported from `i18next` and is made type-safety automatically. Have a look at
6 | [`i18next` docs](https://www.i18next.com/translation-function/essentials) to learn more.
--------------------------------------------------------------------------------
/docs/src/content/docs/usage/client.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Client usage
3 | ---
4 |
5 | Client usage is disabled by default because it sends some JavaScript to the browser.
6 | Enabling any of the following features requires importing the ` ` component.
7 |
8 | | Utility | Required features |
9 | |-------------------------------|----------------------------|
10 | | `t` | `data`, `translations` |
11 | | `getLocale` | `data` |
12 | | `getLocales` | `data` |
13 | | `getDefaultLocale` | `data` |
14 | | `getHtmlAttrs` | `data` |
15 | | `setDynamicParams` | N/A, server only |
16 | | `getLocalePath` | `data`, `paths` |
17 | | `switchLocalePath` | `data`, `paths` |
18 | | `getSwitcherData` | `data`, `paths` |
19 | | `getLocalePlaceholder` | N/A, `getStaticPaths` only |
20 | | `getLocalesPlaceholder` | N/A, `getStaticPaths` only |
21 | | `getDefaultLocalePlaceholder` | N/A, `getStaticPaths` only |
22 |
23 | You can enable them in your integration config:
24 |
25 | ```ts title="astro.config.mjs"
26 | i18n({
27 | // ...
28 | client: {
29 | data: false,
30 | paths: false,
31 | translations: false,
32 | }
33 | })
34 | ```
--------------------------------------------------------------------------------
/docs/src/content/docs/usage/configuration.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Configuration
3 | ---
4 |
5 | import { FileTree } from '@astrojs/starlight/components';
6 |
7 | ## Configure the integration
8 |
9 | You can pass the following options to the integration.
10 |
11 | :::note
12 | The examples below assume the following configuration:
13 |
14 | ```ts showLineNumbers=false
15 | i18n({
16 | defaultLocale: "en",
17 | locales: ["en", "fr"]
18 | })
19 | ```
20 | :::
21 |
22 | ### `defaultLocale` (required)
23 |
24 | **Type:** `string`
25 |
26 | Sets the default locale for your website.
27 |
28 | ### `locales` (required)
29 |
30 | **Type:** `Array`
31 |
32 | Sets the available locales for your website. Must include the default locale.
33 |
34 | ### `strategy`
35 |
36 | **Type:** `"prefixWithoutDefault" | "prefix"`
37 |
38 | **Default:** `"prefixWithoutDefault"`
39 |
40 | Defines how your routes are generated:
41 | - `"prefixWithoutDefault"` will not add a prefix for your default locale:
42 |
43 |
44 |
45 | - src/routes/
46 | - index.astro / and /fr
47 | - about.astro /about and /fr/about
48 |
49 |
50 |
51 | - `"prefix"` will add a prefix for your default locale:
52 |
53 |
54 |
55 | - src/routes/
56 | - index.astro /en and /fr
57 | - about.astro /en/about and /fr/about
58 |
59 |
60 |
61 | ### `pages`
62 |
63 | **Type:** `Record>`
64 |
65 | Allows you to define translated paths for your locales. For example:
66 |
67 | **Integration configuration:**
68 |
69 | ```ts
70 | i18n({
71 | // ...
72 | strategy: "prefixWithoutDefault",
73 | pages: {
74 | "/about": {
75 | fr: "/a-propos"
76 | },
77 | "/blog/[slug]": {
78 | fr: "/le-blog/[slug]"
79 | }
80 | }
81 | })
82 | ```
83 |
84 | **Project structure**
85 |
86 |
87 |
88 | - src/routes/
89 | - index.astro
90 | - about.astro
91 | - contact.astro
92 | - blog/
93 | - [slug].astro
94 |
95 |
96 |
97 | **URL structure**
98 |
99 |
100 |
101 | - /
102 | - about
103 | - contact
104 | - blog/
105 | - a
106 | - b
107 | - fr/
108 | - a-propos
109 | - contact
110 | - le-blog/
111 | - a
112 | - b
113 |
114 |
115 |
116 | ### `localesDir`
117 |
118 | **Type:** `string`
119 |
120 | **Default:** `"./src/locales"`
121 |
122 | A path relative to the root where locales files are located for translations features.
123 |
124 | ### `defaultNamespace`
125 |
126 | **Type:** `string`
127 |
128 | **Default:** `"common"`
129 |
130 | Sets the default namespace for locales. Since this package uses `i18next` under the hood, it allows to split
131 | translations data in multiple json files under `src/locales/[locale]/`. If you're not using a file called `common.json`,
132 | you need to update this property to have proper types completions when using `t`.
133 |
134 |
135 |
136 | - src/locales/
137 | - en/
138 | - shared.json Update this option to `"shared"` if you're not using the default `"common"`
139 | - test.json
140 | - fr/
141 | - shared.json
142 |
143 |
144 |
145 | ### `client`
146 |
147 | **Type:** `false | ClientConfig`
148 |
149 | **Default:** `false`
150 |
151 | Client usage is disabled by default because it sends some JavaScript to the browser.
152 | Enabling any of the following features requires importing the ` ` component.
153 |
154 | #### `ClientConfig`
155 |
156 | | Utility | Required features |
157 | |-------------------------------|----------------------------|
158 | | `t` | `data`, `translations` |
159 | | `getLocale` | `data` |
160 | | `getLocales` | `data` |
161 | | `getDefaultLocale` | `data` |
162 | | `getHtmlAttrs` | `data` |
163 | | `setDynamicParams` | N/A, server only |
164 | | `getLocalePath` | `data`, `paths` |
165 | | `switchLocalePath` | `data`, `paths` |
166 | | `getSwitcherData` | `data`, `paths` |
167 | | `getLocalePlaceholder` | N/A, `getStaticPaths` only |
168 | | `getLocalesPlaceholder` | N/A, `getStaticPaths` only |
169 | | `getDefaultLocalePlaceholder` | N/A, `getStaticPaths` only |
170 |
171 | - `data`: `boolean`, defaults to `false`
172 | - `paths`: `boolean`, defaults to `false`
173 | - `translations`: `boolean`, defaults to `false`
174 |
175 | ### `rootRedirect`
176 |
177 | **Type:** `{ status: number; destination: string }`
178 |
179 | When using `strategy: "prefix"`, you may want to redirect your users from the root
180 | to a specific page (likely the default locale root). This option allows you to do so:
181 |
182 | ```ts
183 | i18n({
184 | // ...
185 | strategy: "prefix",
186 | rootRedirect: {
187 | status: 301,
188 | destination: "/en"
189 | }
190 | })
191 | ```
192 |
193 | ### `sitemap`
194 |
195 | **Type:** `boolean | SitemapOptions`
196 |
197 | ```ts
198 | type SitemapOptions = {
199 | customPages?: string[];
200 | entryLimit?: number;
201 | changefreq?: EnumChangefreq;
202 | lastmod?: Date;
203 | priority?: number;
204 | };
205 | ```
206 |
207 | Allows to generate a sitemap that adapts to your i18n content. Options here
208 | are a subset of the official sitemap integration. You can see what they do in
209 | the [official docs](https://docs.astro.build/en/guides/integrations-guide/sitemap/#configuration).
--------------------------------------------------------------------------------
/docs/src/content/docs/usage/translations.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Translations
3 | ---
4 |
5 | Under the hood, [`i18next`](https://www.i18next.com/) is used to manage translations. We enforce the following conventions:
6 |
7 | 1. Locales must live in the `src/locales/` directory (can be customized with `localesDir`)
8 | 2. A folder for the default locale is required, eg. `src/locales/en/`
9 | 3. Provide at least a `src/locales/en/common.json` file (can be customized with `defaultNamespace`)
10 |
11 | You can then use the `t` function (re-exported from `i18next`) and benefit from type-safety automatically. Have a look at
12 | [`i18next` docs](https://www.i18next.com/translation-function/essentials) to learn more.
--------------------------------------------------------------------------------
/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/docs/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @font-face {
6 | font-family: 'JetBrains Mono Variable';
7 | font-style: normal;
8 | font-display: swap;
9 | font-weight: 100 800;
10 | src: url(https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
11 | unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
12 | }
13 |
14 | :root {
15 | --sl-font-system-mono: 'JetBrains Mono Variable', monospace;
16 | --sl-color-banner-bg: var(--sl-color-orange);
17 | --sl-color-banner-text: #000;
18 | }
19 |
--------------------------------------------------------------------------------
/docs/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | import starlightPlugin from "@astrojs/starlight-tailwind";
2 |
3 | const customColors = {
4 | gray: {
5 | 50: "#EEEFF2",
6 | 100: "#DFE1E7",
7 | 200: "#BFC4CE",
8 | 300: "#9DA3B4",
9 | 400: "#7D869B",
10 | 500: "#616A7F",
11 | 600: "#494F5F",
12 | 700: "#313540",
13 | 800: "#17191E",
14 | 900: "#0B0C0E",
15 | 950: "#070709",
16 | },
17 | accent: {
18 | 50: "#FCEDF4",
19 | 100: "#FAE0EC",
20 | 200: "#F5BDD6",
21 | 300: "#F09DC3",
22 | 400: "#EB7AAD",
23 | 500: "#E65B9A",
24 | 600: "#E13884",
25 | 700: "#B61B61",
26 | 800: "#7C1342",
27 | 900: "#3E0921",
28 | 950: "#1F0511",
29 | },
30 | };
31 |
32 | /** @type {import('tailwindcss').Config} */
33 | export default {
34 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
35 | theme: {
36 | extend: {
37 | colors: {
38 | accent: customColors.accent,
39 | gray: customColors.gray,
40 | },
41 | fontFamily: {
42 | sans: ["Inter"],
43 | mono: ["JetBrains Mono Variable"],
44 | },
45 | },
46 | },
47 | plugins: [starlightPlugin()],
48 | };
49 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "packageManager": "pnpm@9.0.4",
5 | "engines": {
6 | "node": ">=18.19.0"
7 | },
8 | "scripts": {
9 | "package:dev": "pnpm --filter @astrolicious/i18n dev",
10 | "playground:dev": "pnpm --filter playground dev",
11 | "docs:dev": "pnpm --filter docs dev",
12 | "demo:dev": "pnpm --filter demo dev",
13 | "dev": "pnpm --stream -r -parallel dev",
14 | "changeset": "changeset",
15 | "release": "node scripts/release.mjs",
16 | "lint": "biome check .",
17 | "lint:fix": "biome check --apply ."
18 | },
19 | "devDependencies": {
20 | "@biomejs/biome": "1.7.1",
21 | "@changesets/cli": "^2.27.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/package/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @astrolicious/i18n
2 |
3 | ## 0.6.1
4 |
5 | ### Patch Changes
6 |
7 | - a2b75e9: Fixes matching of similar dynamic parameters
8 |
9 | ## 0.6.0
10 |
11 | ### Minor Changes
12 |
13 | - d91fd70: Bumps peer dependencies to support Astro 5. In order to use the integration with Astro 4, you now need to use at least `4.14.0`
14 |
15 | ### Patch Changes
16 |
17 | - d91fd70: Fixes an issue with type generation when a project does not have a `src/env.d.ts` file
18 |
19 | ## 0.5.1
20 |
21 | ### Patch Changes
22 |
23 | - b81d4ed: Fixes an issue where locales would not be properly loaded depending on Astro `trailingSlash` value
24 |
25 | ## 0.5.0
26 |
27 | ### Minor Changes
28 |
29 | - 87ed904: Upgrades some dependencies to fix an issue with Astro >= 4.14.0. The package now requires Astro >= 4.12.0
30 |
31 | ## 0.4.3
32 |
33 | ### Patch Changes
34 |
35 | - ef0b3ee: Fixes an case where i18next typings would not be correct
36 |
37 | ## 0.4.2
38 |
39 | ### Patch Changes
40 |
41 | - 2f00eb3: Fixes a case where non pages were included in the sitemap
42 | - 2f00eb3: Fixes trailing slash handling in sitemap
43 | - 2f00eb3: Fixes duplicated urls with complex routes
44 | - 2f00eb3: Fixes a case where invalid dynamic params would cause wrong alternates to be generated
45 |
46 | ## 0.4.1
47 |
48 | ### Patch Changes
49 |
50 | - 261316d: Fixes an issue with `ZodError`
51 |
52 | ## 0.4.0
53 |
54 | ### Minor Changes
55 |
56 | - 9a97712: Reworks internals to use Astro Integration Kit 0.13, this is non breaking
57 |
58 | ## 0.3.3
59 |
60 | ### Patch Changes
61 |
62 | - 551d663: Fixes a case where having `trailingSlash: "true"` when using the sitemap would not register routes
63 | - 7ba135e: Allows dynamic routes not to always have an equivalent in another locale when using the sitemap
64 |
65 | ## 0.3.2
66 |
67 | ### Patch Changes
68 |
69 | - 6159cd0: Allows using endpoints under `src/routes`
70 |
71 | ## 0.3.1
72 |
73 | ### Patch Changes
74 |
75 | - afcd189: Fixes `switchLocalePath` (and anything that depends on it like `getSwitcherData`) when not in dev mode
76 |
77 | ## 0.3.0
78 |
79 | ### Minor Changes
80 |
81 | - e138d1b: Adds a new `getLocalesPlaceholder` utility to retrieve `locales` provided in the integration config from inside `getStaticPaths`
82 | - e138d1b: Allows passing custom paths to `getLocalePath` (not registered in the integration config). This will simply prefix paths based on the choosen strategy
83 |
84 | ### Patch Changes
85 |
86 | - e138d1b: Fixes `getLocalePath` typing to allow specifying a locale
87 |
88 | ## 0.2.1
89 |
90 | ### Patch Changes
91 |
92 | - c4950e5: Fixes a case where build would fail with the sitemap enabled and a route containing uppercase characters
93 |
94 | ## 0.2.0
95 |
96 | ### Minor Changes
97 |
98 | - 8bc6c8e: Adds a new `getDefaultLocalePlaceholder` utility
99 | - 8bc6c8e: Adds Content Collections helpers
100 | - 8bc6c8e: Adds sitemap support
101 |
102 | ## 0.1.2
103 |
104 | ### Patch Changes
105 |
106 | - 7ba64a1: Refactors to use the latest version of `astro-integration-kit`
107 |
108 | ## 0.1.1
109 |
110 | ### Patch Changes
111 |
112 | - d697474: Adds support for View Transitions
113 |
114 | ## 0.1.0
115 |
116 | ### Minor Changes
117 |
118 | - 868816f: Adds a new `getDefaultLocale` utility
119 |
120 | ## 0.0.4
121 |
122 | ### Patch Changes
123 |
124 | - 0aaff4a: Fixes types by making `i18next` a peer dependency and cleans a few things
125 |
126 | ## 0.0.3
127 |
128 | ### Patch Changes
129 |
130 | - 5a7c47a: Fixes links and package names
131 |
132 | ## 0.0.2
133 |
134 | ### Patch Changes
135 |
136 | - e46e6ca: Initial release
137 |
138 | ## 0.0.1
139 |
140 | ### Patch Changes
141 |
142 | - bf6f9fc: Test release
143 |
--------------------------------------------------------------------------------
/package/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > This integration is unmaintained due to lack of time. It should mostly work but do not expect fixes or new features.
3 |
4 | # `@astrolicious/i18n`
5 |
6 | This is yet another i18n integration for [Astro](https://astro.build/) with server and client utilities, type safety and translations built-in.
7 |
8 | ## Documentation
9 |
10 | Read the [Astro I18n docs](https://astro-i18n.netlify.app/).
11 |
12 | ## Contributing
13 |
14 | This package is structured as a monorepo:
15 |
16 | - `playground` contains code for testing the package
17 | - `package` contains the actual package
18 |
19 | Install dependencies using pnpm:
20 |
21 | ```bash
22 | pnpm i --frozen-lockfile
23 | ```
24 |
25 | Start the playground and package watcher:
26 |
27 | ```bash
28 | pnpm dev
29 | ```
30 |
31 | You can now edit files in `package`. Please note that making changes to those files may require restarting the playground dev server.
32 |
33 | ## Licensing
34 |
35 | [MIT Licensed](https://github.com/astrolicious/i18n/blob/main/LICENSE). Made with ❤️ by [Florian Lefebvre](https://github.com/florian-lefebvre).
36 |
37 | ## Acknowledgements
38 |
39 | - https://github.com/yassinedoghri/astro-i18next
40 | - https://i18n.nuxtjs.org/
--------------------------------------------------------------------------------
/package/assets/components/I18nClient.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { als } from "virtual:astro-i18n/als";
3 | import { clientId } from "virtual:astro-i18n/internal";
4 |
5 | const config = als.getStore();
6 | if (!config) {
7 | throw new Error(
8 | "Using ` ` requires adding the `i18n` integration to your Astro config.",
9 | );
10 | }
11 |
12 | const { clientOptions } = config;
13 |
14 | const enabledClientFeatures = Object.entries(clientOptions)
15 | .map(([name, enabled]) => ({ name, enabled }))
16 | .filter((e) => e.enabled);
17 | if (enabledClientFeatures.length === 0) {
18 | throw new Error(
19 | "You need to enabled at least one client feature (`client: {...}`) in the integration config to use ` `.",
20 | );
21 | }
22 |
23 | const data = clientOptions.data ? config.data : undefined;
24 | const translations = clientOptions.translations
25 | ? { ...config.translations, initialized: false }
26 | : undefined;
27 | const paths = clientOptions.paths ? config.paths : undefined;
28 | ---
29 |
30 |
31 |
--------------------------------------------------------------------------------
/package/assets/components/I18nHead.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getSwitcherData } from "i18n:astro";
3 |
4 | const data = getSwitcherData();
5 | ---
6 |
7 | {
8 | data.map(({ locale, href }) => (
9 |
10 | ))
11 | }
12 |
--------------------------------------------------------------------------------
/package/assets/middleware.ts:
--------------------------------------------------------------------------------
1 | import { als } from "virtual:astro-i18n/als";
2 | import { i18nextConfig, options, routes } from "virtual:astro-i18n/internal";
3 | import { defineMiddleware } from "astro/middleware";
4 |
5 | const extractLocaleFromUrl = (pathname: string) => {
6 | for (const locale of options.locales) {
7 | if (options.strategy === "prefix") {
8 | if (pathname.startsWith(`/${locale}/`)) {
9 | return locale;
10 | }
11 | } else if (options.strategy === "prefixExceptDefault") {
12 | if (
13 | locale !== options.defaultLocale &&
14 | pathname.startsWith(`/${locale}`)
15 | ) {
16 | return locale;
17 | }
18 | }
19 | }
20 | return options.defaultLocale;
21 | };
22 |
23 | export const onRequest = defineMiddleware((context, next) => {
24 | const pathname = context.url.pathname;
25 | const locale = extractLocaleFromUrl(pathname);
26 |
27 | return als.run(
28 | {
29 | clientOptions: options.client,
30 | translations: {
31 | initialized: false,
32 | i18nextConfig,
33 | },
34 | data: {
35 | locale,
36 | locales: options.locales,
37 | defaultLocale: options.defaultLocale,
38 | },
39 | paths: {
40 | pathname,
41 | routes,
42 | dynamicParams: {},
43 | strategy: options.strategy,
44 | },
45 | },
46 | next,
47 | );
48 | });
49 |
--------------------------------------------------------------------------------
/package/assets/stubs/sitemap.d.ts:
--------------------------------------------------------------------------------
1 | declare module "i18n:astro/sitemap" {
2 | const sitemap: (
3 | args: import("@astrolicious/i18n/internal").CallbackSchema,
4 | ) => void;
5 |
6 | export default sitemap;
7 | }
8 |
--------------------------------------------------------------------------------
/package/assets/stubs/virtual.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@@_ID_@@" {
2 | /**
3 | * @description TODO:
4 | * @link TODO:
5 | */
6 | export type Locale = "@@_LOCALE_@@";
7 |
8 | /**
9 | * @description TODO:
10 | * @link TODO:
11 | */
12 | export type LocalePathParams = "@@_LOCALE_PATH_PARAMS_@@";
13 |
14 | /**
15 | * @description TODO:
16 | * @link TODO:
17 | */
18 | export type LocalePath = keyof LocalePathParams;
19 |
20 | /**
21 | * @description TODO:
22 | * @link TODO:
23 | */
24 | export const t: typeof import("i18next").t;
25 |
26 | /**
27 | * @description TODO:
28 | * @link TODO:
29 | */
30 | export const getLocale: () => Locale;
31 |
32 | /**
33 | * @description TODO:
34 | * @link TODO:
35 | */
36 | export const getLocales: () => "@@_LOCALES_@@";
37 |
38 | /**
39 | * @description TODO:
40 | * @link TODO:
41 | */
42 | export const getDefaultLocale: () => Locale;
43 |
44 | /**
45 | * @description TODO:
46 | * @link TODO:
47 | */
48 | export const getHtmlAttrs: () => {
49 | lang: string;
50 | dir: "rtl" | "ltr";
51 | };
52 |
53 | /**
54 | * @description TODO:
55 | * @link TODO:
56 | */
57 | export const setDynamicParams: (
58 | params:
59 | | Partial>>
60 | | Array<{
61 | locale: Locale | (string & {});
62 | params: Record;
63 | }>,
64 | ) => void;
65 |
66 | type Loose = T | (`/${string}` & {});
67 |
68 | type Strictify = T extends `${infer _}` ? T : never;
69 |
70 | /**
71 | * @description TODO:
72 | * @link TODO:
73 | */
74 | export const getLocalePath: >(
75 | path: TPath,
76 | ...args: TPath extends Strictify
77 | ? LocalePathParams[TPath] extends never
78 | ? [params?: null | undefined, locale?: Locale | undefined]
79 | : [params: LocalePathParams[TPath], locale?: Locale | undefined]
80 | : [params?: null | undefined, locale?: Locale | undefined]
81 | ) => string;
82 |
83 | /**
84 | * @description TODO:
85 | * @link TODO:
86 | */
87 | export const switchLocalePath: (locale: Locale) => string;
88 |
89 | /**
90 | * @description TODO:
91 | * @link TODO:
92 | */
93 | export const getSwitcherData: () => Array<{
94 | locale: string;
95 | href: string;
96 | }>;
97 |
98 | /**
99 | * @description TODO:
100 | * @link TODO:
101 | */
102 | export const getLocalePlaceholder: () => Locale;
103 |
104 | /**
105 | * @description TODO:
106 | * @link TODO:
107 | */
108 | export const getLocalesPlaceholder: () => ReturnType;
109 |
110 | /**
111 | * @description TODO:
112 | * @link TODO:
113 | */
114 | export const getDefaultLocalePlaceholder: () => Locale;
115 | }
116 |
--------------------------------------------------------------------------------
/package/assets/stubs/virtual.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @returns {import("../../src/types.ts").I18nConfig}
3 | */
4 | const _getConfig = () => "@@_CONFIG_@@";
5 | /**
6 | * @returns {import("i18next").i18n}
7 | */
8 | const _getI18next = () => "@@_I18NEXT_@@";
9 |
10 | /**
11 | *
12 | * @param {string} name
13 | * @param {{ serverOnly: boolean; clientFeatures: Array<"data" | "translations" | "paths"> }} param0
14 | */
15 | const _envCheck = (name, { serverOnly = false, clientFeatures = [] } = {}) => {
16 | if (serverOnly && !import.meta.env.SSR) {
17 | throw new Error(`\`${name}\` is only available on the server`);
18 | }
19 | if (clientFeatures.length > 0 && !import.meta.env.SSR) {
20 | const config = _getConfig();
21 | if (!config) {
22 | throw new Error(
23 | `\`${name}\` on the client requires using the \` \` component`,
24 | );
25 | }
26 |
27 | for (const feature of Object.keys(config.clientOptions)) {
28 | if (clientFeatures.includes(feature) && !config[feature]) {
29 | throw new Error(
30 | `\`${name}\` on the client requires setting \`client: { ${feature}: true }\` in the integration config`,
31 | );
32 | }
33 | }
34 | }
35 | };
36 |
37 | /**
38 | *
39 | * @param {string} locale
40 | */
41 | const _dir = (locale) => {
42 | const rtlLocales = [
43 | "ar",
44 | "shu",
45 | "sqr",
46 | "ssh",
47 | "xaa",
48 | "yhd",
49 | "yud",
50 | "aao",
51 | "abh",
52 | "abv",
53 | "acm",
54 | "acq",
55 | "acw",
56 | "acx",
57 | "acy",
58 | "adf",
59 | "ads",
60 | "aeb",
61 | "aec",
62 | "afb",
63 | "ajp",
64 | "apc",
65 | "apd",
66 | "arb",
67 | "arq",
68 | "ars",
69 | "ary",
70 | "arz",
71 | "auz",
72 | "avl",
73 | "ayh",
74 | "ayl",
75 | "ayn",
76 | "ayp",
77 | "bbz",
78 | "pga",
79 | "he",
80 | "iw",
81 | "ps",
82 | "pbt",
83 | "pbu",
84 | "pst",
85 | "prp",
86 | "prd",
87 | "ug",
88 | "ur",
89 | "ydd",
90 | "yds",
91 | "yih",
92 | "ji",
93 | "yi",
94 | "hbo",
95 | "men",
96 | "xmn",
97 | "fa",
98 | "jpr",
99 | "peo",
100 | "pes",
101 | "prs",
102 | "dv",
103 | "sam",
104 | "ckb",
105 | ];
106 |
107 | return rtlLocales.includes(locale) ? "rtl" : "ltr";
108 | };
109 |
110 | /**
111 | * @param {string} path
112 | */
113 | const _withoutTrailingSlash = (path) =>
114 | path.endsWith("/") ? path.slice(0, -1) : path;
115 |
116 | export const t = (...args) => {
117 | _envCheck("t", { clientFeatures: ["data", "translations"] });
118 | const config = _getConfig();
119 | const i18next = _getI18next();
120 |
121 | if (!config.translations.initialized) {
122 | i18next.init({
123 | lng: config.data.locale,
124 | defaultNS: config.translations.i18nextConfig.defaultNamespace,
125 | ns: config.translations.i18nextConfig.namespaces,
126 | resources: config.translations.i18nextConfig.resources,
127 | });
128 | config.translations.initialized = true;
129 | }
130 | return i18next.t(...args);
131 | };
132 |
133 | export const getLocale = () => {
134 | _envCheck("getLocale", { clientFeatures: ["data"] });
135 | return _getConfig().data.locale;
136 | };
137 |
138 | export const getLocales = () => {
139 | _envCheck("getLocales", { clientFeatures: ["data"] });
140 | return _getConfig().data.locales;
141 | };
142 |
143 | export const getDefaultLocale = () => {
144 | _envCheck("getDefaultLocale", { clientFeatures: ["data"] });
145 | return _getConfig().data.defaultLocale;
146 | };
147 |
148 | export const getHtmlAttrs = () => {
149 | _envCheck("getHtmlAttrs", { clientFeatures: ["data"] });
150 | return {
151 | lang: getLocale(),
152 | dir: _dir(getLocale()),
153 | };
154 | };
155 |
156 | /**
157 | *
158 | * @param {Record> | Array<{ locale: string; params: Record }>} _params
159 | */
160 | export const setDynamicParams = (_params) => {
161 | _envCheck("setDynamicParams", { serverOnly: true });
162 | const config = _getConfig();
163 |
164 | const params = Array.isArray(_params)
165 | ? _params.reduce((obj, e) => {
166 | obj[e.locale] = {
167 | ...(obj[e.locale] ?? {}),
168 | ...e.params,
169 | };
170 | return obj;
171 | }, {})
172 | : _params;
173 |
174 | config.paths.dynamicParams = {
175 | ...config.paths.dynamicParams,
176 | ...params,
177 | };
178 | };
179 |
180 | /**
181 | *
182 | * @param {string} path
183 | * @param {Record} params
184 | * @param {string} _locale
185 | */
186 | export const getLocalePath = (path, params = {}, _locale = getLocale()) => {
187 | _envCheck("getLocalePath", { clientFeatures: ["data", "paths"] });
188 | const config = _getConfig();
189 |
190 | const route = config.paths.routes.find(
191 | (route) => route.locale === _locale && route.pattern === path,
192 | );
193 | if (!route) {
194 | const prefix =
195 | config.paths.strategy === "prefix"
196 | ? `/${_locale}`
197 | : _locale === config.data.defaultLocale
198 | ? ""
199 | : `/${_locale}`;
200 | return `${prefix}${path}`;
201 | }
202 |
203 | let newPath = route.injectedRoute.pattern;
204 | for (const param of route.params) {
205 | const value = params[param];
206 | if (!value) {
207 | throw new Error(`Must provide "${param}" param`);
208 | }
209 | newPath = newPath.replace(`[${param}]`, value);
210 | }
211 |
212 | return newPath;
213 | };
214 |
215 | /**
216 | *
217 | * @param {string} locale
218 | */
219 | export const switchLocalePath = (locale) => {
220 | _envCheck("switchLocalePath", { clientFeatures: ["data", "paths"] });
221 | const config = _getConfig();
222 |
223 | const currentLocaleRoutes = config.paths.routes.filter(
224 | (route) => route.locale === getLocale(),
225 | );
226 |
227 | // Static
228 | let currentLocaleRoute = currentLocaleRoutes
229 | .filter((route) => route.params.length === 0)
230 | .find(
231 | (route) =>
232 | route.injectedRoute.pattern ===
233 | _withoutTrailingSlash(config.paths.pathname),
234 | );
235 |
236 | // Dynamic
237 | if (!currentLocaleRoute) {
238 | currentLocaleRoute = currentLocaleRoutes
239 | .filter((route) => route.params.length > 0)
240 | .find((route) => {
241 | // Convert the route pattern to a regex pattern
242 |
243 | // Replace all dynamic params with the ".*" regex pattern
244 | let pattern = route.injectedRoute.pattern.replace(/[*.]/g, "\\$&");
245 | pattern = Object.keys(
246 | config.paths.dynamicParams?.[locale] ?? {},
247 | ).reduce((acc, key) => acc.replace(`[${key}]`, ".*"), pattern);
248 |
249 | // Escape all special characters
250 | pattern = pattern.replace(/[-[\]{}()+?,\\^$|#\s]/g, "\\$&");
251 |
252 | return new RegExp(`^${pattern}$`).test(
253 | _withoutTrailingSlash(config.paths.pathname),
254 | );
255 | });
256 | }
257 |
258 | // Fallback
259 | if (!currentLocaleRoute) {
260 | currentLocaleRoute = currentLocaleRoutes.sort(
261 | (a, b) => a.pattern.length - b.pattern.length,
262 | )[0];
263 | }
264 |
265 | const route = config.paths.routes.find(
266 | (route) =>
267 | route.locale === locale && currentLocaleRoute.pattern === route.pattern,
268 | );
269 | if (!route) {
270 | throw new Error("Couldn't find a route. Open an issue");
271 | }
272 |
273 | return getLocalePath(
274 | route.pattern,
275 | config.paths.dynamicParams?.[locale] ?? undefined,
276 | locale,
277 | );
278 | };
279 |
280 | export const getSwitcherData = () => {
281 | _envCheck("getSwitcherData", { clientFeatures: ["data", "paths"] });
282 | return getLocales().map((locale) => ({
283 | locale,
284 | href: switchLocalePath(locale),
285 | }));
286 | };
287 |
288 | export const getLocalePlaceholder = () => {
289 | throw new Error(
290 | "`getLocalePlaceholder` should only be called within `getStaticPaths`",
291 | );
292 | };
293 |
294 | export const getLocalesPlaceholder = () => {
295 | throw new Error(
296 | "`getLocalesPlaceholder` should only be called within `getStaticPaths`",
297 | );
298 | };
299 |
300 | export const getDefaultLocalePlaceholder = () => {
301 | throw new Error(
302 | "`getDefaultLocalePlaceholder` should only be called within `getStaticPaths`",
303 | );
304 | };
305 |
--------------------------------------------------------------------------------
/package/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "virtual:astro-i18n/internal" {
4 | export const options: import("./src/options.js").Options;
5 | export const routes: Array;
6 | export const i18nextConfig: import("./src/types.js").I18nextConfig;
7 | export const clientId: string;
8 | }
9 |
10 | declare module "virtual:astro-i18n/als" {
11 | export const als: import("node:async_hooks").AsyncLocalStorage<
12 | import("./src/types.js").I18nConfig
13 | >;
14 | }
15 |
16 | declare module "i18n:astro" {
17 | export type Locale = string;
18 |
19 | // biome-ignore lint/complexity/noBannedTypes: placeholder for development
20 | export type LocalePathParams = {};
21 |
22 | export type LocalePath = keyof LocalePathParams;
23 | export const t: typeof import("i18next").t;
24 | export const getLocale: () => Locale;
25 | export const getLocales: () => Array;
26 | export const getDefaultLocale: () => Locale;
27 |
28 | export const getHtmlAttrs: () => {
29 | lang: string;
30 | dir: "rtl" | "ltr";
31 | };
32 |
33 | export const setDynamicParams: (
34 | params:
35 | | Partial>>
36 | | Array<{
37 | locale: Locale | (string & {});
38 | params: Record;
39 | }>,
40 | ) => void;
41 |
42 | type Loose = T | (`/${string}` & {});
43 |
44 | type Strictify = T extends `${infer _}` ? T : never;
45 |
46 | export const getLocalePath: >(
47 | path: TPath,
48 | ...args: TPath extends Strictify
49 | ? LocalePathParams[TPath] extends never
50 | ? [params?: null | undefined, locale?: Locale | undefined]
51 | : [params: LocalePathParams[TPath], locale?: Locale | undefined]
52 | : [params?: null | undefined, locale?: Locale | undefined]
53 | ) => string;
54 |
55 | export const switchLocalePath: (locale: Locale) => string;
56 |
57 | export const getSwitcherData: () => Array<{
58 | locale: string;
59 | href: string;
60 | }>;
61 |
62 | export const getLocalePlaceholder: () => Locale;
63 | export const getLocalesPlaceholder: () => ReturnType;
64 | export const getDefaultLocalePlaceholder: () => Locale;
65 | }
66 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@astrolicious/i18n",
3 | "version": "0.6.1",
4 | "description": "Yet another i18n integration for Astro with server and client utilities, type safety and translations built-in.",
5 | "author": {
6 | "email": "contact@florian-lefebvre.dev",
7 | "name": "Florian Lefebvre",
8 | "url": "https://florian-lefebvre.dev"
9 | },
10 | "license": "MIT",
11 | "keywords": [
12 | "astro-integration",
13 | "astro-component",
14 | "withastro",
15 | "astro",
16 | "i18n",
17 | "i18next",
18 | "seo"
19 | ],
20 | "homepage": "https://github.com/astrolicious/i18n",
21 | "publishConfig": {
22 | "access": "public"
23 | },
24 | "sideEffects": false,
25 | "exports": {
26 | ".": {
27 | "types": "./dist/index.d.ts",
28 | "default": "./dist/index.js"
29 | },
30 | "./internal": {
31 | "types": "./dist/internal.d.ts",
32 | "default": "./dist/internal.js"
33 | },
34 | "./content-collections": {
35 | "types": "./dist/content-collections.d.ts",
36 | "default": "./dist/content-collections.js"
37 | },
38 | "./components/I18nClient.astro": "./assets/components/I18nClient.astro",
39 | "./components/I18nHead.astro": "./assets/components/I18nHead.astro"
40 | },
41 | "files": [
42 | "dist",
43 | "assets"
44 | ],
45 | "scripts": {
46 | "dev": "tsup --watch",
47 | "build": "tsup"
48 | },
49 | "type": "module",
50 | "peerDependencies": {
51 | "astro": "^4.14.0 || ^5.0.0",
52 | "i18next": "^23.0.0"
53 | },
54 | "dependencies": {
55 | "@inox-tools/aik-route-config": "^0.7.1",
56 | "astro-integration-kit": "^0.16.1",
57 | "astro-pages": "^0.3.0",
58 | "sitemap": "^7.1.1",
59 | "typescript": "^5.4.5",
60 | "ufo": "^1.5.3",
61 | "vite": "^5.2.10"
62 | },
63 | "devDependencies": {
64 | "@types/node": "^20.12.7",
65 | "tsup": "^8.0.2"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/package/src/content-collections.ts:
--------------------------------------------------------------------------------
1 | type DataEntryConstraint = {
2 | id: string;
3 | collection: string;
4 | data: Record;
5 | };
6 | type ContentEntryConstraint = DataEntryConstraint & { slug: string };
7 | type EntryConstraint = DataEntryConstraint | ContentEntryConstraint;
8 | type ReferenceConstraint = {
9 | collection: TEntry["collection"];
10 | } & (TEntry extends ContentEntryConstraint
11 | ? {
12 | slug: TEntry["slug"];
13 | }
14 | : {
15 | id: TEntry["id"];
16 | });
17 |
18 | type PickKeysByValue = {
19 | [Key in keyof T as T[Key] extends ValueType ? Key : never]: T[Key];
20 | };
21 |
22 | const getEntrySlug = (entry: ReferenceConstraint) =>
23 | "slug" in entry ? entry.slug : entry.id;
24 |
25 | export const handleI18nSlug = (slug: string) => {
26 | const segments = slug.split("/");
27 | if (segments.length < 2) {
28 | throw new Error(
29 | `The slug "${slug}" does not match the correct format "[locale]/[...parts]"`,
30 | );
31 | }
32 | const [locale, ...parts] = segments;
33 |
34 | return {
35 | locale: locale as string,
36 | slug: parts.join("/"),
37 | };
38 | };
39 |
40 | export const collectionFilters = {
41 | byLocale: (
42 | entry: TEntry,
43 | { locale }: { locale: string },
44 | ): boolean => {
45 | const slug = getEntrySlug(entry);
46 |
47 | return handleI18nSlug(slug).locale === locale;
48 | },
49 | matchingEntries: <
50 | TEntry extends EntryConstraint,
51 | TKey extends keyof PickKeysByValue<
52 | TEntry["data"],
53 | ReferenceConstraint | undefined
54 | >,
55 | >(
56 | entry: TEntry,
57 | {
58 | key,
59 | currentEntry,
60 | locale,
61 | defaultLocale,
62 | }: {
63 | currentEntry: TEntry;
64 | key: TKey;
65 | locale: string;
66 | defaultLocale: string;
67 | },
68 | ): boolean => {
69 | const slug = getEntrySlug(entry);
70 | const currentEntrySlug = getEntrySlug(currentEntry);
71 | const reference = entry.data[key] as
72 | | ReferenceConstraint
73 | | undefined;
74 |
75 | if (locale === defaultLocale) {
76 | // Same entry as the current entry
77 | if (slug === currentEntrySlug) return true;
78 | // If there's no reference, that means the entry is not linked to the
79 | // current entry and can be safely ignored
80 | if (!reference) return false;
81 | // Wether or not the referenced entry is the current entry
82 | return getEntrySlug(reference) === currentEntrySlug;
83 | }
84 |
85 | const currentEntryReference = currentEntry.data[key] as
86 | | ReferenceConstraint
87 | | undefined;
88 |
89 | // If the current entry has no reference, it means that is not linked
90 | // to any other entries so we can ignore it
91 | if (!currentEntryReference) return false;
92 | // Same entry as the reference, ie. the default locale entry
93 | if (slug === getEntrySlug(currentEntryReference)) return true;
94 | // If there's no reference, that means the entry is not linked to the
95 | // current entry and can be safely ignored
96 | if (!reference) return false;
97 | // Wether or not the referenced entry is the same as the default locale entry
98 | return getEntrySlug(reference) === getEntrySlug(currentEntryReference);
99 | },
100 | };
101 |
--------------------------------------------------------------------------------
/package/src/i18next/index.ts:
--------------------------------------------------------------------------------
1 | import { join, relative } from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import { defineUtility, watchDirectory } from "astro-integration-kit";
4 | import { normalizePath } from "vite";
5 | import type { Options } from "../options.js";
6 | import { getNamespaces } from "./namespaces.js";
7 | import { getResources } from "./resources.js";
8 |
9 | const getPaths = (root: URL, options: Options) => {
10 | const localesDir = normalizePath(
11 | fileURLToPath(new URL(options.localesDir, root)),
12 | );
13 | const defaultLocalesDir = join(localesDir, options.defaultLocale);
14 |
15 | return {
16 | localesDir,
17 | defaultLocalesDir,
18 | };
19 | };
20 |
21 | const LOGGER_LABEL = "astro-i18n/i18next";
22 |
23 | export const handleI18next = defineUtility("astro:config:setup")(
24 | (params, options: Options) => {
25 | const logger = params.logger.fork(LOGGER_LABEL);
26 |
27 | const paths = getPaths(params.config.root, options);
28 | watchDirectory(params, paths.localesDir);
29 | logger.info(
30 | `Registered watcher for "${normalizePath(
31 | relative(fileURLToPath(params.config.root), paths.localesDir),
32 | )}" directory`,
33 | );
34 |
35 | const { namespaces } = getNamespaces(
36 | paths.defaultLocalesDir,
37 | options.defaultNamespace,
38 | logger,
39 | );
40 | const resources = getResources(logger, options, paths.localesDir);
41 | const dtsContent = `
42 | type Resources = ${JSON.stringify(resources[options.defaultLocale] ?? {})}
43 |
44 | declare module "i18next" {
45 | interface CustomTypeOptions {
46 | defaultNS: "${options.defaultNamespace}";
47 | resources: Resources;
48 | }
49 | }
50 | export {}
51 | `;
52 |
53 | return {
54 | namespaces,
55 | resources,
56 | dtsContent,
57 | };
58 | },
59 | );
60 |
--------------------------------------------------------------------------------
/package/src/i18next/namespaces.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readdirSync } from "node:fs";
2 | import { basename, extname } from "node:path";
3 | import type { AstroIntegrationLogger } from "astro";
4 |
5 | export const getNamespaces = (
6 | defaultLocalesDir: string,
7 | defaultNamespace: string,
8 | logger: AstroIntegrationLogger,
9 | ) => {
10 | const importsData: Array<{
11 | namespaceName: string;
12 | fileName: string;
13 | }> = [];
14 |
15 | if (existsSync(defaultLocalesDir)) {
16 | const filenames = readdirSync(defaultLocalesDir).filter((f) =>
17 | f.endsWith(".json"),
18 | );
19 | for (const fileName of filenames) {
20 | importsData.push({
21 | namespaceName: basename(fileName, extname(fileName)),
22 | fileName,
23 | });
24 | }
25 | }
26 |
27 | const namespaces = importsData.map((e) => e.namespaceName);
28 | logger.info(
29 | `Detected namespaces: ${namespaces.map((ns) => `"${ns}"`).join(",")}`,
30 | );
31 | if (!namespaces.includes(defaultNamespace)) {
32 | logger.warn(`Default namespace "${defaultNamespace}" is not detected`);
33 | }
34 |
35 | return {
36 | namespaces,
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/package/src/i18next/resources.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync, readdirSync } from "node:fs";
2 | import { basename, extname, join } from "node:path";
3 | import type { AstroIntegrationLogger } from "astro";
4 | import { normalizePath } from "vite";
5 | import type { Options } from "../options.js";
6 | import type { I18nextConfig } from "../types.js";
7 |
8 | export const getResources = (
9 | logger: AstroIntegrationLogger,
10 | { locales }: Options,
11 | localesDir: string,
12 | ) => {
13 | const resources: I18nextConfig["resources"] = {};
14 |
15 | const localesDirs = locales
16 | .map((locale) => ({
17 | locale,
18 | dir: normalizePath(join(localesDir, locale)),
19 | }))
20 | .filter((e) => existsSync(e.dir));
21 |
22 | for (const { locale, dir } of localesDirs) {
23 | const filenames = readdirSync(dir).filter((f) => f.endsWith(".json"));
24 |
25 | for (const fileName of filenames) {
26 | const path = normalizePath(join(dir, fileName));
27 | try {
28 | const content = JSON.parse(readFileSync(path, "utf-8"));
29 |
30 | resources[locale] ??= {};
31 | // biome-ignore lint/style/noNonNullAssertion: fallback is set above
32 | resources[locale]![basename(fileName, extname(fileName))] = content;
33 | } catch (err) {
34 | logger.warn(`Can't parse "${path}", skipping.`);
35 | }
36 | }
37 | }
38 |
39 | logger.info(
40 | `${Object.keys(Object.values(resources)).length} resources registered`,
41 | );
42 | return resources;
43 | };
44 |
--------------------------------------------------------------------------------
/package/src/index.ts:
--------------------------------------------------------------------------------
1 | import { integration } from "./integration.js";
2 |
3 | export default integration;
4 |
--------------------------------------------------------------------------------
/package/src/integration.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from "node:fs";
2 | import {
3 | addIntegration,
4 | addVirtualImports,
5 | createResolver,
6 | defineIntegration,
7 | } from "astro-integration-kit";
8 | import { handleI18next } from "./i18next/index.js";
9 | import { optionsSchema } from "./options.js";
10 | import { handleRouting } from "./routing/index.js";
11 | import { integration as sitemapIntegration } from "./sitemap/integration.js";
12 |
13 | const VIRTUAL_MODULE_ID = "i18n:astro";
14 | const CLIENT_ID = "__INTERNAL_ASTRO_I18N_CONFIG__";
15 |
16 | export const integration = defineIntegration({
17 | name: "astro-i18n",
18 | optionsSchema,
19 | setup({ options, name }) {
20 | const { resolve } = createResolver(import.meta.url);
21 |
22 | let dtsContent: string;
23 | let i18nextDtsContent: string;
24 |
25 | return {
26 | hooks: {
27 | "astro:config:setup": (params) => {
28 | const { addMiddleware, logger, updateConfig } = params;
29 |
30 | const { routes } = handleRouting(params, options);
31 | const {
32 | namespaces,
33 | resources,
34 | dtsContent: _dtsContent,
35 | } = handleI18next(params, options);
36 | i18nextDtsContent = _dtsContent;
37 |
38 | addMiddleware({
39 | entrypoint: resolve("../assets/middleware.ts"),
40 | order: "pre",
41 | });
42 |
43 | const defaultLocaleRoutes = routes.filter(
44 | (route) => route.locale === options.defaultLocale,
45 | );
46 |
47 | const virtualTypesStub = readFileSync(
48 | resolve("../assets/stubs/virtual.d.ts"),
49 | "utf-8",
50 | );
51 | const typesPlaceholders = {
52 | id: "@@_ID_@@",
53 | locale: '"@@_LOCALE_@@"',
54 | localePathParams: '"@@_LOCALE_PATH_PARAMS_@@"',
55 | locales: '"@@_LOCALES_@@"',
56 | };
57 |
58 | dtsContent = virtualTypesStub
59 | .replace(typesPlaceholders.id, VIRTUAL_MODULE_ID)
60 | .replace(
61 | typesPlaceholders.locale,
62 | options.locales.map((locale) => `"${locale}"`).join(" | "),
63 | )
64 | .replace(
65 | typesPlaceholders.localePathParams,
66 | `{${defaultLocaleRoutes
67 | .map(
68 | (route) =>
69 | `"${route.pattern}": ${
70 | route.params.length === 0
71 | ? "never"
72 | : `{
73 | ${route.params
74 | .map((param) => `"${param}": string;`)
75 | .join("\n")}
76 | }`
77 | }`,
78 | )
79 | .join(";\n")}}`,
80 | )
81 | .replace(
82 | typesPlaceholders.locales,
83 | JSON.stringify(options.locales),
84 | );
85 |
86 | if (options.sitemap) {
87 | addIntegration(params, {
88 | integration: sitemapIntegration({
89 | ...options.sitemap,
90 | internal: {
91 | i18n: {
92 | defaultLocale: options.defaultLocale,
93 | locales: options.locales,
94 | },
95 | routes,
96 | },
97 | }),
98 | });
99 |
100 | const virtualSitemapTypesStub = readFileSync(
101 | resolve("../assets/stubs/sitemap.d.ts"),
102 | "utf-8",
103 | );
104 |
105 | dtsContent += virtualSitemapTypesStub;
106 | }
107 |
108 | const enabledClientFeatures = Object.entries(options.client)
109 | .map(([name, enabled]) => ({ name, enabled }))
110 | .filter((e) => e.enabled);
111 | if (enabledClientFeatures.length > 0) {
112 | logger.info(
113 | `Client features enabled: ${enabledClientFeatures
114 | .map((e) => `"${e.name}"`)
115 | .join(
116 | ", ",
117 | )}. Make sure to use the \` \` component`,
118 | );
119 | }
120 |
121 | const virtualModuleStub = readFileSync(
122 | resolve("../assets/stubs/virtual.mjs"),
123 | "utf-8",
124 | );
125 | const scriptPlaceholders = {
126 | config: '"@@_CONFIG_@@"',
127 | i18next: '"@@_I18NEXT_@@"',
128 | };
129 |
130 | addVirtualImports(params, {
131 | name,
132 | imports: [
133 | {
134 | id: "virtual:astro-i18n/internal",
135 | content: `
136 | export const options = ${JSON.stringify(options)};
137 | export const routes = ${JSON.stringify(routes)};
138 | export const i18nextConfig = ${JSON.stringify({
139 | namespaces,
140 | defaultNamespace: options.defaultNamespace,
141 | resources,
142 | })};
143 | export const clientId = ${JSON.stringify(CLIENT_ID)};
144 | `,
145 | },
146 | {
147 | id: "virtual:astro-i18n/als",
148 | content: `
149 | import { AsyncLocalStorage } from "node:async_hooks";
150 | export const als = new AsyncLocalStorage;
151 | `,
152 | },
153 | {
154 | id: VIRTUAL_MODULE_ID,
155 | content: `
156 | import { als } from "virtual:astro-i18n/als";
157 | import _i18next from "i18next";
158 | ${virtualModuleStub
159 | .replaceAll(scriptPlaceholders.config, "als.getStore()")
160 | .replaceAll(scriptPlaceholders.i18next, "_i18next")}`,
161 | context: "server",
162 | },
163 | {
164 | id: VIRTUAL_MODULE_ID,
165 | content: (() => {
166 | let content = "";
167 | if (options.client.translations) {
168 | content += `import _i18next from "i18next"; `;
169 | }
170 |
171 | content += virtualModuleStub.replaceAll(
172 | scriptPlaceholders.config,
173 | `JSON.parse(document.getElementById(${JSON.stringify(
174 | CLIENT_ID,
175 | )}).textContent)`,
176 | );
177 |
178 | if (options.client.translations) {
179 | content = content.replaceAll(
180 | scriptPlaceholders.i18next,
181 | "_i18next",
182 | );
183 | }
184 |
185 | return content;
186 | })(),
187 | context: "client",
188 | },
189 | ],
190 | });
191 |
192 | logger.info("Types injected");
193 |
194 | if (options.strategy === "prefix" && options.rootRedirect) {
195 | updateConfig({
196 | redirects: {
197 | "/": options.rootRedirect,
198 | },
199 | });
200 | }
201 | },
202 | "astro:config:done": (params) => {
203 | params.injectTypes({
204 | filename: "astro-i18n.d.ts",
205 | content: dtsContent,
206 | });
207 | params.injectTypes({
208 | filename: "i18next.d.ts",
209 | content: i18nextDtsContent,
210 | });
211 | },
212 | },
213 | };
214 | },
215 | });
216 |
--------------------------------------------------------------------------------
/package/src/internal.ts:
--------------------------------------------------------------------------------
1 | export type { CallbackSchema } from "./sitemap/route-config.js";
2 |
--------------------------------------------------------------------------------
/package/src/options.ts:
--------------------------------------------------------------------------------
1 | import { z } from "astro/zod";
2 | import { withLeadingSlash, withoutTrailingSlash } from "ufo";
3 | import {
4 | type SitemapOptions,
5 | publicOptionsSchema as sitemapOptionsSchema,
6 | } from "./sitemap/options.js";
7 |
8 | const routeStringSchema = z.string().regex(/^[a-zA-Z0-9_/[\]-]+$/);
9 | const redirectStatusSchema = z
10 | .literal(300)
11 | .or(z.literal(301))
12 | .or(z.literal(302))
13 | .or(z.literal(303))
14 | .or(z.literal(304))
15 | .or(z.literal(307))
16 | .or(z.literal(308));
17 |
18 | export const optionsSchema = z
19 | .object({
20 | /**
21 | * @description Sets the default locale for your website.
22 | * @link https://astro-i18n.netlify.app/usage/configuration/#defaultlocale-required
23 | */
24 | defaultLocale: z.string(),
25 | /**
26 | * @description Sets the available locales for your website. Must include the default locale.
27 | * @link https://astro-i18n.netlify.app/usage/configuration/#locales-required
28 | */
29 | locales: z.array(z.string()),
30 | /**
31 | * @description Defines how your routes are generated:
32 | *
33 | * - `"prefixWithoutDefault"` will not add a prefix for your default locale *
34 | * - `"prefix"` will add a prefix for your default locale.
35 | *
36 | * @default `"prefixWithoutDefault"`
37 | * @link https://astro-i18n.netlify.app/usage/configuration/#strategy
38 | */
39 | strategy: z
40 | .enum(["prefix", "prefixExceptDefault"])
41 | .optional()
42 | .default("prefixExceptDefault"),
43 | /**
44 | * @description Allows you to define translated paths for your locales.
45 | * @link https://astro-i18n.netlify.app/usage/configuration/#pages
46 | */
47 | pages: z
48 | .record(
49 | routeStringSchema,
50 | z.record(z.string(), routeStringSchema.optional()),
51 | )
52 | .optional()
53 | .default({})
54 | .transform((val) =>
55 | Object.fromEntries(
56 | Object.entries(val).map(([key, value]) => [
57 | withLeadingSlash(withoutTrailingSlash(key)),
58 | value,
59 | ]),
60 | ),
61 | ),
62 | /**
63 | * @description A path relative to the root where locales files are located for translations features.
64 | * @default `"./src/locales"`
65 | * @link https://astro-i18n.netlify.app/usage/configuration/#localesdir
66 | */
67 | localesDir: z
68 | .string()
69 | .optional()
70 | .default("./src/locales")
71 | .refine((val) => val.startsWith("./") || val.startsWith("../"), {
72 | message: "Must be a relative path (ie. start with `./` or `../`)",
73 | }),
74 | /**
75 | * @description Sets the default namespace for locales. Since `astro-i18n` uses `i18next` under the hood,
76 | * it allows to split translations data in multiple json files under `src/locales/[locale]/`. If you're not
77 | * using a file called `common.json`, you need to update this property to have proper types completions
78 | * when using `t`.
79 | *
80 | * @default `"common"`
81 | * @link https://astro-i18n.netlify.app/usage/configuration/#defaultnamespace
82 | */
83 | defaultNamespace: z.string().optional().default("common"),
84 | /**
85 | * @description Client usage is disabled by default because it sends some JavaScript to the browser. Enabling
86 | * any of the following features requires importing the ` ` component.
87 | *
88 | * - `t`: `data`, `translations`
89 | * - `getLocale`: `data`
90 | * - `getLocales`: `data`
91 | * - `getDefaultLocale`: `data`
92 | * - `getHtmlAttrs`: `data`
93 | * - `setDynamicParams`: N/A, server only
94 | * - `getLocalePath`: `data`, `paths`
95 | * - `switchLocalePath`: `data`, `paths`
96 | * - `getSwitcherData`: `data`, `paths`
97 | * - `getLocalePlaceholder`: N/A, `getStaticPaths` only
98 | * - `getLocalesPlaceholder`: N/A, `getStaticPaths` only
99 | * - `getDefaultLocalePlaceholder`: N/A, `getStaticPaths` only
100 | *
101 | * @default `false`
102 | * @link https://astro-i18n.netlify.app/usage/configuration/#client
103 | */
104 | client: z
105 | .literal(false)
106 | .or(
107 | z.object({
108 | /**
109 | * @description Allows using `t` on the client.
110 | * @default `false`
111 | * @link https://astro-i18n.netlify.app/usage/configuration/#client
112 | */
113 | translations: z.boolean().optional().default(false),
114 | /**
115 | * @description Allows using `t`, `getLocale`, `getLocales`, `getHtmlAttrs`, `getLocalePath`,
116 | * `switchLocalePath` and `getSwitcherData` on the client.
117 | *
118 | * @default `false`
119 | * @link https://astro-i18n.netlify.app/usage/configuration/#client
120 | */
121 | data: z.boolean().optional().default(false),
122 | /**
123 | * @description Allows using `getLocalePath`, `switchLocalePath` and `getSwitcherData` on the client.
124 | * @default `false`
125 | * @link https://astro-i18n.netlify.app/usage/configuration/#client
126 | */
127 | paths: z.boolean().optional().default(false),
128 | }),
129 | )
130 | .optional()
131 | .default(false)
132 | .transform((val) =>
133 | typeof val === "boolean"
134 | ? {
135 | data: val,
136 | translations: val,
137 | paths: val,
138 | }
139 | : val,
140 | ),
141 | /**
142 | * @description When using `strategy: "prefix"`, you may want to redirect your users from the root to a specific
143 | * page (likely the default locale root). This option allows you to do so.
144 | * @link https://astro-i18n.netlify.app/usage/configuration/#rootredirect
145 | */
146 | rootRedirect: z
147 | .object({
148 | status: redirectStatusSchema,
149 | destination: z.string(),
150 | })
151 | .optional(),
152 | /**
153 | * @description Allows to generate a sitemap that adapts to your i18n content
154 | * @link https://astro-i18n.netlify.app/usage/configuration/#sitemap
155 | */
156 | sitemap: z
157 | .union([z.boolean(), sitemapOptionsSchema])
158 | .optional()
159 | .default(false)
160 | .transform((val) =>
161 | val === false ? undefined : val === true ? ({} as SitemapOptions) : val,
162 | ),
163 | })
164 | .refine(({ locales, defaultLocale }) => locales.includes(defaultLocale), {
165 | message: "`locales` must include the `defaultLocale`",
166 | path: ["locales"],
167 | })
168 | .refine(
169 | ({ pages, locales }) =>
170 | Object.values(pages).every((record) =>
171 | Object.keys(record).every((locale) => locales.includes(locale)),
172 | ),
173 | {
174 | message: "`pages` locale keys must be included in `locales`",
175 | path: ["pages"],
176 | },
177 | )
178 | .refine(
179 | ({ strategy, rootRedirect }) => {
180 | if (strategy === "prefix") {
181 | return true;
182 | }
183 | return rootRedirect === undefined;
184 | },
185 | {
186 | message: "`rootRedirect` should only be used with `strategy: 'prefix'`",
187 | path: ["rootRedirect"],
188 | },
189 | );
190 |
191 | export type Options = z.infer;
192 |
--------------------------------------------------------------------------------
/package/src/routing/hmr.ts:
--------------------------------------------------------------------------------
1 | import { join, relative } from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import type { AstroIntegrationLogger } from "astro";
4 | import { defineUtility, watchDirectory } from "astro-integration-kit";
5 | import { normalizePath } from "vite";
6 | import { ROUTES_DIR } from "./index.js";
7 |
8 | export const handleRoutesHMR = defineUtility("astro:config:setup")(
9 | (params, logger: AstroIntegrationLogger) => {
10 | const { config } = params;
11 |
12 | const dir = normalizePath(join(fileURLToPath(config.srcDir), ROUTES_DIR));
13 | watchDirectory(params, dir);
14 | logger.info(
15 | `Registered watcher for "${normalizePath(
16 | relative(fileURLToPath(params.config.root), dir),
17 | )}" directory`,
18 | );
19 | },
20 | );
21 |
--------------------------------------------------------------------------------
/package/src/routing/index.ts:
--------------------------------------------------------------------------------
1 | import { defineUtility } from "astro-integration-kit";
2 | import type { Options } from "../options.js";
3 | import { handleRoutesHMR } from "./hmr.js";
4 | import { registerRoutes } from "./register.js";
5 |
6 | export const ROUTES_DIR = "routes";
7 | const LOGGER_LABEL = "astro-i18n/routing";
8 |
9 | export const handleRouting = defineUtility("astro:config:setup")(
10 | (params, options: Options) => {
11 | const logger = params.logger.fork(LOGGER_LABEL);
12 |
13 | handleRoutesHMR(params, logger);
14 | const { routes } = registerRoutes(params, options, logger);
15 |
16 | return { routes };
17 | },
18 | );
19 |
--------------------------------------------------------------------------------
/package/src/routing/register.ts:
--------------------------------------------------------------------------------
1 | import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2 | import { dirname, join, relative, resolve } from "node:path";
3 | import { fileURLToPath } from "node:url";
4 | import type {
5 | AstroIntegrationLogger,
6 | HookParameters,
7 | InjectedRoute,
8 | } from "astro";
9 | import { defineUtility } from "astro-integration-kit";
10 | import { addPageDir } from "astro-pages";
11 | import { AstroError } from "astro/errors";
12 | import { withLeadingSlash } from "ufo";
13 | import { normalizePath } from "vite";
14 | import type { Options } from "../options.js";
15 | import type { Route } from "../types.js";
16 | import { ROUTES_DIR } from "./index.js";
17 |
18 | const isPrerendered = (str: string) => {
19 | const match = str.match(/export const prerender = (\w+)/);
20 | if (match) {
21 | return match[1] === "true";
22 | }
23 | return undefined;
24 | };
25 |
26 | const getPages = (
27 | params: HookParameters<"astro:config:setup">,
28 | ): Array => {
29 | // @ts-ignore TODO: update astro-apges when types are fixed
30 | return Object.entries(addPageDir({ ...params, dir: ROUTES_DIR }).pages).map(
31 | ([pattern, entrypoint]) => ({ pattern, entrypoint }),
32 | );
33 | };
34 |
35 | const getPaths = defineUtility("astro:config:setup")(({ config }) => {
36 | const routesDir = fileURLToPath(new URL(ROUTES_DIR, config.srcDir));
37 | const entrypointsDir = resolve(
38 | fileURLToPath(config.root),
39 | "./.astro/astro-i18n/entrypoints",
40 | );
41 |
42 | return {
43 | routesDir,
44 | entrypointsDir,
45 | };
46 | });
47 |
48 | const generateRoute = (
49 | { strategy, defaultLocale, locales, pages }: Options,
50 | locale: string,
51 | page: InjectedRoute,
52 | paths: ReturnType,
53 | logger: AstroIntegrationLogger,
54 | ): Route => {
55 | const getPattern = () => {
56 | const isDefaultLocale = locale === defaultLocale;
57 | const prefix =
58 | isDefaultLocale && strategy === "prefixExceptDefault" ? "" : `/${locale}`;
59 | const suffix = withLeadingSlash(
60 | isDefaultLocale
61 | ? page.pattern
62 | : pages?.[page.pattern]?.[locale] ?? page.pattern,
63 | );
64 | return prefix + suffix;
65 | };
66 |
67 | const transformContent = (entrypoint: string) => {
68 | const updateRelativeImports = (
69 | originalPath: string,
70 | currentFilePath: string,
71 | newFilePath: string,
72 | ) => {
73 | const absolutePath = resolve(dirname(currentFilePath), originalPath);
74 | const relativePath = relative(dirname(newFilePath), absolutePath);
75 | return normalizePath(relativePath);
76 | };
77 |
78 | mkdirSync(dirname(entrypoint), { recursive: true });
79 |
80 | let content = readFileSync(page.entrypoint, "utf-8");
81 |
82 | if (page.entrypoint.endsWith(".astro")) {
83 | try {
84 | content = content
85 | .replaceAll("getLocalePlaceholder()", `"${locale}"`)
86 | .replaceAll(
87 | "getLocalesPlaceholder()",
88 | `[${locales.map((locale) => `"${locale}"`).join(", ")}]`,
89 | )
90 | .replaceAll("getDefaultLocalePlaceholder()", `"${defaultLocale}"`);
91 |
92 | let [, frontmatter, ...body] = content.split("---");
93 | if (!frontmatter) {
94 | throw new Error("No frontmatter found");
95 | }
96 | // Handle static imports
97 | frontmatter = frontmatter.replace(
98 | /import\s+([\s\S]*?)\s+from\s+['"](.+?)['"]/g,
99 | (_match, p1: string, p2: string) => {
100 | const updatedPath =
101 | p2.startsWith("./") || p2.startsWith("../")
102 | ? updateRelativeImports(p2, page.entrypoint, entrypoint)
103 | : p2;
104 | return `import ${p1} from '${updatedPath}'`;
105 | },
106 | );
107 | // Handle dynamic imports
108 | frontmatter = frontmatter.replace(
109 | /import\s*\(\s*['"](.+?)['"]\s*\)/g,
110 | (_match, p1: string) => {
111 | const updatedPath =
112 | p1.startsWith("./") || p1.startsWith("../")
113 | ? updateRelativeImports(p1, page.entrypoint, entrypoint)
114 | : p1;
115 | return `import('${updatedPath}')`;
116 | },
117 | );
118 |
119 | content = `---${frontmatter}---${body.join("---")}`;
120 | } catch (err) {
121 | throw new AstroError(
122 | `An error occured while transforming "${page.entrypoint}".`,
123 | "Make sure it has a valid frontmatter, even empty",
124 | );
125 | }
126 | }
127 |
128 | writeFileSync(entrypoint, content, "utf-8");
129 |
130 | return {
131 | prerender: isPrerendered(content),
132 | };
133 | };
134 |
135 | const getParams = (pattern: string) => {
136 | const params: Array = [];
137 |
138 | const matches = pattern.match(/\[([^\]]+)]/g);
139 | if (matches) {
140 | for (const match of matches) {
141 | params.push(match.slice(1, -1));
142 | }
143 | }
144 |
145 | return params;
146 | };
147 |
148 | const pattern = getPattern();
149 | const entrypoint = join(
150 | paths.entrypointsDir,
151 | locale,
152 | normalizePath(relative(paths.routesDir, page.entrypoint)),
153 | );
154 | const { prerender } = transformContent(entrypoint);
155 |
156 | logger.info(`Injecting "${pattern}" route`);
157 | return {
158 | locale,
159 | params: getParams(pattern),
160 | pattern: page.pattern,
161 | injectedRoute: {
162 | pattern,
163 | entrypoint,
164 | ...(prerender ? { prerender } : {}),
165 | },
166 | };
167 | };
168 |
169 | export const registerRoutes = (
170 | params: HookParameters<"astro:config:setup">,
171 | options: Options,
172 | logger: AstroIntegrationLogger,
173 | ) => {
174 | const { config, injectRoute } = params;
175 | const { locales } = options;
176 | logger.info("Starting routes injection...");
177 |
178 | const paths = getPaths(params);
179 | rmSync(paths.entrypointsDir, { recursive: true, force: true });
180 | logger.info(
181 | `Cleaned "${normalizePath(
182 | relative(fileURLToPath(config.root), paths.entrypointsDir),
183 | )}" directory`,
184 | );
185 |
186 | const routes: Array = [];
187 | const pages = getPages(params);
188 |
189 | for (const locale of locales) {
190 | for (const page of pages) {
191 | routes.push(generateRoute(options, locale, page, paths, logger));
192 | }
193 | }
194 |
195 | for (const { injectedRoute } of routes) {
196 | injectRoute(injectedRoute);
197 | }
198 |
199 | return { routes };
200 | };
201 |
--------------------------------------------------------------------------------
/package/src/sitemap/generate-sitemap.ts:
--------------------------------------------------------------------------------
1 | import type { AstroConfig } from "astro";
2 | import type { LinkItem, SitemapItemLoose } from "sitemap";
3 | import type { Route } from "./integration.js";
4 | import type { SitemapOptions } from "./options.js";
5 | import {
6 | createImpossibleError,
7 | handleTrailingSlash,
8 | normalizeDynamicParams,
9 | } from "./utils.js";
10 |
11 | type NoUndefinedField = {
12 | [P in keyof T]-?: NonNullable;
13 | };
14 |
15 | /** Construct sitemap.xml given a set of URLs */
16 | export function generateSitemap(
17 | routes: Array,
18 | _finalSiteUrl: string,
19 | opts: SitemapOptions,
20 | config: AstroConfig,
21 | ) {
22 | const { changefreq, priority, lastmod: lastmodSrc } = opts;
23 | const lastmod = lastmodSrc?.toISOString();
24 |
25 | const getLinksFromRoute = (route: NoUndefinedField, page: string) => {
26 | if (!route.route) {
27 | return [];
28 | }
29 |
30 | const links: Array = [];
31 |
32 | const equivalentRoutes = routes.filter(
33 | (e) =>
34 | e.route &&
35 | e.route.pattern === route.route.pattern &&
36 | e.route.locale !== route.route.locale,
37 | ) as Array>;
38 |
39 | links.push({
40 | lang: route.route.locale,
41 | url: page,
42 | });
43 |
44 | // Handle static links
45 | if (route.routeData.params.length === 0) {
46 | for (const equivalentRoute of equivalentRoutes) {
47 | links.push({
48 | lang: equivalentRoute.route.locale,
49 | url: handleTrailingSlash(
50 | `${new URL(page).origin}${
51 | equivalentRoute.route.injectedRoute.pattern
52 | }`,
53 | config,
54 | ),
55 | });
56 | }
57 |
58 | return [...links].sort((a, b) =>
59 | a.lang.localeCompare(b.lang, "en", { numeric: true }),
60 | );
61 | }
62 |
63 | const index = route.pages.indexOf(page);
64 | const sitemapOptions = route.sitemapOptions.filter(
65 | (e) =>
66 | e.dynamicParams &&
67 | (Array.isArray(e.dynamicParams)
68 | ? e.dynamicParams
69 | : Object.entries(e.dynamicParams)
70 | ).length > 0,
71 | )[index];
72 | if (!sitemapOptions || !sitemapOptions.dynamicParams) {
73 | return [];
74 | }
75 |
76 | for (const equivalentRoute of equivalentRoutes) {
77 | const options = normalizeDynamicParams(sitemapOptions.dynamicParams).find(
78 | (e) => e.locale === equivalentRoute.route.locale,
79 | );
80 |
81 | if (!options) {
82 | // A dynamic route is not required to always have an equivalent in another language eg.
83 | // en: /blog/a
84 | // fr: /fr/le-blog/b
85 | // it: none
86 | continue;
87 | }
88 |
89 | let newPage = equivalentRoute.route.injectedRoute.pattern;
90 | for (const [key, value] of Object.entries(options.params)) {
91 | if (!value) {
92 | throw createImpossibleError(
93 | "This situation should never occur (value is not set)",
94 | );
95 | }
96 |
97 | newPage = newPage.replace(`[${key}]`, value);
98 | }
99 | newPage = handleTrailingSlash(
100 | `${new URL(page).origin}${newPage}`,
101 | config,
102 | );
103 | links.push({
104 | lang: equivalentRoute.route.locale,
105 | url: newPage,
106 | });
107 | }
108 | return [...links].sort((a, b) =>
109 | a.lang.localeCompare(b.lang, "en", { numeric: true }),
110 | );
111 | };
112 |
113 | const urlData: Array = [];
114 | for (const route of routes) {
115 | for (const page of route.pages) {
116 | const links: Array = [];
117 | if (route.route) {
118 | const _links = getLinksFromRoute(
119 | // Required because TS
120 | {
121 | ...route,
122 | route: route.route,
123 | },
124 | page,
125 | );
126 | links.push(..._links);
127 | }
128 |
129 | const obj: SitemapItemLoose = {
130 | url: page,
131 | links,
132 | };
133 |
134 | // TODO: get from sitemap options first
135 | if (changefreq) {
136 | Object.assign(obj, { changefreq });
137 | }
138 | if (lastmod) {
139 | Object.assign(obj, { lastmod });
140 | }
141 | if (priority) {
142 | Object.assign(obj, { priority });
143 | }
144 |
145 | urlData.push(obj);
146 | }
147 | }
148 |
149 | return [...urlData].sort((a, b) =>
150 | a.url.localeCompare(b.url, "en", { numeric: true }),
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/package/src/sitemap/integration.ts:
--------------------------------------------------------------------------------
1 | import { relative } from "node:path";
2 | import { fileURLToPath } from "node:url";
3 | import routeConfigPlugin from "@inox-tools/aik-route-config";
4 | import type { AstroConfig, InjectedRoute, RouteData } from "astro";
5 | import {
6 | defineIntegration,
7 | hasIntegration,
8 | withPlugins,
9 | } from "astro-integration-kit";
10 | import { AstroError } from "astro/errors";
11 | import { z } from "astro/zod";
12 | import { simpleSitemapAndIndex } from "sitemap";
13 | import { withoutTrailingSlash } from "ufo";
14 | import { normalizePath } from "vite";
15 | import type { Route as InternalRoute } from "../types.js";
16 | import { generateSitemap } from "./generate-sitemap.js";
17 | import { optionsSchema } from "./options.js";
18 | import { type CallbackSchema, callbackSchema } from "./route-config.js";
19 | import {
20 | createImpossibleError,
21 | formatConfigErrorMessage,
22 | getPathnameFromRouteData,
23 | handleTrailingSlash,
24 | isStatusCodePage,
25 | normalizeDynamicParams,
26 | } from "./utils.js";
27 |
28 | const OUTFILE = "sitemap-index.xml";
29 |
30 | // strictest forces us to do weird things
31 | type _RouteRoute = Omit & {
32 | injectedRoute: Omit & {
33 | prerender?: boolean | undefined;
34 | };
35 | };
36 |
37 | export type Route = {
38 | pages: Array;
39 | route: _RouteRoute | undefined;
40 | routeData: RouteData;
41 | sitemapOptions: Array>;
42 | include: boolean;
43 | };
44 |
45 | export const integration = defineIntegration({
46 | name: "astro-i18n/sitemap",
47 | optionsSchema,
48 | setup({ options, name }) {
49 | const initialRoutes: Array = options.internal.routes.map(
50 | (route) => ({
51 | pages: [],
52 | route,
53 | routeData: undefined as unknown as RouteData,
54 | sitemapOptions: [],
55 | include: true,
56 | }),
57 | );
58 |
59 | let config: AstroConfig;
60 |
61 | return withPlugins({
62 | name,
63 | plugins: [routeConfigPlugin],
64 | hooks: {
65 | "astro:config:setup": ({ defineRouteConfig, ...params }) => {
66 | const { logger } = params;
67 |
68 | if (hasIntegration(params, { name: "@astrojs/sitemap" })) {
69 | throw new AstroError(
70 | "Cannot use both `@astrolicious/i18n` sitemap and `@astrojs/sitemap` integrations at the same time.",
71 | "Remove the `@astrojs/sitemap` integration from your project.",
72 | );
73 | }
74 |
75 | config = params.config;
76 |
77 | defineRouteConfig({
78 | importName: "i18n:astro/sitemap",
79 | callbackHandler: ({ routeData }, callback) => {
80 | const response = callbackSchema.safeParse(callback);
81 | if (!response.success) {
82 | throw new AstroError(
83 | formatConfigErrorMessage(response.error),
84 | "Check your usage of `astro:i18n/sitemap`",
85 | );
86 | }
87 | for (const r of routeData) {
88 | const route = initialRoutes.find(
89 | (e) =>
90 | e.route?.injectedRoute.pattern ===
91 | getPathnameFromRouteData(r),
92 | );
93 | if (!route) {
94 | continue;
95 | }
96 |
97 | route.routeData = r;
98 | route.include = response.data !== false;
99 | if (response.data !== false) {
100 | if (
101 | response.data.changefreq ||
102 | response.data.lastmod ||
103 | response.data.priority
104 | ) {
105 | logger.warn(
106 | `Setting \`changefreq\`, \`lastmod\` or \`priority\` on a route basis is not implemented yet (eg. on "${r.component}")`,
107 | );
108 | }
109 | route.sitemapOptions.push(response.data);
110 | if (route.route) {
111 | const { locale, injectedRoute } = route.route;
112 | const params = normalizeDynamicParams(
113 | response.data.dynamicParams,
114 | )?.find((e) => e.locale === locale);
115 | if (params) {
116 | let page = injectedRoute.pattern;
117 | for (const [key, value] of Object.entries(
118 | params.params,
119 | )) {
120 | if (value) {
121 | page = page.replace(`[${key}]`, value);
122 | }
123 | }
124 | route.pages.push(page);
125 | }
126 | }
127 | }
128 | }
129 | },
130 | });
131 | },
132 | "astro:build:done": async (params) => {
133 | const { logger } = params;
134 |
135 | for (const route of initialRoutes) {
136 | if (route.pages.length === 0 && route.route) {
137 | route.pages.push(route.route.injectedRoute.pattern);
138 | }
139 | }
140 |
141 | for (const r of initialRoutes.filter((e) => !e.routeData)) {
142 | const routeData = params.routes.find(
143 | (e) =>
144 | withoutTrailingSlash(r.route?.injectedRoute.pattern) ===
145 | getPathnameFromRouteData(e),
146 | );
147 | if (!routeData) {
148 | throw createImpossibleError(
149 | "This situation should never occur (a corresponding routeData should always be found)",
150 | );
151 | }
152 | r.routeData = routeData;
153 | r.include = routeData.type === "page";
154 | }
155 |
156 | const _routes = [
157 | ...initialRoutes,
158 | ...params.routes
159 | .filter(
160 | (e) =>
161 | !initialRoutes
162 | .map((e) => getPathnameFromRouteData(e.routeData))
163 | .includes(getPathnameFromRouteData(e)),
164 | )
165 | .map((routeData) => {
166 | const route: Route = {
167 | include: true,
168 | routeData,
169 | pages: [],
170 | route: undefined,
171 | sitemapOptions: [],
172 | };
173 |
174 | return route;
175 | }),
176 | ];
177 |
178 | try {
179 | if (!config.site) {
180 | logger.warn(
181 | "The Sitemap integration requires the `site` astro.config option. Skipping.",
182 | );
183 | return;
184 | }
185 |
186 | const { customPages, entryLimit } = options;
187 |
188 | if (!config.site) {
189 | logger.warn(
190 | "The `site` astro.config option is required. Skipping.",
191 | );
192 | return;
193 | }
194 | const finalSiteUrl = new URL(config.base, config.site);
195 |
196 | let pageUrls = params.pages
197 | .filter((p) => !isStatusCodePage(p.pathname))
198 | .map((p) => {
199 | if (p.pathname !== "" && !finalSiteUrl.pathname.endsWith("/"))
200 | finalSiteUrl.pathname += "/";
201 | if (p.pathname.startsWith("/"))
202 | p.pathname = p.pathname.slice(1);
203 | const fullPath = finalSiteUrl.pathname + p.pathname;
204 | return new URL(fullPath, finalSiteUrl).href;
205 | });
206 |
207 | const routeUrls = _routes.reduce((urls, route) => {
208 | const r = route.routeData;
209 | if (!r) {
210 | return urls;
211 | }
212 | // Only expose pages, not endpoints or redirects
213 | if (r.type !== "page") return urls;
214 |
215 | /**
216 | * Dynamic URLs have entries with `undefined` pathnames
217 | */
218 | if (r.pathname) {
219 | if (isStatusCodePage(r.pathname ?? r.route)) return urls;
220 |
221 | // `finalSiteUrl` may end with a trailing slash
222 | // or not because of base paths.
223 | let fullPath = finalSiteUrl.pathname;
224 | if (fullPath.endsWith("/"))
225 | fullPath += r.generate(r.pathname).substring(1);
226 | else fullPath += r.generate(r.pathname);
227 |
228 | const newUrl = new URL(fullPath, finalSiteUrl).href;
229 |
230 | urls.push(handleTrailingSlash(newUrl, config));
231 | }
232 |
233 | return urls;
234 | }, []);
235 |
236 | pageUrls = Array.from(
237 | new Set([...pageUrls, ...routeUrls, ...(customPages ?? [])]),
238 | );
239 |
240 | pageUrls = pageUrls.filter((page) => {
241 | const route = normalizePath(
242 | `/${relative(config.base, new URL(page).pathname)}`,
243 | );
244 |
245 | const excludedRoutes = _routes.filter((e) => !e.include);
246 | for (const { routeData } of excludedRoutes) {
247 | // biome-ignore lint/style/noNonNullAssertion:
248 | if (routeData!.pattern.test(route)) {
249 | return false;
250 | }
251 | }
252 | return true;
253 | });
254 |
255 | if (pageUrls.length === 0) {
256 | logger.warn(`No pages found!\n\`${OUTFILE}\` not created.`);
257 | return;
258 | }
259 |
260 | for (const route of _routes.filter((e) => e.include)) {
261 | route.pages = route.pages.map((page) =>
262 | page.startsWith("/")
263 | ? handleTrailingSlash(
264 | new URL(page, finalSiteUrl).href,
265 | config,
266 | )
267 | : page,
268 | );
269 | }
270 |
271 | const urlData = generateSitemap(
272 | _routes.filter((e) => e.include),
273 | finalSiteUrl.href,
274 | options,
275 | config,
276 | );
277 |
278 | const destDir = fileURLToPath(params.dir);
279 | await simpleSitemapAndIndex({
280 | hostname: finalSiteUrl.href,
281 | destinationDir: destDir,
282 | sourceData: urlData,
283 | limit: entryLimit,
284 | gzip: false,
285 | });
286 | logger.info(
287 | `\`${OUTFILE}\` created at \`${relative(
288 | process.cwd(),
289 | destDir,
290 | )}\``,
291 | );
292 | } catch (err) {
293 | if (err instanceof z.ZodError) {
294 | logger.warn(formatConfigErrorMessage(err));
295 | } else {
296 | throw err;
297 | }
298 | }
299 | },
300 | },
301 | });
302 | },
303 | });
304 |
--------------------------------------------------------------------------------
/package/src/sitemap/options.ts:
--------------------------------------------------------------------------------
1 | import { z } from "astro/zod";
2 | import { EnumChangefreq } from "sitemap";
3 |
4 | export const publicOptionsSchema = z.object({
5 | customPages: z.array(z.string().url()).optional(),
6 | entryLimit: z.number().min(1).optional().default(45000),
7 | changefreq: z.nativeEnum(EnumChangefreq).optional(),
8 | lastmod: z.date().optional(),
9 | priority: z.number().min(0).max(1).optional(),
10 | });
11 | export const privateOptionsSchema = z.object({
12 | internal: z.object({
13 | i18n: z.object({
14 | defaultLocale: z.string(),
15 | locales: z.array(z.string()),
16 | }),
17 | routes: z.array(
18 | z.object({
19 | locale: z.string(),
20 | params: z.array(z.string()),
21 | pattern: z.string(),
22 | injectedRoute: z.object({
23 | pattern: z.string(),
24 | entrypoint: z.string(),
25 | prerender: z.boolean().optional(),
26 | }),
27 | }),
28 | ),
29 | }),
30 | });
31 |
32 | export const optionsSchema = publicOptionsSchema.and(privateOptionsSchema);
33 |
34 | export type SitemapOptions = z.infer;
35 |
--------------------------------------------------------------------------------
/package/src/sitemap/route-config.ts:
--------------------------------------------------------------------------------
1 | import { z } from "astro/zod";
2 | import { publicOptionsSchema } from "./options.js";
3 |
4 | export const callbackSchema = z
5 | .union([
6 | z.literal(false),
7 | z
8 | .object({
9 | dynamicParams: z
10 | .union([
11 | z.record(z.record(z.string().optional())),
12 | z.array(
13 | z.object({
14 | locale: z.string(),
15 | params: z.record(z.string()),
16 | }),
17 | ),
18 | ])
19 | .optional(),
20 | })
21 | .and(
22 | publicOptionsSchema
23 | .pick({
24 | lastmod: true,
25 | priority: true,
26 | changefreq: true,
27 | })
28 | .partial(),
29 | ),
30 | ])
31 | .optional()
32 | .default({});
33 |
34 | export type CallbackSchema = z.infer;
35 |
--------------------------------------------------------------------------------
/package/src/sitemap/utils.ts:
--------------------------------------------------------------------------------
1 | import type { AstroConfig, RouteData } from "astro";
2 | import { AstroError } from "astro/errors";
3 | import type { ZodError } from "astro/zod";
4 | import type { Route } from "./integration.js";
5 |
6 | const STATUS_CODE_PAGES = new Set(["404", "500"]);
7 |
8 | export const isStatusCodePage = (_pathname: string): boolean => {
9 | let pathname = _pathname;
10 | if (pathname.endsWith("/")) {
11 | pathname = pathname.slice(0, -1);
12 | }
13 | const end = pathname.split("/").pop() ?? "";
14 | return STATUS_CODE_PAGES.has(end);
15 | };
16 |
17 | export const formatConfigErrorMessage = (err: ZodError) => {
18 | const errorList = err.issues.map(
19 | (issue) => ` ${issue.path.join(".")} ${`${issue.message}.`}`,
20 | );
21 | return errorList.join("\n");
22 | };
23 |
24 | export const createImpossibleError = (message: string) =>
25 | new AstroError(
26 | message,
27 | "Please open an issue on GitHub at https://github.com/astrolicious/i18n/issues",
28 | );
29 |
30 | export const getPathnameFromRouteData = ({ segments }: RouteData) => {
31 | const pathname = segments
32 | .map((segment) => {
33 | return segment
34 | .map((rp) => (rp.dynamic ? `[${rp.content}]` : rp.content))
35 | .join("");
36 | })
37 | .join("/");
38 |
39 | return `/${pathname}`;
40 | };
41 |
42 | export const normalizeDynamicParams = (
43 | _params: Route["sitemapOptions"][number]["dynamicParams"],
44 | ) => {
45 | if (!_params) {
46 | return [];
47 | }
48 |
49 | if (Array.isArray(_params)) {
50 | return _params;
51 | }
52 |
53 | return Object.entries(_params).map(([locale, params]) => ({
54 | locale,
55 | params,
56 | }));
57 | };
58 |
59 | export const handleTrailingSlash = (url: string, config: AstroConfig) => {
60 | if (config.trailingSlash === "never") {
61 | return url;
62 | }
63 | if (config.build.format === "directory" && !url.endsWith("/")) {
64 | return `${url}/`;
65 | }
66 | return url;
67 | };
68 |
--------------------------------------------------------------------------------
/package/src/types.ts:
--------------------------------------------------------------------------------
1 | export type I18nextConfig = {
2 | namespaces: Array;
3 | defaultNamespace: string;
4 | resources: Record>;
5 | };
6 |
7 | export type Route = {
8 | locale: string;
9 | params: Array;
10 | pattern: string;
11 | injectedRoute: import("astro").InjectedRoute;
12 | };
13 |
14 | export type I18nConfig = {
15 | clientOptions: import("./options.js").Options["client"];
16 | translations: {
17 | initialized: boolean;
18 | i18nextConfig: I18nextConfig;
19 | };
20 | data: {
21 | locale: string;
22 | locales: Array;
23 | defaultLocale: string;
24 | };
25 | paths: {
26 | pathname: string;
27 | routes: Array;
28 | dynamicParams: Record>;
29 | strategy: import("./options.js").Options["strategy"];
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/package/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "jsx": "preserve"
7 | },
8 | "exclude": ["dist", "assets/stubs/virtual.d.ts", "assets/stubs/sitemap.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/package/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 | import { peerDependencies } from "./package.json";
3 |
4 | export default defineConfig((options) => {
5 | const dev = !!options.watch;
6 | return {
7 | entry: ["src/**/*.(ts|js)"],
8 | format: ["esm"],
9 | target: "node18",
10 | bundle: true,
11 | dts: true,
12 | sourcemap: true,
13 | clean: true,
14 | splitting: false,
15 | minify: !dev,
16 | external: [
17 | ...Object.keys(peerDependencies),
18 | "virtual:astro-i18n/als",
19 | "virtual:astro-i18n/internal",
20 | ],
21 | tsconfig: "tsconfig.json",
22 | };
23 | });
24 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/playground/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/playground/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/playground/astro.config.mts:
--------------------------------------------------------------------------------
1 | import node from "@astrojs/node";
2 | import react from "@astrojs/react";
3 | import tailwind from "@astrojs/tailwind";
4 | import { createResolver } from "astro-integration-kit";
5 | import { hmrIntegration } from "astro-integration-kit/dev";
6 | import { defineConfig } from "astro/config";
7 |
8 | const { default: i18n } = await import("@astrolicious/i18n");
9 |
10 | // https://astro.build/config
11 | export default defineConfig({
12 | site: "https://example.com",
13 | // trailingSlash: "always",
14 | integrations: [
15 | i18n({
16 | strategy: "prefixExceptDefault",
17 | defaultLocale: "en",
18 | locales: ["en", "fr"],
19 | pages: {
20 | about: {
21 | fr: "a-propos",
22 | },
23 | blog: {
24 | fr: "le-blog",
25 | },
26 | "blog/[slug]": {
27 | fr: "le-blog/[slug]",
28 | },
29 | "blog/[category]/[slug]": {
30 | fr: "le-blog/[category]/[slug]",
31 | },
32 | },
33 | localesDir: "./src/locales",
34 | defaultNamespace: "common",
35 | client: {
36 | data: true,
37 | translations: true,
38 | paths: true,
39 | },
40 | sitemap: true,
41 | // rootRedirect: {
42 | // status: 301,
43 | // destination: "/en",
44 | // },
45 | }),
46 | react(),
47 | tailwind(),
48 | hmrIntegration({
49 | directory: createResolver(import.meta.url).resolve("../package/dist"),
50 | }),
51 | ],
52 | output: "hybrid",
53 | adapter: node({
54 | mode: "standalone",
55 | }),
56 | });
57 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "dev": "astro dev",
8 | "start": "astro dev",
9 | "build": "astro check && astro build",
10 | "preview": "astro preview",
11 | "astro": "astro"
12 | },
13 | "dependencies": {
14 | "@astrojs/check": "^0.5.10",
15 | "@astrojs/node": "^8.2.5",
16 | "@astrojs/react": "^3.3.1",
17 | "@astrojs/tailwind": "^5.1.0",
18 | "@astrolicious/i18n": "workspace:*",
19 | "@types/react": "^18.3.1",
20 | "@types/react-dom": "^18.3.0",
21 | "astro": "^4.14.0",
22 | "i18next": "^23.11.3",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.3.1",
25 | "tailwindcss": "^3.4.3",
26 | "typescript": "^5.4.5"
27 | },
28 | "devDependencies": {
29 | "@tailwindcss/forms": "^0.5.7",
30 | "astro-integration-kit": "^0.16.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/playground/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/src/components/Counter.tsx:
--------------------------------------------------------------------------------
1 | import { getLocale, getLocalePath, switchLocalePath, t } from "i18n:astro";
2 | import { useState } from "react";
3 |
4 | export default function Counter({
5 | children,
6 | count: initialCount,
7 | }: {
8 | children: JSX.Element;
9 | count: number;
10 | }) {
11 | console.log("A");
12 | console.log({
13 | locale: getLocale(),
14 | path: getLocalePath("/about"),
15 | switch: {
16 | en: switchLocalePath("en"),
17 | fr: switchLocalePath("fr"),
18 | },
19 | });
20 | console.log("B");
21 | console.log(t("home"));
22 | const [count, setCount] = useState(initialCount);
23 | const add = () => setCount((i) => i + 1);
24 | const subtract = () => setCount((i) => i - 1);
25 |
26 | return (
27 | <>
28 |
29 |
30 | -
31 |
32 |
{count}
33 |
34 | +
35 |
36 |
37 | {children}
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/playground/src/components/LocaleSwitcher.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocale, getSwitcherData } from "i18n:astro";
3 |
4 | const locale = getLocale();
5 | const data = getSwitcherData();
6 | ---
7 |
8 |
9 | {
10 | data.map((e) => (
11 |
12 | {e.locale}
13 |
14 | ))
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/playground/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/playground/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getHtmlAttrs, getLocalePath, t } from "i18n:astro";
3 | import I18NClient from "@astrolicious/i18n/components/I18nClient.astro";
4 | import I18NHead from "@astrolicious/i18n/components/I18nHead.astro";
5 | import LocaleSwitcher from "~/components/LocaleSwitcher.astro";
6 |
7 | interface Props {
8 | title: string;
9 | }
10 |
11 | const { title } = Astro.props;
12 | ---
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {title}
21 |
22 |
23 |
24 |
25 |
28 | astro-i18n demo
29 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/playground/src/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "about": "About",
4 | "blog": "The blog",
5 | "user": "The user"
6 | }
7 |
--------------------------------------------------------------------------------
/playground/src/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Home",
3 | "description": "This is the home, see {{value}}",
4 | "array": ["a", "b", "c"]
5 | }
6 |
--------------------------------------------------------------------------------
/playground/src/locales/fr/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Accueil",
3 | "about": "A propos",
4 | "blog": "Le blog",
5 | "user": "Utilisateur"
6 | }
7 |
--------------------------------------------------------------------------------
/playground/src/locales/fr/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Accueil",
3 | "description": "Voici l'accueil, voir {{value}}",
4 | "array": ["a", "b", "c"]
5 | }
6 |
--------------------------------------------------------------------------------
/playground/src/pages/test.astro:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 | Test
--------------------------------------------------------------------------------
/playground/src/routes/TEST-UPPER.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import sitemap from "i18n:astro/sitemap";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | sitemap({});
6 |
7 | const title = "TEST-UPPER";
8 | ---
9 |
10 |
11 | {title}
12 |
13 |
--------------------------------------------------------------------------------
/playground/src/routes/about.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { t } from "i18n:astro";
3 | import sitemap from "i18n:astro/sitemap";
4 | import Layout from "~/layouts/Layout.astro";
5 | import Counter from "../components/Counter";
6 |
7 | sitemap(false);
8 |
9 | const title = t("about");
10 |
11 | export const prerender = false;
12 | ---
13 |
14 |
15 | {title}
16 |
17 | Test
18 |
19 |
20 |
--------------------------------------------------------------------------------
/playground/src/routes/blog.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocale, getLocalePath, t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = t("blog");
6 |
7 | const slugs = [
8 | {
9 | en: "a",
10 | fr: "d",
11 | },
12 | {
13 | en: "b",
14 | fr: "e",
15 | },
16 | {
17 | en: "c",
18 | fr: "f",
19 | },
20 | ];
21 |
22 | const currentLocaleSlugs = slugs.map((e) => e[getLocale()]);
23 | ---
24 |
25 |
26 | {title}
27 | {
28 | currentLocaleSlugs.map((slug) => (
29 | {slug}
30 | ))
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/playground/src/routes/blog/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | getDefaultLocalePlaceholder,
4 | getLocalePlaceholder,
5 | getLocalesPlaceholder,
6 | setDynamicParams,
7 | t,
8 | } from "i18n:astro";
9 | import sitemap from "i18n:astro/sitemap";
10 | import type { GetStaticPaths } from "astro";
11 | import Layout from "~/layouts/Layout.astro";
12 |
13 | export const getStaticPaths = (() => {
14 | const locale = getLocalePlaceholder();
15 | const locales = getLocalesPlaceholder();
16 | const defaultLocale = getDefaultLocalePlaceholder();
17 | console.log({ locale, locales, defaultLocale });
18 |
19 | const slugs = [
20 | {
21 | en: "a",
22 | fr: "d",
23 | },
24 | {
25 | en: "b",
26 | fr: "e",
27 | },
28 | {
29 | en: "c",
30 | fr: "f",
31 | },
32 | ];
33 |
34 | return slugs.map((slug) => {
35 | const dynamicParams = Object.entries(slug).map(([locale, slug]) => ({
36 | locale,
37 | params: { slug },
38 | }));
39 | sitemap({
40 | dynamicParams,
41 | });
42 | return {
43 | params: {
44 | slug: slug[locale],
45 | },
46 | props: {
47 | dynamicParams,
48 | },
49 | };
50 | });
51 | }) satisfies GetStaticPaths;
52 |
53 | const { slug } = Astro.params;
54 | const { dynamicParams } = Astro.props;
55 |
56 | setDynamicParams(dynamicParams);
57 |
58 | const title = t("blog");
59 | ---
60 |
61 |
62 | {title} {slug}
63 |
64 |
--------------------------------------------------------------------------------
/playground/src/routes/data.json.ts:
--------------------------------------------------------------------------------
1 | import { getLocale } from "i18n:astro";
2 | import type { APIRoute } from "astro";
3 |
4 | export const GET: APIRoute = () => {
5 | return Response.json({ locale: getLocale(), foo: "bar" });
6 | };
7 |
--------------------------------------------------------------------------------
/playground/src/routes/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocalePath, t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = t("home:title");
6 | console.log(getLocalePath("/about", null, "fr"));
7 | console.log(getLocalePath("/abc", null, "en"));
8 | console.log(getLocalePath("/def", null, "fr"));
9 | console.log(t("home:array", { returnObjects: true }));
10 | ---
11 |
12 |
13 | {title}
14 | {t("home:description", { value: "9" })}
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/playground/src/routes/user.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLocale, getLocalePath, t } from "i18n:astro";
3 | import Layout from "~/layouts/Layout.astro";
4 |
5 | const title = t("user");
6 |
7 | const slugs = [
8 | {
9 | en: "a",
10 | fr: "d",
11 | },
12 | {
13 | en: "b",
14 | fr: "e",
15 | },
16 | {
17 | en: "c",
18 | fr: "f",
19 | },
20 | ];
21 |
22 | const currentLocaleSlugs = slugs.map((e) => e[getLocale()]);
23 | ---
24 |
25 |
26 | {title}
27 | {
28 | currentLocaleSlugs.map((slug) => (
29 | {slug}
30 | ))
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/playground/src/routes/user/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | getDefaultLocalePlaceholder,
4 | getLocalePlaceholder,
5 | getLocalesPlaceholder,
6 | setDynamicParams,
7 | t,
8 | } from "i18n:astro";
9 | import sitemap from "i18n:astro/sitemap";
10 | import type { GetStaticPaths } from "astro";
11 | import Layout from "~/layouts/Layout.astro";
12 |
13 | export const getStaticPaths = (() => {
14 | const locale = getLocalePlaceholder();
15 | const locales = getLocalesPlaceholder();
16 | const defaultLocale = getDefaultLocalePlaceholder();
17 | console.log({ locale, locales, defaultLocale });
18 |
19 | const slugs = [
20 | {
21 | en: "a",
22 | fr: "d",
23 | },
24 | {
25 | en: "b",
26 | fr: "e",
27 | },
28 | {
29 | en: "c",
30 | fr: "f",
31 | },
32 | ];
33 |
34 | return slugs.map((slug) => {
35 | const dynamicParams = Object.entries(slug).map(([locale, slug]) => ({
36 | locale,
37 | params: { slug },
38 | }));
39 | sitemap({
40 | dynamicParams,
41 | });
42 | return {
43 | params: {
44 | slug: slug[locale],
45 | },
46 | props: {
47 | dynamicParams,
48 | },
49 | };
50 | });
51 | }) satisfies GetStaticPaths;
52 |
53 | const { slug } = Astro.params;
54 | const { dynamicParams } = Astro.props;
55 |
56 | setDynamicParams(dynamicParams);
57 |
58 | const title = t("user");
59 | ---
60 |
61 |
62 | {title} {slug}
63 |
64 |
--------------------------------------------------------------------------------
/playground/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require("@tailwindcss/forms")],
8 | };
9 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"]
7 | },
8 | "jsx": "react-jsx",
9 | "jsxImportSource": "react"
10 | },
11 | "exclude": ["dist"]
12 | }
13 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - package
3 | - packages/*
4 | - playground
5 | - docs
6 | - demo
7 |
--------------------------------------------------------------------------------
/scripts/release.mjs:
--------------------------------------------------------------------------------
1 | import { spawn } from "node:child_process";
2 | import { resolve } from "node:path";
3 |
4 | /**
5 | *
6 | * @param {string} command
7 | * @param {...Array} args
8 | *
9 | * @returns {Promise}
10 | */
11 | const run = async (command, ...args) => {
12 | const cwd = resolve();
13 | return new Promise((resolve) => {
14 | const cmd = spawn(command, args, {
15 | stdio: ["inherit", "pipe", "pipe"], // Inherit stdin, pipe stdout, pipe stderr
16 | shell: true,
17 | cwd,
18 | });
19 |
20 | let output = "";
21 |
22 | cmd.stdout.on("data", (data) => {
23 | process.stdout.write(data.toString());
24 | output += data.toString();
25 | });
26 |
27 | cmd.stderr.on("data", (data) => {
28 | process.stderr.write(data.toString());
29 | });
30 |
31 | cmd.on("close", () => {
32 | resolve(output);
33 | });
34 | });
35 | };
36 |
37 | const main = async () => {
38 | await run("pnpm changeset version");
39 | await run("git add .");
40 | await run('git commit -m "chore: update version"');
41 | await run("git push");
42 | await run("pnpm --filter @astrolicious/i18n build");
43 | await run("pnpm changeset publish");
44 | await run("git push --follow-tags");
45 | const tag = (await run("git describe --abbrev=0")).replace("\n", "");
46 | await run(
47 | `gh release create ${tag} --title ${tag} --notes "Please refer to [CHANGELOG.md](https://github.com/astrolicious/i18n/blob/main/package/CHANGELOG.md) for details."`,
48 | );
49 | };
50 |
51 | main();
52 |
--------------------------------------------------------------------------------