Add bilingual manual and localized docs routes

This commit is contained in:
2026-08-14 11:16:37 +09:00
parent 3645a2cc2d
commit cda32cc260
85 changed files with 2316 additions and 71 deletions
+106 -22
View File
@@ -1,31 +1,48 @@
import { marked } from 'marked';
import { escapeAttribute, highlightCode } from './highlight.js';
const modules = import.meta.glob('../../../../doc/manual/souce/**/*.md', {
const modules = import.meta.glob('../../../../doc/manual/source/**/*.md', {
query: '?raw',
import: 'default',
eager: true,
});
const documentEntries = Object.entries(modules).map(([path, content]) => {
const sourcePath = path
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/souce\//, '')
const relativePath = path
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/source\//, '')
.replace(/\.md$/, '');
const parts = sourcePath.split('/');
parts.pop();
const pathParts = relativePath.split('/');
const locale = pathParts[0] === 'jp' ? 'ja' : 'en';
const localizedParts = locale === 'ja' ? pathParts.slice(1) : pathParts;
const sourcePath = localizedParts.join('/');
const base = localizedParts.slice(0, -1);
const slug = sourcePath.replace(/\/index$/, '') || 'index';
return { slug, content, base: parts };
return { locale, slug, content, base };
});
export const docs = Object.fromEntries(
documentEntries.map(({ slug, content }) => [slug, content]),
const docsByLocale = Object.fromEntries(
['en', 'ja'].map((locale) => [
locale,
Object.fromEntries(
documentEntries
.filter((entry) => entry.locale === locale)
.map(({ slug, content }) => [slug, content]),
),
]),
);
const docBases = Object.fromEntries(
documentEntries.map(({ slug, base }) => [slug, base]),
const docBasesByLocale = Object.fromEntries(
['en', 'ja'].map((locale) => [
locale,
Object.fromEntries(
documentEntries
.filter((entry) => entry.locale === locale)
.map(({ slug, base }) => [slug, base]),
),
]),
);
export const nav = [
const englishNav = [
{ title: 'Introduction', slug: 'introduction' },
{
title: 'Language Specification',
@@ -78,6 +95,64 @@ export const nav = [
{ title: 'Packages and Integrations', slug: 'components' },
];
const japaneseNav = [
{ title: 'はじめに', slug: 'introduction' },
{
title: '言語仕様',
slug: 'language',
children: [
{ title: '構文と字句', slug: 'language/syntax' },
{ title: '文法', slug: 'language/grammar' },
{
title: '値',
slug: 'language/value',
children: [
{ title: 'String', slug: 'language/value/string' },
{ title: 'Int', slug: 'language/value/int' },
{ title: 'Float', slug: 'language/value/float' },
{ title: 'Bool', slug: 'language/value/bool' },
],
},
{
title: '式',
slug: 'language/expression',
children: [
{ title: 'リテラル', slug: 'language/expression/literal' },
{ title: '識別子', slug: 'language/expression/identifier' },
{ title: 'パス参照', slug: 'language/expression/path-reference' },
{ title: 'オブジェクト', slug: 'language/expression/object' },
{ title: '配列', slug: 'language/expression/array' },
{ title: '関数', slug: 'language/expression/function' },
{ title: '関数呼び出し', slug: 'language/expression/function-call' },
{ title: 'let', slug: 'language/expression/let' },
{ title: 'match', slug: 'language/expression/match' },
{ title: 'import', slug: 'language/expression/import' },
{ title: '合成', slug: 'language/expression/composition' },
{ title: '範囲の絞り込み', slug: 'language/expression/ascription' },
{ title: 'default', slug: 'language/expression/default' },
{ title: '算術式', slug: 'language/expression/arithmetic' },
{ title: '論理式と比較式', slug: 'language/expression/logical-and-comparison' },
],
},
{ title: '制約とdefault', slug: 'language/constraints-and-defaults' },
{ title: '演算子', slug: 'language/operators' },
{ title: '関数', slug: 'language/functions' },
{ title: 'モジュールとimport', slug: 'language/modules-and-imports' },
{ title: '遅延評価', slug: 'language/evaluation' },
{ title: '具体化とエラー', slug: 'language/materialization-and-errors' },
{ title: '命名規約', slug: 'language/naming' },
{ title: '例', slug: 'language/examples' },
],
},
{ title: '組み込み', slug: 'embedding' },
{ title: 'パッケージと統合', slug: 'components' },
];
const navByLocale = {
en: englishNav,
ja: japaneseNav,
};
const renderer = new marked.Renderer();
renderer.code = (code, language = '') => {
@@ -90,33 +165,42 @@ renderer.codespan = (code) => `<code>${code}</code>`;
marked.setOptions({ gfm: true, renderer });
export function allDocSlugs() {
return Object.keys(docs).filter((slug) => slug !== 'index');
export function getNav(locale = 'en') {
return navByLocale[locale] ?? englishNav;
}
export function docTitle(slug, items = nav) {
export function allDocSlugs(locale = 'en') {
return Object.keys(docsByLocale[locale] ?? {}).filter((slug) => slug !== 'index');
}
function findDocTitle(slug, items) {
for (const item of items) {
if (item.slug === slug) return item.title;
if (item.children) {
const childTitle = docTitle(slug, item.children);
const childTitle = findDocTitle(slug, item.children);
if (childTitle) return childTitle;
}
}
return slug;
return undefined;
}
export function renderMarkdown(slug) {
const source = docs[slug] ?? docs.index;
export function docTitle(slug, locale = 'en') {
return findDocTitle(slug, getNav(locale)) ?? slug;
}
export function renderMarkdown(slug, locale = 'en') {
const localizedDocs = docsByLocale[locale] ?? docsByLocale.en;
const source = localizedDocs[slug] ?? localizedDocs.index;
const html = marked.parse(source ?? '# Not found\n');
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
const target = normalizeDocLink(slug, href);
const pathname = target === 'index' ? '/docs/' : `/docs/${target}/`;
const target = normalizeDocLink(locale, slug, href);
const pathname = target === 'index' ? `/docs/${locale}/` : `/docs/${locale}/${target}/`;
return `href="${pathname}${hash}"`;
});
}
function normalizeDocLink(currentSlug, href) {
const base = docBases[currentSlug] ?? [];
function normalizeDocLink(locale, currentSlug, href) {
const base = docBasesByLocale[locale]?.[currentSlug] ?? [];
const parts = [...base, ...href.split('/')];
const out = [];
for (const part of parts) {