Add bilingual manual and localized docs routes
This commit is contained in:
@@ -3,6 +3,7 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
||||
import { highlightCode } from '../lib/highlight.js';
|
||||
|
||||
const { locale = 'en' } = Astro.props;
|
||||
const docsLocale = locale === 'ja' ? 'ja' : 'en';
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
@@ -121,7 +122,7 @@ const result = `{
|
||||
<p class="home-lead phrased" set:html={phrase(copy.heroLead)} />
|
||||
<div class="home-actions">
|
||||
<a class="button primary" href="/playground/">{copy.playground}</a>
|
||||
<a class="text-action" href="/docs/introduction/">{copy.introduction} <span aria-hidden="true">→</span></a>
|
||||
<a class="text-action" href={`/docs/${docsLocale}/introduction/`}>{copy.introduction} <span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
<dl class="home-facts">
|
||||
{copy.facts.map(([label, value], index) => (
|
||||
@@ -177,7 +178,7 @@ const result = `{
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p class="section-link"><a href="/docs/embedding/">{copy.embedding} <span aria-hidden="true">→</span></a></p>
|
||||
<p class="section-link"><a href={`/docs/${docsLocale}/embedding/`}>{copy.embedding} <span aria-hidden="true">→</span></a></p>
|
||||
</section>
|
||||
|
||||
<section class="home-section package-section" aria-labelledby="packages-title">
|
||||
@@ -206,6 +207,6 @@ const result = `{
|
||||
<span class="package-links"><a href="https://jsr.io/@hare/decodal-codemirror@0.3.0">JSR</a><a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></span>
|
||||
</article>
|
||||
</div>
|
||||
<p class="section-link"><a href="/docs/components/">{copy.packagesLink} <span aria-hidden="true">→</span></a></p>
|
||||
<p class="section-link"><a href={`/docs/${docsLocale}/components/`}>{copy.packagesLink} <span aria-hidden="true">→</span></a></p>
|
||||
</section>
|
||||
</ManualLayout>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
import { nav } from '../lib/docs.js';
|
||||
import { getNav } from '../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-mono/latin-400.css';
|
||||
import '@fontsource/ibm-plex-mono/latin-600.css';
|
||||
import '../style.css';
|
||||
|
||||
const {
|
||||
title = 'Decodal',
|
||||
description = 'Decodal is a deterministic data language for configuration, constraints, and defaults.',
|
||||
description,
|
||||
active = '',
|
||||
playground = false,
|
||||
frontpage = false,
|
||||
@@ -14,11 +14,24 @@ const {
|
||||
} = Astro.props;
|
||||
|
||||
const pathname = Astro.url.pathname;
|
||||
const language = locale ?? (pathname.startsWith('/docs/') ? 'ja' : 'en');
|
||||
const language = locale ?? 'en';
|
||||
const pageDescription = description ?? (language === 'ja'
|
||||
? 'Decodalは、設定、制約、デフォルトのための決定的なデータ言語です。'
|
||||
: 'Decodal is a deterministic data language for configuration, constraints, and defaults.');
|
||||
const docsLocale = language === 'ja' ? 'ja' : 'en';
|
||||
const otherDocsLocale = docsLocale === 'ja' ? 'en' : 'ja';
|
||||
const isDocsPage = pathname.startsWith('/docs/');
|
||||
const localizedDocPath = active && active !== 'index' ? `${active}/` : '';
|
||||
const homeHref = language === 'ja' ? '/ja/' : '/';
|
||||
const alternateHref = language === 'ja' ? '/' : '/ja/';
|
||||
const alternateHref = frontpage
|
||||
? language === 'ja' ? '/' : '/ja/'
|
||||
: `/docs/${otherDocsLocale}/${localizedDocPath}`;
|
||||
const alternateLabel = language === 'ja' ? 'English' : '日本語';
|
||||
const alternateAriaLabel = language === 'ja' ? 'Switch to English' : '日本語に切り替える';
|
||||
const manualLabel = language === 'ja' ? 'マニュアル' : 'Manual';
|
||||
const manualHref = `/docs/${docsLocale}/introduction/`;
|
||||
const manualIndexHref = `/docs/${docsLocale}/`;
|
||||
const nav = getNav(docsLocale);
|
||||
|
||||
function isCurrent(path) {
|
||||
return pathname === path || (path !== '/' && pathname.startsWith(path));
|
||||
@@ -28,7 +41,7 @@ function renderNav(items, prefix = []) {
|
||||
return `<ul class="nav-tree">${items
|
||||
.map((item, index) => {
|
||||
const number = [...prefix, index + 1];
|
||||
const href = `/docs/${item.slug}/`;
|
||||
const href = `/docs/${docsLocale}/${item.slug}/`;
|
||||
const activeClass = active === item.slug ? ' class="active"' : '';
|
||||
const children = item.children ? renderNav(item.children, number) : '';
|
||||
return `<li><a${activeClass} href="${href}"><span class="nav-number">${number.join('.')}.</span>${item.title}</a>${children}</li>`;
|
||||
@@ -41,10 +54,10 @@ function renderNav(items, prefix = []) {
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
{frontpage && <link rel="alternate" hreflang="en" href="/" />}
|
||||
{frontpage && <link rel="alternate" hreflang="ja" href="/ja/" />}
|
||||
{frontpage && <link rel="alternate" hreflang="x-default" href="/" />}
|
||||
<meta name="description" content={pageDescription} />
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="en" href={frontpage ? '/' : `/docs/en/${localizedDocPath}`} />}
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="ja" href={frontpage ? '/ja/' : `/docs/ja/${localizedDocPath}`} />}
|
||||
{(frontpage || isDocsPage) && <link rel="alternate" hreflang="x-default" href={frontpage ? '/' : `/docs/en/${localizedDocPath}`} />}
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -54,17 +67,17 @@ function renderNav(items, prefix = []) {
|
||||
<span>Deferred Constraint Data Language</span>
|
||||
</a>
|
||||
<nav class="topnav" aria-label="Primary navigation">
|
||||
<a class:list={{ current: isCurrent('/docs/') }} aria-current={isCurrent('/docs/') ? 'page' : undefined} href="/docs/introduction/">Manual</a>
|
||||
<a class:list={{ current: isCurrent('/docs/') }} aria-current={isCurrent('/docs/') ? 'page' : undefined} href={manualHref}>{manualLabel}</a>
|
||||
<a class:list={{ current: isCurrent('/playground/') }} aria-current={isCurrent('/playground/') ? 'page' : undefined} href="/playground/">Playground</a>
|
||||
<a class="source-link" href="https://gitea.hareworks.net/Hare/Decodal">Source</a>
|
||||
{frontpage && <a class="language-switch" href={alternateHref} hreflang={language === 'ja' ? 'en' : 'ja'} aria-label={alternateAriaLabel}>{alternateLabel}</a>}
|
||||
{(frontpage || isDocsPage) && <a class="language-switch" href={alternateHref} hreflang={otherDocsLocale} aria-label={alternateAriaLabel}>{alternateLabel}</a>}
|
||||
</nav>
|
||||
</header>
|
||||
<div class:list={{ layout: true, 'wide-layout': playground || frontpage, 'playground-layout': playground }}>
|
||||
{!playground && !frontpage && (
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
<a class="sidebar-title" href="/docs/">Manual</a>
|
||||
<a class="sidebar-title" href={manualIndexHref}>{manualLabel}</a>
|
||||
<nav set:html={renderNav(nav)} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import ManualLayout from '../../layouts/ManualLayout.astro';
|
||||
import { allDocSlugs, docTitle, renderMarkdown } from '../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/600.css';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return allDocSlugs().map((slug) => ({
|
||||
params: { slug },
|
||||
props: { slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
const html = renderMarkdown(slug);
|
||||
const title = docTitle(slug);
|
||||
---
|
||||
<ManualLayout title={`${title} — Decodal Manual`} active={slug}>
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { allDocSlugs, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-600.css';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return allDocSlugs('en').map((slug) => ({
|
||||
params: { slug },
|
||||
props: { slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
const html = renderMarkdown(slug, 'en');
|
||||
const title = docTitle(slug, 'en');
|
||||
---
|
||||
<ManualLayout title={`${title} — Decodal Manual`} active={slug} locale="en">
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/latin-600.css';
|
||||
|
||||
const html = renderMarkdown('index', 'en');
|
||||
---
|
||||
<ManualLayout
|
||||
title="Decodal Manual"
|
||||
description="The Decodal language specification and public APIs for embedding it in applications."
|
||||
active="index"
|
||||
locale="en"
|
||||
>
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
@@ -1,11 +1,3 @@
|
||||
---
|
||||
import ManualLayout from '../../layouts/ManualLayout.astro';
|
||||
import { renderMarkdown } from '../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/600.css';
|
||||
|
||||
const html = renderMarkdown('index');
|
||||
return Astro.redirect('/docs/en/');
|
||||
---
|
||||
<ManualLayout title="Decodal Manual" description="Decodalの言語仕様と、アプリケーションへ組み込むための公開API。" active="index">
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { allDocSlugs, docTitle, renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/600.css';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return allDocSlugs('ja').map((slug) => ({
|
||||
params: { slug },
|
||||
props: { slug },
|
||||
}));
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
const html = renderMarkdown(slug, 'ja');
|
||||
const title = docTitle(slug, 'ja');
|
||||
---
|
||||
<ManualLayout title={`${title} — Decodalマニュアル`} active={slug} locale="ja">
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import ManualLayout from '../../../layouts/ManualLayout.astro';
|
||||
import { renderMarkdown } from '../../../lib/docs.js';
|
||||
import '@fontsource/ibm-plex-sans-jp/400.css';
|
||||
import '@fontsource/ibm-plex-sans-jp/600.css';
|
||||
|
||||
const html = renderMarkdown('index', 'ja');
|
||||
---
|
||||
<ManualLayout
|
||||
title="Decodalマニュアル"
|
||||
description="Decodalの言語仕様と、アプリケーションへ組み込むための公開API。"
|
||||
active="index"
|
||||
locale="ja"
|
||||
>
|
||||
<article class="markdown" set:html={html} />
|
||||
</ManualLayout>
|
||||
Reference in New Issue
Block a user