Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 3 additions & 1 deletion .github/workflows/deploy-gh-pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- "master"
workflow_dispatch: {}

permissions:
contents: write
Expand All @@ -22,7 +23,8 @@ jobs:
uses: peaceiris/actions-mdbook@v2.0.0
with:
mdbook-version: "0.5.4"
- run: mdbook build
- run: cargo install mdbook-pdf --version 0.1.13 --locked
- run: node i18n/build.js

- name: Deploy
uses: peaceiris/actions-gh-pages@v4.0.0
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/deploy-release-pdf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ jobs:
with:
mdbook-version: "0.5.4"
- run: cargo install mdbook-pdf --version 0.1.13 --locked
- run: mdbook build
- run: node i18n/build.js

- name: Rename PDF
run: mv book/pdf/output.pdf book/pdf/f4e_manual.pdf

- name: Release PDF
- name: Release PDFs
uses: ncipollo/release-action@v1.14.0
with:
artifactErrorsFailBuild: true
artifacts: "book/pdf/f4e_manual.pdf"
artifacts: "book/pdf/*.pdf"
artifactContentType: "application/pdf"
generateReleaseNotes: true
omitBody: true
omitName: true
allowUpdates: true
replacesArtifacts: true
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- markdown-link-check-disable-next-line -->
[![license](https://img.shields.io/badge/license-CC_BY--NC--ND_4.0-blue)](https://github.com/Heatblur-Simulations/f-4e-manual/blob/master/LICENSE)

![Phantom from back](src/img/phantom_flying_back.jpg)
![Phantom from back](src/en/img/phantom_flying_back.jpg)

Manual of the F-4E Phantom by Heatblur Simulations.
This is a [mdBook](https://rust-lang.github.io/mdBook/) project, content is
Expand Down Expand Up @@ -52,6 +52,13 @@ To build the PDF locally, first run
`cargo install mdbook-pdf --version 0.1.13 --locked`. Then, when running
`mdbook build`, the PDF will be created in `book/pdf`.

### Building every language

The commands above only build `src/en`. To build every language in one go
(one HTML site and one PDF per language) run `npm run build`, or
`npm run serve` to build and preview the result at
[localhost:3000](http://localhost:3000/) — see [i18n/README.md](i18n/README.md).

## GH Pages

The project is automatically configured via a CI/CD to build and publish any
Expand Down
4 changes: 3 additions & 1 deletion book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title = "Heatblur F-4E Phantom II"
description = "Manual of the F-4E Phantom by Heatblur Simulations."
authors = ["Heatblur Simulations", "Contributions from the Community"]
language = "en"
src = "src/en"

[output.html]
git-repository-url = "https://github.com/Heatblur-Simulations/f-4e-manual"
Expand All @@ -17,7 +18,8 @@ additional-css = [
]
additional-js = [
'js/jquery-3.7.1.min.js',
'js/extra.js'
'js/extra.js',
'js/i18n/switcher.js'
]
mathjax-support = false
smart-punctuation = false
Expand Down
61 changes: 61 additions & 0 deletions i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# i18n

## Launching it

```

Check failure on line 5 in i18n/README.md

View workflow job for this annotation

GitHub Actions / Markdown Linter

Fenced code blocks should have a language specified [Context: "```"]
npm run build # -> book/html/en, book/html/fr, book/pdf/*.pdf
npm run serve # builds, then serves book/html on http://localhost:3000
```

(`npm run build` is `node i18n/build.js`, `npm run serve` is `node i18n/serve.js` —
no npm dependencies are involved, `npm` is only used to remember the commands.)

Do **not** use plain `mdbook serve` to test the language switcher or the
landing page: it only serves a single language directly at the site root
(no `/en/`, `/fr/`, ... siblings), so switching language 404s. `mdbook
serve`/`mdbook build` are still fine for iterating on the English text
itself (book.toml defaults `src` to `src/en`).

## How it works

- `src/<code>/` holds a full copy of the book for language `<code>` (e.g. `src/en`,
`src/fr`). Files must keep the exact same relative paths across languages —
the language switcher swaps the `/en/`/`/fr/` prefix in the URL and expects
the rest of the path to match.
- Translations don't need to be 1:1 with the English source. Any file missing
under `src/<code>/` (a page that hasn't been translated yet, or an image
with on-image text that hasn't been redone) is filled in from the default
language for the duration of that language's build and removed again
straight after — mdbook only bundles what lives inside the src directory
it's building, and needs every page listed in `SUMMARY.md` to actually
exist. The result: an untranslated page still builds and shows the English
content instead of breaking the build. To ship a real translation of a page
or image, just commit a real file at the same relative path under
`src/<code>/` — the build never overwrites or removes anything that's
actually there.
- On top of that, the language switcher (`js/i18n/switcher.js`) checks the
target page actually exists before navigating, and falls back to that
language's home page instead of a 404 in the rare case a language's own
`SUMMARY.md` doesn't reference the equivalent path at all.
- `i18n/languages.json` lists the languages to build.
- `i18n/build.js` runs a normal `mdbook build` once per language (overriding
`book.src`, `book.language` and `book.title` via env vars), collects each
into `book/html/<code>` and `book/pdf/<pdfName>`, then writes
`book/html/index.html` from `i18n/index.template.html` — a small language
picker that also auto-redirects returning visitors to their last choice.
- `i18n/serve.js` runs the build above and serves `book/html` with a plain
Node http server, so the multi-language layout can be clicked through
locally exactly as it will be deployed.
- The in-page language dropdown (`js/i18n/switcher.js`, menu bar, next to the
theme toggle) is wired up from the `<li data-lang="...">` entries in
`theme/index.hbs`.

## Adding a language

1. Add an entry to `i18n/languages.json`.
2. Copy `src/en` (without `img/`) to `src/<code>` and translate it
(`SUMMARY.md` included). Keep every image reference's `../` depth
unchanged — it must still count the file's own depth under `src/<code>`.
3. Add a matching `<li role="none"><a role="menuitem" class="theme" data-lang="<code>">...</a></li>`
entry to the language list in `theme/index.hbs`.
4. Run `npm run serve` and click through `book/html/<code>`.
102 changes: 102 additions & 0 deletions i18n/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env node
// Builds every language listed in i18n/languages.json with a single command.
// Each language is a normal mdbook build (src/<code> -> book/html/<code>,
// book/pdf/output.pdf), pointed at via MDBOOK_* env overrides so book.toml
// stays untouched. See i18n/README.md for how this fits together.

const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const root = path.resolve(__dirname, '..');
const languages = JSON.parse(fs.readFileSync(path.join(__dirname, 'languages.json'), 'utf8'));

const bookOut = path.join(root, 'book');
const htmlOut = path.join(bookOut, 'html');
const pdfOut = path.join(bookOut, 'pdf');
fs.rmSync(bookOut, { recursive: true, force: true });
fs.mkdirSync(htmlOut, { recursive: true });
fs.mkdirSync(pdfOut, { recursive: true });

// Translations don't have to be complete: any page (.md) or image missing
// under src/<lang>/ is filled in from the default language for the duration
// of that language's build and removed again straight after, so a
// not-yet-translated page still builds and shows the English content instead
// of breaking the build or 404ing. Anything actually committed under
// src/<lang>/ (a real translation, or a translated image with on-image text)
// is never touched.
const defaultLang = languages.find((l) => l.default) || languages[0];
const sharedSrc = path.join(root, 'src', defaultLang.code);

function listFiles(dir, base = dir, out = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) listFiles(full, base, out);
else out.push(path.relative(base, full));
}
return out;
}

function removeEmptyDirs(dir) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) removeEmptyDirs(path.join(dir, entry.name));
}
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
}

for (const lang of languages) {
console.log(`\n==> Building ${lang.code} (${lang.native})`);

const langSrc = path.join(root, 'src', lang.code);
const isDefault = lang.code === defaultLang.code;
const filledIn = [];
if (!isDefault) {
for (const rel of listFiles(sharedSrc)) {
const dest = path.join(langSrc, rel);
if (fs.existsSync(dest)) continue; // real translation/override, keep it
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(path.join(sharedSrc, rel), dest);
filledIn.push(dest);
}
}

const langBuildDir = `book/.${lang.code}`;
const env = {
...process.env,
MDBOOK_BOOK__SRC: `src/${lang.code}`,
MDBOOK_BOOK__LANGUAGE: lang.mdbookLang,
MDBOOK_BOOK__TITLE: lang.title,
};

try {
execFileSync('mdbook', ['build', '--dest-dir', langBuildDir], { cwd: root, env, stdio: 'inherit' });
} finally {
for (const file of filledIn) fs.rmSync(file, { force: true });
if (!isDefault) removeEmptyDirs(langSrc);
}

fs.renameSync(path.join(root, langBuildDir, 'html'), path.join(htmlOut, lang.code));

const builtPdf = path.join(root, langBuildDir, 'pdf', 'output.pdf');
if (fs.existsSync(builtPdf)) {
fs.renameSync(builtPdf, path.join(pdfOut, lang.pdfName));
}
fs.rmSync(path.join(root, langBuildDir), { recursive: true, force: true });
}

const chooserTemplate = fs.readFileSync(path.join(__dirname, 'index.template.html'), 'utf8');
const chooser = chooserTemplate
.replace('__BOOK_TITLE__', languages[0].title)
.replace('__DEFAULT_LANG__', defaultLang.code)
.replace(
'__LANGUAGE_LINKS__',
languages
.map((l) => `<a class="lang-option" href="./${l.code}/" data-lang="${l.code}">${l.native}</a>`)
.join('\n ')
)
.replace('__LANGUAGE_JSON__', JSON.stringify(languages.map((l) => l.code)));

fs.writeFileSync(path.join(htmlOut, 'index.html'), chooser);

console.log('\ni18n build complete.');
68 changes: 68 additions & 0 deletions i18n/index.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>__BOOK_TITLE__</title>
<style>
:root { --bg: #ffffff; --fg: #404040; --card-bg: #e6e9ef; --card-border: #9ca0b0; --accent: #1e66f5; }
@media (prefers-color-scheme: dark) {
:root { --bg: #1d1f21; --fg: #d9d9d9; --card-bg: #282c2f; --card-border: #4c4f51; --accent: #58a6ff; }
}
html, body { height: 100%; margin: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
main { text-align: center; padding: 2rem; }
h1 { font-weight: 600; margin-bottom: 1.5rem; }
.lang-options { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.lang-option {
display: block;
min-width: 10rem;
padding: 0.9rem 1.5rem;
border-radius: 0.4rem;
border: 1px solid var(--card-border);
background: var(--card-bg);
color: var(--fg);
text-decoration: none;
font-size: 1.05rem;
transition: border-color 0.15s ease;
}
.lang-option:hover { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<main>
<h1>__BOOK_TITLE__</h1>
<div class="lang-options">
__LANGUAGE_LINKS__
</div>
</main>
<script>
// Redirect straight through if a language was already picked, otherwise
// suggest (never force) the browser's language.
(function () {
var available = __LANGUAGE_JSON__;
var saved = localStorage.getItem('hb-manual-lang');
if (saved && available.indexOf(saved) !== -1) {
location.replace('./' + saved + '/');
return;
}
var browserLang = (navigator.language || '__DEFAULT_LANG__').slice(0, 2);
if (available.indexOf(browserLang) !== -1) {
location.replace('./' + browserLang + '/');
}
})();
document.querySelectorAll('.lang-option').forEach(function (el) {
el.addEventListener('click', function () {
localStorage.setItem('hb-manual-lang', el.dataset.lang);
});
});
</script>
</body>
</html>
20 changes: 20 additions & 0 deletions i18n/languages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"code": "en",
"mdbookLang": "en",
"name": "English",
"native": "English",
"title": "Heatblur F-4E Phantom II",
"pdfName": "f4e_manual_en.pdf",
"default": true
},
{
"code": "fr",
"mdbookLang": "fr",
"name": "French",
"native": "Français",
"title": "Heatblur F-4E Phantom II",
"pdfName": "f4e_manual_fr.pdf",
"default": false
}
]
Loading
Loading