withastro / roadmap

Ideas, suggestions, and formal RFC proposals for the Astro project.
290 stars 29 forks source link

i18n routing RFC #734

Closed ematipico closed 8 months ago

ematipico commented 10 months ago

Summary

i18n routing support baked into Astro.

Links

ematipico commented 10 months ago

@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.

maddsua commented 10 months ago

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

SudoCat commented 10 months ago

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:

site: 'https://example.com'
i18n: {
  defaultLocale: 'en',
  locales: ['en', 'fr']
  domains: {
    fr: 'https://fr.example.com'
  }
}

What will the user see if they visit https://example.com/fr? What will the user see if they visit https://fr.example.com/en?

Middleware

I'm unsure about the middleware setup

Usage

I 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 i18n for consistency.

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 Localisation

I 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

lilnasy commented 10 months ago

I don't understand the motivation behind getRelativeLocaleUrl and related functions. What use case do they enable? What would an example application of it look like?

SudoCat commented 10 months ago

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.

However you raise a good point - relativeLocaleUrl might be the wrong term - the URL returned is still an absolute pathname, but without a protocol and domain. Perhaps we should rename these to getLocalePathname and getLocaleHref to closer conform to the URL object behaviour.

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.

lilnasy commented 10 months ago

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>
jlarmstrongiv commented 10 months ago

@lilnasy I created several helpers in astro-i18n-aut. I found that several settings affect building urls:

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.

SudoCat commented 10 months ago

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 getRelativeLocaleUrl(locale: string, pathname: string) and getAbsoluteLocaleUrl(locale: string, pathname: string).

As a stretch goal, we should maybe consider handling Astro route urls: getRelativeLocaleUrl(locale: string, pathname: string, params: Record<string, string>?). E.g. getRelativeLocaleUrl("en", "products/[sku]", { sku: 'foo' }) :eyes:

As @jlarmstrongiv points out, these sorts of helper URLs are great for avoiding common errors when building links.

Tc-001 commented 10 months ago

IMO it currently seems way too repetitive to be used all across a project (even more if migrating to it)

Maybe something like prependLocale(url, locale?) prependLocale("/foo") // /docs/en/foo

A a nice approach could actually be a special i8n <A> tag that handle everything for you if you just give it the href (without the base or language)

ematipico commented 10 months ago

@maddsua

I might've missed it, but do we have a function that just returns language code?

We don't, but we can definitely add it


@SudoCat

I see Redirect to default locale if prefix enabled; is mentioned in goals, but there doesn't seem to be a configuration specification designed.

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.

If i18n is enabled, should all routes be localised?

No, they should not. The idea is that the user decides where to have the localized folders/routes. Astro helps in doing proper routing.

if we are to handle this, my suggested solution would be a rollup extension similar to how SSG/SSR/Hybriad works, with an export const localize: boolean;.

How would this help? You might be onto something here 🤔

I suppose a lot of this is dependent on the adapters?

Yes, and it's possible that domain support needs to make various assumptions in order to work.

What will the user see if they visit https://example.com/fr? What will the user see if they visit https://fr.example.com/en?

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 https://example.com/fr needs to be available at least in development and production. https://fr.example.com/en should not be available.

Is Astro now capable of injecting middleware into a users middleware choices? (I was not aware of this capability)

This was always a possibility. With the sequence API you can compose middleware functions.

Would this middleware be responsible for determining which locale the user is on? Surely this would need to be executed before the user's middleware, so that their middleware could also read the locale

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.

ematipico commented 10 months ago

The reason why I created getRelativeLocaleUrl is mostly to help users to get the base URL right. Astro provides many options that make links very difficult to get right:

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 prependLocale(url, locale?) is what we need?

SudoCat commented 10 months ago

@ematipico

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.

Yeah I definitely think we can do a little better than that. I think for starters, we should just add a routingStrategy configuration, with an enum for values. Just stick to two simple values for now, with room to expand if needed. This could potentially be expanded upon for any domain routing configuration requirements.

type RoutingStrategy = {
  PrefixExceptDefault,
  PrefixAlways,
}

If PrefixExceptDefault is selected, (the default option), then the default locale will not be prefixed, and all other locales shall be prefixed. If PrefixAlways is selected, then all locales, including the default will use a prefix.

If we need later on, we could consider options like PrefixExceptDomain or something.

No, they should not. The idea is that the user decides where to have the localized folders/routes. Astro helps in doing proper routing.

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 src/pages/[locale] to put their localised pages/endpoints in?

How would this help? You might be onto something here 🤔

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 index.astro files:

export const localization = { en: 'about', fr: 'à-propos' }

This was always a possibility. With the sequence API you can compose middleware functions.

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.

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.

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 createI18nMiddleware is the last middleware in the sequence, then the i18n middleware response will not be available to user-defined middleware. If this i18n middleware is responsible for detecting and parsing the current locale, redirecting routes, wouldn't other middleware need to be aware of these changes first?

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.

ematipico commented 10 months ago

@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.

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 src/pages/[locale] to put their localised pages/endpoints in?

Users can have their localised routes wherever they want, e.g. src/pages/en_AU, or src/pages/blog/en_AU. Conversely, I also understand the need for a possible opt-out feature with an export const localize: boolean. I will think about it.

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

I see your point now, and it makes sense.

So here's how your middleware functions work. Suppose you have three middleware functions: hello, validation and auth, in this order. The journey of Request and Response happens as follows:

        Request             Request
hello --------> validation --------> auth ┐
                                          |
                                          | Rendering, create Response
                                          |
hello <-------- validation <-------- auth ┘
        Response            Response

auth is the last one the get the Request and the first one to get the Response. If we swap auth with our i18n middleware, the reason why I put it there is because I want to give the chance to the users to handle the Response emitted by the i18n middleware, in case it's needed.

However, your argument that we need to calculate the current locale is very important.

itsmatteomanf commented 10 months ago

We don't, but we can definitely add it

This is a must for implementing <select> to allow for language selection. A list of locales and the current locale are a must...

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.

ematipico commented 10 months ago

The signature of the APIs has been updated here 4718adb (#734)

itsmatteomanf commented 10 months ago

@ematipico I have seen the changes. I get what you are going for with the path argument... but I'd add a simple getCurrentLocale() and getAllLocales() or similar, you can do it easily with these, but it's an additional step.

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.

ematipico commented 10 months ago

I'll add getCurrentLocale() to the RFC when I have more technical information I can share. I am exploring the API and how it will work both in the backend and frontend. I don't feel comfortable adding an API if I can't explain the internals.

About getAllLocales(), can you give me more info about it? Like use cases

itsmatteomanf commented 10 months ago

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 getAllLocale(), the main use case would be a page (or component, like a <select>) that lists all locales with links. The first can be done currently relatively easily, the second one it means the values of each option would be a path, which is not the worst (but may be problematic with long URLs). The issues arise when I want to get a name of the language from something like i18next, which wants the language code... what do I do then? Do I manually keep a separate list? Do I strip the URL of extra stuff from values with path = ""?

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

jlarmstrongiv commented 10 months ago

@itsmatteomanf and @ematipico I had a similar function called getAllLocaleUrls for that exact purpose of a language picker.


Another helpful addition may be to expose parts of the config in astro:18 for users and custom middleware. For example, accessing the config below would be helpful to build my own utilities and handle my own edge cases:

{
  i18n: {
        defaultLocaLe: 'en',
        locales: ['en', 'es', 'pt', 'fr'],
        domains: {
            pt: "https://example.pt"
        }
    }
}

I like export const localize: boolean and export const localization = { en: 'about', fr: 'à-propos' }. It’s important to be able to opt-out and having a way to translate the urls would be amazing!


Users can have their localised routes wherever they want, e.g. src/pages/en_AU, or src/pages/blog/en_AU. Conversely, I also understand the need for a possible opt-out feature with an export const localize: boolean. I will think about it.

In astro-i18n-aut, I assumed every page was localized unless the globs excluded it. I like the idea of using the default pages directory with export const localize: boolean if it’s not localized, similar to the export const prerender: boolean.

Though, I remember a previous conversation that emphasized that some content would be region locked, meaning some translations and product pages would be available in some locales, but not in others.

The benefit of export const localization = { en: 'about', fr: 'à-propos' } would allow for both localizing urls and listing only partial translations, if a page is supported in English and French, but not Spanish for example. The getAllLocaleUrls would also need to take into account the translated urls and partial page translations too.

Just something to think about

matthewp commented 10 months ago

I like export const localize: boolean and export const localization = { en: 'about', fr: 'à-propos' }. It’s important to be able to opt-out and having a way to translate the urls would be amazing!

In the past we've avoided similar types of APIs (someone suggested something like this for redirects) because Astro doesn't read all of your files at startup. It only reads them when requested. This makes our startup time faster. So I don't think we should add this kind of feature that would require first compiling every page. Even though I agree it's a nice sort of API.

artursopelnik commented 10 months ago

Hello everyone. I co-authored the official i18n workaround and worked on an enhanced version with content collection, trailing slash and basic support. Maybe you can learn something for this RFC or use this workaround in the meantime.

Repo: https://github.com/artursopelnik/astro-translate-routes-example

Demo: https://artursopelnik.github.io/astro-translate-routes-example/

Article: https://www.webdesign-sopelnik.de/en/blog/translate-routes-for-astro-content-collections-or-subpages-with-trailingslash-and-base-support/

itsmatteomanf commented 10 months ago

@ematipico re: root page redirect.

If I have the routing strategy set as prefix-other-locales the root page (pages/index.astro) will be the default language home page. And that is fine and expected.

If I were to set the routing strategy to prefix-always the pages/index.astro page would be empty, or a redirect. It'd be nice to have the option to select if you wanted to have the redirect or not, so one could do a landing page to select a language, or anything the user wants. I know it's currently not implemented, so it's perfectly fine and allows both use cases, but I'd like for there to be an opt-out to that behaviour if it ever were to become standard :)

itsmatteomanf commented 9 months ago

An additional, and important, thing to consider is how to handle the pages/404.astro and related files.

Some services (Cloudflare Pages, for example) expect a 404.html file in the root of the public folder to define if an app is a SPA or not. It will not be an issue for prefix-other-locales, but not sure how it currently is handled for prefix-always. Does it keep that file or causes issues? Or is it overwritten with a redirect?

lilnasy commented 9 months ago

I think there needs to be a story for error pages in general.

delucis commented 9 months ago

Locale => language mapping

We discussed this in early planning, but @ematipico and I were just chatting and I realised this wasn’t covered in this RFC: currently locales lets you set the URL path segments to be treated as a specific locale, but there’s no explicit mapping from that to a language:

locales: ['en', 'it']
// Enables: example.com/en and example.com/it to be treated as separate locales

The current design implicitly assumes that 'en' and 'it' in this case are ALSO the current language for features such as Astro.preferredLanguage or redirecting according to Accept-Language headers. However, languages must be specified in the restrictive BCP-47 format in order to be interoperable with things like Accept-Language headers or Intl JavaScript APIs. Because the current design makes a 1:1 mapping between URL path and language, it places a restriction on how people design their URLs: they must use a valid BCP-47 tag for their locales.

Some example use cases:

To offer one possibility, the config could look something like

interface PartialI18nConfig {
    locales: Array<string | { path: string, lang: string }>
}

So for the examples above config could look like:

{
    locales: [{ path: 'usa', lang: 'en-US' }, { path: 'uk', lang: 'en-GB' }]
}
{
    locales: [{ path: 'pt-br', lang: 'pt-BR' }]
}

You could still user the string shorthand when your locale and lang are the same, but have a way to explicitly set the locale => language mapping when needed.

Another option would be switching to a map of locale keys to language values. This would mirror what @astrojs/sitemap does in its i18n config.

itsmatteomanf commented 9 months ago

@delucis one variation not covered in you post is that one may want content to be the same for multiple variations.

For example Italian has 4 BCP-47 options:

All these span a geographic area which almost never brings different content, there are cases, of course, but not many. So a mapping that allows multiple Accept-Language values to map to a single it, both as main locale (which HTML support as lang="it") and path would be good. Especially if this RFC ever gets expanded to a more fully fledged i18n implementation that offers a way to get the current locale for the build.

delucis commented 9 months ago

Routing strategy config

_Picking up some discussion around the shape of the routing config started in https://github.com/withastro/astro/pull/9006#discussion_r1383559633 so it doesn’t get lost._

Current design

Currently, the RFC proposes a routingStrategy option with an enum of potential values:

interface PartialI18nConfig {
    routingStrategy: 'prefix-always' | 'prefix-other-locales';
}

The intention is to expand these values in the future if/when we add support for subdomains (e.g. fr.example.com instead of example.com/fr).

I raised the concern that the current options are not very clear:

The configuration options do not make it easy to understand what they do.

Aligning with user thinking

Given the control users are currently being given (ignoring subdomains for a minute), we can boil down the user question to:

Do I want to use a subpath for my default locale or not? (example.com/en/ vs example.com)

This is an easy question to answer for most users, but the current design requires you to answer prefix-always for “Yes, please prefix my default locale” or prefix-other-locales for “No, please don’t prefix my default locale”.

One option to align the configuration with the question a user will ask themselves would be:

interface PartialI18nConfig {
    routing: {
        prefixDefaultLocale: boolean; // default: false
    }
}

This gives users a simple yes/no toggle that aligns with their answer to the question above.

Subdomains

This brings us to the future and subdomains. IIUC, the string enum solution was chosen to help leave room for subdomains in the routingStrategy picture. For example, by adding 'subdomain-always' and 'sudomain-other-locales' options (assuming something similar to the current design).

I would argue that this is combining two distinct user questions into one option.

The second user question is:

Do I want to use subdomains for my locales or subpaths? (en.example.com vs example.com/en)

This is a different question entirely from “Do I want to use a subpath/subdomain for my default locale?”

Given this, I think it’s ideal to separate the config into two options aligned with these two user questions, for example:

interface PartialI18nConfig {
    routing: {
        prefixDefaultLocale: boolean; // default: false
        strategy: 'pathname' | 'subdomain'; // default: 'pathname'
    }
}

@ematipico raised a concern here:

I would like to avoid breaking down the feature in too many flags because they are a footgun, and they become more difficult to maintain.

I can’t comment on the maintenance difference between these setups, but if it is harder to maintain, it may still be worth the trade-off IMO, if the result is a clearer experience for Astro users. If we really prefer the 4-way enum, Zod can happily help us out under the hood:

type RoutingStrategies = 'pathname-always' | 'pathname-non-default' | 'subdomain-always' | 'subdomain-non-default';

const RoutingStrategySchema = z.object({
    prefixDefaultLocale: z.boolean().default(false),
    strategy: z.enum(['pathname', 'subdomain']).default('pathname'),
}).transform(routing =>
    `${routing.strategy}-${routing.prefixDefaultLocale ? 'always' : 'non-default'}` as RoutingStrategies
);

One counter proposal in the original discussion uses suburl where I initially proposed subpath:

I used "URL" because that's what the users see, and "path" is misleading because it can be used for files too.

In my examples here I updated the suggestion to pathname to match what the standard URL object calls that portion of the URL.

ematipico commented 9 months ago

Thank you for the feedbacks and sorry for my late reply, I have been working on the feature and I was super focused on landing the first batch of features.

@itsmatteomanf

If I were to set the routing strategy to prefix-always the pages/index.astro page would be empty, or a redirect. It'd be nice to have the option to select if you wanted to have the redirect or not, so one could do a landing page to select a language, or anything the user wants.

Do you think it is vital to this feature? I believe this can be created in userland by creating a middleware. We will soon release the feature under the experimental flag, and you can let us know how's the experience.

Some services (Cloudflare Pages, for example) expect a 404.html file in the root of the public folder to define if an app is a SPA or not. It will not be an issue for prefix-other-locales, but not sure how it currently is handled for prefix-always. Does it keep that file or cause issues? Or is it overwritten with a redirect?

I believe - I need to check the code because I don't remember - that when we render the 404, we don't trigger the middleware, which means that 404.astro is exonerated from the i18n routing logic. Although, we should test it and see if that is correct.


@delucis

I like the locale mapping; I think it's something that makes sense to have baked into Astro since it does involve routing.

Considering @itsmatteomanf comment, I believe we should go even further and have something like this, too:

{
    locales: [
        { path: 'it', lang: [ "it", "it-CH", "it-IT",  "it-SM",  "it-VA"] }
    ]
}

Or to have lang as an array to begin with.

Regarding the routing strategy, I like where this is going, and I like the new proposed names, I think we can adopt them.

tysian commented 9 months ago

Hey! I've finished reading docs for experimental i18 features in Astro 3.5.0 and thought this place will be good for my question.

Most of websites I'm working on are translated in couple different languages and data is fetched from CMS with language parameter.

So let's say I need to create 10 pages in 10 languages. Basing on current documentation... I have to manually create (or rather copy/paste) 100 astro files?

Is it (or will be) achievable to render /[lang]/example route using src/pages/example.astro file?

Example/Proposal

Files structure

Config

import { defineConfig } from "astro/config";

export default defineConfig({
  experimental: {
    i18n: {
      defaultLocale: "en",
      locales: ["es", "fr"],
    },
  },
});

Output

This will generate routes for multiple languages:

We can get current language as prop or using some helper function from astro:i18n

---
const { lang } = Astro.props;

const data = await fetchFromCMS({language: lang})
---

Is it achievable with current state of this feature? Or is this planned for the future?

itsmatteomanf commented 9 months ago

@ematipico

Do you think it is vital to this feature? I believe this can be created in userland by creating a middleware. We will soon release the feature under the experimental flag, and you can let us know how's the experience.

Vital? No. Nice to have? Definitely. To me it's a simple config option: redirectRootToDefaultLocale: true, which I guess does allow for unprefixed routes to go to the default locale, if they exist there.

I believe - I need to check the code because I don't remember - that when we render the 404, we don't trigger the middleware, which means that 404.astro is exonerated from the i18n routing logic. Although, we should test it and see if that is correct.

I'm gonna try and do a test... whoever does first will let this thread know. Might be true, if I remember correctly there was some other thing that this behaviour causes in a completely different topic, but I'm gonna check anyway.

Considering @itsmatteomanf comment, I believe we should go even further and have something like this, too:

{
  locales: [
      { path: 'it', lang: [ "it", "it-CH", "it-IT",  "it-SM",  "it-VA"] }
  ]
}

Or to have lang as an array to begin with.

Yes, agreed there. But at this point you really should offer a way to get all these config read from the pages, I know I could do a separate file and load that in both places, but a way to get all languages I configured, with a default language string (something needs to be put in the lang HTML attribute...), though.

So maybe something like?

{
    locales: [
        {
            path: 'it',
            default: "it", // this is the one you want whenever you have to chose like a single value for the specific locale 
            lang: [ "it", "it-CH", "it-IT",  "it-SM",  "it-VA"] // these you use for like canonicals, matching routes, etc.
        }
    ]
}

This would then, ideally, offer a Astro.i18n that exposes these settings? So that I can do things simpler next, without actually going into dictating how the user should setup things?


@tysian

That was always possible, without this proposal. This proposal currently allows you to simplify routing between languages.

Just enclose your language pages in a [lang]/ folder, and use that param to generate all of them. That'd be just { lang } = Astro.params; and handling like any other param, be it in static paths or SSR.

I guess the only case in which you may need duplicated files, unless you want to do the [...lang]/ folder, but it's a mess if you have a need for that in other things, is if you want the default locale without a prefix.

jlarmstrongiv commented 9 months ago

In hindsight, I was expecting what @tysian described. I got close with astro-i18n-aut, but still run into problems with astro core in dev. It’s not possible to have a defaultLocale and render all your pages using a template file. It would be great for Astro to support that use-case

tysian commented 9 months ago

@itsmatteomanf

Yeah, this is the way I was doing it before. I was just creating src/pages/[lang]/example.astro and were doing it this way.

But this wasn't possible, as you mentioned, when I want to skip lang prefix for default locale, so I had to copy/paste all my routes pages at least twice (for default lang, and other languages).

The [...lang] is not the best solution also, as if pages need to have different tamplates, I need to make conditionals to match exact template needed.

My final workaround was to create views directory and using its components instead. The pages in that scenario is only for routing purposes, it imports required template from views directory uses it immidately passing down all the props, etc.

Views

Pages

So and the end of the day, this can't be done without weird workarounds and bad developer experience 😕

It would be pretty cool, to have some solution for it. I've tried to use fallback feature, but it redirected me to fallback page and I couldn't get lang code from Astro.url (it returned object of fallbacked page instead).

@jlarmstrongiv

This library looks pretty cool, I'll take a deeper dive into it in my next project, thank you 😉

delucis commented 9 months ago

Congrats on the first public release of the experimental i18n API @ematipico! 🎊 Really excited to see what everyone builds with it and discovers trying it out.


Re: locale mapping, I’m not sure I understand what that suggested config would do:

{ path: 'it', lang: [ "it", "it-CH", "it-IT",  "it-SM",  "it-VA"] }

If you only have a single path (it), wouldn’t it always serve with the same lang?

Or is the proposal here only about resolving preferred locale (e.g. via Accept-Language)? So that Astro knows that for a request with Accept-Language: it-SM, it’s valid to respond with the /it/ route? Wondering if that could already be handled by fallback without needing to complicate this 🤔

itsmatteomanf commented 9 months ago

@tysian

The [...lang] is not the best solution also, as if pages need to have different tamplates, I need to make conditionals to match exact template needed.

No, you don't need any complex conditional logic there.

You can do src/pages/[...lang]/product/4321.astro, and inside you do a getStaticPaths() which returns the lang param to be either the language or '' if it's the default locale. The only limit here is that if you needed another param after lang (the other way round works) you can't have it, but it will be incorporated into lang itself.

You'd then get:

- src/pages/product/4321.astro
- src/pages/it/product/4321.astro
- src/pages/es/product/4321.astro

Personally I'd keep it to all pages with the language in the url, much simpler to do, much cleaner as it clearly states the current locale, and way easier to handle locale changes. Of course, though, not all projects can or want this.

I then use Astro locals to make sure all components have the locale set. I could probably do a prop on the layout or something, but it's easier given you don't need to make sure it's passed through multiple layers. There it's available everywhere.

tysian commented 9 months ago

@itsmatteomanf

Woah, I've never thought of that this way. Thank you for all explanations, I'll definitely check this out 😄

Thanks!

itsmatteomanf commented 9 months ago

@ematipico

I tested the 404 page on a prefix-always build. It gets ignored, as do any other routes/pages without a prefix. I have src/pages/index.astro, src/pages/404.astro, src/pages/browserconfig.xml.ts and it ends up with just a index.html.

This won't work with Cloudflare Pages, for example.

itsmatteomanf commented 9 months ago

@ematipico cont: message above.

It seems to ignore also any other folder, I had a folder with routes which created .js files for analytics purposes and it gets removed from the build, too. I guess if it's not in a /[lang]/ path it gets ignored if the setting is prefix-always.

fatbobman commented 9 months ago

@ematipico

Do you think it is vital to this feature? I believe this can be created in userland by creating a middleware. We will soon release the feature under the experimental flag, and you can let us know how's the experience.

Vital? No. Nice to have? Definitely. To me it's a simple config option: redirectRootToDefaultLocale: true, which I guess does allow for unprefixed routes to go to the default locale, if they exist there.

Definitely! At least for me, this feature is very important, and I hope to be able to easily implement the logic based on the root index.

such as based on the browser language or by creating a static language selection page on the root index.

itsmatteomanf commented 9 months ago

I did manage a workaround for the problems in the prefixing, but I'd love for that to be solved in core.

You can use x-default as the default locale, which is the correct language code to use for unspecified language, and then set it as prefix-other-locales. There it would work, but of course when you ask for all links you get a unprefixed URL which you need to ignore.

ematipico commented 9 months ago

@ematipico cont: message above.

It seems to ignore also any other folder, I had a folder with routes which created .js files for analytics purposes and it gets removed from the build, too. I guess if it's not in a /[lang]/ path it gets ignored if the setting is prefix-always.

@itsmatteomanf

Yeah, that's the idea of the routing strategy prefix-always. Even endpoints fall under the same logic.

Are there use cases where 404 pages need to be translated? If not, we could safely assume that 404 pages and 500 pages can always be rendered at the root.

itsmatteomanf commented 9 months ago

@ematipico

Yeah, that's the idea of the routing strategy prefix-always. Even endpoints fall under the same logic.

Then it's, for me at least, unusable. The main benefit is the automatic URL generation (does it require a non-prefixed path or does a prefixed path would work? I have to try that...) and the auto redirect for fallback. They are not hard to implement, albeit either with a custom middleware or a bunch of duplicated code, which isn't ideal.

If I need to provide a language agnostic endpoint, like an analytics script, I don't want to split it per language, it makes no sense to do so.

I need to be able to add root files at will, whatever they may be and not only if those are in the public/ folder, I may need to generate a robots.txt file, anything. Or even a sitemap, without using the plugin for whatever reason, security.txt is another option. Those all become impossible unless they are static and in the public/ folder.

IMHO if the implementation needs to be agnostic to what the user does (which I can agree with, it allows more freedom, which is what Astro does in general), it doesn't have to force me to put everything under a locale path. If it does, then let's avoid the folder altogether and make the implementation handle things fully, because there is little difference there, just more set-up for the user. Then you need to provide current locale, etc. options and methods though.

One thing I need to try is sub folders with the locale, do they work or is the locale folder expected to be a top-level folder in the pages directory?

ematipico commented 9 months ago

@itsmatteomanf

How do you think we should handle that? I can totally see the use case, although it's kind of the point of prefix-always routing strategy.

So, how do you think we should make sure that all routes have the prefix in their prefix while having some routes be exonerated from the routing logic?

itsmatteomanf commented 9 months ago

@ematipico

So, how do you think we should make sure that all routes have the prefix in their prefix while having some routes be exonerated from the routing logic?

I'd personally avoid getting involved with any of the page routing decisions, bar the redirects which are added according to the choices in the config. You could expose a warning for the pages without a path locale, which can be silenced with some config option...

I presume given this discussion you can't do just a single nested localised folder.

ematipico commented 9 months ago

Thank you @itsmatteomanf, for your answer and your point of view! Yeah, routing is a soft subject and not easy to get right.

As for now, we internally decided to allow-list all endpoints: https://github.com/withastro/roadmap/pull/734/commits/6954fb224f162911bdd2f46fc885344572c3669f

In case something changes, I will update the RFC.

I am unsure this is the right way to go regarding your suggestion. You seem to be an experienced developer, and you know your way around Astro and the subject, so a warning can be enough, although I fear that this won't be sufficient for beginners or less experienced on the subject. We should at least come up with a compromise.

itsmatteomanf commented 9 months ago

@ematipico yeah, you could also go the other way around, default to ignoring all root things and then allow users to chose with a config option. Easier for beginners, but must be documented, as not all root things get ignored depending on where they are put and when they are generated in the Astro build process.

sandstrom commented 9 months ago

The new i18n stuff is great!

I've read all the docs, and this thread. We're considering switching to Astro, and a few i18n-related things that I still haven't figured out how to do in Astro are:

Locales folder example

// example structure for our `src/locales` folder
.
├── components
│   ├── de.yml
│   ├── en.yml
│   └── fr.yml
├── dict
│   ├── de.yml
│   ├── en.yml
│   └── se.yml
├── layout
│   ├── de.yml
│   ├── en.yml
│   └── fr.yml
└── pages
    ├── about
    │   ├── careers
    │   │   ├── en.yml
    │   │   └── fr.yml
    │   └── press
    │       ├── de.yml
    │       ├── en.yml
    │       └── fr.yml
    ├── benefits
    │   ├── de.yml
    │   ├── en.yml
    │   └── fr.yml
    ├── schedule
    │   ├── en.yml
    │   └── fr.yml
    ├── features
    │   ├── users
    │   │   ├── de.yml
    │   │   ├── en.yml
    │   │   └── fr.yml
    │   ├── calendar
    │   │   ├── de.yml
    │   │   ├── en.yml
    │   │   └── fr.yml
ematipico commented 9 months ago

Added currentLocale: https://github.com/withastro/roadmap/pull/734/commits/f934880ab5835492e56ddbb69e3f32662310c8bf

ematipico commented 9 months ago

We landed to the conclusion that routing strategies should be applied only to pages: https://github.com/withastro/roadmap/pull/734/commits/c93015d44a8dc9e0794c4067899d8f587aaabf47

itsmatteomanf commented 9 months ago

@ematipico

We landed to the conclusion that routing strategies should be applied only to pages: c93015d

This would solve most of the issues, but the routing is still pretty messed up which makes it hard to check. Especially the fallback part... I bet you saw the issues I opened.

ematipico commented 9 months ago

Added more information about domain support: https://github.com/withastro/roadmap/pull/734/commits/713fce74bb742f18ad985055758c3818b5eed9aa