-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
i18n routing RFC #734
i18n routing RFC #734
Conversation
bdd3bc5
to
6b5de3c
Compare
@SudoCat @jlarmstrongiv @maddsua @delucis I am looping you in because you gave excellent support and advice, so it would be great if we could shape this RFC together and answer all the questions. |
I might've missed it, but do we have a function that just returns language code? A use case for it would be like follows: // frontmatter
import { getLanguage } from 'whatever';
const lang = getLanguage(); // two-character language code // template
<p>
{ {
uk: 'Щось українською',
en: 'Something in English',
es: 'Algo en español',
fr: 'Quelque chose en français'
}[lang] }
</p> This is how I currently do localization for pages that can't be shoved into separate md file for each separate language. This approach works well in both SSR mode, where we pass language code on the fly, and SSG, where this variable gets passed through getStaticPath's props: // src/pages/[...lang].astro
import { localesList } from '@/data/locales.json';
const mkIntlPath = (langs: string[] | 'auto', dynamicRouteSlug: string) => {
const pageLanguages = langs === 'auto' ? localesList.map(item => item.code) : langs;
return pageLanguages.map(lang => ({
// The first element in localesList is considered to be the default language,
// so instead of it's code an undefined value is passed to create 'index' file
params: { [dynamicRouteSlug]: (lang === localesList[0].code ? undefined : lang) },
props: { lang }
}));
};
export function getStaticPaths() {
return mkIntlPath('auto', 'index');
}
const { lang } = Astro.props;
...
/*
Will render a page for each language in localesList:
--> index.html
--> en.html
--> fr.html
--> es.html
and so...
Call mkIntlPath with an array of language codes
to use something different than app-global language list, like so
mkIntlPath(['es', 'fr'], 'index');
*/ Edits: typo fixes cause I can't write |
This looks like a great start! I think I'll need a lil more time to gather some more detailed thoughts, but at first glance: Default Locale and Prefixes
Locale Standardisation and edge cases
Domain Routing
I suppose a lot of this is dependent on the adapters? given:
What will the user see if they visit MiddlewareI'm unsure about the middleware setup
UsageI believe we'll need to specify how users can access the Locale, and what information should be available. It will need to be available to pages, endpoints and middleware. With this in mind, I think this should be added as a separate key on the Astro global, likely named Something like this: type i18nGlobal = {
locale: string; // the locale identifier, as defined
locales: string[] // the list of all available locales
} I don't believe anything else should be needed on this object immediately. This should provide what is needed to render localised content, and show information relating to other locales if required. Route LocalisationI don't think the RFC touches on how we'd tackle route localisation. For individual pages using dynamic routes, this is quite simple - different route, different content. GetStaticPaths in SSG and some data-fetching in SSR. However for static routes and folders, this gets a little more tricky. As far as I can see, there's only a few options:
The only downside to these solutions is they don't allow for dynamic localisation, driven by an external source like a CMS. I personally do not see this as a problem, as these are unlikely to change frequently, and adding an asynchronous step into routing would be disastrous for performance! Misc
|
I don't understand the motivation behind |
It's quite common to only use relative URLs, especially on the frontend - you don't tend to define your navigation using absolute URLs with domains. Most websites I've worked on use absolute pathnames for their navigation links, without defining a hostname.
EDIT: wait no I spoke too soon and had a dumb brain moment. Confusing a Relative Path with a Relative URL. The original names of the functions are correct. A relative URL is a URL without a domain and protocol. An absolute URL is a URL with domain, protocol and pathname. I confused it with a relative/absolute path. |
Thanks, I think that explains it well - you would use these functions to manually construct links. ---
import { getRelativeLocaleUrl } from "astro:18";
import { removeLocaleBase } from "../helper-functions.ts"
---
<a href={getRelativeLocaleUrl('es') + '/' + removeLocaleBase(Astro.request.url)}>Read this article in Spanish</a>
<a href={getRelativeLocaleUrl('it') + '/' + removeLocaleBase(Astro.request.url)}>Read this article in Italian</a>
<a href={getRelativeLocaleUrl('fr') + '/' + removeLocaleBase(Astro.request.url)}>Read this article in French</a> |
@lilnasy I created several helpers in
There’s so many options and config variations that I decided to build these helpers into the package. The only case I haven’t had the time yet to implement is the relative urls. But, my goal was to avoid manual concatenation to avoid mistakes in joining paths. I think it would be nice for Astro to provide helpers like the proposal described because there are so many cases and it’s very easy to get it wrong. It’d be nice to use translated urls without thinking about it every time. I’m traveling this week and haven’t had a chance to go over the full proposal or questions above. I’ll check back this weekend. |
hmmm your code examples raises a really good point that perhaps we should have a method that can return a specific link in a specific locale, that will automatically handle replacing the locale if found with the one requested, with handling for default locale prefix removal and everything. Something like As a stretch goal, we should maybe consider handling Astro route urls: As @jlarmstrongiv points out, these sorts of helper URLs are great for avoiding common errors when building links. |
IMO it currently seems way too repetitive to be used all across a project (even more if migrating to it) Maybe something like A a nice approach could actually be a special i8n |
We don't, but we can definitely add it
Yeah, I want to strike a solution in the middle. I just merged a PR where we implement the Next.js behaviour. Although, I would like to understand the needs and shortcomings. I like the Nuxt approach, but there are too many options IMHO and their documentation isn't the best because it lacks examples in the documentation.
No, they should not. The idea is that the user decides where to have the localized folders/routes. Astro helps in doing proper routing.
How would this help? You might be onto something here 🤔
Yes, and it's possible that domain support needs to make various assumptions in order to work.
It's possible that the adapters should give the ability to do so, Astro will provide the proper information to do so. However, to set expectations right, I believe that
This was always a possibility. With the
Can you expand on this? The user can already read the locale from the headers. The RFC explains why the middleware is set after the user middleware, although we can put it before if you think it's best, although we need to have a stronger argument. |
The reason why I created
These are a lot combinations, as you might see from the tests. Astro already solved the issue internally, so we should take advantage and help users to create URLs with locales. Although I am not entirely sold on this utility, because this utility has some assumptions that I don't like. Maybe |
Yeah I definitely think we can do a little better than that. I think for starters, we should just add a
If PrefixExceptDefault is selected, (the default option), then the default locale will not be prefixed, and all other locales shall be prefixed. If we need later on, we could consider options like
Right okay, I'd like to see some examples of how this should work to better understand it. I'm not sure it's clear right now how this would work. Would this mean a user would create a folder like
It would allow users to opt specific pages in to localisation, regardless of where the file lived. It would avoid needing to restructure your project around localisation. It's not without flaws though - potentially more complicated and could increase build time overheads. This could also be extended to allow more complicated configuration objects - such as localising static routes or even folder names via export const localization = { en: 'about', fr: 'à-propos' }
I've used Sequence before, but I wasn't aware Astro core could inject middleware into the user's defined sequence. I presumed the user's would be required to manually include the middleware in their own sequence. I remember that integrations could not inject middleware into a sequence, so didn't expect there to be an internal API for it.
Would this be via reading the Accept-Language header, or by parsing the hostname manually to detect the locale? I guess my question hinges on how Astro handles parsing the locale, and providing that information to the developer, and what exactly the middleware will be doing. My understanding is that most of the i18n implementation will need to happen within Astro before the request even reaches the middleware, at the routing layer. What else would the middleware do? I might be misunderstanding middleware here, but aren't they processed in order of definition, not reverse? So if For example, in the website I recently worked on, we used middleware for creating our CMS API Client instance, so it was available to all pages. This API Client needed to be created with the locale as a parameter. Sorry for a lot of questions here, but I think I need to better understand the underlying implementation of the middleware/localisation to know where the middleware should sit in the chain. |
@SudoCat don't worry about the questions, that's what the RFC is for. I really like the proposal strategy, I will update the RFC accordingly.
Users can have their localised routes wherever they want, e.g.
I see your point now, and it makes sense. So here's how your middleware functions work. Suppose you have three middleware functions: Request Request
hello --------> validation --------> auth ┐
|
| Rendering, create Response
|
hello <-------- validation <-------- auth ┘
Response Response
However, your argument that we need to calculate the current locale is very important. |
This is a must for implementing Ideally, it would allow for adding a name to each locale, so that it can be shown and fetched without relying on separate config options, but that is not a big issue to work around. |
The signature of the APIs has been updated here |
@ematipico I have seen the changes. I get what you are going for with the path argument... but I'd add a simple Maybe return an array of objects with code, domain, root URL, etc.? As with the domain support it gets complicated if you just return the ISO code strings. |
I'll add About |
Understood, thank you. It's the first RFC I'm following, so I don't know exactly how they work here on Astro 😊 Regarding the This is of course less of a problem when you start having domains, as you need that in the front end to redirect, but when you have just a path which includes the language code, the JS is a lot simpler if you just keep it to the language code. Especially if you have very long URLs as I do... |
proposals/0041-i18n-routing.md
Outdated
i18n: { | ||
defaultLocaLe: 'en', | ||
locales: ['en', 'es', 'pt_BR', 'pt', 'fr'], | ||
detectBrowserLangauge: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bikeshed: Should it be detectLanguage
or something else? Not necessarily coming from a browser.
Due to time and difficulty issues, we removed the domain support from this RFC. We will land domain support in Astro 4.* under a feature flag. 22d192b |
Calling for consensus for this RFC. The period will last for 3 days and if there's no objections, this RFC will be merged. |
In 089253d, I removed {
routing: {
strategy: "pathname",
prefixDefaultLocale: true
}
} As discussed and proposed in #734 (comment) Also, myself and Chris had a chat, we changed the shape slightly to align it with the current one, and take advantage of the zod |
Looks good to me, @ematipico. The only thing I don't see, but may be there just not explained fully, is a way to get all locales, in the more granular configuration option, with their URLs, even if they are repeated. This is very useful for the link alternate i18n configurations (and possibly even for the sitemap plugin). |
I'll be happy to add such APIs, could you propose something with the expected result? It would help to get the API right easily |
Sorry, was on mobile... Given a config file like this: // astro.config.mjs
import {defineConfig} from "astro/config"
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', {
path: "portugues",
codes: ["pt", "pt-BR", "pt-AO"]
}]
}
}) Return something like this: [
["en", "/blog"],
["es", "/es/blog"],
["fr", "/fr/blog"],
["pt", "/portugues/blog"],
["pt-BR", "/portugues/blog"],
["pt-AO", "/portugues/blog"]
] Two issues I see, thinking about this now. The current Maybe an array of objects here, e.g. And probably the same or similar in the other cases? 🤔 |
I'll have to think about it, and we can definitely land a new API after we merge the RFC, there's nothing to prevent us from doing so. |
@ematipico Would be nice to have a feedback about code duplication (#734 (comment)) as from a quick look on the RFC it's not clear to me how, after the implementation, the projects files should be structured, (I probably missed between the comments) is there a sample repo to showcase how the page structure will look like? Thanks |
The RFC is meant for routing and fallback, not content (labels, dictionaries, etc.). We wanted to keep the first round of implementation small and provide as many utilises as possible so the community can build around them. Let's remember that nothing prevents us from adding more features. In fact, we already have a few features that we will add after this first RFC is merged, The fallback system is one of the most significant changes we added because it adds patterns that weren't possible in Astro core before. Once we deem that feature stable enough, we could use the same logic to generate pages to avoid code duplication. |
@ematipico Thanks for the clarification |
Co-authored-by: Matteo Manfredi <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working through all the feedback on this RFC @ematipico!
proposals/0041-i18n-routing.md
Outdated
Use `example.com/content/` for the default locale. Use `example.com/[lang]/content/` for other locales. | ||
Trying to access to use `example.com/[defaultLocale]/content/` will result into a 404. | ||
|
||
- `domain`: SSR only, it enables support for different domains. When a locale is mapped to domain, all the URLs won't have the language prefix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- `domain`: SSR only, it enables support for different domains. When a locale is mapped to domain, all the URLs won't have the language prefix. | |
- `domain`: SSR only, it enables support for different domains. When a locale is mapped to domain, the pathname in the URL won't have the language prefix. |
If a user has a `middleware.ts` file when the i18n routing is enabled, Astro will place its i18n middleware right after the one of the user: | ||
|
||
```js | ||
pipeline.setMiddlewareFunction( | ||
sequence(createI18nMiddleware(config), userMiddleware.onRequest) | ||
) | ||
``` | ||
|
||
By placing the middleware **after** the one of the user, Astro allows users to apply their business logic to the emitted `Response` of the i18n middleware. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused about this part:
Astro will place its i18n middleware right after the one of the user
The code showed that the i18n middleware is before the user's instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, the description is outdated. We place the middleware at the very beginning. I will update the description, good catch, thank you
For example, if `i18n.locales` contains `['pt', 'fr', 'de']`, and the value of the `Accept-Header` value is `en, fr;q=0.2, de;q=0.8, *;q=0.5`, then | ||
`Astro.preferredLocaleList` will be `['de', 'fr']` because `pt` isn't inside the header, and `en` isn't supported by the website. `de` comes first because it has a highest quality value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Astro.preferredLocaleList
will be['de', 'fr']
becausept
isn't inside the header
Isn't pt
covered by the *;q=0.5
? Or perhaps I misunderstood how the parsing works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, but it isn't part of the supported locales inside the configuration, so it isn't returned. If we had to parse and return the locales from the header, it wouldn't add much value, because the users can do that themselves. We cross-match the locales from the header with the ones of the configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noticed now, the description isn't correct, I will fix it.
Oops didn't notice this is merged, but anyways my comments aren't urgent, just some questions that I wonder if it's a typo or not. |
Hey folks, I am quite late to the party but I am currently discovering Astro, and I got a good knowledge of Next.js behavior and recent changes regarding i18n, so I'd like to share some feedback after spending ~1h playing with Astro i18n setup and reading the guide :
Most of these may just be documentation issue but I am also unsure about the architecture itself. Astro made a quite opinionated choice here, with a solution quite similar how Next.js worked before version 12. However since that, Next.js has started considering i18n routing as a part of the broader family of personnalization via redirection. User-land middlewares, which are meant to run also for static pages, allow devs to get full control of i18n. My feeling is that this is the inevitable direction that all frameworks are meant to take at some point. In Astro, to implement i18n routing, you seem to use an internal feature allowing to have middleware that can run for each HTTP request but also preserve the static nature of the page. Making this feature available to end user would allow them to take full control over i18n instead of relying on the framework provided implementation, but also cover similar personalization use cases. Edit: got answers to my questions, for now using an Edge Middleware is fine (so I can have a tiny redirection server in front of my Astro server) and discussion should keep going there: #795 |
Summary
i18n routing support baked into Astro.
Links