#What is the best way to import YAML translations?

150 messages · Page 1 of 1 (latest)

vagrant fractal
#

Yo?

muted veldtBOT
#

Hi @vagrant fractal. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

There are a couple of approaches for importing YAML translations in Astro, depending on your use case:

1. Using Starlight's i18nLoader() (for Starlight docs sites)

If you're using Starlight, the recommended approach is to use the built-in i18nLoader(), which automatically loads JSON and YAML files from the src/content/i18n/ directory:

import { defineCollection } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';

export const collections = {
  docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
  i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};

[Starlight config]

2. Using a Rollup plugin (for general Astro projects)

For non-Starlight projects where you want to directly import YAML files, you can install @rollup/plugin-yaml and configure it in your Astro config:

npm install @rollup/plugin-yaml --save-dev
// astro.config.mjs
import { defineConfig } from 'astro/config';
import yaml from '@rollup/plugin-yaml';

export default defineConfig({
  vite: {
    plugins: [yaml()]
  }
});

Then import YAML data directly:

import yml from './data.yml';

[YAML support recipe]

3. Using the file() loader in content collections

You can also use Astro's built-in file() loader to load YAML files as a content collection:

import { defineCollection } from 'astro:content';
import { file } from 'astro/loaders';

const translations = defineCollection({
  loader: file("src/data/translations.yaml"),
});

export const collections = { translations };
#

[file() loader]

Which to choose? If you're on a Starlight site, use i18nLoader(). For general Astro projects using content collections, use the file() loader. If you need to directly import YAML files in components, use the Rollup plugin approach.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

I am building my translation system (i18n) which approach is best?

vagrant fractal
#

@muted veldt I am building my translation system (i18n) which approach is best?

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your previous question about YAML translations, here's guidance on choosing the best i18n approach:

If you're using Starlight

Use the built-in i18nLoader() with the i18nSchema(). This is the most integrated approach for Starlight docs sites:

import { defineCollection } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';

export const collections = {
  docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
  i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
};

Place your translation JSON/YAML files in src/content/i18n/ (e.g., en.json, fr.json). You can then access translations in components via Astro.locals.t('your.key'). [Starlight i18n guide]

Starlight also supports plugin-provided translations via the i18n:setup hook if you're building a plugin. [Starlight plugins]

If you're building a custom i18n system in plain Astro

Use dynamic routing with content collections. Create language-specific subdirectories and use [lang] route parameters:

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';

const blogCollection = defineCollection({
  schema: z.object({ title: z.string(), author: z.string(), date: z.date() })
});

export const collections = { blog: blogCollection };

Then serve content at routes like /en/blog/ and /fr/blog/ using dynamic pages. [i18n recipe]

#

Summary:

  • Starlight site → use i18nLoader() + i18nSchema()
  • Custom Astro site → use content collections with dynamic [lang] routing
  • Need to directly import YAML files → use the @rollup/plugin-yaml Rollup plugin [YAML recipe]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

Do I need directly yaml or no? Also can I use the code from starlight if its not starlight actually? Or rather no

#

do I need directly yaml or no? Also can I use the code from starlight if its not starlight actually? Or rather no @muted veldt

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Great questions! Let me address both:

Do you need to directly import YAML?

Not necessarily. For UI translation strings in a custom Astro project, the simpler approach from the knowledge sources is to create a plain TypeScript/JavaScript dictionary file (no YAML needed):

// src/i18n/ui.ts
export const ui = {
  en: {
    'nav.home': 'Home',
    'nav.about': 'About',
  },
  fr: {
    'nav.home': 'Accueil',
    'nav.about': 'À propos',
  },
} as const;

Then use helper functions to access the right strings based on the current URL. [i18n recipe] YAML is just one option if you prefer that file format — it's not required.

Can you use Starlight's i18n code in a non-Starlight project?

No. The Starlight-specific APIs (i18nLoader(), i18nSchema(), Astro.locals.t(), etc.) are part of the @astrojs/starlight package and depend on Starlight being installed and configured. They won't work in a plain Astro project. [Starlight i18n guide]

What to use instead for a plain Astro project

For a custom Astro site, the recommended approach is:

  1. A UI strings dictionary (TS/JS file) as shown above
  2. Helper functions like getLangFromUrl() and useTranslations() that you write yourself
  3. Dynamic [lang] routing for translated content pages

[i18n recipe]

Bottom line: Skip YAML unless you have a specific reason to use it, and don't use Starlight's APIs outside of a Starlight project.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt i meant like should i use yaml through rollup or content collections

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

For a custom i18n system, content collections with the file() loader is the better choice over the Rollup plugin approach. Here's why:

  • The file() loader is purpose-built for loading structured data (including YAML) into collections, giving you type safety, schema validation, and Astro's content APIs (getCollection(), etc.). [file() loader]

  • The Rollup plugin (@rollup/plugin-yaml) is simpler but just lets you import YAML files directly — no schema validation, no content APIs, and your editor won't provide types without extra setup. [YAML recipe]

That said, for UI translation strings specifically (nav labels, button text, etc.), the knowledge sources actually recommend a plain TypeScript dictionary file (src/i18n/ui.ts) — no YAML or content collections needed at all. [i18n recipe]

Bottom line:

  • UI strings → TypeScript dictionary (simplest, no YAML needed)
  • Translated content/pages → content collections with file() loader (if YAML) or glob() loader (if Markdown files per language)
  • Rollup plugin → only if you need to directly import YAML in components and don't want to use collections

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt why not yaml? i will need it for a CMS is it bad practice to put it in yaml?

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

YAML is not bad practice at all! My previous answer may have been misleading. To clarify:

  • Astro's built-in file() loader natively supports YAML files without any extra setup. You can point it directly at your YAML files and get full schema validation and type safety. [file() loader]

  • If you're fetching content from a CMS at build time, you could also build a custom loader that fetches from your CMS API and loads it into a content collection — giving you all the same benefits regardless of the underlying format. [custom loaders]

So for your use case (CMS + YAML translations), the file() loader is a perfectly valid and clean approach:

import { defineCollection } from 'astro:content';
import { file } from 'astro/loaders';

const translations = defineCollection({
  loader: file("src/data/translations.yaml"),
});

export const collections = { translations };

The Rollup plugin is only needed if you want to import YAML files directly in components outside of the content collections system. For CMS-driven content, content collections with the file() loader (or a custom loader) is the recommended path. [file() loader]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt Create me a schema for this

    projects: Projects
    services: Services
    switchLanguage: Change language
    cta: Let's discuss
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the YAML structure you've shared, here's a Zod schema you can use with Astro's file() loader:

import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { file } from 'astro/loaders';

const translations = defineCollection({
  loader: file("src/data/translations.yaml"),
  schema: z.object({
    nav: z.object({
      projects: z.string(),
      services: z.string(),
      switchLanguage: z.string(),
      cta: z.string(),
    }),
  }),
});

export const collections = { translations };

[defining schemas]

A couple of notes:

  • Import z from "astro/zod" (not directly from "zod") to ensure version compatibility with Astro. [Zod API]
  • The file() loader natively supports YAML, so no extra parsing setup is needed. [file() loader]
  • If you want fields to be optional (e.g. if some locales might not have all keys), append .optional() to any z.string() field.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt can you add there my translatiosn i have them in src/content/i18n/x.yml

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your YAML file location (src/content/i18n/x.yml), here's the updated config:

import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { file } from 'astro/loaders';

const translations = defineCollection({
  loader: file("src/content/i18n/x.yml"),
  schema: z.object({
    nav: z.object({
      projects: z.string(),
      services: z.string(),
      switchLanguage: z.string(),
      cta: z.string(),
    }),
  }),
});

export const collections = { translations };

The only change from the previous example is the path passed to file() — now pointing to src/content/i18n/x.yml. The file() loader natively supports YAML, so no custom parser is needed. [file() loader]

Also, remember that each entry in the YAML file needs a unique id field (or be structured as an object where the key acts as the id), since the file() loader requires this to identify entries. [file() loader]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

i meant like @muted veldt i have en.yml and sk.yml files

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Got it! Since you have multiple YAML files (one per locale), use the glob() loader instead of file(), as it can load all matching files from a directory at once:

import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';

const translations = defineCollection({
  loader: glob({ pattern: "**/*.yml", base: "./src/content/i18n" }),
  schema: z.object({
    nav: z.object({
      projects: z.string(),
      services: z.string(),
      switchLanguage: z.string(),
      cta: z.string(),
    }),
  }),
});

export const collections = { translations };

This will automatically pick up both en.yml and sk.yml (and any future locale files you add). Each file's name (e.g. en, sk) will be used as the entry id, so you can query a specific locale with getEntry('translations', 'en'). [glob() loader]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt OK now whats the best way to use the translations in actual files

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your setup (content collections with glob() loader for en.yml and sk.yml), here's how to use the translations in your pages and components:

1. Query the translation entry for the current language

Use getEntry() to fetch the right locale's translations:

---
import { getEntry } from 'astro:content';

// Get the current language from the URL (e.g. /en/ or /sk/)
const lang = Astro.currentLocale ?? 'en';

// Fetch the matching translation file (en.yml or sk.yml)
const t = await getEntry('translations', lang);
---

<nav>
  <a href={`/${lang}/projects`}>{t.data.nav.projects}</a>
  <a href={`/${lang}/services`}>{t.data.nav.services}</a>
  <button>{t.data.nav.cta}</button>
</nav>

[querying collections; currentLocale]

2. Get the language from the URL (without built-in i18n routing)

If you're not using Astro's built-in i18n routing, you can extract the language from the URL manually using a helper, as shown in the Astro i18n recipe:

// src/i18n/utils.ts
export function getLangFromUrl(url: URL) {
  const [, lang] = url.pathname.split('/');
  if (['en', 'sk'].includes(lang)) return lang;
  return 'en'; // default
}

Then in your component:

---
import { getEntry } from 'astro:content';
import { getLangFromUrl } from '../i18n/utils';

const lang = getLangFromUrl(Astro.url);
const t = await getEntry('translations', lang);
---

<p>{t.data.nav.switchLanguage}</p>

[i18n recipe]

Summary

#
  • Use getEntry('translations', lang) to fetch the right YAML file for the current language.
  • Access your strings via t.data.nav.projects, t.data.nav.cta, etc. (matching your schema).
  • Use Astro.currentLocale if you have Astro's built-in i18n routing configured, or a getLangFromUrl() helper otherwise.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt how does the helper compare to Astros built in locale function

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the knowledge sources, here's how the manual getLangFromUrl() helper compares to Astro's built-in Astro.currentLocale:

getLangFromUrl() (manual helper)

  • A custom function you write yourself, typically in src/i18n/utils.ts
  • Parses the language from the URL pathname by splitting on /
  • Works regardless of whether Astro's built-in i18n routing is configured
  • Useful when building a fully custom i18n system [i18n recipe]
export function getLangFromUrl(url: URL) {
  const [, lang] = url.pathname.split('/');
  if (lang in ui) return lang as keyof typeof ui;
  return defaultLang;
}

Astro.currentLocale (built-in)

  • Provided automatically by Astro when you configure the i18n option in astro.config.mjs
  • Computes the locale from the current URL using your configured locales
  • If the URL has no /[locale]/ prefix, it defaults to i18n.defaultLocale automatically
  • Available on all pages, including statically prerendered ones [currentLocale]

Which to use?

  • If you have Astro's built-in i18n routing configured (i18n.locales, i18n.defaultLocale in astro.config.mjs), use Astro.currentLocale — it's simpler and automatically handles edge cases like missing prefixes.
  • If you're building a fully manual i18n system without Astro's routing config, use the getLangFromUrl() helper.
#

Since you already have en.yml and sk.yml set up, configuring Astro's built-in i18n routing and using Astro.currentLocale would be the cleaner path. [configure i18n routing]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

and where should i set the language labels etc @muted veldt

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your setup, the language labels (for things like a language switcher) are best defined in a simple TypeScript/JS file. The Astro i18n recipe recommends a src/i18n/ui.ts file:

// src/i18n/ui.ts
export const languages = {
  en: 'English',
  sk: 'Slovenčina',
};

export const defaultLang = 'en';

Then you can use it in a language switcher component like this:

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

[i18n recipe]

So in your case:

  • Language labels (e.g. "English", "Slovenčina" for the switcher UI) → src/i18n/ui.ts
  • Page/component translations (nav, buttons, etc.) → your en.yml / sk.yml files in src/content/i18n/, queried via getEntry('translations', lang)

This keeps the language switcher metadata separate from your actual translation strings.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

and how to write proper helper function for it something like this


import { getEntry } from 'astro:content';


export function useTranslations(lang: keyof typeof ui) {
    return function t(key: keyof (typeof ui)[typeof defaultLang]) {
        return await getEntry('translations', lang);
    };
}

btw whats the ui file for

#

@muted veldt and how to write proper helper function for it something like this


import { getEntry } from 'astro:content';


export function useTranslations(lang: keyof typeof ui) {
    return function t(key: keyof (typeof ui)[typeof defaultLang]) {
        return await getEntry('translations', lang);
    };
}

btw whats the ui file for

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Good questions! Let me address both.

The ui.ts file — what's it for?

The src/i18n/ui.ts file from the recipe serves two purposes:

  1. Language labels for the switcher (e.g. "English", "Slovenčina")
  2. UI string dictionaries — a fallback/alternative to YAML files for simple translation strings

Since you're already using YAML files via content collections, you mainly need ui.ts for the language labels and defaultLang. You don't need to duplicate your translations there. [i18n recipe]


Fixing your helper function

Your approach has a couple of issues:

  • getEntry() is async, so it can't be called inside a synchronous helper that returns a sync function
  • The inner t() function should accept a key and return the specific string, not the whole entry

The cleanest pattern for your setup is to fetch the entry once and pass the data to a helper:

// src/i18n/utils.ts
import { getEntry } from 'astro:content';

export async function useTranslations(lang: string) {
  const entry = await getEntry('translations', lang);
  const translations = entry?.data;

  return function t(key: string) {
    // Navigate nested keys like "nav.projects"
    return key.split('.').reduce((obj: any, k) => obj?.[k], translations) ?? key;
  };
}

Then use it in your components like this:

---
import { useTranslations } from '../i18n/utils';
import { getLangFromUrl } from '../i18n/utils';

const lang = getLangFromUrl(Astro.url);
const t = await useTranslations(lang);
---

<a href="/">{t('nav.projects')}</a>
<button>{t('nav.cta')}</button>
#

The key difference from the recipe's original useTranslations() is that yours needs to be async because it calls getEntry(). [querying collections]

The knowledge sources don't explicitly show this exact async pattern combined with content collections, so treat the nested key traversal (key.split('.')) as a practical extension — the core getEntry() usage is directly from the docs.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt and put default language there or elsewhere

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the conversation so far, the defaultLang is best kept in your src/i18n/ui.ts file alongside the language labels — that's exactly where the Astro i18n recipe places it:

// src/i18n/ui.ts
export const languages = {
  en: 'English',
  sk: 'Slovenčina',
};

export const defaultLang = 'en';

Then import it wherever you need it — for example in your utils.ts:

// src/i18n/utils.ts
import { getEntry } from 'astro:content';
import { defaultLang } from './ui';

export function getLangFromUrl(url: URL) {
  const [, lang] = url.pathname.split('/');
  if (['en', 'sk'].includes(lang)) return lang;
  return defaultLang; // falls back to 'en'
}

export async function useTranslations(lang: string) {
  const entry = await getEntry('translations', lang);
  const translations = entry?.data;

  return function t(key: string) {
    return key.split('.').reduce((obj: any, k) => obj?.[k], translations) ?? key;
  };
}

[i18n recipe]

So the split is:

  • src/i18n/ui.tsdefaultLang, language labels for the switcher
  • src/content/i18n/en.yml / sk.yml → actual translation strings
  • src/i18n/utils.ts → helper functions that tie it all together

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt why ui.ts shouldnt it be smth else or cant i fetch from astro settings

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Good question! You're right to question this. If you configure Astro's built-in i18n routing, you can get the default locale directly from Astro's config rather than maintaining a separate ui.ts file.

With Astro's built-in i18n routing configured in astro.config.mjs:

import { defineConfig } from "astro/config";

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

You can then use Astro.currentLocale in your pages/components, which automatically reads from your config and defaults to i18n.defaultLocale if no locale prefix is found in the URL. [configure i18n routing]

This means:

  • No need for defaultLang in ui.ts — Astro handles it via config
  • No need for getLangFromUrl()Astro.currentLocale does this for you [browser language detection]

You'd still keep ui.ts for the language labels (for the switcher UI), but it becomes much simpler:

// src/i18n/ui.ts
export const languages = {
  en: 'English',
  sk: 'Slovenčina',
};

And your utils.ts no longer needs to import defaultLang from ui.ts at all — just use Astro.currentLocale directly in your components.

Bottom line: If you set up Astro's built-in i18n routing, ui.ts only needs the language labels for the switcher. Everything else (defaultLocale, current locale detection) comes from Astro's config and built-in APIs.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#


export function useTranslations(lang: LanguageCode) {
    return function t(key: TranslationKey) {
        return ui[lang][key] || ui[defaultLang][key];
    };
}
import { getEntry } from "astro:content";
import { languages, defaultLang, type LanguageCode } from "../i18n/ui";

export async function useTranslations(lang: string) {
    const entry = await getEntry("translations", lang);
    const translations = entry?.data;

    return function t(key: string) {
        return key.split(".").reduce((obj: any, k) => obj?.[k], translations) ?? key;
    };
}
``` @muted veldt what the differences and which
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Here's a breakdown of the two approaches:

Approach 1 — TypeScript dictionary (ui.ts)

import { ui, defaultLang, type LanguageCode, type TranslationKey } from "./ui";

export function useTranslations(lang: LanguageCode) {
    return function t(key: TranslationKey) {
        return ui[lang][key] || ui[defaultLang][key];
    };
}
  • Synchronous — no await needed
  • Translations live in a TypeScript object in ui.ts
  • Fully typedTranslationKey is inferred from the object, so you get autocomplete and type errors for missing keys
  • Automatic fallback to defaultLang if a key is missing in the current language
  • No content collections involved [i18n recipe]

Approach 2 — Content collections (en.yml / sk.yml)

export async function useTranslations(lang: string) {
    const entry = await getEntry("translations", lang);
    const translations = entry?.data;

    return function t(key: string) {
        return key.split(".").reduce((obj: any, k) => obj?.[k], translations) ?? key;
    };
}
  • Async — requires await everywhere you call it
  • Translations live in YAML files loaded via content collections
  • Key type is just string — less type safety, no autocomplete for keys
  • Falls back to returning the key itself if not found (not the default language's value)
  • Better suited for CMS-driven or large-scale content [querying collections]

Which to use?

Given that you mentioned you're using YAML files from a CMS, Approach 2 fits your setup better. The main trade-off is losing the strong typing on translation keys.

#

If type safety on keys matters to you, Approach 1 is cleaner — but you'd have to move your translations from YAML into a TypeScript file, which may not suit a CMS workflow.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt genereate also TS types for everything

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your setup (YAML files via content collections), here's Approach 2 with TypeScript types added throughout:

// src/i18n/utils.ts
import { getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";

// Type for the nested nav structure matching your YAML schema
interface NavTranslations {
  projects: string;
  services: string;
  switchLanguage: string;
  cta: string;
}

// Type for the full translation data shape
interface TranslationData {
  nav: NavTranslations;
}

// Type for supported language codes
export type LanguageCode = "en" | "sk";

// Type for dot-notation keys (e.g. "nav.projects")
type DotNotationKeys<T, Prefix extends string = ""> = {
  [K in keyof T]: T[K] extends object
    ? DotNotationKeys<T[K], `${Prefix}${K & string}.`>
    : `${Prefix}${K & string}`;
}[keyof T];

export type TranslationKey = DotNotationKeys<TranslationData>;

export async function useTranslations(lang: LanguageCode) {
  const entry: CollectionEntry<"translations"> | undefined = await getEntry(
    "translations",
    lang
  );
  const translations = entry?.data as TranslationData | undefined;

  return function t(key: TranslationKey): string {
    return (
      key.split(".").reduce((obj: unknown, k: string) =>
        obj && typeof obj === "object" ? (obj as Record<string, unknown>)[k] : undefined,
        translations
      ) as string ?? key
    );
  };
}

The DotNotationKeys utility type will automatically derive all valid keys ("nav.projects", "nav.services", "nav.switchLanguage", "nav.cta") from your TranslationData interface, so you get autocomplete and type errors if you use an invalid key.

#

Note that the DotNotationKeys recursive type and the CollectionEntry typing are based on standard TypeScript patterns and Astro's content collection types — the knowledge sources confirm CollectionEntry is available from astro:content [defining schemas], but the recursive dot-notation type is a TypeScript utility not explicitly shown in the knowledge sources, so treat that part as a practical extension.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

export async function useTranslations(lang: string) {
    const entry = await getEntry("translations", lang);
    const translations = entry?.data;

    return function t(key: string) {
        return key.split(".").reduce((obj: any, k) => obj?.[k], translations) ?? key;
    };
}
``````export const languages = {
    sk: "Slovenčina",
    en: "English",
} as const;

export type LanguageCode = keyof typeof languages;

export const defaultLang = "sk";
```make @muted veldt type for ts
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Here's your code with full TypeScript types, using LanguageCode from your ui.ts and deriving translation keys from your YAML schema:

src/i18n/ui.ts — no changes needed, already well-typed:

export const languages = {
  sk: "Slovenčina",
  en: "English",
} as const;

export type LanguageCode = keyof typeof languages;

export const defaultLang: LanguageCode = "sk";

src/i18n/utils.ts — with full types:

import { getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { LanguageCode } from "./ui";

// Mirror your YAML schema
interface NavTranslations {
  projects: string;
  services: string;
  switchLanguage: string;
  cta: string;
}

interface TranslationData {
  nav: NavTranslations;
}

// Derive dot-notation keys from TranslationData (e.g. "nav.projects")
type DotNotationKeys<T, Prefix extends string = ""> = {
  [K in keyof T]: T[K] extends object
    ? DotNotationKeys<T[K], `${Prefix}${K & string}.`>
    : `${Prefix}${K & string}`;
}[keyof T];

export type TranslationKey = DotNotationKeys<TranslationData>;

export async function useTranslations(lang: LanguageCode) {
  const entry: CollectionEntry<"translations"> | undefined = await getEntry(
    "translations",
    lang
  );
  const translations = entry?.data as TranslationData | undefined;

  return function t(key: TranslationKey): string {
    return (
      key.split(".").reduce(
        (obj: unknown, k: string) =>
          obj && typeof obj === "object"
            ? (obj as Record<string, unknown>)[k]
            : undefined,
        translations
      ) as string ?? key
    );
  };
}
#

Key changes:

  • lang is now typed as LanguageCode instead of string, so only "en" or "sk" are accepted
  • TranslationKey is derived automatically from TranslationData, giving you autocomplete on valid keys like "nav.projects", "nav.cta", etc.
  • CollectionEntry<"translations"> types the entry from getEntry() [astro:content types]

Note: The DotNotationKeys recursive utility type is a standard TypeScript pattern — the knowledge sources confirm CollectionEntry is available from astro:content [querying collections], but the recursive type itself is a TypeScript extension not explicitly shown in the docs.


Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt do i need to mirror it loos useless

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You're right to question it — the TranslationData interface is essentially duplicating your Zod schema. It's not strictly necessary, but the knowledge sources don't provide a built-in way to automatically infer the TypeScript type from a content collection's schema for use outside of Astro files.

What you can do is use Astro's generated types. When you define a schema in your collection, Astro automatically generates TypeScript types for it. [defining schemas] So CollectionEntry<"translations">["data"] will already be typed based on your Zod schema in content.config.ts.

This means you can simplify by removing the manual TranslationData interface and just use the inferred type:

import { getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { LanguageCode } from "./ui";

type TranslationData = CollectionEntry<"translations">["data"];

type DotNotationKeys<T, Prefix extends string = ""> = {
  [K in keyof T]: T[K] extends object
    ? DotNotationKeys<T[K], `${Prefix}${K & string}.`>
    : `${Prefix}${K & string}`;
}[keyof T];

export type TranslationKey = DotNotationKeys<TranslationData>;

export async function useTranslations(lang: LanguageCode) {
  const entry = await getEntry("translations", lang);
  const translations = entry?.data;

  return function t(key: TranslationKey): string {
    return (
      key.split(".").reduce(
        (obj: unknown, k: string) =>
          obj && typeof obj === "object"
            ? (obj as Record<string, unknown>)[k]
            : undefined,
        translations
      ) as string ?? key
    );
  };
}
#

Now TranslationData is derived directly from your collection schema — no duplication needed. If you update your Zod schema in content.config.ts, the types here update automatically. [astro:content types]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#
    sk: "Slovenčina",
    en: "Englis``` any type here @muted veldt
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You don't need any extra type annotation here — TypeScript already infers the type correctly from as const. Your LanguageCode type derived from it is all you need:

export const languages = {
  sk: "Slovenčina",
  en: "English",
} as const;

export type LanguageCode = keyof typeof languages;
// LanguageCode = "sk" | "en"

export type LanguageLabel = (typeof languages)[LanguageCode];
// LanguageLabel = "Slovenčina" | "English"

The as const assertion makes the values readonly string literals instead of just string, which is what allows keyof typeof languages to produce the precise "sk" | "en" union type rather than just string. No explicit type annotation on languages itself is needed or useful here. [i18n recipe]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt type DotNotationKeys<T, Prefix extends string = ""> = {
[K in keyof T]: T[K] extends object
? DotNotationKeys<T[K], ${Prefix}${K & string}.>
: ${Prefix}${K & string};
}[keyof T];

whats thsi

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This is a TypeScript utility type, not specific to Astro — it's a general TypeScript pattern I used in the previous answers to derive dot-notation string keys from a nested object type.

Here's what it does step by step:

type DotNotationKeys<T, Prefix extends string = ""> = {
    [K in keyof T]: T[K] extends object
        ? DotNotationKeys<T[K], `${Prefix}${K & string}.`>
        : `${Prefix}${K & string}`;
}[keyof T];
  • [K in keyof T] — iterates over every key in type T
  • T[K] extends object — checks if the value at that key is a nested object
    • If yes → recurse deeper, appending the current key + . to the prefix
    • If no → it's a leaf value, so return the full dot-notation string (e.g. "nav.projects")
  • [keyof T] at the end — collapses the mapped type into a union of all the resulting strings

So given your TranslationData shape:

// nav.projects, nav.services, nav.switchLanguage, nav.cta
type TranslationKey = DotNotationKeys<TranslationData>;

It produces: "nav.projects" | "nav.services" | "nav.switchLanguage" | "nav.cta"

This gives you autocomplete and type safety when calling t("nav.projects") — TypeScript will error if you pass an invalid key.

That said, this is a pure TypeScript utility — it's not from the Astro docs. I introduced it as a practical extension to type your t() function.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your full setup, here's how to use everything together in an actual Astro component:

---
// src/components/Nav.astro
import { useTranslations } from '../i18n/utils';
import { languages } from '../i18n/ui';
import type { LanguageCode } from '../i18n/ui';

const lang = (Astro.currentLocale ?? 'sk') as LanguageCode;
const t = await useTranslations(lang);
---

<nav>
  <a href={`/${lang}/projects`}>{t('nav.projects')}</a>
  <a href={`/${lang}/services`}>{t('nav.services')}</a>
  <button>{t('nav.cta')}</button>
</nav>

<!-- Language switcher -->
<ul>
  {Object.entries(languages).map(([code, label]) => (
    <li>
      <a href={`/${code}/`}>{label}</a>
    </li>
  ))}
</ul>

Key points:

  • Astro.currentLocale gives you the current language from Astro's built-in i18n routing — cast it to LanguageCode since it returns string | undefined [configure i18n routing]
  • await useTranslations(lang) fetches the right YAML file and returns the typed t() function
  • t('nav.projects') — TypeScript will autocomplete and error on invalid keys thanks to TranslationKey (the DotNotationKeys utility)
  • languages from ui.ts is used for the switcher labels, typed as "sk" | "en" via as const

The TypeScript types flow through automatically:

  • LanguageCode → constrains what you can pass to useTranslations()
  • TranslationKey → constrains what you can pass to t()
  • CollectionEntry<"translations">["data"] → inferred from your Zod schema in content.config.ts [astro:content types]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt how to make the one i am using rn bold and also how to sort it from bold one to normal

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the conversation, you're asking about making the current language bold in your language switcher and sorting it first. Here's how to do that in your Nav component:

---
import { useTranslations } from '../i18n/utils';
import { languages } from '../i18n/ui';
import type { LanguageCode } from '../i18n/ui';

const lang = (Astro.currentLocale ?? 'sk') as LanguageCode;
const t = await useTranslations(lang);

// Sort so current language comes first
const sortedLanguages = Object.entries(languages).sort(([code]) =>
  code === lang ? -1 : 1
);
---

<ul>
  {sortedLanguages.map(([code, label]) => (
    <li>
      <a
        href={`/${code}/`}
        aria-current={code === lang ? 'true' : undefined}
        style={code === lang ? 'font-weight: bold;' : undefined}
      >
        {label}
      </a>
    </li>
  ))}
</ul>

Key points:

  • code === lang compares each language code against Astro.currentLocale to identify the active one [currentLocale]
  • font-weight: bold is applied inline only to the current language
  • aria-current="true" is good practice for accessibility on the active language link
  • .sort() moves the current language to the top — entries returning -1 sort before those returning 1

You can also use a CSS class instead of inline styles if you prefer:

<a
  href={`/${code}/`}
  class={code === lang ? 'active' : undefined}
>
  {label}
</a>

<style>
  .active {
    font-weight: bold;
  }
</style>

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#
                        Object.entries(languages).map(([code, label]) => (
                            <a
                                href={`/${code}`}
                                class:list={[
                                    "flex items-center p-4 transition-colors duration-300 ease-in-out hover:bg-black/5",
                                    code == lang && "font-medium",
                                ]}
                            >
                                {label}
                            </a>
                        ))
                    }``` y ts not work @muted veldt current item
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The issue is that code == lang uses loose equality (==) instead of strict equality (===), but more likely the real problem is a type mismatch: code from Object.entries(languages) is typed as string, while lang is typed as LanguageCode ("sk" | "en"). This can cause unexpected behavior.

Also, class:list accepts strings, objects, and arrays — your usage is correct syntax-wise. [class:list directive]

Try this fix — use strict equality and ensure the types align:

{
  Object.entries(languages).map(([code, label]) => (
    <a
      href={`/${code}`}
      class:list={[
        "flex items-center p-4 transition-colors duration-300 ease-in-out hover:bg-black/5",
        { "font-medium": code === lang },
      ]}
    >
      {label}
    </a>
  ))
}

Key changes:

  • === instead of == — strict equality is safer with TypeScript
  • Object syntax { "font-medium": code === lang } — this is the idiomatic class:list pattern for conditional classes; truthy keys get added to the class [class:list directive]

Also double-check that lang is correctly set from Astro.currentLocale before the map:

const lang = (Astro.currentLocale ?? 'sk') as LanguageCode;

If Astro.currentLocale is undefined (e.g. i18n routing not configured), lang will always be 'sk' and no item will match code === lang for 'en'.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the conversation history, you're asking about why the current language isn't being highlighted in your language switcher. Looking at your code, the most likely issue is that Astro.currentLocale may not be returning what you expect.

A few things to check:

  1. Is Astro's built-in i18n routing configured? Astro.currentLocale only works correctly if you have i18n.locales and i18n.defaultLocale set in astro.config.mjs. Without it, Astro.currentLocale will be undefined. [currentLocale]

  2. Does your URL have a /[locale]/ prefix? Astro.currentLocale reads the locale from the URL. If your page is at / instead of /sk/ or /en/, it defaults to i18n.defaultLocale — so make sure your routes include the locale prefix.

  3. Use the object syntax for conditional classes — your class:list should look like:

class:list={[
  "flex items-center p-4 transition-colors duration-300 ease-in-out hover:bg-black/5",
  { "font-medium": code === lang },
]}

Try adding a quick debug check in your frontmatter to see what Astro.currentLocale actually returns:

---
const lang = (Astro.currentLocale ?? 'sk') as LanguageCode;
console.log('currentLocale:', Astro.currentLocale, 'lang:', lang);
---

That will tell you if the locale is being detected correctly from the URL.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

does ts look ok?

export const languages = {
sk: "Slovenčina",
en: "English",
} as const;

export type LanguageCode = keyof typeof languages;

export const defaultLang: LanguageCode = "sk";

import { getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { LanguageCode } from "./ui";

type TranslationData = CollectionEntry<"translations">["data"];

type DotNotationKeys<T, Prefix extends string = ""> = {
[K in keyof T]: T[K] extends object
? DotNotationKeys<T[K], ${Prefix}${K & string}.>
: ${Prefix}${K & string};
}[keyof T];

export type TranslationKey = DotNotationKeys<TranslationData>;

export async function useTranslations(lang: LanguageCode) {
const entry = await getEntry("translations", lang);
const translations = entry?.data;

return function t(key: TranslationKey) {
    return key.split(".").reduce((obj: any, k) => obj?.[k], translations) ?? key;
};

}

@muted veldt

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The TypeScript looks good overall! A couple of small things to note:

1. Use import type for type-only imports

Astro recommends using import type for type imports to avoid bundling issues: [type imports]

import { getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { LanguageCode } from "./ui";

You already have this correct — good.

2. The t() return type could be more explicit

Currently t() implicitly returns any because of the reduce with obj: any. You can tighten it:

return function t(key: TranslationKey): string {
    return (
        key.split(".").reduce(
            (obj: unknown, k: string) =>
                obj && typeof obj === "object"
                    ? (obj as Record<string, unknown>)[k]
                    : undefined,
            translations
        ) as string ?? key
    );
};

3. Everything else looks correct:

  • as const + keyof typeof languages for LanguageCode
  • defaultLang: LanguageCode annotation ✅
  • CollectionEntry<"translations">["data"] to infer TranslationData from your Zod schema automatically ✅ [astro:content types]
  • DotNotationKeys utility for typed key autocomplete ✅
  • useTranslations typed to only accept LanguageCode

The main improvement worth making is replacing obj: any with obj: unknown and a proper type guard to avoid the implicit any leaking through.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt

return function t(key: TranslationKey): string {
const result = key
.split(".")
.reduce((obj: unknown, k) => (obj as any)?.[k], translations);

return typeof result === "string" ? result : key;

};

is this better

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, this is better than the previous version. Here's why it's an improvement:

return function t(key: TranslationKey): string {
    const result = key
        .split(".")
        .reduce((obj: unknown, k) => (obj as any)?.[k], translations);

    return typeof result === "string" ? result : key;
};

Improvements over the previous version:

  • Explicit runtime type checktypeof result === "string" properly validates the result before returning it, rather than blindly casting with as string
  • Safe fallback — if the key doesn't resolve to a string (e.g. missing key, wrong path), it falls back to returning key itself, which is the correct behavior
  • Cleaner reduce — using (obj as any)?.[k] is a reasonable pragmatic choice here since the intermediate values during traversal are genuinely unknown-shaped nested objects

The only minor caveat is the as any cast inside the reduce — it's a practical trade-off to avoid verbose type gymnastics for intermediate traversal steps, and the final typeof result === "string" check at the end ensures type safety where it actually matters (the return value).

Overall this is a solid, clean implementation. The explicit typeof guard is the right approach here.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

return function t<K extends TranslationKey>(
key: K
): DotNotationValue<typeof translations, K> | K {
const result = key
.split(".")
.reduce<unknown>(
(obj, k) =>
obj && typeof obj === "object"
? (obj as Record<string, unknown>)[k]
: undefined,
translations
);

return result !== undefined ? (result as DotNotationValue<typeof translations, K>) : key;
};

@muted veldt is this better than previous or too much

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This is more type-safe in theory, but it's likely too much for this use case. Here's why:

What it adds:

  • DotNotationValue<typeof translations, K> attempts to infer the exact return type based on the key passed in, rather than just returning string
  • The generic K extends TranslationKey preserves the specific key type through the function

Why it's probably overkill:

  • DotNotationValue is another recursive utility type you'd need to define and maintain — it's not from the Astro docs or knowledge sources, it's pure TypeScript complexity
  • In practice, all your translation values are string (as defined in your Zod schema), so the return type will always be string | K — the extra generic inference buys you nothing over just string
  • It makes the code harder to read and maintain for no real practical benefit

The previous version is the right balance:

return function t(key: TranslationKey): string {
    const result = key
        .split(".")
        .reduce<unknown>(
            (obj, k) =>
                obj && typeof obj === "object"
                    ? (obj as Record<string, unknown>)[k]
                    : undefined,
            translations
        );

    return typeof result === "string" ? result : key;
};

This is already type-safe where it matters — TranslationKey constrains valid inputs, and the runtime typeof check ensures a safe string return. The more complex version would only be worth it if your translation values had different types (e.g. some keys return numbers or objects), which yours don't.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt so ishould verify the t() keys

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on the conversation, you're already verifying t() keys through the TranslationKey type — TypeScript will error at compile time if you pass an invalid key.

Your current setup already handles this:

  1. Compile-timeTranslationKey (derived from DotNotationKeys<TranslationData>) constrains what keys are valid. Passing t('nav.nonexistent') will be a TypeScript error.

  2. Runtime — the typeof result === "string" check handles the case where a key doesn't resolve, falling back to returning the key itself:

return typeof result === "string" ? result : key;

So yes, you're already verifying keys at both levels. The only thing you could add is a dev-mode warning when a key falls back:

return function t(key: TranslationKey): string {
    const result = key
        .split(".")
        .reduce<unknown>(
            (obj, k) =>
                obj && typeof obj === "object"
                    ? (obj as Record<string, unknown>)[k]
                    : undefined,
            translations
        );

    if (typeof result !== "string") {
        console.warn(`[i18n] Missing translation key: "${key}" for lang: "${lang}"`);
    }

    return typeof result === "string" ? result : key;
};

This is optional but useful during development to catch keys that exist in one locale's YAML but are missing in another. The TypeScript type checking is the primary verification mechanism here — the runtime warning is just a safety net for mismatched YAML files.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt is it worth it to impelment this using astro middleware

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your current setup, implementing i18n translations via Astro middleware is not necessary and likely overkill for what you're doing.

Here's when middleware would make sense for i18n:

  • Redirecting to a default locale — e.g. redirecting / to /sk/ automatically
  • Fallback routing — redirecting missing translated pages to another locale
  • Manual routing control — if you set routing: "manual" and need full control over locale detection and redirects [manual routing]

For example, Astro's middleware helper functions like redirectToDefaultLocale() and redirectToFallback() are only available when routing: "manual" is set. [redirectToFallback]

For your use case (YAML translations via content collections + t() helper), middleware adds no value because:

  • You're already using Astro.currentLocale to detect the language from the URL
  • Your useTranslations() fetches the right YAML file per page render
  • Translation lookup happens at the component level, not the request level

The only scenario where middleware could help your setup is if you want to store the translation function in context.locals so you don't have to call await useTranslations(lang) in every component individually:

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { useTranslations } from "./i18n/utils";
import type { LanguageCode } from "./i18n/ui";

export const onRequest = defineMiddleware(async (context, next) => {
  const lang = (context.currentLocale ?? "sk") as LanguageCode;
  context.locals.t = await useTranslations(lang);
  return next();
});
#

[middleware guide]

That's a reasonable convenience, but it's not required. Stick with your current approach unless you find yourself repeating await useTranslations(lang) in many components.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt i meant like use Astro.locals.t() in files instead of like now with t()

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, that's exactly what the middleware approach enables — you store the t() function in context.locals once, and then access it as Astro.locals.t() in any component without calling await useTranslations(lang) each time.

Here's how to set it up:

src/middleware.ts

import { defineMiddleware } from "astro:middleware";
import { useTranslations } from "./i18n/utils";
import type { LanguageCode } from "./i18n/ui";

export const onRequest = defineMiddleware(async (context, next) => {
  const lang = (context.currentLocale ?? "sk") as LanguageCode;
  context.locals.t = await useTranslations(lang);
  return next();
});

[middleware guide]

Then in any component:

---
// No need to import useTranslations or call await
const t = Astro.locals.t;
---

<a href="/">{t('nav.projects')}</a>
<button>{t('nav.cta')}</button>

[Astro.locals]

To get TypeScript to recognize Astro.locals.t, add a type declaration in your src/env.d.ts:

import type { TranslationKey } from "./i18n/utils";

declare namespace App {
  interface Locals {
    t: (key: TranslationKey) => string;
  }
}

This gives you full autocomplete on Astro.locals.t('nav.projects') across all your components, and you only pay the cost of fetching the translation entry once per request in the middleware rather than per component.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt so should i implement it or pass

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your setup, here's a quick decision guide:

Implement it (middleware approach) if:

  • You have many components that all need t() — it removes the repetitive await useTranslations(lang) boilerplate from every component
  • You want a cleaner, more centralized i18n setup

Skip it (keep current approach) if:

  • You only use t() in a few components — the overhead of setting up middleware isn't worth it
  • You prefer keeping things simple and explicit

The middleware approach is a convenience optimization, not a requirement. Your current await useTranslations(lang) approach works correctly. The only real benefit of moving to Astro.locals.t() is ergonomics — one less await call per component. [middleware guide]

If you're building a site with many pages and components that all use translations, it's worth implementing. If it's a small site, pass.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

@muted veldt create me a name for this task on my todo list so i can deal with it later

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I'm sorry, but I'm only able to help with questions about Astro! For naming your todo task, you'd need a general-purpose assistant.

That said, if you'd like to come back and implement the middleware-based Astro.locals.t() approach we discussed, I'm here to help whenever you're ready.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

mf

#

@muted veldt well right now i have this at top of frontmatter

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

It looks like your message got cut off! Could you share the code you have at the top of your frontmatter? That way I can help you figure out how to simplify it with the middleware approach or advise on your current setup.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@vagrant fractal Kapa.ai is still learning and improving, please let me know how I did by reacting below.

vagrant fractal
#

import { useTranslations } from "../../i18n/utils";
import { languages, defaultLang } from "../../i18n/ui";
import type { LanguageCode } from "../../i18n/ui";

const lang = (Astro.currentLocale ?? defaultLang) as LanguageCode;
const { t, translations } = await useTranslations(lang);

@muted veldt frontmatter

muted veldtBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Based on your current frontmatter, here's how the middleware approach would simplify it:

Current (per component):

import { useTranslations } from "../../i18n/utils";
import { languages, defaultLang } from "../../i18n/ui";
import type { LanguageCode } from "../../i18n/ui";

const lang = (Astro.currentLocale ?? defaultLang) as LanguageCode;
const { t, translations } = await useTranslations(lang);

With middleware (Astro.locals.t):

const t = Astro.locals.t;
// That's it

All the imports, the lang detection, and the await call move into the middleware once, and every component just uses Astro.locals.t directly. [middleware guide]

Given that you have this boilerplate at the top of every component, it's worth implementing the middleware approach — it's clearly repetitive enough to justify it.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: