Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,19 @@ jobs:
if: github.event_name != 'pull_request'
uses: actions/configure-pages@v6

# The docs live under a language prefix : English under /en/, built from source/, and every
# other language of docs_languages (source/_config.yml) under its own code, built from
# source/ with its translated pages laid over it (scripts/build-language.sh). English comes
# first : the other languages reuse its heading anchors. The install script, the Helm
# repository and the 404 page stay at the root, served from the English build.
- name: Build with Jekyll
run: bundle exec jekyll build --source source --destination _site --baseurl "${{ steps.pages.outputs.base_path }}"
run: |
set -euo pipefail
scripts/build-language.sh en _site "${{ steps.pages.outputs.base_path }}"
for code in $(ruby -ryaml -e 'puts YAML.load_file("source/_config.yml")["docs_languages"].map { |l| l["code"] } - ["en"]'); do
scripts/build-language.sh "$code" _site "${{ steps.pages.outputs.base_path }}"
scripts/i18n-status.sh "$code"
done
env:
JEKYLL_ENV: production

Expand All @@ -101,6 +112,13 @@ jobs:
env:
JEKYLL_ENV: production

# Every page that used to live at the root (ansibleforms.com/seed) keeps its address,
# as a page that sends the browser on to /en/. Released apps link to these addresses.
- name: Redirect the old addresses to /en/
env:
SITE_URL: ${{ steps.pages.outputs.base_url || 'https://ansibleforms.com' }}
run: scripts/redirects.sh _site "${{ steps.pages.outputs.base_path }}"

# curl -fsSL https://ansibleforms.com/install | sh : source/install, served as it is. It
# must reach the site unchanged (no front matter, so Jekyll copies it) and be valid sh.
- name: The install script is served as it is
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ vendor/
_app/
_app6/
source/_data/help.yaml

# A language other than English is assembled here before it is built (scripts/build-language.sh)
_build/
36 changes: 35 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ lives in [ansibleforms/ansibleforms](https://github.com/ansibleforms/ansibleform
- [What lives where](#what-lives-where)
- [Branches and pull requests](#branches-and-pull-requests)
- [Previewing the site](#previewing-the-site)
- [Translations](#translations)
- [A change in the app that needs a page here](#a-change-in-the-app-that-needs-a-page-here)

---
Expand All @@ -24,6 +25,13 @@ The site is everything under `source/`. Most of it is written here, but two file
| `/helm-charts/` (the Helm chart repository) | the chart packages of the `ansibleforms/helm-charts` releases, indexed at build time | the `helm-charts` repository |
| everything else in `source/` | this repository | here |

The docs are served under a language prefix (`/en/` for English), and the language selector in the header lists
every language in `docs_languages` (`source/_config.yml`). Every page that used to live at the root keeps
its address as a redirect, written at build time by `scripts/redirects.sh`, to the reader's language: the
one they last picked, else their browser's, else English. The install script (`/install`) and the Helm chart
repository (`/helm-charts/`) stay at the root, without a redirect. The translations are described in
[Translations](#translations).

The 6.x pages under `source/v6/` are a frozen copy of the maintenance line's docs. Only correct them when
something there is plainly wrong for 6.x users.

Expand Down Expand Up @@ -53,7 +61,33 @@ bundle install
bundle exec jekyll serve --source source
```

Then open <http://localhost:4000>. `source/_data/help.yaml` is ignored by git, so the copy never ends up in a commit.
Then open <http://localhost:4000>. The preview serves the English docs at the root, without the `/en/` prefix the published site adds. `source/_data/help.yaml` is ignored by git, so the copy never ends up in a commit.

---

## Translations

English is written in `source/`, and every other language lives next to it, built over a copy of the English pages.

| File | What it holds |
|---|---|
| `i18n/<code>/<path>` | the translated page, at the same path as its English page in `source/` |
| `i18n/<code>.yml` | the language's settings: the menu group names, the site title, the version labels |
| `source/_data/ui.yml` | the site's own interface strings (table headings, previous and next, search) per language |
| `source/_includes/flag.html` | the flags, the same drawings as the app's language switcher |

A page that is not translated yet shows in English, so the site never has a gap. Keep the translated page's
headings in the same order as the English ones: the build gives them their English anchors back by position
(`scripts/i18n-anchors.py`), so links and the language selector land on the same section in every language.

Each translated page records which version of its English page it was translated from, in `translated_from:`.
When you change an English page, update its translations in the same pull request and stamp them again:

```bash
scripts/i18n-status.sh es --stamp _configure/runners/registration.md
```

`scripts/i18n-status.sh es` lists every page not translated or out of date. The build prints the same list as warnings.

---

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The site covers everything a user of AnsibleForms needs, from the first install
- The app's changelog, rebuilt into the site on every release
- The Helm chart repository, served under `/helm-charts/`
- The 6.x documentation, kept alongside under `/v6/`
- The docs under a language prefix (`/en/`), the old addresses redirecting to the reader's language

## Previewing locally

Expand Down
78 changes: 78 additions & 0 deletions scripts/build-language.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# =============================================================================
# Build the docs of one language into <site directory>/<code>/
# =============================================================================
#
# English is written in source/ and built from there. Every other language is
# built from a copy of source/ with its translated pages laid over it
# (i18n/<code>/, the same paths as in source/) and its settings read after the
# English ones (i18n/<code>.yml) : the layouts, includes, styles and images are
# shared, and a page that is not translated yet shows in English rather than
# going missing.
#
# The install script, the Helm repository page and the 404 page are served from
# the root of the site, never from a language : the English build hands them to
# the root, the other languages drop their copy.
#
# Spanish headings get their English anchors back afterwards (scripts/i18n-anchors.py),
# so a link to /es/page.html#some-section works like the English one.
#
# Usage:
# scripts/build-language.sh <code> <site directory> [<base path>]
#
# Args:
# code : the language, as listed in docs_languages (source/_config.yml)
# site directory : where the whole site is assembled, e.g. _site
# base path : the path the site is served under, empty for the custom domain
#
# Example:
# scripts/build-language.sh es _site ""
# =============================================================================

set -euo pipefail

code="${1:?usage: scripts/build-language.sh <code> <site directory> [<base path>]}"
site="${2:?usage: scripts/build-language.sh <code> <site directory> [<base path>]}"
base="${3:-}"
out="$site/$code"

# -----------------------------------------------------------------------------
# English : straight from source/
# -----------------------------------------------------------------------------

if [ "$code" = "en" ]; then
bundle exec jekyll build --source source --destination "$out" --baseurl "$base/en"
# Step 1 : the pages served from the root move there
mv "$out/install" "$out/helm-charts" "$out/404.html" "$site/"
exit 0
fi

# -----------------------------------------------------------------------------
# Another language : source/ with i18n/<code>/ laid over it
# -----------------------------------------------------------------------------

[ -f "i18n/$code.yml" ] || { echo "::error::i18n/$code.yml does not exist : no settings for language $code."; exit 1; }
[ -d "$site/en" ] || { echo "::error::$site/en does not exist : build English first, its anchors are reused."; exit 1; }

# Step 1 : a copy of the English source, without the 6.x docs, with the translations on top
work="_build/$code"
rm -rf "$work"
mkdir -p "$work"
cp -a source/. "$work/"
rm -rf "$work/v6" "$work/.jekyll-cache"
if [ -d "i18n/$code" ]; then
cp -a "i18n/$code/." "$work/"
fi

# Step 2 : the pages not translated yet stay under their translated parent in the menu
python3 scripts/i18n-fallback.py "$work" "$code"

# Step 3 : the build, with the language's settings over the English ones
bundle exec jekyll build --source "$work" --config "source/_config.yml,i18n/$code.yml" \
--destination "$out" --baseurl "$base/$code"

# Step 4 : the pages served from the root are the English build's
rm -rf "$out/install" "$out/helm-charts" "$out/404.html"

# Step 5 : the English anchors on the translated headings
python3 scripts/i18n-anchors.py "$site" "$code"
129 changes: 129 additions & 0 deletions scripts/i18n-anchors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# =============================================================================
# Give the headings of a translated build their English anchors back
# =============================================================================
#
# Kramdown builds a heading's anchor from its text, so a translated heading gets
# a translated anchor : "## Tokens de unión" becomes #tokens-de-unión. The
# translated pages keep their links in English (../runners/registration.html#join-tokens),
# and switching language should keep the reader on the same section, so every
# translated heading takes the anchor of the English heading it translates.
#
# A translated page has the same headings as its English page, in the same order,
# so the two are paired by position. A page whose heading count differs is left
# as it is and reported : it is out of step with its English page.
#
# Rewritten, in the built site of the language :
# - the id of each heading, and every same-page link to it (the table of
# contents, the anchor links next to the headings, links written in the page) ;
# - the section urls of the search index (assets/js/search-data.json).
#
# Usage:
# python3 scripts/i18n-anchors.py <site directory> <code>
# =============================================================================

import json
import re
import sys
from pathlib import Path

# A heading with an id, as kramdown writes it : <h2 id="join-tokens"> (other attributes may come first)
HEADING_ID = re.compile(r'<h([1-6])\b([^>]*?)\bid="([^"]*)"')


def heading_ids(html):
"""Return the ids of the headings of a page, in document order.

Args:
html: the page's HTML.

Returns:
The list of heading ids.
"""
return [m.group(3) for m in HEADING_ID.finditer(html)]


def page_url(rel, code):
"""Return the url a page is served at, as the search index writes it.

Args:
rel: the page's path inside the language's build, e.g. runners/index.html.
code: the language code.

Returns:
The url, e.g. /es/runners/.
"""
url = f"/{code}/{rel}"
return url[: -len("index.html")] if url.endswith("/index.html") else url


def main():
"""Rewrite the anchors of one language's build.

Returns:
The process exit code.
"""
if len(sys.argv) != 3:
print("usage: python3 scripts/i18n-anchors.py <site directory> <code>", file=sys.stderr)
return 2
site, code = Path(sys.argv[1]), sys.argv[2]
en_root, lang_root = site / "en", site / code

# -------------------------------------------------------------------------
# Step 1 : the pages, heading by heading
# -------------------------------------------------------------------------
by_url = {} # page url -> {translated anchor: English anchor}
pages = 0
out_of_step = []
for page in sorted(lang_root.rglob("*.html")):
rel = page.relative_to(lang_root).as_posix()
english = en_root / rel
if not english.is_file():
continue
html = page.read_text(encoding="utf-8")
ours, theirs = heading_ids(html), heading_ids(english.read_text(encoding="utf-8"))
if len(ours) != len(theirs):
out_of_step.append(f"{rel} ({len(ours)} headings, English has {len(theirs)})")
continue
mapping = {o: t for o, t in zip(ours, theirs) if o != t}
if not mapping:
continue

# one pass each, so an anchor renamed to another's old name is never renamed twice
html = HEADING_ID.sub(
lambda m: f'<h{m.group(1)}{m.group(2)}id="{mapping.get(m.group(3), m.group(3))}"', html
)
html = re.sub(r'href="#([^"]*)"', lambda m: f'href="#{mapping.get(m.group(1), m.group(1))}"', html)
page.write_text(html, encoding="utf-8")
by_url[page_url(rel, code)] = mapping
pages += 1

# -------------------------------------------------------------------------
# Step 2 : the search index, which links to each section
# -------------------------------------------------------------------------
index = lang_root / "assets/js/search-data.json"
if index.is_file():
data = json.loads(index.read_text(encoding="utf-8"))
for entry in data.values():
for key in ("url", "relUrl"):
url = entry.get(key, "")
if "#" not in url:
continue
path, anchor = url.split("#", 1)
full = path if key == "url" else f"/{code}{path}"
mapping = by_url.get(full)
if mapping and anchor in mapping:
entry[key] = f"{path}#{mapping[anchor]}"
index.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

# -------------------------------------------------------------------------
# Step 3 : the report
# -------------------------------------------------------------------------
print(f"{code}: English anchors restored on {pages} pages")
for line in out_of_step:
print(f"::warning::{code}: {line} : the anchors were left as they are")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading