summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-05-03 22:33:11 +0200
committerGitHub2023-05-03 22:33:11 +0200
commit7b9f27372c18a3737e83954ad303e94f74d62fa0 (patch)
tree6fc23fe9d3a73e2c01a641d3c9687596101afdd0
parent46f06dab8c8a53c3dacde2ed9df5369595b92e2f (diff)
downloadIT.starlight-7b9f27372c18a3737e83954ad303e94f74d62fa0.tar.gz
IT.starlight-7b9f27372c18a3737e83954ad303e94f74d62fa0.tar.bz2
IT.starlight-7b9f27372c18a3737e83954ad303e94f74d62fa0.zip
Enable fallback content for missing translations (#27)
-rw-r--r--docs/src/content/docs/guides/i18n.md2
-rw-r--r--docs/src/content/docs/reference/configuration.md12
-rw-r--r--packages/starbook/components/FallbackContentNotice.astro27
-rw-r--r--packages/starbook/components/HeadSEO.astro2
-rw-r--r--packages/starbook/components/LanguageSelect.astro2
-rw-r--r--packages/starbook/index.astro41
-rw-r--r--packages/starbook/utils/navigation.ts100
-rw-r--r--packages/starbook/utils/routing.ts109
-rw-r--r--packages/starbook/utils/slugs.ts57
-rw-r--r--packages/starbook/utils/user-config.ts53
10 files changed, 311 insertions, 94 deletions
diff --git a/docs/src/content/docs/guides/i18n.md b/docs/src/content/docs/guides/i18n.md
index 28c7cc10..a6f48ffc 100644
--- a/docs/src/content/docs/guides/i18n.md
+++ b/docs/src/content/docs/guides/i18n.md
@@ -16,6 +16,8 @@ StarBook provides built-in support for multilingual sites.
export default defineConfig({
integrations: [
starbook({
+ // Set English as the default language for this site.
+ defaultLocale: 'en',
locales: {
// English docs in `src/content/docs/en/`
en: {
diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md
index fa99e5f7..53ba8746 100644
--- a/docs/src/content/docs/reference/configuration.md
+++ b/docs/src/content/docs/reference/configuration.md
@@ -114,6 +114,8 @@ import starbook from 'starbook';
export default defineConfig({
integrations: [
starbook({
+ // Set English as the default language for this site.
+ defaultLocale: 'en',
locales: {
// English docs in `src/content/docs/en/`
en: {
@@ -177,6 +179,16 @@ starbook({
For example, this allows you to serve `/getting-started/` as an English route and use `/fr/getting-started/` as the equivalent French page.
+### `defaultLocale`
+
+**type:** `string`
+
+Set the language which is the default for this site.
+The value should match one of the keys of your [`locales`](#locales) object.
+(If your default language is your [root locale](#root-locale), you can skip this.)
+
+The default locale will be used to provide fallback content where translations are missing.
+
### `social`
Optional details about the social media accounts for this site.
diff --git a/packages/starbook/components/FallbackContentNotice.astro b/packages/starbook/components/FallbackContentNotice.astro
new file mode 100644
index 00000000..e4dc3a4a
--- /dev/null
+++ b/packages/starbook/components/FallbackContentNotice.astro
@@ -0,0 +1,27 @@
+---
+import Icon from './Icon.astro';
+---
+
+<p>
+ <Icon
+ name={'warning'}
+ size="1.5em"
+ color="var(--sb-color-orange-high)"
+ /><span>This content is not available in your language yet.</span>
+</p>
+
+<style>
+ p {
+ border: 1px solid var(--sb-color-orange);
+ padding: 0.75em 1em;
+ background-color: var(--sb-color-orange-low);
+ color: var(--sb-color-orange-high);
+ width: max-content;
+ max-width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 0.75em;
+ font-size: var(--sb-text-body-sm);
+ line-height: var(--sb-line-height-headings);
+ }
+</style>
diff --git a/packages/starbook/components/HeadSEO.astro b/packages/starbook/components/HeadSEO.astro
index 2eafbc4f..9ebdde57 100644
--- a/packages/starbook/components/HeadSEO.astro
+++ b/packages/starbook/components/HeadSEO.astro
@@ -22,7 +22,7 @@ const description = data.description || config.description;
<link rel="canonical" href={canonical} />
{
canonical &&
- config.locales &&
+ config.isMultilingual &&
Object.entries(config.locales).map(
([locale, localeOpts]) =>
localeOpts && (
diff --git a/packages/starbook/components/LanguageSelect.astro b/packages/starbook/components/LanguageSelect.astro
index 04343c1f..65d978e2 100644
--- a/packages/starbook/components/LanguageSelect.astro
+++ b/packages/starbook/components/LanguageSelect.astro
@@ -16,7 +16,7 @@ function localizedPathname(locale: string | undefined): string {
---
{
- config.locales && Object.keys(config.locales).length > 1 && (
+ config.isMultilingual && (
<starbook-lang-select>
<Select
icon="translate"
diff --git a/packages/starbook/index.astro b/packages/starbook/index.astro
index e200e317..4ae47c32 100644
--- a/packages/starbook/index.astro
+++ b/packages/starbook/index.astro
@@ -1,15 +1,9 @@
---
-import type { GetStaticPathsResult, InferGetStaticPropsType } from 'astro';
-import { getCollection } from 'astro:content';
+import type { InferGetStaticPropsType } from 'astro';
import config from 'virtual:starbook/user-config';
import { getPrevNextLinks, getSidebar } from './utils/navigation';
-import {
- slugToDir,
- slugToLang,
- slugToLocale,
- slugToParam,
-} from './utils/slugs';
+import { paths } from './utils/routing';
// Built-in CSS styles.
import './style/props.css';
@@ -20,6 +14,7 @@ import './style/util.css';
// Components — can override built-in CSS, but not user CSS.
import ContentPanel from './components/ContentPanel.astro';
import EditLink from './components/EditLink.astro';
+import FallbackContentNotice from './components/FallbackContentNotice.astro';
import HeadSEO from './components/HeadSEO.astro';
import Header from './components/Header.astro';
import LastUpdated from './components/LastUpdated.astro';
@@ -40,21 +35,13 @@ import './style/asides.css';
import 'virtual:starbook/user-css';
export async function getStaticPaths() {
- const docs = await getCollection('docs');
-
- return docs.map((doc) => ({
- params: { slug: slugToParam(doc.slug) },
- props: doc,
- })) satisfies GetStaticPathsResult;
+ return paths;
}
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
-const { render, data, slug, id } = Astro.props;
-const { Content, headings } = await render();
-const lang = slugToLang(slug);
-const locale = slugToLocale(slug);
-const dir = slugToDir(slug);
+const { dir, entry, entryMeta, isFallback, lang, locale } = Astro.props;
+const { Content, headings } = await entry.render();
const sidebar = getSidebar(Astro.url.pathname, locale);
const prevNextLinks = getPrevNextLinks(sidebar);
---
@@ -63,7 +50,7 @@ const prevNextLinks = getPrevNextLinks(sidebar);
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
- <HeadSEO data={data} lang={lang} />
+ <HeadSEO data={entry.data} lang={lang} />
</head>
<body>
<ThemeProvider />
@@ -81,24 +68,30 @@ const prevNextLinks = getPrevNextLinks(sidebar);
config.editLink.baseUrl && (
<>
<h2>Contribute</h2>
- <EditLink data={data} id={id} />
+ <EditLink data={entry.data} id={entry.id} />
</>
)
}
</RightSidebarPanel>
</Fragment>
- <main id="starbook__overview" data-pagefind-body>
+ <main
+ id="starbook__overview"
+ data-pagefind-body
+ lang={entryMeta.lang}
+ dir={entryMeta.dir}
+ >
<ContentPanel>
<h1
style="font-size: var(--sb-text-h1); line-height: var(--sb-line-height-headings); font-weight: 600; color: var(--sb-color-white); margin-top: 1rem;"
>
- {data.title}
+ {entry.data.title}
</h1>
+ {isFallback && <FallbackContentNotice />}
</ContentPanel>
<ContentPanel>
<MarkdownContent><Content /></MarkdownContent>
<footer>
- <LastUpdated id={id} lang={lang} />
+ <LastUpdated id={entry.id} lang={lang} />
<PrevNextLinks {...prevNextLinks} dir={dir} />
</footer>
</ContentPanel>
diff --git a/packages/starbook/utils/navigation.ts b/packages/starbook/utils/navigation.ts
index 7c829daf..5c45bde6 100644
--- a/packages/starbook/utils/navigation.ts
+++ b/packages/starbook/utils/navigation.ts
@@ -1,15 +1,13 @@
-import { CollectionEntry, getCollection } from 'astro:content';
import { basename, dirname } from 'node:path';
-import { slugToPathname } from '../utils/slugs';
import config from 'virtual:starbook/user-config';
+import { slugToPathname } from '../utils/slugs';
+import { Route, getLocaleRoutes, routes } from './routing';
import type {
AutoSidebarGroup,
SidebarItem,
SidebarLinkItem,
} from './user-config';
-const allDocs = await getCollection('docs');
-
export interface Link {
type: 'link';
label: string;
@@ -26,13 +24,13 @@ interface Group {
export type SidebarEntry = Link | Group;
/**
- * A representation of the file structure. For each object entry:
- * if it's a folder, the key is the directory name, and value is the directory
- * content; if it's a doc file, the key is the doc's source file name, and value
- * is the collection entry.
+ * A representation of the route structure. For each object entry:
+ * if it’s a folder, the key is the directory name, and value is the directory
+ * content; if it’s a route entry, the key is the last segment of the route, and value
+ * is the entry’s full slug.
*/
interface Dir {
- [item: string]: Dir | CollectionEntry<'docs'>['id'];
+ [item: string]: Dir | string;
}
/** Convert an item in a user’s sidebar config to a sidebar entry. */
@@ -40,18 +38,18 @@ function configItemToEntry(
item: SidebarItem,
currentPathname: string,
locale: string | undefined,
- docs: CollectionEntry<'docs'>[]
+ routes: Route[]
): SidebarEntry {
if ('link' in item) {
return linkFromConfig(item, locale, currentPathname);
} else if ('autogenerate' in item) {
- return groupFromAutogenerateConfig(item, locale, docs, currentPathname);
+ return groupFromAutogenerateConfig(item, locale, routes, currentPathname);
} else {
return {
type: 'group',
label: item.label,
entries: item.items.map((i) =>
- configItemToEntry(i, currentPathname, locale, docs)
+ configItemToEntry(i, currentPathname, locale, routes)
),
};
}
@@ -61,12 +59,12 @@ function configItemToEntry(
function groupFromAutogenerateConfig(
item: AutoSidebarGroup,
locale: string | undefined,
- docs: CollectionEntry<'docs'>[],
+ routes: Route[],
currentPathname: string
): Group {
const { directory } = item.autogenerate;
const localeDir = locale ? locale + '/' + directory : directory;
- const dirDocs = docs.filter((doc) => doc.id.startsWith(localeDir));
+ const dirDocs = routes.filter((doc) => doc.slug.startsWith(localeDir));
const tree = treeify(dirDocs, localeDir);
return {
type: 'group',
@@ -113,27 +111,26 @@ function makeLink(href: string, label: string, currentPathname: string): Link {
}
/** Get the segments leading to a page. */
-function getBreadcrumbs(
- id: CollectionEntry<'docs'>['id'],
- baseDir: string
-): string[] {
+function getBreadcrumbs(slug: string, baseDir: string): string[] {
// Ensure base directory ends in a trailing slash.
if (!baseDir.endsWith('/')) baseDir += '/';
- // Strip base directory from file ID if present.
- const relativeId = id.startsWith(baseDir) ? id.replace(baseDir, '') : id;
- let dir = dirname(relativeId);
+ // Strip base directory from slug if present.
+ const relativeSlug = slug.startsWith(baseDir)
+ ? slug.replace(baseDir, '')
+ : slug;
+ let dir = dirname(relativeSlug);
// Return no breadcrumbs for items in the root directory.
if (dir === '.') return [];
return dir.split('/');
}
-/** Turn a flat array of docs into a tree structure. */
-function treeify(docs: CollectionEntry<'docs'>[], baseDir: string): Dir {
+/** Turn a flat array of routes into a tree structure. */
+function treeify(routes: Route[], baseDir: string): Dir {
const treeRoot: Dir = {};
- docs.forEach((doc) => {
- const breadcrumbs = getBreadcrumbs(doc.id, baseDir);
+ routes.forEach((doc) => {
+ const breadcrumbs = getBreadcrumbs(doc.slug, baseDir);
- // Walk down the file's path to generate the fs structure
+ // Walk down the route’s path to generate the tree.
let currentDir = treeRoot;
breadcrumbs.forEach((dir) => {
// Create new folder if needed.
@@ -141,19 +138,20 @@ function treeify(docs: CollectionEntry<'docs'>[], baseDir: string): Dir {
// Go into the subdirectory.
currentDir = currentDir[dir] as Dir;
});
- // We've walked through the path. Register the file in this directory.
- currentDir[basename(doc.id)] = doc.id;
+ // We’ve walked through the path. Register the route in this directory.
+ currentDir[basename(doc.slug)] = doc.slug;
});
return treeRoot;
}
/** Create a link entry for a given content collection entry. */
-function linkFromId(
- id: CollectionEntry<'docs'>['id'],
- currentPathname: string
-): Link {
- const doc = allDocs.find((doc) => doc.id === id)!;
- return makeLink(slugToPathname(doc.slug), doc.data.title, currentPathname);
+function linkFromSlug(slug: string, currentPathname: string): Link {
+ const doc = routes.find((doc) => doc.slug === slug)!;
+ return makeLink(
+ slugToPathname(doc.slug),
+ doc.entry.data.title,
+ currentPathname
+ );
}
/** Create a group entry for a given content collection directory. */
@@ -164,8 +162,8 @@ function groupFromDir(
currentPathname: string,
locale: string | undefined
): Group {
- const entries = Object.entries(dir).map(([key, dirOrId]) =>
- dirToItem(dirOrId, `${fullPath}/${key}`, key, currentPathname, locale)
+ const entries = Object.entries(dir).map(([key, dirOrSlug]) =>
+ dirToItem(dirOrSlug, `${fullPath}/${key}`, key, currentPathname, locale)
);
return {
type: 'group',
@@ -174,17 +172,17 @@ function groupFromDir(
};
}
-/** Create a sidebar entry for a directory or content ID. */
+/** Create a sidebar entry for a directory or content slug. */
function dirToItem(
- dirOrId: Dir[string],
+ dirOrSlug: Dir[string],
fullPath: string,
dirName: string,
currentPathname: string,
locale: string | undefined
): SidebarEntry {
- return typeof dirOrId === 'string'
- ? linkFromId(dirOrId, currentPathname)
- : groupFromDir(dirOrId, fullPath, dirName, currentPathname, locale);
+ return typeof dirOrSlug === 'string'
+ ? linkFromSlug(dirOrSlug, currentPathname)
+ : groupFromDir(dirOrSlug, fullPath, dirName, currentPathname, locale);
}
/** Create a sidebar entry for a given content directory. */
@@ -193,8 +191,8 @@ function sidebarFromDir(
currentPathname: string,
locale: string | undefined
) {
- return Object.entries(tree).map(([key, dirOrId]) =>
- dirToItem(dirOrId, key, key, currentPathname, locale)
+ return Object.entries(tree).map(([key, dirOrSlug]) =>
+ dirToItem(dirOrSlug, key, key, currentPathname, locale)
);
}
@@ -203,23 +201,13 @@ export function getSidebar(
pathname: string,
locale: string | undefined
): SidebarEntry[] {
- let docs = allDocs;
- if (config.locales) {
- if (locale && locale in config.locales) {
- docs = allDocs.filter((doc) => doc.id.startsWith(locale + '/'));
- } else if (config.locales.root) {
- const langKeys = Object.keys(config.locales).filter((k) => k !== 'root');
- const isLangDir = new RegExp(`^(${langKeys.join('|')})/`);
- docs = allDocs.filter((doc) => !isLangDir.test(doc.id));
- }
- }
-
+ const routes = getLocaleRoutes(locale);
if (config.sidebar) {
return config.sidebar.map((group) =>
- configItemToEntry(group, pathname, locale, docs)
+ configItemToEntry(group, pathname, locale, routes)
);
} else {
- const tree = treeify(docs, locale || '');
+ const tree = treeify(routes, locale || '');
return sidebarFromDir(tree, pathname, locale);
}
}
diff --git a/packages/starbook/utils/routing.ts b/packages/starbook/utils/routing.ts
new file mode 100644
index 00000000..38154e66
--- /dev/null
+++ b/packages/starbook/utils/routing.ts
@@ -0,0 +1,109 @@
+import type { GetStaticPathsItem } from 'astro';
+import { CollectionEntry, getCollection } from 'astro:content';
+import config from 'virtual:starbook/user-config';
+import {
+ LocaleData,
+ localizedSlug,
+ slugToLocaleData,
+ slugToParam,
+} from './slugs';
+
+export interface Route extends LocaleData {
+ entry: CollectionEntry<'docs'>;
+ entryMeta: LocaleData;
+ slug: string;
+ isFallback?: true;
+ [key: string]: unknown;
+}
+
+interface Path extends GetStaticPathsItem {
+ params: { slug: string | undefined };
+ props: Route;
+}
+
+/** All entries in the docs content collection. */
+const docs = await getCollection('docs');
+
+function getRoutes(): Route[] {
+ const routes: Route[] = docs.map((entry) => ({
+ entry,
+ slug: entry.slug,
+ entryMeta: slugToLocaleData(entry.slug),
+ ...slugToLocaleData(entry.slug),
+ }));
+
+ // In multilingual sites, add required fallback routes.
+ if (config.isMultilingual) {
+ /** Entries in the docs content collection for the default locale. */
+ const defaultLocaleDocs = getLocaleDocs(
+ config.defaultLocale?.locale === 'root'
+ ? undefined
+ : config.defaultLocale?.locale
+ );
+ for (const key in config.locales) {
+ if (key === config.defaultLocale.locale) continue;
+ const localeConfig = config.locales[key];
+ if (!localeConfig) continue;
+ const locale = key === 'root' ? undefined : key;
+ const localeDocs = getLocaleDocs(locale);
+ for (const fallback of defaultLocaleDocs) {
+ const slug = localizedSlug(fallback.slug, locale);
+ const doesNotNeedFallback = localeDocs.some((doc) => doc.slug === slug);
+ if (doesNotNeedFallback) continue;
+ routes.push({
+ entry: fallback,
+ slug,
+ isFallback: true,
+ lang: localeConfig.lang || 'en',
+ locale,
+ dir: localeConfig.dir,
+ entryMeta: slugToLocaleData(fallback.slug),
+ });
+ }
+ }
+ }
+
+ return routes;
+}
+export const routes = getRoutes();
+
+function getPaths(): Path[] {
+ return routes.map((route) => ({
+ params: { slug: slugToParam(route.slug) },
+ props: route,
+ }));
+}
+export const paths = getPaths();
+
+/**
+ * Get all routes for a specific locale.
+ * A locale of `undefined` is treated as the “root” locale, if configured.
+ */
+export function getLocaleRoutes(locale: string | undefined): Route[] {
+ return filterByLocale(routes, locale);
+}
+
+/**
+ * Get all entries in the docs content collection for a specific locale.
+ * A locale of `undefined` is treated as the “root” locale, if configured.
+ */
+function getLocaleDocs(locale: string | undefined): CollectionEntry<'docs'>[] {
+ return filterByLocale(docs, locale);
+}
+
+/** Filter an array to find items whose slug matches the passed locale. */
+function filterByLocale<T extends { slug: string }>(
+ items: T[],
+ locale: string | undefined
+): T[] {
+ if (config.locales) {
+ if (locale && locale in config.locales) {
+ return items.filter((i) => i.slug.startsWith(locale + '/'));
+ } else if (config.locales.root) {
+ const langKeys = Object.keys(config.locales).filter((k) => k !== 'root');
+ const isLangDir = new RegExp(`^(${langKeys.join('|')})/`);
+ return items.filter((i) => !isLangDir.test(i.slug));
+ }
+ }
+ return items;
+}
diff --git a/packages/starbook/utils/slugs.ts b/packages/starbook/utils/slugs.ts
index 2e0c193e..34eef630 100644
--- a/packages/starbook/utils/slugs.ts
+++ b/packages/starbook/utils/slugs.ts
@@ -1,13 +1,22 @@
import type { CollectionEntry } from 'astro:content';
import config from 'virtual:starbook/user-config';
+export interface LocaleData {
+ /** Writing direction. */
+ dir: 'ltr' | 'rtl';
+ /** BCP-47 language tag. */
+ lang: string;
+ /** The base path at which a language is served. `undefined` for root locale slugs. */
+ locale: string | undefined;
+}
+
/**
* Get the “locale” of a slug. This is the base path at which a language is served.
* For example, if French docs are in `src/content/docs/french/`, the locale is `french`.
* Root locale slugs will return `undefined`.
* @param slug A collection entry slug
*/
-export function slugToLocale(
+function slugToLocale(
slug: CollectionEntry<'docs'>['slug']
): string | undefined {
const locales = Object.keys(config.locales || {});
@@ -16,12 +25,19 @@ export function slugToLocale(
return undefined;
}
+/** Get locale information for a given slug. */
+export function slugToLocaleData(
+ slug: CollectionEntry<'docs'>['slug']
+): LocaleData {
+ const locale = slugToLocale(slug);
+ return { dir: localeToDir(locale), lang: localeToLang(locale), locale };
+}
+
/**
- * Get the BCP-47 language tag for the locale of a slug.
- * @param slug A collection entry slug
+ * Get the BCP-47 language tag for the given locale.
+ * @param locale Locale string or `undefined` for the root locale.
*/
-export function slugToLang(slug: CollectionEntry<'docs'>['slug']): string {
- const locale = slugToLocale(slug);
+function localeToLang(locale: string | undefined): string {
const lang = locale
? config.locales?.[locale]?.lang
: config.locales?.root?.lang;
@@ -29,13 +45,10 @@ export function slugToLang(slug: CollectionEntry<'docs'>['slug']): string {
}
/**
- * Get the configured writing direction for the locale of a slug.
- * @param slug A collection entry slug
+ * Get the configured writing direction for the given locale.
+ * @param locale Locale string or `undefined` for the root locale.
*/
-export function slugToDir(
- slug: CollectionEntry<'docs'>['slug']
-): 'ltr' | 'rtl' {
- const locale = slugToLocale(slug);
+function localeToDir(locale: string | undefined): 'ltr' | 'rtl' {
const dir = locale
? config.locales?.[locale]?.dir
: config.locales?.root?.dir;
@@ -54,3 +67,25 @@ export function slugToPathname(slug: string): string {
const param = slugToParam(slug);
return param ? '/' + param + '/' : '/';
}
+
+/**
+ * Convert a slug to a different locale.
+ * For example, passing a slug of `en/home` and a locale of `fr` results in `fr/home`.
+ * An undefined locale is treated as the root locale, resulting in `home`
+ * @param slug A collection entry slug
+ * @param locale The target locale
+ * @example
+ * localizedSlug('en/home', 'fr') // => 'fr/home'
+ * localizedSlug('en/home', undefined) // => 'home'
+ */
+export function localizedSlug(
+ slug: CollectionEntry<'docs'>['slug'],
+ locale: string | undefined
+): string {
+ const slugLocale = slugToLocale(slug);
+ if (slugLocale === locale) return slug;
+ if (slugLocale) {
+ return slug.replace(slugLocale + '/', locale ? locale + '/' : '');
+ }
+ return locale + '/' + slug;
+}
diff --git a/packages/starbook/utils/user-config.ts b/packages/starbook/utils/user-config.ts
index ed4e514f..6b6064ec 100644
--- a/packages/starbook/utils/user-config.ts
+++ b/packages/starbook/utils/user-config.ts
@@ -84,7 +84,7 @@ const SidebarGroupSchema: z.ZodType<
ManualSidebarGroup | z.infer<typeof AutoSidebarGroupSchema>
> = z.union([ManualSidebarGroupSchema, AutoSidebarGroupSchema]);
-export const StarbookConfigSchema = z.object({
+const StarbookUserConfigSchema = z.object({
/** Title for your website. Will be used in metadata and as browser tab title. */
title: z
.string()
@@ -177,6 +177,13 @@ export const StarbookConfigSchema = z.object({
.optional()
.describe('Configure locales for internationalization (i18n).'),
+ /**
+ * Specify the default language for this site.
+ *
+ * The default locale will be used to provide fallback content where translations are missing.
+ */
+ defaultLocale: z.string().optional(),
+
/** Configure your site’s sidebar navigation items. */
sidebar: SidebarGroupSchema.array().optional(),
@@ -195,5 +202,49 @@ export const StarbookConfigSchema = z.object({
customCss: z.string().array().optional().default([]),
});
+export const StarbookConfigSchema = StarbookUserConfigSchema.strict().transform(
+ ({ locales, defaultLocale, ...config }, ctx) => {
+ if (locales !== undefined && Object.keys(locales).length > 1) {
+ // This is a multilingual site (more than one locale configured).
+ // Make sure we can find the default locale and if not, help the user set it.
+ // We treat the root locale as the default if present and no explicit default is set.
+ const defaultLocaleConfig = locales[defaultLocale || 'root'];
+
+ if (!defaultLocaleConfig) {
+ const availableLocales = Object.keys(locales)
+ .map((l) => `"${l}"`)
+ .join(', ');
+ ctx.addIssue({
+ code: 'custom',
+ message:
+ 'Could not determine the default locale. ' +
+ 'Please make sure `defaultLocale` in your StarBook config is one of ' +
+ availableLocales,
+ });
+ return z.NEVER;
+ }
+
+ return {
+ ...config,
+ /** Flag indicating if this site has multiple locales set up. */
+ isMultilingual: true,
+ /** Full locale object for this site’s default language. */
+ defaultLocale: { ...defaultLocaleConfig, locale: defaultLocale },
+ locales,
+ } as const;
+ }
+
+ // This is a monolingual site, so things are pretty simple.
+ return {
+ ...config,
+ /** Flag indicating if this site has multiple locales set up. */
+ isMultilingual: false,
+ /** Full locale object for this site’s default language. */
+ defaultLocale: undefined,
+ locales: undefined,
+ } as const;
+ }
+);
+
export type StarbookConfig = z.infer<typeof StarbookConfigSchema>;
export type StarBookUserConfig = z.input<typeof StarbookConfigSchema>;