GithubHelp home page GithubHelp logo

alexandre-fernandez / astro-i18n Goto Github PK

View Code? Open in Web Editor NEW
230.0 2.0 9.0 1.11 MB

A TypeScript-first internationalization library for Astro.

Home Page: https://www.npmjs.com/package/astro-i18n

License: MIT License

JavaScript 3.02% TypeScript 94.56% Astro 2.42%
astro i18n internationalization library localization typescript

astro-i18n's Introduction

astro-i18n logo

astro-i18n

A TypeScript-first internationalization library for Astro.

Features

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

Introduction

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

Installation

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

Get started

Run the quick install command :

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

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

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

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

export const onRequest = sequence(astroI18n)

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

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

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

You can put your translations in four locations :

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

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

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

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

Filesystem translations screenshot

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Configuration

primaryLocale

The default locale for your app.

secondaryLocales

All the other locales supported by your app.

fallbackLocale

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

showPrimaryLocale

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

trailingSlash

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

run

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

translations

Your app translations in the following format :

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

translationLoadingRules

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

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

translationDirectory

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

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

routes

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

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

Variants

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

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

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

Interpolations

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

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

astroI18n.locale // "en"

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

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

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

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

Interpolation formatters

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

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

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

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

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

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

Reference

defineAstroI18nConfig

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

Utility function to get type safety when defining the config.

useAstroI18n

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

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

createGetStaticPaths

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

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

t

Alias for astroI18n.t.

l

Alias for astroI18n.l.

astroI18n.environment

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

The current detected environment.

astroI18n.route

route: string

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

astroI18n.pages

pages: string[]

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

astroI18n.page

page: string

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

astroI18n.locale

locale: string

The locale for the current page.

astroI18n.locales

locales: string[]

All the supported locales.

astroI18n.primaryLocale

primaryLocale: string

The configured primary locale.

astroI18n.secondaryLocales

secondaryLocales: string

The configured secondary locales.

astroI18n.fallbackLocale

fallbackLocale: string

The configured fallback locale.

astroI18n.isInitialized

isInitialized: boolean

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

astroI18n.t

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

The main translation function (t is an alias).

astroI18n.l

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

The main routing function (l is an alias).

astroI18n.addTranslations

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

Adds new translations at runtime.

astroI18n.addFormatters

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

Adds new formatters at runtime.

astroI18n.addTranslationLoadingRules

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

Adds new translation loading rules at runtime.

astroI18n.addRoutes

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

Adds new route segment translations at runtime.

astroI18n.extractRouteLocale

function extractRouteLocale(route: string): string

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

astroI18n.redirect

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

Use this instead of Astro.redirect to redirect users.

Components

You can use these premade components in your project :

HrefLangs

SEO component, see hreflang.

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

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

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

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

LocaleSwitcher

Simple component to switch locales.

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

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

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

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

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

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

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

CLI

astro-i18n install

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

astro-i18n generate:pages

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

astro-i18n generate:types

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

astro-i18n extract:keys

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

astro-i18n's People

Contributors

alexandre-fernandez avatar fabianlars avatar lorenzolewis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

astro-i18n's Issues

Language switch example not working

Describe the bug
The current language switch example does not work. When swapping the language, every route still links to old locale

To Reproduce
Steps to reproduce the behavior:

  1. Simply start up the project
  2. Add language switch example code
  3. Create link in your app to other page
  4. Change lenguage (do not reload page)
  5. Click link
  6. The route will use the previous locale

Expected behavior
To use the new locale. It works with full page reload

Mandatory reproduction repository
Add a git repository to reproduce the bug.

Version 2.2.2 middleware break Picture tags

Describe the bug
When I was developing app, I noticed images weren't loading properly (only alt image)

After messing around I disabled middleware for i18n and images came to life

Rollbacking to 2.2.1 proved it was astro-i18n update since on 2.2.1 it works

To Reproduce
Steps to reproduce the behavior:

  1. Install latest astro-i18n
  2. Add some Picture and Image tags
  3. Check the result of _image query (it'll tell image is invalid and alt text will show up)

Expected behavior
Image loading properly

Mandatory reproduction repository
https://github.com/Herob527/astro-i18n-image-reproduction-repo

Additional images
Result without middleware:
without-middleware

Result with middleware:
with-middleware
image

Fallback language

It's a common option to specify a fallback language so that if translations are not available in a certain language, the t() function attempts to get the values from the fallback language, for example en.json.

Is it something that can be added?

translate function does not handle markup in text

Describe the bug
Translating text with markup is a bit of a pain. It would be ideal if there were a capability similar to this to allow me to include markup in my text.

To Reproduce
Steps to reproduce the behavior:

  1. Clone https://github.com/nibblesnbits/astro-i18n-repro
  2. npm install
  3. npm start
  4. See title at top of page

Expected behavior
The current behavior is expected, but there should be a way to have it optionally process the markup as markup rather than text.

Mandatory reproduction repository
https://github.com/nibblesnbits/astro-i18n-repro

```i18n:sync``` keep creating new folder for languages

Hi,

I'm new to Astro and astro-i18n.
Is it normal that each time i run the commande i18n:sync the folder fr keep incrementing with more fr folders ?

image

Here is my astro.i18n.config.ts
image

I have read the documentation and I don't understand where the issue come from.

Versions :
"astro": "^2.4.1",
"astro-i18n": "^1.8.1",

'getStaticPaths' not detected in CRLF Files

Describe the bug
The getStaticPaths method is not found in CRLF (\r\n) files for dynamic routes.

To Reproduce
Steps to reproduce the behavior:

  1. Clone any Git-Repo with astro-i18n
  2. Change the line separators to CRLF or be on a Windows machine.
  3. Run the astro-i18n generate:pages command and check the generated file

Expected behavior
The generated File for the Language should also include the getStaticPaths method.

Mandatory reproduction repository
https://steamwar.de/devlabs/SteamWar/Website

Question about client-side translations

Hello and thank you for your work!

I tried your package and noticed something. In the output script tag for client-side translations, there are translations for all locales, even if we only need one. Is there a reason for this? It might not be very efficient, especially with lots of locales.

Can we make it better by including only the translations needed for the current page? This could make things faster and save space on the client's side.

Minimal example is not working

Describe the bug
Hey there, thanks for checking this. These are the steps I took:

  1. Created a new astro project and added the library with the serverless command:
./node_modules/.bin/astro-i18n install --serverless
  1. Created src/i18n/common/en.json file.

  2. Run pnpm dev.

Once I visit the index page, I get this error:

node:fs:1527
  handleErrorFromBinding(ctx);
  ^

Error: ENOENT: no such file or directory, scandir

Thanks!

To Reproduce
Steps to reproduce the behavior:

  1. Run pnpm dev.
  2. Visit index page.

Expected behavior
It should work, at least it should not give any error.

Mandatory reproduction repository
Git repository

i18n:generate:pages misplaces generated pages for nested dynamic routes

Describe the bug
Running npm run i18n:generate:pages results in localized pages being created that do not follow the base locale.

Example:
A valid route in my base locale is: /nested/static
After generation the localized page resides at /de/static

To Reproduce
Steps to reproduce the behavior:

  1. Create a route src/pages/foo/index.astro with dynamic subroutes src/pages/foo/[slug].astro.
  2. npm run i18n:generate:pages

or:

  1. Clone this repo
  2. npm run i18n:generate:pages

Expected behavior
The script generates the pages at the correct route: de/nested/static

Mandatory reproduction repository
See here

The exported `prerender` property is not copied to files generated by `sync`

My /pages/index.astro

---
import { astroI18n } from "astro-i18n";
astroI18n.init(Astro);

export const prerender = true;
---
...

Generated file (/pages/ua/index.astro):

---
import Page from "../index.astro"

const { props } = Astro
// would be nice to have `export const prerender = true;`
---

<Page {...props} />

Of course, I can just edit them manually, but it would be great to automatically detect exported properties or to be able to enable copying them.

Using astro-i18n with new content folder

Hi, first great job, I'm enjoying using the library and would be potentially keen to contribute.

I have successfully published a website using it, but now that I want to add a blog section, I wonder how to combine this lib with the new Astro content folder.

if simply using it naively with a [slug].astro page and my md files in the content folder we will endup having duplicated route in each language.

What I'm expecting to have:

  • /blog/article-1-in-english. // default language
  • /fr/blog/article-1-in-french // second language

But what is being generated let you access:

  • /blog/article-1-in-english.
  • /blog/article-1-in-french.
  • /fr/blog/article-1-in-english
  • /fr/blog/article-1-in-french

Which is normal considering how both systems works. So I'm wondering, at which level could we make sure this does not happen ? I'm not an Astro expert yet but if something could make it happen I'd be happy to try and implement it.

Maybe by giving the possibility to ignore some page in the i18n.config it could helps 🤔

Variables in snake cases are ignored during translation

Describe the bug
t() is ignoring any variables passed in snake case and accepting camel cases. I am calling t function like this

index.astro

t('home.title', {'user_name': 'Test user', userName: 'Demo user'});

astro.i18n.config.ts

export default defineAstroI18nConfig({
    defaultLangCode: 'en-gb',
    supportedLangCodes: ['fr-fr'],
    showDefaultLangCode: true,
    translations: {
        'en-gb':{
            "home.title": "Welcome to {user_name} {userName}!"
        }
    }
})

Current result
Welcome to Demo user!

Expected result
Welcome to Test user Demo user!

Am I missing something or is this a bug?

[email protected] broke collection content translation

Hi!

Updating to [email protected] broke my collection translation logic, I could not find a workaround yet.
I suspect it has to do with this PR but I can't be certain…

Right now I have a content collection called 'articles' with two subfolders (en and fr).
Inside src/pages/articles/[slug].astro, I'm using the following logic to get the translated articles:

import { astroI18n } from "astro-i18n";
astroI18n.init(Astro); // does not change anything if present or not

export async function getStaticPaths() {
	const articles = await getCollection("articles", ({ data }) => {
		// I have a frontmatter property called lang in each article.
		// I tried using the id filter as seen in the astro docs but no luck either.
		return data.lang === astroI18n.langCode;
	});
	return articles.map((article) => ({
		params: { slug: article.slug },
		props: { article },
	}));
}

What happens is the frontmatter translates correctly but the acutal content does not. When building the website, the same version of the content will be displayed on both languages instead of its translation.

Reverting back to [email protected] resolves everything 😅
I tested with astro-i18n v1.7.0 and v1.6.1 and [email protected] as well.
I have a branch available for reproduction.

`sync` doesn't seem to happen automatically during dev cycle?

@Alexandre-Fernandez This is a fantastic library and I've really enjoyed using it. Thank you!

When developing my Astro 2 project, I regularly add pages and translation strings with the dev server running (npm run dev). I would expect that these new translations would show up automatically; however, I seem to have to stop the dev server, run i18n:sync, and then restart it for them to show up in the template, or for the new routes to be built.

I can go into more detail if this sounds unexpected, but first I'm just curious: are you supposed to be able to add new templates, routes, translations, etc. while the dev server is running, and they Just Work? Or is it known that you have to stop, resync, and restart?

Declare translations directly in pages does not work

Describe the bug
Declare your translations directly in pages does not work

To Reproduce
Steps to reproduce the behavior:

  1. Follow all basic setup
  2. Translation on defineAstroI18nConfig works
  3. Add i18n folder to the page's folders
  4. Route translation works but text translation does not

`sync` command not working correctly

Hello!

image

NodeJS: 18.14.0
OS: Windows 11 x64

package.json

{
  "name": "@example/basics",
  "type": "module",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro",
    "i18n:sync": "astro-i18n sync"
  },
  "dependencies": {
    "astro": "^2.0.8",
    "astro-i18n":"^1.6.4"
  }
}

astro.config.mjs

import { defineConfig } from 'astro/config';
import i18n from "astro-i18n"

// https://astro.build/config
export default defineConfig({
    integrations: [i18n()]
});

Here is output of this command:

PS G:\projects\astro-test> npm run i18n:sync

> @example/[email protected] i18n:sync
> astro-i18n sync

G:\projects\astro-test\node_modules\astro-i18n\dist\src\cli\index.cjs:2
"use strict";var he=Object.create;var on=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var $e=Object.getPrototypeOf,Ce=Object.prototype.hasOwnProperty;var Re=(n,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of xe(t))!Ce.call(n,o)&&o!==e&&on(n,o,{get:()=>t[o],enumerable:!(r=Te(t,o))||r.enumerable});return n};var Ae=(n,t,e)=>(e=n!=null?he($e(n)):{},Re(t||!n||!n.__esModule?on(e,"default",{value:n,enumerable:!0}):e,n));var Nt=(n,t,e)=>{if(!t.has(n))throw TypeError("Cannot "+e)};var h=(n,t,e)=>(Nt(n,t,"read from private field"),e?e.call(n):t.get(n)),O=(n,t,e)=>{if(t.has(n))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(n):t.set(n,e)},W=(n,t,e,r)=>(Nt(n,t,"write to private field"),r?r.call(n,e):t.set(n,e),e);var Y=(n,t,e)=>(Nt(n,t,"access private method"),e);var St="default",sn="argv.main",j,at,an,ft,fn,Dt=class{constructor(t=[]){O(this,at);O(this,ft);this.command=St;this.args=[];this.options={};O(this,j,{});let[e,r]=process.argv;this.node=e??"",this.file=r??"",Y(this,at,an).call(this,t),Y(this,ft,fn).call(this,process.argv.slice(2),t.length===1&&t[0].name===St)}};j=new WeakMap,at=new WeakSet,an=function(t){for(let e of t){h(this,j)[e.name]||(h(this,j)[e.name]={});for(let r of e.options)h(this,j)[e.name][`--${r.name}`]=r.name,r.shortcut&&(h(this,j)[e.name][`-${r.shortcut}`]=r.name)}},ft=new WeakSet,fn=function(t,e=!1){var i;if(t.length===0)return;let r=t;e?this.command=St:(this.command=t.at(0),r=t.slice(1));let o=sn;for(let s of r){let a=h(this,j)[this.command][s];if(!a)s.startsWith("--")&&(a=s.replace("--","")),s.startsWith("-")&&(a=s.replace("-",""));else{this.options[a]=[],o=a;continue}if(o===sn){this.args.push(s);continue}(i=this.options[o])==null||i.push(s)}};function Ft(n=[]){return new Dt(n)}var ot=require("path");var x=require("fs"),F=require("path");function L(n){try{return JSON.parse((0,x.readFileSync)(n,"utf8"))}catch{return}}function q(n,t,e=[]){try{let r=(0,x.readdirSync)(n);for(let o=0;o<r.length;o+=1){let i=(0,F.join)(n,r[o]);e.some(s=>s.test(i))?(r.splice(o,1),o-=1):$(i,!1)&&q(i,t)}t(r,n)}catch{}}function $(n,t=!0){return t?(0,x.existsSync)(n)&&(0,x.lstatSync)(n).isDirectory():(0,x.lstatSync)(n).isDirectory()}function A(n,t=!0){return t?(0,x.existsSync)(n)&&!(0,x.lstatSync)(n).isDirectory():!(0,x.lstatSync)(n).isDirectory()}function H(...n){let t=(0,F.join)(...n);if(!(0,x.existsSync)(t))throw new ReferenceError(`Path: ${t} doesn't exist.`);return t}function Lt(n){return n.endsWith(F.sep)?n.slice(0,-1):n}function cn(n){return n.startsWith(F.sep)?n.slice(1):n}function p(n,t,e){let r=Lt(n).split(F.sep),o=r.pop()||"",i=r.join(F.sep);(0,x.mkdirSync)(i,{recursive:!0}),(0,x.writeFileSync)((0,F.join)(i,o),t,{encoding:"utf8",flag:e})}function un(n){let t=n.lastIndexOf(".");return t<0?[n,""]:[n.slice(0,t),n.slice(t+1)]}function k(n){let t=n.lastIndexOf(".");return t<0?"":n.slice(t+1).toLowerCase()}function ct(n){let t=n.lastIndexOf(".");return t<0?n:n.slice(0,t)}function pn(n){let t=n.lastIndexOf(".");return t<0?[n,""]:[n.slice(0,t),n.slice(t+1)]}function E(n){return Object.getPrototypeOf(n)===Object.prototype}function ut(n){return Array.isArray(n)&&n.every(t=>typeof t=="string")}function ln(n){if(!E(n))return!1;for(let t of Object.values(n))if(typeof t!="string")return!1;return!0}function gn(n){if(!E(n))throw new Error(`"${n}" is not of type Record<string, string>.`);for(let t of Object.values(n))if(typeof t!="string")throw new TypeError(`"${t}" is not of type string.`)}function _t(n,t){if(!n.includes(t))throw new Error(`${t} is not in [${n.join(", ")}].`)}function w(n){return Object.entries(n)}function pt(n){let t=typeof n;if(t==="bigint")return"number";if(t==="function")return"(...args: unknown[]) => unknown";if(t!=="object")return t;if(!n)return"null";if(Array.isArray(n))return n.length===0?"unknown[]":`(${[...new Set(n.map(r=>pt(r)))].join(" | ")})[]`;if(E(n)){let e=Object.entries(n).map(([r,o])=>`"${r}": ${pt(o)}`);return e.length===0?"Record<string, unknown>":`{ ${e.join(", ")} }`}return"unknown"}function mn(n,t,e){let r=n;for(let[o,i]of t.entries()){if(o===t.length-1){r[i]=e;break}if(!dn(r,i)){r[i]={},r=r[i];continue}if(Object.getPrototypeOf(r[i])===Object.prototype){r=r[i];continue}break}}function d(n,t,e){let{mode:r,modifyBase:o}={mode:"replace",modifyBase:!0,...e},i=o?n:jt(n);for(let[s,a]of w(t)){let f=i[s];if(dn(i,s)){E(f)&&E(a)?d(f,a):r==="replace"&&(i[s]=t[s]);continue}i[s]=t[s]}return i}function jt(n){return!n||typeof n!="object"?n:Array.isArray(n)?n.map(t=>typeof t=="object"?jt(t):t):w(n).reduce((t,[e,r])=>(t[e]=jt(r),t),{})}function dn(n,t){return Object.prototype.hasOwnProperty.call(n,t)}var l="astro.i18n.config",_=".astro",Z="i18n",B="{route}",C="astro-i18n",yn="tsconfig.json",lt=".",U=`.${C}`,gt="generated.d.ts",hn="generated.keys.json",Tn="env.d.ts",mt="astro.config",dt="i18n",xn="package.json",$n="deno.json",Cn="deno.jsonc",Rn=":",bt="|",An=",",En=":";function N(){return new Error("Cannot resolve astro's working directory.")}function S(n){return new Error(`Cannot resolve astro's working directory.
                                                                                                                                                                                                                
                                                                                                                                                                                                                
                                                                                                                                                                                                                
                                                                                                                                                                                                                


Error: Cannot resolve astro's working directory.
    at N (G:\projects\astro-test\node_modules\astro-i18n\dist\src\cli\index.cjs:2:5003)
    at Object.Hn [as sync] (G:\projects\astro-test\node_modules\astro-i18n\dist\src\cli\index.cjs:69:139)
    at Object.<anonymous> (G:\projects\astro-test\node_modules\astro-i18n\dist\src\cli\index.cjs:103:2276)
    at Module._compile (node:internal/modules/cjs/loader:1226:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1280:10)
    at Module.load (node:internal/modules/cjs/loader:1089:32)
    at Module._load (node:internal/modules/cjs/loader:930:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47

I think I missed something from starting guide, anyway hope you helps!

Cannot redirect from secondary locale page

Describe the bug

When you create the translated pages with the script you get this code in the translated route:
---
import Page from "../../about.astro"
const { props } = Astro
---
<Page {...props} />

According to the docs:

A page (and not a child component) must return the result of Astro.redirect() for the redirect to occur.

So if you set a redirect in the primaryLocale page, once you translate Astro gives you an error: The response has already been sent to the browser and cannot be altered.

#56 @fprl

Mandatory reproduction repository
Add a git repository to reproduce the bug.

To Reproduce
Go to /nl/about

Expected behavior
Get redirected to index.

Extract Function Enhancements

With the extract function added in #15 (thank you!!!) it's a really good start!

It supports the use case where you are only storing keys. Another use case is where you're storing the actual string of your source language (i.e. English) in your JS/TS files directly.

So instead of a function like this:

<html>
  <body>
    <p>{t("index.content")}</p>
  </body>
</html>

You would instead do this:

<html>
  <body>
    <p>{t("I'm the title of a page. I have white spaces, punctuation, and all kinds of things that could make escaping difficult!")}</p>
  </body>
</html>

Right now this isn't very robust. For example, if the string of text ends up wrapping like this:

<html>
  <body>
    <p>{
      t(
          "I'm the title of a page. I have white spaces, punctuation, and all kinds of things that could make escaping difficult!"
      )
    }</p>
  </body>
</html>

It won't extract.

All this to be said, I appreciate the many different edge cases this could potentially open up. Would you be open to a system that's parsing the AST of an astro file instead of a more brute-force regex method? If so then I could look into opening a PR for it potentially.

astroI18n.langCode switched after build

Hi, this is a weird one.

While on dev mode, astroI18n.langCode works as expected. But after running astro build, it is actually reversed. I tested with two and three locales available. The en locale seems to become the default, messing up translations (but not routing).

See reproduction here.

`extract` Command

One of the functionalities that are missing for me are extracting all of the values from the t functions out to the .json file.

If you don't already have a JSON file with your translations setup in a structured format this can be a sort of "quickstart" way to generate that file.

The workflow I could see is like this:

  1. Add in t() into my Astro files
  2. Run astro-i18n extract to extract/update all keys in the en.json (or default locale) file
  3. Upload those English JSON files to a translation service (like Crowdin)
  4. Crowdin generates and updates the respective files for other languages
  5. Download the non-English JSON files from Crowdin
  6. Run astro-i18n sync to then generate the non-English routes based on the downloaded JSON files

I'm excited to see this project and this part of the Astro platform shaping up! <3

Wrong paths generated on sync

Hello,

When syncing, the generated page files have imports containing backward slashes, and this produces an error:

---
import Page from "..\index.astro"
const { props } = Astro
---
<Page {...props} />

I am using Astro on Windows 10.

l function doesn't translate back to default lang & l & t functions ts error on generic string argument with generated types and astroI18n object has no generated types

Hello!

First of all a huge thank you for this plugin. I've been searching and trying to make a multilingual astro site for months!
This is by far the simplest way to make i18n work with astro so again, thanks a lot!


Now, I've been trying to implement a way to switch the language of a website from a link in the header, and be taken to the equivalent page in another locale.

I feel like I'm almost there but can't figure out the last part. So far, what's missing is the ability to get the current route name and to navigate to its other languages alternatives.

Here is my component
---
// LanguageSwitcher.astro

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

// get the locale currently in use
const currentLocale = astroI18n.langCode;
// get all the locales available on the website and remove the one currently in use
const availableLocales = astroI18n.langCodes.filter(
	(locale) => locale !== currentLocale
);
---

<ul>
	{
		// create a list of available alternative locales
		availableLocales.map((locale) => (
			<li>
				// first parameter is supposed to be a dynamic route name
				<a href={l("/", {}, "", locale)}>{locale}</a>
			</li>
		))
	}
</ul>

Using a static route like /articles and having two locales fr (default) and en is working. I can switch from /articles to /en/blog and vice versa. I just can't find a way to do it dynamically.

Also, I can't make it work for the root index page with /

I'm using the folder structure for i18n implementation
index.astro
i18n
  en.json
  fr.json
articles
  index.astro
  i18n
    en.json
    fr.json

Maybe this is not something this module is supposed to be doing, I'm coming from nuxt-i18n where I was able to use a function to navigate to the page:

switchLocalePath()
<nuxt-link
	v-for="locale in availableLocales"
	:key="locale.code"
	:to="switchLocalePath(locale.code)"
	class="nice-link"
	>
  {{ locale.name }}
</nuxt-link>

Any idea on how to make this work?
Thanks!

Could not find a declaration file for module 'astro-i18n'.

Describe the bug
When trying to import the library in any file, whether its a .astro or .ts file, TypeScript throws an error:

Could not find a declaration file for module 'astro-i18n'.

To Reproduce
Steps to reproduce the behavior:

  1. Create / edit a file with TypeScript support
  2. Try doing import { astroI18n } from "astro-i18n";

Expected behavior
No errors to be thrown and have types

Mandatory reproduction repository
None

To fix
When exporting / publishing the library, the type definition file should end with .d.mts, not .d.ts as the exported JavaScript file ends in .mjs

Can't build on Vercel edge worker

When trying to build a test project using astro-i18n on Vercel edge workers, I get the following error:

[commonjs--resolver] Cannot bundle Node.js built-in "url" imported from "node_modules/.pnpm/[email protected]/node_modules/astro-i18n/dist/src/index.mjs". Consider disabling ssr.noExternal or remove the built-in dependency.

I couldn't find any documentation about using this library without Node.js available. Is that not supported? Would there be a way to not depend on Node.js APIs and make this compatible with, for instance, @astrojs/vercel/edge ?

Translated routes and translation does not work with 404 pages

Describe the bug
I've noted that translated routes and and translation does not work with 404 pages because Astro.url.pathname returns 404 and not the actual pathname.

To Reproduce
Steps to reproduce the behavior:

  1. Go to a translates and non existing url
  2. Click on the language switcher component and select a language
  3. The url exibits /404 and translation does not work

Expected behavior
The url should switch from /articles to /pt-BR/artigos

i18n:sync does not generate folder after deleting them once

Describe the bug
i18n:sync does not generate folder after deleting them once

To Reproduce
Steps to reproduce the behavior:

  1. Do basic setup
  2. Run i18n:sync and new folders generated
  3. Delete generated sync folders
  4. Run i18n:sync again but nothing generated more

Trailing slash option causes translated page imports to be wrong

Describe the bug
When the trailingSlash option on the astro-i18n config is set to always, import paths for the primary locale's page from secondary pages includes extra ../.

To Reproduce

  • Setup an Astro repo then setup astro-i18n by running ./node_modules/.bin/astro-i18n install.
  • Use the below astro-i18n.config.ts.
import { defineAstroI18nConfig } from "astro-i18n"

export default defineAstroI18nConfig({
  primaryLocale: "en",
  secondaryLocales: ["ja"],
  fallbackLocale: "en",
  trailingSlash: "always",
  run: "client+server",
  showPrimaryLocale: false,
  translationLoadingRules: [],
  translationDirectory: {},
  translations: {},
  routes: {},
})
  • Create an index page at either src/pages/index.astro or src/pages/en/index.astro if there isn't one already.
  • Run npm run i18n:sync and then npm run build.
  • You will get an error like so:
$ astro build
12:17:43 AM [content] No content directory found. Skipping type generation.
12:17:43 AM [build] output target: static
12:17:43 AM [build] Collecting build info...
12:17:43 AM [build] Completed in 20ms.
12:17:43 AM [build] Building static entrypoints...
Could not resolve "../../index.astro" from "src/pages/ja/index.astro"
  • Note that if you change trailingSlash to never and rerun npm run i18n:sync then src/pages/ja/index.astro will have the correct import meaning npm run build will complete successfully.

Expected behavior
I would expect the build to succeed with trailingSlash configured to either never or always.

Astro-i18n in NX monorepo

Describe the bug
When using NX and Astro i18n the method for detecting the root is not working.
With nx, we don't have a local "dependencies" in the package.json.

I tried to change some undocumented variables from the config but it failed.

Can't we add param or a setting (args or env var) to specify :

  • base path (root folder of the astro website)
  • config file path (path of the astro-i18n config file)

I'm ready to make a PR

Locale is not set when an internal API request happens during page render

Describe the bug
Sometimes (leaving the question of whether this is a good idea to the side for now ;)), you may do an internal API request as part of rendering a page. In this scenario, astro-i18n shows the default locale, and translations do not work.

---
import { t, astroI18n } from 'astro-i18n';
const { data } = await fetch(Astro.url.origin + '/api/data').then((res) => res.json());
---
<html lang="en">
	<body>
		<h1>{t('title')}</h1>
		<p>Locale: {astroI18n.locale}</p>
		<p>API data: {data}</p>
	</body>
</html>

Node will issue an API request, which will ultimately come back to Astro, which the i18n middleware will handle, before the original request is finished.

I am curious what the core issue is here -- shouldn't the request to /api/data be isolated, meaning that the original request to http://localhost:4321/es still gets the correct locale?

To Reproduce

  1. Clone https://github.com/noahtallen/astro-i18n-minimal-bug-report
  2. Run npm i && npm run dev
  3. Access http://localhost:4321/es
  4. You can see the locale is "en" and the page is not translated.
  5. Checkout commit c63fd1b to see the translations working.

Expected behavior
Locale & translations should still work

Mandatory reproduction repository
https://github.com/noahtallen/astro-i18n-minimal-bug-report

Translate in .ts file -> Cannot perform operation, astro-i18n is not initialized.

Describe the bug
I cannot load a translation from a .ts file - something the previous i18n implementation allowed.
Do I have to change this behaviour? Suggestions on how to do it?

I tried to find the minimum attack surface for my project - it is attached.

To Reproduce
Steps to reproduce the behavior:

  1. Clone Repo
  2. Start Astro
  3. Visit Page

Expected behavior
It should work ;)

Mandatory reproduction repository
https://github.com/Nkay/i18n-bug-reproduce-2

Cyrillic URLs seem to cause issues

Describe the bug
When using cyrillic in URLs, and switching from locale to locale (with the provided LocaleSwitcher component, everything works as expected except when going from a URL containing cyrillic to a locale which URL is not cyrillic.

To Reproduce
With this config:

routes: {
    "de": {
      "cookies": "cookie-richtlinien",
      "contact": "kontakt",
    },
    "ru": {
      "cookies": "cookies",
      "contact": "контакты",
    },
  },

Starting on de/kontakt, the link to the russian version is ru/контакты.
Going to ru/cookies with the navigation works as well. On that page, the German page is de/cookie-richlinien as expected.
Going back to ru/контакты, the German versions of the page show up as de/контакты leading to a 404.
This works with any other language as destination: they are all listed with the cyrillic URL.

I was able to fix this by decoding the URI in LocaleSwitcher:

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

Mandatory reproduction repository
Reproduction of the bug

`sync:pages` doesn't remove old pages

If you create a page in your standard route, run sync:pages, and then remove that page in your standard route it will still be there. This can cause an issue where the old page for the non-standard route still gets built (or throws an error if it isn't able to be built).

It'd be a nice DX/quality of life improvement to either:

a) Have the sync:pages command automatically purge any files in the target directories to be written to or...
b) Introduce another CLI command (maybe sync:purge or similar) that a dev could explicitly run prior to sync:pages.

I'd personally go for option a) above (maybe with a parameter in the config to enable/disable) as this would seem to be the better DX.

If you agree with this I can take a stab at putting in a PR for this one (just let me know which option you prefer).

Trailing Slash

When forcing a trailing slash during dev (using trailingSlash: 'always' in astro.config.js you'll want to include trailing slashes in the URLs generated by the l() function. Right now astro-i18n will always remove the trailing slash:

export function removeTrailingSep(path: string) {
if (path.endsWith(sep)) return path.slice(0, -1)
return path
}

I'd like to either have a separate config option in astro-i18n like trailingSlash: 'always' | 'never' and/or read from the Astro config's trailingSlash option (although I'm not sure how ignore would be handled if matching the Astro config).

More than happy to work on a PR if you agree with this (and thanks for an amazing project)!

Common translations aren't loaded from src/i18n

Describe the bug
Translations from the folder src/i18n/common/ aren't loaded with the config generated by ./node_modules/.bin/astro-i18n install. I've only added a secondary locale to my config in my reproduction.

To Reproduce
Steps to reproduce the behavior:

  1. create a new astro project
  2. install latest version of astro-i18n
  3. perform setup as described in the README.md
  4. create the translation files in src/i18n/common/

Expected behavior
t('common.my_common') should return the translation with no configuration in astro-i18n.config.ts as implied by the README.md

Mandatory reproduction repository
See here

"readFileSync" is not exported by "__vite-browser-external", imported by "node_modules/astro-i18n/dist/src/index.mjs".

Describe the bug
Getting weird vite error on build; "readFileSync" is not exported by "__vite-browser-external", imported by "node_modules/astro-i18n/dist/src/index.mjs".

Any help here? Is this error related to i18n or rather to Vite?
Honestly have no idea what's causing this.

Console error log
"readFileSync" is not exported by "__vite-browser-external", imported by "node_modules/astro-i18n/dist/src/index.mjs". file: C:/Users/user/mistmedia-be/node_modules/astro-i18n/dist/src/index.mjs:1:1713 1: var J=(t,n,r)=>{if(!n.has(t))throw TypeError("Cannot "+r)};var h=(t,n,r)=>(J(t,n,"read from private field"),r?r.call(t):n.get(t)),C=(t,n,r)=>{if(n.has(t))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(t):n.set(t,r)},$=(t,n,r,e)=>(J(t,n,"write to private field"),e?e.call(t,r):n.set(t,r),r);var at=(t,n,r)=>(J(t,n,"access private method"),r);import{fileURLToPath as Un}from"url";function m(t){return Object.getPrototypeOf(t)===Object.prototype}function k(t){return Array.isArray(t)&&t.every(n=>typeof n=="string")}function ft(t){if(!m(t))return!1;for(let n of Object.values(t))if(typeof n!="string")return!1;return!0}function ut(t){if(!m(t))throw new Error(`"${t}" is not of type Record.`);for(let n of Object.values(t))if(typeof n!="string")throw new TypeError(`"${n}" is not of type string.`)}function Y(t,n){if(!t.includes(n))throw new Error(`${n} is not in [${t.join(", ")}].`)}function A(t){return Object.entries(t)}function lt(t,n,r){let e=t;for(let[o,i]of n.entries()){if(o===n.length-1){e[i]=r;break}if(!ct(e,i)){e[i]={},e=e[i];continue}if(Object.getPrototypeOf(e[i])===Object.prototype){e=e[i];continue}break}}function y(t,n,r){let{mode:e,modifyBase:o}={mode:"replace",modifyBase:!0,...r},i=o?t:H(t);for(let[a,s]of A(n)){let f=i[a];if(ct(i,a)){m(f)&&m(s)?y(f,s):e==="replace"&&(i[a]=n[a]);continue}i[a]=n[a]}return i}function H(t){return!t||typeof t!="object"?t:Array.isArray(t)?t.map(n=>typeof n=="object"?H(n):n):A(t).reduce((n,[r,e])=>{let o=n;return o[r]=H(e),n},{})}function ct(t,n){return Object.prototype.hasOwnProperty.call(t,n)}import Zt from"get-file-exports";import{readdirSync as Bt}from"fs";import{join as qt}from"path";import{readFileSy...

2: Add "${o}" to ${c}.supportedLangCodes or ${c}.defaultLangCode).`);if(typeof i!="string"){if(!m(i))throw new TypeError(`${c}.translations.${o} must be either a translation object or a (string) path to a json one.`);for(let[,a]of Object.entries(i))Rt(a)}}}function Ht(t,n){if(!m(t))throw new TypeError(`${c}.routeTranslations must be an object literal.`);let r=[];k(n)&&r.push(...n);for(let[e,o]of Object.entries(t)){if(r.length>0&&!r.includes(e))throw new TypeError(`${c}.routeTranslations.${e}, unsupported lang code. 3: Add "${e}" to ${c}.supportedLangCodes).`);if(typeof o!="string"&&!ft(o))throw new TypeError(`${c}.routeTranslations.${e} must be either a Record or a (string) path to a json one.`)}}function D(t){if(typeof t=="string")return!0;if(!m(t))return!1;for(let n of Object.values(t))if(!D(n))return!1;return!0}function Rt(t){if(typeof t!="string"){if(!m(t))throw new TypeError(`"${t}" must be either a string or a translation object.`);for(let n of Object.values(t))Rt(n)}}var Et=new Set(["json","js","cjs","mjs","ts","cts","mts"]),Xt={defaultLangCode:"en",supportedLangCodes:[],showDefaultLangCode:!1,trailingSlash:"never",translations:{},routeTranslations:{}};function Q(){return{...Xt}}function Qt(t){return t}async function Ot(t,n=""){let r=tn(t,n),e=r.endsWith(".json")?I(r):(await Zt(r)).default;return At(e),nn(e,t)}function tn(t,n=""){if(n){if(!q(n))throw new Error(`${n} is not an astro-i18n config file.`);let r=dt(n);if(!Et.has(r))throw new Error(`The astro-i18n config file must be a JavaScript or TypeScript file (found "${r}").`);return n}if(!N(t))throw new Error(`${t} is not the project's root directory.`);for(let r of Bt(t)){let[e,o]=Tt(r);if(!!Et.has(o)&&e===c)return qt(t,r)}throw new Error(`Could not resolve the astro-i18n config file, verify that you have a ${c}<.js/.ts> file in your project root directory "${t}".`)}function nn({defaultLangCode:t,supportedLangCodes:n,showDefaultLangCode:r,trailingSlash:e,translations:o,routeTranslations:i}={},a=""){let s=Q();if(t&&(s.defaultLangCode=t),n&&(s.supportedLangCodes=n),r!==void 0&&(s.showDefaultLangCode=r),e&&(s.trailingSlash=e),a){let f=[s.defaultLangCode,...s.supportedLangCodes];o&&(s.translations=rn(o,f,a)),i&&(s.routeTranslations=en(i,... error "readFileSync" is not exported by "__vite-browser-external", imported by "node_modules/astro-i18n/dist/src/index.mjs". File: C:/Users/user/mistmedia-be/node_modules/astro-i18n/dist/src/index.mjs:1:1713 Code: 1: var J=(t,n,r)=>{if(!n.has(t))throw TypeError("Cannot "+r)};var h=(t,n,r)=>(J(t,n,"read from private field"),r?r.call(t):n.get(t)),C=(t,n,r)=>{if(n.has(t))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(t):n.set(t,r)},$=(t,n,r,e)=>(J(t,n,"write to private field"),e?e.call(t,r):n.set(t,r),r);var at=(t,n,r)=>(J(t,n,"access private method"),r);import{fileURLToPath as Un}from"url";function m(t){return Object.getPrototypeOf(t)===Object.prototype}function k(t){return Array.isArray(t)&&t.every(n=>typeof n=="string")}function ft(t){if(!m(t))return!1;for(let n of Object.values(t))if(typeof n!="string")return!1;return!0}function ut(t){if(!m(t))throw new Error(`"${t}" is not of type Record.`);for(let n of Object.values(t))if(typeof n!="string")throw new TypeError(`"${n}" is not of type string.`)}function Y(t,n){if(!t.includes(n))throw new Error(`${n} is not in [${t.join(", ")}].`)}function A(t){return Object.entries(t)}function lt(t,n,r){let e=t;for(let[o,i]of n.entries()){if(o===n.length-1){e[i]=r;break}if(!ct(e,i)){e[i]={},e=e[i];continue}if(Object.getPrototypeOf(e[i])===Object.prototype){e=e[i];continue}break}}function y(t,n,r){let{mode:e,modifyBase:o}={mode:"replace",modifyBase:!0,...r},i=o?t:H(t);for(let[a,s]of A(n)){let f=i[a];if(ct(i,a)){m(f)&&m(s)?y(f,s):e==="replace"&&(i[a]=n[a]);continue}i[a]=n[a]}return i}function H(t){return!t||typeof t!="object"?t:Array.isArray(t)?t.map(n=>typeof n=="object"?H(n):n):A(t).reduce((n,[r,e])=>{let o=n;return o[r]=H(e),n},{})}function ct(t,n){return Object.prototype.hasOwnProperty.call(t,n)}import Zt from"get-file-exports";import{readdirSync as Bt}from"fs";import{join as qt}from"path";import{readFileSy..

To Reproduce
Steps to reproduce the behavior:

  1. Use my config
  2. Astro build
  3. Error

Expected behavior
Build process should work.

Mandatory reproduction repository
https://github.com/raremiroir/mistmedia-be

Error 404 : Unable to have any routes working.

Describe the bug
I'm working on my Astro boilerplate and would like to integrate your package for i18n.
After following the README.md I still not been able to make any routes working. All return 404. Althought astroI18n.pages & astroI18n.locales return the desired paths and languages. So my astro-i18n.config.mjs seems to be read and pages understood.

I also tried to copy/paste the config directly into the middleware without success.
Like : const astroI18n = useAstroI18n({...});

I'm on Apple OS M1 Pro.
I'm using astro ^3.5.4 & astro-i18n ^2.1.18.

To Reproduce
Steps to reproduce the behavior:

  1. Download https://github.com/Jeremboo/astro-boilerplate/tree/refactor-use-astro-i18n
  2. Install npm i and start npm run dev
  3. Click on 'Go to About.' or 'fr'.
  4. 404 returned

Expected behavior
Be capable to navigate through my astro pages src/pages/index.astro & src/pages/about.astro.

Mandatory reproduction repository
https://github.com/Jeremboo/astro-boilerplate/tree/refactor-use-astro-i18n

`createStaticPaths` callback receives incorrect `langCode`

Describe the bug
I'm building a site with Astro using content collections, and I'm facing something that might be a bug in createStaticPaths (or just me doing something wrong).

It all works well in development, but when building a site, the createStaticPaths callback receives an incorrect langCode (always the default one). The reason seems to be that import.meta.url is different when building: there is no language code in the file import url.

Because of this, content entries don't get translated, they all are in the default language.

I added a few console.logs inside getStaticPaths, ran npm run build and this is what I see:

Screenshot from 2023-07-23 22-40-59

Despite building a page from src/pages/en/... directory, the langCode is ru. And import.meta.url seems to be inside of the dist directory.

Right now I'm able to work around this if I change this code in the autogenerated src/pages/en/... directory:

export const getStaticPaths = (props) => proxyGetStaticPaths({
	...props,
-	langCode: extractRouteLangCode(import.meta.url),
+	langCode: "en",
})

This way I just hardcode the en locale for content pages inside the en directory. Then everything works fine.

To Reproduce
I put up a live demo of the bug, based on an Astro Blog example: https://effortless-malabi-bc87b1.netlify.app/
Navigate to Blog, then change the language to EN. Verify that the only entry's title says "First post (EN)" and open it. You will see the post, but now its title will be "First Post (RU)".

Expected behavior
I expect langCode to be correct when building content collection pages.

Mandatory reproduction repository
https://github.com/Klavionik/astro-i18n-bug

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.