Add bilingual homepage and self-hosted fonts

This commit is contained in:
2026-08-14 11:00:11 +09:00
parent e50458595b
commit 3645a2cc2d
11 changed files with 288 additions and 169 deletions
+20
View File
@@ -11,6 +11,8 @@
"@astrojs/check": "^0.9.4",
"@codemirror/language": "^6.12.4",
"@codemirror/view": "^6.43.6",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-sans-jp": "^5.3.0",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10",
"astro": "^4.16.18",
@@ -1178,6 +1180,24 @@
"node": ">=12"
}
},
"node_modules/@fontsource/ibm-plex-mono": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/ibm-plex-sans-jp": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans-jp/-/ibm-plex-sans-jp-5.3.0.tgz",
"integrity": "sha512-Eas8EEVkh0UbITQMskrCF5Ha1L24GagNsvjMfrKwhv3KNPFRjbZAt0fLO4lSgfx4U4rHWpX5JeMDJNXF+hcguQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+2
View File
@@ -19,6 +19,8 @@
"@astrojs/check": "^0.9.4",
"@codemirror/language": "^6.12.4",
"@codemirror/view": "^6.43.6",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-sans-jp": "^5.3.0",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10",
"astro": "^4.16.18",
@@ -0,0 +1,211 @@
---
import ManualLayout from '../layouts/ManualLayout.astro';
import { highlightCode } from '../lib/highlight.js';
const { locale = 'en' } = Astro.props;
const translations = {
en: {
title: 'Decodal — Deferred Constraint Data Language',
description: 'Decodal is a deterministic data language for describing, composing, validating, and materializing structured data.',
heroTitle: 'Configuration, constraints, and defaults are values.',
heroLead: 'Decodal is a small deterministic language for describing, composing, validating, and materializing structured data. The same expression can carry concrete data and the range that data must satisfy.',
playground: 'Open the playground',
introduction: 'Read the introduction',
facts: [
['Source', '.dcdl'],
['Runtime', 'Rust · WebAssembly'],
['Output', 'Validated structured data'],
],
exampleLabel: 'Decodal example and materialized result',
materialize: 'materialize',
modelTitle: 'One expression system, from abstract range to concrete data.',
definitions: [
['&', 'Compose without discarding constraints', 'Symmetric composition keeps information from both sides and fails when their ranges cannot overlap.'],
['as', 'Verify that a value is more specific', 'The left side must fit inside the wider range on the right. Wider abstract fields remain available for later materialization.'],
['//', 'Apply an explicit structural patch', 'Right-biased composition replaces the selected structure while preserving unrelated fields.'],
['default', 'Choose a fallback only when materializing', 'A default does not erase the range. It is selected only when no more concrete value has been supplied.'],
],
hostTitle: 'The host owns I/O. Decodal owns evaluation.',
hostDescription: 'Filesystems, Markdown, JSON, network resources, and application globals stay under host control. Decodal receives source or structured values and evaluates them with the same rules in production and editor tooling.',
flowLabel: 'Evaluation flow',
flow: [
['Host environment', 'globals · source · structured imports'],
['Evaluate', 'lazy references · constraints · composition'],
['Materialize', 'defaults · validation · diagnostics'],
['Application data', 'Data · Rust type · JavaScript value'],
],
embedding: 'Read the embedding guide',
packagesTitle: 'Use only the layer your application needs.',
packages: [
['Runtime, host embedding, schema derivation, and typed decoding.'],
['Browser runtime and host-configurable semantic language service.'],
['Language Server Protocol with application-defined environments.'],
['CodeMirror 6 syntax, indentation, folding, highlighting, and formatter integration.'],
],
packagesLink: 'Compare packages and integrations',
},
ja: {
title: 'Decodal — 制約とデフォルトを値として扱うデータ言語',
description: 'Decodalは、構造化データの記述、合成、検証、具体化のための決定的なデータ言語です。',
heroTitle: '設定、|制約、|デフォルトは、|すべて値です。',
heroLead: 'Decodalは、|構造化データを|記述・合成・検証・具体化するための、|小さく決定的な言語です。|同じ式の中に、|具体的なデータと、|そのデータが満たすべき範囲を|保持できます。',
playground: 'Playgroundを開く',
introduction: 'はじめに読む',
facts: [
['ソース', '.dcdl'],
['ランタイム', 'Rust · WebAssembly'],
['出力', '検証済みの構造化データ'],
],
exampleLabel: 'Decodalのコード例と具体化結果',
materialize: '具体化',
modelTitle: '抽象的な範囲から|具体的なデータまで、|ひとつの式体系で。',
definitions: [
['&', '制約を失わずに|合成する', '対称な合成は|両辺の情報を残し、|範囲が重ならない場合は|失敗します。'],
['as', '値がより具体的であることを|検証する', '左辺は、|右辺のより広い範囲に|収まる必要があります。|右辺に残る抽象的なフィールドは、|後の具体化に|利用できます。'],
['//', '構造を明示的に|上書きする', '右辺優先の合成で、|指定した構造を置き換えながら、|関係のないフィールドを|保持します。'],
['default', '具体化するときだけ|フォールバックを選ぶ', 'defaultは範囲を消しません。|より具体的な値が|与えられていない場合にだけ|選択されます。'],
],
hostTitle: 'I/Oはホストが担い、|評価はDecodalが担う。',
hostDescription: 'ファイルシステム、|Markdown、JSON、ネットワーク資源、|アプリケーションのグローバル値は、|ホストの管理下に置かれます。|Decodalはソースまたは|構造化された値を受け取り、|本番環境とエディタツールで|同じ規則を使って評価します。',
flowLabel: '評価の流れ',
flow: [
['ホスト環境', 'globals · source ·|構造化import'],
['評価', '遅延参照 · 制約 ·|合成'],
['具体化', 'default · 検証 ·|診断'],
['アプリケーション|データ', 'Data · Rust型 ·|JavaScript値'],
],
embedding: '組み込みガイドを読む',
packagesTitle: 'アプリケーションに|必要な層だけを|使えます。',
packages: [
['ランタイム、|ホストへの組み込み、|スキーマ導出、|型付きデコード。'],
['ブラウザランタイムと、|ホストから設定できる|セマンティック言語サービス。'],
['アプリケーション定義の環境を|利用できる|Language Server Protocol実装。'],
['CodeMirror 6向けの構文、|インデント、折りたたみ、|ハイライト、|フォーマッタ連携。'],
],
packagesLink: 'パッケージと統合方法を比較する',
},
};
const copy = translations[locale] ?? translations.en;
const escapeHtml = (text) => String(text)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const phrase = (text) => locale === 'ja'
? String(text).split('|').map((part) => `<span class="phrase-unit">${escapeHtml(part)}</span>`).join('<wbr>')
: escapeHtml(text);
const example = `let
Port = Int & >= 1 & <= 65535;
Service = {
host = String;
port = Port default 8080;
};
in
{
host = "127.0.0.1";
} as Service;`;
const result = `{
"host": "127.0.0.1",
"port": 8080
}`;
---
<ManualLayout title={copy.title} description={copy.description} locale={locale} frontpage>
<section class="home-intro" aria-labelledby="home-title">
<div class="home-intro-copy">
<h1 id="home-title" class="phrased" set:html={phrase(copy.heroTitle)} />
<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>
</div>
<dl class="home-facts">
{copy.facts.map(([label, value], index) => (
<div>
<dt>{label}</dt>
<dd>{index === 0 ? <code>{value}</code> : value}</dd>
</div>
))}
</dl>
</div>
<div class="home-code" aria-label={copy.exampleLabel}>
<div class="code-caption">
<span>service.dcdl</span>
<span>DCDL</span>
</div>
<pre class="code-block"><code class="language-dcdl" set:html={highlightCode(example, 'dcdl')} /></pre>
<div class="materialized-result">
<span>{copy.materialize}</span>
<pre><code>{result}</code></pre>
</div>
</div>
</section>
<section class="home-section" aria-labelledby="model-title">
<header class="section-heading">
<h2 id="model-title" class="phrased" set:html={phrase(copy.modelTitle)} />
</header>
<div class="definition-list">
{copy.definitions.map(([operator, title, description]) => (
<article>
<code>{operator}</code>
<div>
<h3 class="phrased" set:html={phrase(title)} />
<p class="phrased" set:html={phrase(description)} />
</div>
</article>
))}
</div>
</section>
<section class="home-section host-section" aria-labelledby="host-title">
<header class="section-heading">
<h2 id="host-title" class="phrased" set:html={phrase(copy.hostTitle)} />
<p class="phrased" set:html={phrase(copy.hostDescription)} />
</header>
<ol class="evaluation-flow" aria-label={copy.flowLabel}>
{copy.flow.map(([title, description], index) => (
<li>
<span>{index + 1}</span>
<strong class="phrased" set:html={phrase(title)} />
<small class="phrased" set:html={phrase(description)} />
</li>
))}
</ol>
<p class="section-link"><a href="/docs/embedding/">{copy.embedding} <span aria-hidden="true">→</span></a></p>
</section>
<section class="home-section package-section" aria-labelledby="packages-title">
<header class="section-heading">
<h2 id="packages-title" class="phrased" set:html={phrase(copy.packagesTitle)} />
</header>
<div class="package-rows">
<article>
<div><code>decodal</code><span>Rust</span></div>
<p class="phrased" set:html={phrase(copy.packages[0][0])} />
<a href="https://crates.io/crates/decodal">crates.io</a>
</article>
<article>
<div><code>decodal-wasm</code><span>JavaScript</span></div>
<p class="phrased" set:html={phrase(copy.packages[1][0])} />
<span class="package-links"><a href="https://jsr.io/@hare/decodal-wasm@0.3.0">JSR</a><a href="https://www.npmjs.com/package/decodal-wasm">npm</a></span>
</article>
<article>
<div><code>decodal-lsp</code><span>Rust</span></div>
<p class="phrased" set:html={phrase(copy.packages[2][0])} />
<a href="https://crates.io/crates/decodal-lsp">crates.io</a>
</article>
<article>
<div><code>decodal-codemirror</code><span>JavaScript</span></div>
<p class="phrased" set:html={phrase(copy.packages[3][0])} />
<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>
</section>
</ManualLayout>
@@ -1,5 +1,7 @@
---
import { nav } from '../lib/docs.js';
import '@fontsource/ibm-plex-mono/latin-400.css';
import '@fontsource/ibm-plex-mono/latin-600.css';
import '../style.css';
const {
@@ -8,10 +10,15 @@ const {
active = '',
playground = false,
frontpage = false,
locale,
} = Astro.props;
const pathname = Astro.url.pathname;
const language = active ? 'ja' : 'en';
const language = locale ?? (pathname.startsWith('/docs/') ? 'ja' : 'en');
const homeHref = language === 'ja' ? '/ja/' : '/';
const alternateHref = language === 'ja' ? '/' : '/ja/';
const alternateLabel = language === 'ja' ? 'English' : '日本語';
const alternateAriaLabel = language === 'ja' ? 'Switch to English' : '日本語に切り替える';
function isCurrent(path) {
return pathname === path || (path !== '/' && pathname.startsWith(path));
@@ -35,18 +42,22 @@ function renderNav(items, prefix = []) {
<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="/" />}
<title>{title}</title>
</head>
<body>
<header class="topbar">
<a class="brand" href="/" aria-label="Decodal home">
<a class="brand" href={homeHref} aria-label="Decodal home">
<strong>Decodal</strong>
<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('/playground/') }} aria-current={isCurrent('/playground/') ? 'page' : undefined} href="/playground/">Playground</a>
<a href="https://gitea.hareworks.net/Hare/Decodal">Source</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>}
</nav>
</header>
<div class:list={{ layout: true, 'wide-layout': playground || frontpage, 'playground-layout': playground }}>
@@ -1,6 +1,8 @@
---
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) => ({
@@ -1,6 +1,8 @@
---
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');
---
+4 -161
View File
@@ -1,163 +1,6 @@
---
import ManualLayout from '../layouts/ManualLayout.astro';
import { highlightCode } from '../lib/highlight.js';
const example = `let
Port = Int & >= 1 & <= 65535;
Service = {
host = String;
port = Port default 8080;
};
in
{
host = "127.0.0.1";
} as Service;`;
const result = `{
"host": "127.0.0.1",
"port": 8080
}`;
import HomePage from '../components/HomePage.astro';
import '@fontsource/ibm-plex-sans-jp/latin-400.css';
import '@fontsource/ibm-plex-sans-jp/latin-600.css';
---
<ManualLayout
title="Decodal — Deferred Constraint Data Language"
description="Decodal is a deterministic data language for describing, composing, validating, and materializing structured data."
frontpage
>
<section class="home-intro" aria-labelledby="home-title">
<div class="home-intro-copy">
<h1 id="home-title">Configuration, constraints, and defaults are values.</h1>
<p class="home-lead">
Decodal is a small deterministic language for describing, composing, validating, and materializing
structured data. The same expression can carry concrete data and the range that data must satisfy.
</p>
<div class="home-actions">
<a class="button primary" href="/playground/">Open the playground</a>
<a class="text-action" href="/docs/introduction/">Read the introduction <span aria-hidden="true">→</span></a>
</div>
<dl class="home-facts">
<div>
<dt>Source</dt>
<dd><code>.dcdl</code></dd>
</div>
<div>
<dt>Runtime</dt>
<dd>Rust · WebAssembly</dd>
</div>
<div>
<dt>Output</dt>
<dd>Validated structured data</dd>
</div>
</dl>
</div>
<div class="home-code" aria-label="Decodal example and materialized result">
<div class="code-caption">
<span>service.dcdl</span>
<span>DCDL</span>
</div>
<pre class="code-block"><code class="language-dcdl" set:html={highlightCode(example, 'dcdl')} /></pre>
<div class="materialized-result">
<span>materialize</span>
<pre><code>{result}</code></pre>
</div>
</div>
</section>
<section class="home-section" aria-labelledby="model-title">
<header class="section-heading">
<h2 id="model-title">One expression system, from abstract range to concrete data.</h2>
</header>
<div class="definition-list">
<article>
<code>&amp;</code>
<div>
<h3>Compose without discarding constraints</h3>
<p>Symmetric composition keeps information from both sides and fails when their ranges cannot overlap.</p>
</div>
</article>
<article>
<code>as</code>
<div>
<h3>Verify that a value is more specific</h3>
<p>The left side must fit inside the wider range on the right. Wider abstract fields remain available for later materialization.</p>
</div>
</article>
<article>
<code>//</code>
<div>
<h3>Apply an explicit structural patch</h3>
<p>Right-biased composition replaces the selected structure while preserving unrelated fields.</p>
</div>
</article>
<article>
<code>default</code>
<div>
<h3>Choose a fallback only when materializing</h3>
<p>A default does not erase the range. It is selected only when no more concrete value has been supplied.</p>
</div>
</article>
</div>
</section>
<section class="home-section host-section" aria-labelledby="host-title">
<header class="section-heading">
<h2 id="host-title">The host owns I/O. Decodal owns evaluation.</h2>
<p>
Filesystems, Markdown, JSON, network resources, and application globals stay under host control. Decodal
receives source or structured values and evaluates them with the same rules in production and editor tooling.
</p>
</header>
<ol class="evaluation-flow" aria-label="Evaluation flow">
<li>
<span>1</span>
<strong>Host environment</strong>
<small>globals · source · structured imports</small>
</li>
<li>
<span>2</span>
<strong>Evaluate</strong>
<small>lazy references · constraints · composition</small>
</li>
<li>
<span>3</span>
<strong>Materialize</strong>
<small>defaults · validation · diagnostics</small>
</li>
<li>
<span>4</span>
<strong>Application data</strong>
<small>Data · Rust type · JavaScript value</small>
</li>
</ol>
<p class="section-link"><a href="/docs/embedding/">Read the embedding guide <span aria-hidden="true">→</span></a></p>
</section>
<section class="home-section package-section" aria-labelledby="packages-title">
<header class="section-heading">
<h2 id="packages-title">Use only the layer your application needs.</h2>
</header>
<div class="package-rows">
<article>
<div><code>decodal</code><span>Rust</span></div>
<p>Runtime, host embedding, schema derivation, and typed decoding.</p>
<a href="https://crates.io/crates/decodal">crates.io</a>
</article>
<article>
<div><code>decodal-wasm</code><span>JavaScript</span></div>
<p>Browser runtime and host-configurable semantic language service.</p>
<span class="package-links"><a href="https://jsr.io/@hare/decodal-wasm@0.3.0">JSR</a><a href="https://www.npmjs.com/package/decodal-wasm">npm</a></span>
</article>
<article>
<div><code>decodal-lsp</code><span>Rust</span></div>
<p>Language Server Protocol with application-defined environments.</p>
<a href="https://crates.io/crates/decodal-lsp">crates.io</a>
</article>
<article>
<div><code>decodal-codemirror</code><span>JavaScript</span></div>
<p>CodeMirror 6 syntax, indentation, folding, highlighting, and formatter integration.</p>
<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/">Compare packages and integrations <span aria-hidden="true">→</span></a></p>
</section>
</ManualLayout>
<HomePage locale="en" />
@@ -0,0 +1,6 @@
---
import HomePage from '../../components/HomePage.astro';
import '@fontsource/ibm-plex-sans-jp/400.css';
import '@fontsource/ibm-plex-sans-jp/600.css';
---
<HomePage locale="ja" />
@@ -1,5 +1,7 @@
---
import ManualLayout from '../layouts/ManualLayout.astro';
import '@fontsource/ibm-plex-sans-jp/latin-400.css';
import '@fontsource/ibm-plex-sans-jp/latin-600.css';
---
<ManualLayout title="Decodal Playground" playground>
<section class="playground-page">
+1 -1
View File
@@ -36,7 +36,7 @@ const editorTheme = EditorView.theme({
height: '100%',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontFamily: '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontSize: '13px',
lineHeight: '1.45',
overflow: 'auto',
+24 -4
View File
@@ -1,10 +1,10 @@
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-family: var(--font-ui);
font-synthesis: none;
--font-ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-code: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--font-ui: "IBM Plex Sans JP", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-code: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--canvas: #f6f7f8;
--surface: #ffffff;
@@ -524,6 +524,11 @@ main.frontpage {
max-width: 660px;
}
[lang="ja"] .home-intro h1 {
letter-spacing: -0.035em;
line-height: 1.15;
}
.home-lead {
color: var(--muted);
font-size: 1.05rem;
@@ -657,6 +662,21 @@ main.frontpage {
margin: 0;
}
[lang="ja"] .section-heading h2 {
letter-spacing: -0.025em;
line-height: 1.3;
}
[lang="ja"] .frontpage .phrased {
line-break: strict;
overflow-wrap: normal;
word-break: keep-all;
}
[lang="ja"] .frontpage .phrase-unit {
white-space: nowrap;
}
.section-heading > p:last-child {
color: var(--muted);
line-height: 1.75;
@@ -1230,7 +1250,7 @@ main.playground {
padding-right: 8px;
}
.topnav a:last-child {
.topnav .source-link {
display: none;
}