#i18n content collections
52 messages · Page 1 of 1 (latest)
Hi @zinc elk. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
To configure content collections for i18n in Astro with prefixDefaultLocale: false, you need to ensure your file and folder structure matches Astro's routing expectations for this setting.
Key Points from the Docs
- With
prefixDefaultLocale: false(the default), the default language does not use a/[locale]/prefix in URLs or folder structure. Only non-default locales use a language prefix. - Your default locale's content should be at the root of
src/pages/, not inside a language folder. - For dynamic routes (like blogs), you should have:
src/pages/blog/[...slug].astrofor the default locale (root, no prefix)src/pages/[lang]/blog/[...slug].astrofor other locales (with prefix)
Example Structure
Suppose your default locale is "en" and you also support "fr":
src/
pages/
blog/
[...slug].astro // Handles default locale (en)
fr/
blog/
[...slug].astro // Handles French locale
Content Collections
Your content collections should be organized by language, for example:
src/
content/
blog/
en/
post-1.md
post-2.md
fr/
post-1.md
post-2.md
Dynamic Route Example
In src/pages/blog/[...slug].astro (for default locale):
---
// For static rendering
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const pages = await getCollection('blog');
// Only include default locale (en)
const paths = pages
.filter(page => page.id.startsWith('en/'))
.map(page => {
const [, ...slug] = page.id.split('/');
return { params: { slug: slug.join('/') || undefined }, props: page };
});
return paths;
}
const { slug } = Astro.params;
const page = Astro.props;
const { Content } = await render(page);
---
<Content />
In src/pages/[lang]/blog/[...slug].astro (for other locales):
---
// For static rendering
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const pages = await getCollection('blog');
// Exclude default locale (en)
const paths = pages
.filter(page => !page.id.startsWith('en/'))
.map(page => {
const [lang, ...slug] = page.id.split('/');
return { params: { lang, slug: slug.join('/') || undefined }, props: page };
});
return paths;
}
const { lang, slug } = Astro.params;
const page = Astro.props;
const { Content } = await render(page);
---
<Content />
Why Your Default Locale Might Not Work
If you only have src/pages/[lang]/blog/[...slug].astro, then with prefixDefaultLocale: false, the default locale (e.g., /blog/post-1) will not be matched, because Astro expects the default locale's routes to be at the root, not under a language folder. You must have a root-level dynamic route for the default locale.
References
If you follow this structure, your default locale should work as expected without a language prefix in the URL.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak so the moral of the story is to have 2 [...slug] files. One for default and one for other languages
Let me check the knowledge base for you again. One moment! :books:
Yes, that's correct. When using prefixDefaultLocale: false, you need two dynamic route files for your content collections:
- One at the root (e.g.,
src/pages/blog/[...slug].astro) for your default locale, which does not use a language prefix in the URL. - One inside a language folder (e.g.,
src/pages/[lang]/blog/[...slug].astro) for all other locales, which do use a language prefix in the URL.
This setup matches Astro's routing expectations for i18n and ensures both your default and translated content are properly routed. The documentation explicitly shows this pattern for translated content collections and dynamic routes, and explains that the default locale's content should be at the root when prefixDefaultLocale: false is used Use collections for translated content Routing: prefixDefaultLocale.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const entries = await getCollection('games');
// Only include default locale (en)
const paths = entries
.filter(entry => entry.id.startsWith('en/'))
.map(entry => {
const [, ...slug] = entry.id.split('/');
return { params: { slug: slug.join('/') || undefined }, props: entry };
});
return paths;
}
const { slug } = Astro.params;
const entry = Astro.props;
const { Content } = await render(entry); >>> im getting error RenderUndefinedEntryError
Attempted to render an undefined content collection entry.
See Docs Reference
Check if the entry is undefined before passing it to render()
node/module-runner.js:1062:11
Let me check the knowledge base for you again. One moment! :books:
The error RenderUndefinedEntryError occurs when you attempt to render a content collection entry that is undefined. According to the Astro documentation, this can happen if you try to render an entry that does not exist. The recommended solution is to check if the entry is defined before passing it to render() (Attempted to render an undefined content collection entry).
In your code, you are using Astro.props to get the entry, which should work if you are returning the entry as props in your getStaticPaths. However, if for some reason the entry is not being passed correctly, or if the entry is missing, you will get this error.
How to fix:
Add a check before calling render(entry):
---
const entry = Astro.props;
if (!entry) {
throw new Error('Content entry not found.');
}
const { Content } = await render(entry);
---
Or, you can handle it more gracefully, for example by showing a 404 or a message.
Why this happens:
- If your
getStaticPathsdoes not return the correctpropsfor every path, or if the entry is missing from your collection,Astro.propswill beundefined. - The Astro documentation shows that you should always check if the entry exists before rendering.
Summary:
Check if entry is defined before calling render(entry). This is required because the return type of getEntry and the props you pass can be undefined, and Astro will throw this error if you try to render an undefined entry (RenderUndefinedEntryError).
If you continue to see this error, double-check that your getStaticPaths is correctly mapping and passing the right entry as props for each path.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak ---
import { getLangFromUrl, useTranslations } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
<ul>
<li>
<a href={/${lang}/home/}>
{t('nav.home')}
</a>
</li> >>> how do it use this when use prefixdefaultlocal=false ?
Let me check the knowledge base for you again. One moment! :books:
When using prefixDefaultLocale: false, the default locale should not have a language prefix in the URL. Only non-default locales use the /[lang]/ prefix. So, your link generation logic needs to account for this.
You can update your code to conditionally include the language prefix only for non-default locales. The Astro i18n recipe provides a helper for this:
Example helper in src/i18n/utils.ts:
import { defaultLang, showDefaultLang } from './ui';
export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
return !showDefaultLang && l === defaultLang ? path : `/${l}${path}`;
}
}
Set showDefaultLang = false in your ui.ts to hide the default language in the URL.
Usage in your component:
---
// imports
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
const translatePath = useTranslatedPath(lang);
---
<ul>
<li>
<a href={translatePath('/home/')}>
{t('nav.home')}
</a>
</li>
</ul>
With this setup, when the current language is the default, the link will be /home/. For other languages, it will be /<lang>/home/ Hide default language in the URL.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak Got it, that works. Now how do i translate the /home/ slug for each lang?
Let me check the knowledge base for you again. One moment! :books:
To translate the /home/ slug for each language, you can create a route mapping object and a helper function to generate the correct path for each language. The Astro documentation provides an example of this approach:
1. Add route mappings in src/i18n/ui.ts:
export const routes = {
de: {
'home': 'startseite',
},
fr: {
'home': 'accueil',
},
// Add more languages as needed
};
2. Update your helper in src/i18n/utils.ts:
import { defaultLang, showDefaultLang, routes } from './ui';
export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
const pathName = path.replaceAll('/', '');
const hasTranslation = defaultLang !== l && routes[l] !== undefined && routes[l][pathName] !== undefined;
const translatedPath = hasTranslation ? '/' + routes[l][pathName] : path;
return !showDefaultLang && l === defaultLang ? translatedPath : `/${l}${translatedPath}`;
}
}
3. Use the helper in your component:
---
import { getLangFromUrl, useTranslations, useTranslatedPath } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
const translatePath = useTranslatedPath(lang);
---
<ul>
<li>
<a href={translatePath('/home/')}>
{t('nav.home')}
</a>
</li>
</ul>
With this setup, the /home/ slug will be translated according to the current language, e.g. /accueil/ for French, /startseite/ for German, and /home/ for English (default) Translate Routes.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak do i need to change the page name as well in my pages folder? For example /pages/fr/accueil.astro
Let me check the knowledge base for you again. One moment! :books:
Yes, you do need to change the page name in your pages folder to match the translated slug for each language.
For example, if you want the French /home/ route to be /fr/accueil/, you should create the file as src/pages/fr/accueil.astro. This is because Astro’s file-based routing uses the file and folder names to generate the URLs. So, the translated route must correspond to the actual file name in the appropriate language folder Translate Routes.
This means:
- For English (default):
src/pages/home.astro→/home/ - For French:
src/pages/fr/accueil.astro→/fr/accueil/
This approach ensures that your translated slugs appear in the URLs as intended.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak im using trailing slash, so issue with your current implementation that im getting /fr/accueil instead of /fr/accueil/
Let me check the knowledge base for you again. One moment! :books:
When using Astro with trailingSlash: 'always', URLs should end with a trailing slash (e.g., /fr/accueil/). According to the documentation, Astro’s routing will handle this for you if your file structure matches the expected pattern and your trailingSlash config is set to 'always' (Configuration Reference: trailingSlash).
If you are generating links manually or with a helper, make sure your helper returns paths with a trailing slash. For example, in your useTranslatedPath helper, you can ensure the returned path always ends with a /:
export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
let translatedPath = ... // your translation logic
// Ensure trailing slash
if (!translatedPath.endsWith('/')) {
translatedPath += '/';
}
return !showDefaultLang && l === defaultLang ? translatedPath : `/${l}${translatedPath}`;
}
}
This will ensure that links like /fr/accueil/ are generated, matching your trailingSlash: 'always' configuration. Astro will also redirect to the correct URL with a trailing slash in production if needed, but generating the correct URLs in your templates is best practice (Configuration Reference: trailingSlash).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak so i guess we need to merge these? export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
const pathName = path.replaceAll('/', '');
const hasTranslation = defaultLang !== l && routes[l] !== undefined && routes[l][pathName] !== undefined;
const translatedPath = hasTranslation ? '/' + routes[l][pathName] : path;
return !showDefaultLang && l === defaultLang ? translatedPath : /${l}${translatedPath};
}
}
export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
let translatedPath = ... // your translation logic
// Ensure trailing slash
if (!translatedPath.endsWith('/')) {
translatedPath += '/';
}
return !showDefaultLang && l === defaultLang ? translatedPath : /${l}${translatedPath};
}
}
Let me check the knowledge base for you again. One moment! :books:
Yes, you should merge the translation logic and the trailing slash logic into a single helper function. While the knowledge sources do not provide a ready-made merged example, they do show both the translation logic for slugs and the need to match your trailingSlash configuration when generating URLs (Translate Routes, Configuration Reference: trailingSlash).
Here’s how you can combine both:
export function useTranslatedPath(lang) {
return function translatePath(path, l = lang) {
const pathName = path.replaceAll('/', '');
const hasTranslation =
defaultLang !== l &&
routes[l] !== undefined &&
routes[l][pathName] !== undefined;
let translatedPath = hasTranslation ? '/' + routes[l][pathName] : path;
// Ensure trailing slash
if (!translatedPath.endsWith('/')) {
translatedPath += '/';
}
return !showDefaultLang && l === defaultLang
? translatedPath
: `/${l}${translatedPath}`;
}
}
This ensures:
- The slug is translated if a mapping exists.
- The returned path always ends with a trailing slash, matching your
trailingSlash: 'always'config. - The default locale does not get a prefix if
showDefaultLangisfalse.
This approach is consistent with the official Astro recipes and configuration guidance (Translate Routes, Hide default language in the URL, Configuration Reference: trailingSlash).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak I need to implement hreflang tags now sitewide
Let me check the knowledge base for you again. One moment! :books:
To implement hreflang tags sitewide in Astro, the recommended approach is to use the @astrojs/sitemap integration, which can automatically generate localized sitemaps with the correct hreflang attributes for all your pages. This is the only method explicitly documented in the knowledge sources.
Example configuration:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: '<https://your-site.com',>
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: {
en: 'en-US',
es: 'es-ES',
fr: 'fr-CA',
},
},
}),
],
});
This will generate a sitemap where each URL entry includes the appropriate xhtml:link elements for hreflang, like so:
<url>
<loc><https://your-site.com/</loc>>
<xhtml:link rel="alternate" hreflang="en-US" href="<https://your-site.com/"/>>
<xhtml:link rel="alternate" hreflang="es-ES" href="<https://your-site.com/es/"/>>
<xhtml:link rel="alternate" hreflang="fr-CA" href="<https://your-site.com/fr/"/>>
</url>
This approach is recommended for SEO and is supported directly by Astro’s official integrations Astro Sitemap i18n.
Note:
The knowledge sources do not provide a built-in or recommended way to inject hreflang tags directly into the <head> of every page. If you need to do this manually, you would need to generate the tags yourself in your layout components, but this is not covered in the provided documentation. The sitemap approach is the documented and supported method.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Thanks @carmine peak - I need a language picker now.
Let me check the knowledge base for you again. One moment! :books:
You can add a language picker to your Astro site by creating a component that lists all supported languages and generates the correct links for each. The Astro documentation provides a clear example of this approach:
1. Create a language picker component:
src/components/LanguagePicker.astro
---
import { languages } from '../i18n/ui';
import { getLangFromUrl, useTranslatedPath } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const translatePath = useTranslatedPath(lang);
---
<ul>
{Object.entries(languages).map(([lang, label]) => (
<li>
<a href={translatePath('/', lang)}>{label}</a>
</li>
))}
</ul>
This example assumes you have a languages object in your ui.ts file, and a useTranslatedPath helper to generate the correct URL for each language, as described in the Astro i18n recipe.
2. Add the language picker to your layout:
For example, in your base layout:
---
import LanguagePicker from '../components/LanguagePicker.astro';
---
<html>
<body>
<slot />
<footer>
<LanguagePicker />
</footer>
</body>
</html>
This will display the language picker on every page Let users switch between languages.
Note:
If you are using translated slugs or hiding the default language in the URL, make sure your useTranslatedPath helper accounts for those cases, as shown in the previous answers and in the Astro i18n recipes.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak the implementation you share doesnt matches the current page to the translated page. It seems to be just for homepage only. Is that correct?
Let me check the knowledge base for you again. One moment! :books:
You are correct—the basic language picker example from the documentation generates links to the homepage for each language only, not to the translated equivalent of the current page. This is confirmed in the Astro i18n recipe, where the example uses translatePath('/', lang).
To generate links to the translated version of the current page, you need to implement route translation logic. The documentation provides an extended example for this in the Translate Routes section. It shows how to map the current route to its translated equivalent using a helper function (getRouteFromUrl) and then use that in your language picker:
---
// src/components/LanguagePicker.astro
import { languages } from '../i18n/ui';
import { getRouteFromUrl, useTranslatedPath } from '../i18n/utils';
const route = getRouteFromUrl(Astro.url);
---
<ul>
{Object.entries(languages).map(([lang, label]) => {
const translatePath = useTranslatedPath(lang);
return (
<li>
<a href={translatePath(`/${route ? route : ''}`)}>{label}</a>
</li>
);
})}
</ul>
This approach attempts to link to the translated version of the current page for each language. If a translation does not exist, you can handle it (e.g., redirect to the homepage) as shown in the documentation Translate Routes.
Summary:
The basic example only links to the homepage. For a language picker that matches the current page to its translation, you should use the route translation logic provided in the documentation.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@zinc elk Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@carmine peak thats working for pages, but not for content collection routes
Let me check the knowledge base for you again. One moment! :books:
You're correct—while the documentation provides a way to translate static page routes for the language picker, it does not provide a built-in or fully documented solution for matching content collection routes (such as blog posts or dynamic content) across languages in the language picker.
The Translate Routes recipe shows how to map static routes (like /home/ to /fr/accueil/), but it does not address how to map dynamic slugs or content collection entries (e.g., /blog/post-1/ to /fr/blog/post-1/ or /fr/blog/article-1/). There is no documented helper or pattern for linking translated content collection entries between languages.
What this means:
- For static pages, you can use the documented route mapping approach.
- For content collections, the knowledge sources do not provide a solution for matching or linking translated entries in the language picker.
If you need this functionality, you would need to implement your own logic to map content entries across languages, but this is not covered in the official documentation or recipes.