diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8356fe1c..7509b18b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,18 @@ name: CI on: + schedule: # Build and deploy multilingual books weekly + - cron: '0 0 * * 1' push: # Run CI for all branches except GitHub merge queue tmp branches branches-ignore: - "gh-readonly-queue/**" pull_request: # Run CI for PRs on any branch merge_group: # Run CI for the GitHub merge queue +env: + MDBOOK_VERSION: 0.5.1 + MDBOOK_I18N_HELPERS_VERSION: 0.4.0 + jobs: build: runs-on: ubuntu-24.04 @@ -17,38 +23,68 @@ jobs: - name: Install Python dependencies run: | pip3 install --user python-dateutil linkchecker + - name: Put pip binary directory into path run: echo "~/.local/bin" >> $GITHUB_PATH - - name: Cache Cargo installed binaries - uses: actions/cache@v4 - id: cache-cargo - with: - path: ~/cargo-bin - key: cache-cargo + - name: Update rustup + run: rustup self update + + - name: Install Rust + run: | + rustup set profile minimal + rustup toolchain install nightly -c rust-docs + rustup default nightly + - name: Install mdbook - if: steps.cache-cargo.outputs.cache-hit != 'true' - uses: actions-rs/install@v0.1 - with: - crate: mdbook - version: latest - - name: Copy mdbook to cache directory - if: steps.cache-cargo.outputs.cache-hit != 'true' run: | - mkdir ~/cargo-bin - cp ~/.cargo/bin/mdbook ~/cargo-bin - - name: Put new cargo binary directory into path - run: echo "~/cargo-bin" >> $GITHUB_PATH + cargo install mdbook --locked --version ${{ env.MDBOOK_VERSION }} + + - name: Install mdbook-i18n-helpers + run: | + cargo install mdbook-i18n-helpers --locked --version ${{ env.MDBOOK_I18N_HELPERS_VERSION }} + + - name: Download the .po files from localizethedocs/rust-embedded-book-l10n + run: | + git clone --branch po/master https://github.com/localizethedocs/rust-embedded-book-l10n.git po + + - name: Build books + run: | + for langcode in $(jq -r '.languages[].code' switcher.json); do + echo "::group::Building $langcode" + langtag=$(jq -r --arg lang "$langcode" '.languages[] | select(.code == $lang) | .langtag' switcher.json) + MDBOOK_BOOK__LANGUAGE=$langcode mdbook build -d book/$langtag/ + echo "::endgroup::" + done - - name: Build book - run: mdbook build - name: Test book run: mdbook test + - name: Check links run: linkchecker book + - name: Create root index.html redirect + run: | + default_langtag=$(jq -r '.languages[] | select(.code == "en_US") | .langtag' switcher.json) + target="./${default_langtag}/" + cat > book/index.html << EOF + + +
+ + + +Redirecting to ${target}
+ + + EOF + + - name: Copy switcher.json to book root + run: cp switcher.json book/ + - name: Deploy book - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || + ( github.event_name == 'schedule' ) }} uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/book.toml b/book.toml index 5e31b0c1..38e68a80 100644 --- a/book.toml +++ b/book.toml @@ -5,3 +5,13 @@ title = "The Embedded Rust Book" [output.html] git-repository-url = "https://github.com/rust-embedded/book" +additional-css = [ + "theme/css/switcher.css", +] +additional-js = [ + "theme/js/switcher.js", +] + +[preprocessor.gettext] +optional = true +after = ["links"] diff --git a/switcher.json b/switcher.json new file mode 100644 index 00000000..f7ca9bac --- /dev/null +++ b/switcher.json @@ -0,0 +1,7 @@ +{ + "languages": [ + { "code": "en_US", "langtag": "en-us", "langname": "English" }, + { "code": "zh_CN", "langtag": "zh-cn", "langname": "简体中文" }, + { "code": "zh_TW", "langtag": "zh-tw", "langname": "繁體中文" } + ] +} diff --git a/theme/css/switcher.css b/theme/css/switcher.css new file mode 100644 index 00000000..2e476827 --- /dev/null +++ b/theme/css/switcher.css @@ -0,0 +1,50 @@ +/* Ensure right-buttons uses flex alignment (mirrors cairo-book chrome.css) */ +.right-buttons { + display: flex; + align-items: center; + margin: 0 15px; +} + +.right-buttons a { + text-decoration: none; +} + +/* ---- Shared switcher styles ---- */ + +.language-switcher, +.version-switcher { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: 10px; +} + +/* Deterministic order: both switchers always leftmost, language before version */ +.language-switcher { order: -2; } +.version-switcher { order: -1; } + +.language-switcher label, +.version-switcher label { + display: flex; + align-items: center; + font-size: 1.6rem; + color: var(--icons); + cursor: pointer; +} + +.language-switcher select, +.version-switcher select { + padding: 0.25rem 0.5rem; + border-radius: 4px; + border: 1px solid var(--icons); + background: var(--bg); + color: var(--icons); + font-size: 1.6rem; + cursor: pointer; +} + +.language-switcher select:hover, +.version-switcher select:hover { + border-color: var(--icons-hover); + color: var(--icons-hover); +} diff --git a/theme/js/switcher.js b/theme/js/switcher.js new file mode 100644 index 00000000..b8e5ba56 --- /dev/null +++ b/theme/js/switcher.js @@ -0,0 +1,129 @@ +(() => { + const fullPath = window.location.pathname; + + // Derive siteRoot from a langtag segment (e.g. /xx-xx/) in the URL. + function deriveSiteRoot() { + const m = fullPath.match(/^(.+?)\/([a-z]{2}-[a-z]{2})\//); + return m ? m[1] : ""; + } + + const siteRoot = deriveSiteRoot(); + + function loadConfig() { + return fetch(siteRoot + "/switcher.json") + .then((r) => { if (!r.ok) throw new Error("not found"); return r.json(); }) + .catch(() => null); + } + + function whenReady(fn) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", fn); + } else { + fn(); + } + } + + whenReady(() => { + loadConfig().then((data) => { + if (!data) return; + + const afterSite = fullPath.substring(siteRoot.length); + const parts = afterSite.split("/").filter(Boolean); + + if (data.languages) renderLanguageSwitcher(data.languages, parts); + if (data.versions) renderVersionSwitcher(data.versions, parts); + }); + }); + + // ---- Language switcher ---- + + function renderLanguageSwitcher(languages, parts) { + const knownLangtags = new Set(languages.map((l) => l.langtag)); + + let currentLangtag = ""; + let langtagIdx = -1; + for (let i = 0; i < parts.length; i++) { + if (knownLangtags.has(parts[i])) { + langtagIdx = i; + currentLangtag = parts[i]; + break; + } + } + if (langtagIdx < 0) return; + + const tail = "/" + parts.slice(langtagIdx + 1).join("/"); + + const container = document.createElement("div"); + container.className = "language-switcher"; + container.innerHTML = + '' + + ''; + + insert(container); + + document.getElementById("language-select").addEventListener("change", (e) => { + window.location.href = siteRoot + "/" + e.target.value + tail; + }); + } + + // ---- Version switcher ---- + + function renderVersionSwitcher(versions, parts) { + const knownVersions = new Set(versions.map((v) => v.version)); + const defaultVersion = versions[0].version; + + let currentVersion = defaultVersion; + let versionIdx = -1; + for (let i = 0; i < parts.length; i++) { + if (knownVersions.has(parts[i])) { + versionIdx = i; + currentVersion = parts[i]; + break; + } + } + if (versionIdx < 0) return; + + const prefix = "/" + parts.slice(0, versionIdx).join("/"); + const pagePath = "/" + parts.slice(versionIdx + 1).join("/"); + + const container = document.createElement("div"); + container.className = "version-switcher"; + container.innerHTML = + '' + + ''; + + insert(container); + + document.getElementById("version-select").addEventListener("change", (e) => { + const newVersion = e.target.value; + window.location.href = siteRoot + prefix + "/" + newVersion + pagePath; + }); + } + + function insert(container) { + const menuBar = document.querySelector(".right-buttons"); + if (menuBar) { + menuBar.insertBefore(container, menuBar.firstChild); + } + } +})();