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
157 changes: 157 additions & 0 deletions .claude/skills/translate-page/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
name: translate-page
description: Translate a documentation page from docs/en/ into another language and stamp it. Use when asked to translate a page, add a language, or bring a stale translation up to date in this repo.
---

# Translate a documentation page

Translation here is a repeatable operation, not an ad-hoc prompt. The rules below
exist because each of them was broken once and cost real work.

## Inputs

- A page path under `docs/en/`, or a page reported by
`uv run python scripts/translation_status.py` as `missing` or `stale`.
- A target language directory, e.g. `docs/fi/`.

## Before translating

1. **Read the glossary for the target language** —
`solutions/translation/finnish-glossary.md` for Finnish, and its equivalent
for other languages. It fixes terminology, unit formatting, address form and
what stays in English. Follow it exactly.
2. If the page introduces a term the glossary does not cover, **add it to the
glossary** in the same change. Do not invent a one-off translation: the whole
point is that the same English term reads the same way on every page.
3. If the page is `stale` rather than `missing`, read the English diff the
status report prints. Translate the change, not the whole page.

## Translating

The translation lives at the mirrored path — `docs/en/hardware/index.md`
becomes `docs/fi/hardware/index.md`. Only markdown goes under the language
directory; images stay with the English source and are shared.

**Preserve structure exactly.** Same headings, list items, numbered steps,
images, admonitions, table rows, footnotes and code fences, in the same order.

**Never touch:**

- Code fences and their contents, including comments inside them
- Inline code: commands, file paths, hostnames, config keys
- UI strings the reader will see on their own screen in English
- Product, protocol and hardware names
- Image filenames and paths

**Always convert:** units to SI spacing and decimal comma (`0.9A` → `0,9 A`,
`5.5 x 2.1 mm` → `5,5 × 2,1 mm`). This is not optional formatting; it is the
correct way to write the value.

**Two markdown traps** that neither `--strict` nor GitHub's preview catches —
both are documented in `solutions/best-practices/`:

- A blank line before the first item of a list
- Four spaces, not three, for a sub-list under a numbered step

**Never write an `en/` or `fi/` segment into a path inside a page.** The
language comes from which directory the file lives in.

## Anchors

Anchors derive from heading text, so translating a heading changes its slug.
Slugs strip diacritics and lowercase: `Mikä HALMET on?` → `mika-halmet-on`.

Two distinct jobs:

1. **Inside the page you are translating** — rewrite every `](#…)` to the
translated heading's slug.
2. **In pages you are not touching** — a link like
`](./operation.md#status-led-indicators)` in an already-translated page keeps
working until `operation.md` is translated, and breaks the moment it is. This
is a delayed fault. After translating, run the anchor check across the whole
built site, not just your page.

Do not guess slugs. Build, then read the real ids out of the generated HTML.

## Stamping

The stamp records the git blob hash of the English source the translation was
written against. Write it with the helper, never by hand:

```bash
uv run python scripts/stamp_translation.py docs/fi/hardware/index.md
```

**Stamp only when you have actually translated.** A stamp updated without real
translation work reports green and makes the staleness invisible — that is the
one failure the status check cannot detect, and this skill is where the
discipline lives. If you touched only the target language (fixing wording,
fixing a typo), the English source did not change: leave the stamp alone.

## Verifying

All four, every time:

```bash
uv run mkdocs build --strict
uv run python scripts/check_anchors.py site
uv run python scripts/translation_status.py
uv run python scripts/check_glossary.py fi
uv run python scripts/check_typography.py fi
```

**Leave every anchor fragment in its English form while translating**, then map
them all at once once the language is complete and the site has been built:

```bash
uv run python scripts/map_anchors.py site fi # report
uv run python scripts/map_anchors.py site fi --apply # rewrite
```

The mapping is positional — the nth heading of the English page and the nth
heading of the translation are the same heading — which is why the structure
comparison below has to pass first. Matching on heading text cannot work once
the text is in another language.

**Measure the glossary, do not reread it.** Rereading your own pages confirms
whatever they already say, so the terminology looks consistent right up until a
reviewer finds the same connector under two names on adjacent pages. Every
language so far shipped that mistake, and each time it landed on the last pages
translated, once the glossary had stopped being opened. `check_glossary.py`
reports terms the glossary prescribes and the pages never use — the signature of
a rival word having quietly taken over.

The same applies to whatever typography rules the glossary sets. Test them
against the text: count the quotation marks and check they pair, count the
spaces before `;:!?`, count the address form. A rule that was read looks
followed.

and a structure comparison against the source:

```bash
python3 - <<'PY'
import re
en = 'docs/en/hardware/index.md'; fi = 'docs/fi/hardware/index.md'
def stats(p):
t = re.sub(r'^---\n.*?\n---\n', '', open(p, encoding='utf-8').read(), flags=re.S)
return {k: len(re.findall(v, t, re.M)) for k, v in {
'headings': r'^#{1,6} ', 'bullets': r'^\s*[-*] ', 'numbered': r'^\s*\d+\. ',
'images': r'!\[', 'admonitions': r'^!!! ', 'table rows': r'^\|',
'fences': r'^```'}.items()}
a, b = stats(en), stats(fi)
print(a); print(b); print('match' if a == b else 'MISMATCH')
PY
```

A mismatch means content was dropped or merged. Find it before committing.

Finally, confirm no numeric value drifted: every number in the English text
should appear in the translation, unless it was deliberately spelled out as a
word. A wrong voltage or current in an installation guide is a safety problem,
not a typo.

## Committing

One commit per logical group of pages. If pages cross-link each other, translate
and commit them together — otherwise the intermediate commit has links pointing
at headings that do not exist yet.
95 changes: 95 additions & 0 deletions .github/workflows/translation-status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Translation Status

on:
pull_request:
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'scripts/**'
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pull-requests: write

concurrency:
group: translation-status-${{ github.ref }}
cancel-in-progress: true

jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history: the report resolves the stamped blob to show the
# English diff since a translation was written.
fetch-depth: 0

- uses: astral-sh/setup-uv@v5
- run: uv sync

- name: Report translation status
run: |
# tee, not plain redirection: a report only in the job summary is
# invisible in the logs, which is where you look when it misbehaves.
uv run python scripts/translation_status.py --format markdown --diff \
| tee report.md
cat report.md >> "$GITHUB_STEP_SUMMARY"

- name: Comment on the pull request
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.number }}
run: |
# Only the English pages this PR actually touches. Which paths a PR
# touched is a fact, so a PR editing only translations says nothing.
pages=$(git diff --name-only \
"origin/${{ github.base_ref }}...HEAD" -- 'docs/en/**/*.md' \
| sed 's|^docs/en/||')
if [ -z "$pages" ]; then
echo "No English pages touched; nothing to report."
exit 0
fi

# shellcheck disable=SC2086
uv run python scripts/translation_status.py \
--format markdown --diff --only-pages $pages > comment.md
printf '\n<!-- translation-status -->\n' >> comment.md

existing=$(gh api "repos/${{ github.repository }}/issues/$PR/comments" \
--jq 'map(select(.body | contains("<!-- translation-status -->"))) | .[0].id // empty')
if [ -n "$existing" ]; then
gh api "repos/${{ github.repository }}/issues/comments/$existing" \
-X PATCH -F body=@comment.md --silent
echo "Updated comment $existing"
else
gh api "repos/${{ github.repository }}/issues/$PR/comments" \
-F body=@comment.md --silent
echo "Created comment"
fi

# Last, because unlike a stale translation a broken anchor is actual
# breakage and fails the run — and the report above must still be
# published when it does.
- name: Check anchors
run: |
uv run mkdocs build --strict
# PIPESTATUS, not $?: piping into tee would otherwise mask the
# checker's exit status behind tee's.
set +e
uv run python scripts/check_anchors.py site | tee anchors.txt
broken=${PIPESTATUS[0]}
set -e
{
echo ""
echo "## Anchor check"
echo ""
echo '```'
cat anchors.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$broken"
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes
15 changes: 15 additions & 0 deletions docs/fi/errata/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Tunnetut virheet
translated_from: 5ef927a8a1dd611d3215899a52dd6b3bfca86859
---

# Tunnetut virheet

Tällä sivulla luetellaan kaikki tunnetut laitteistoviat HALMETin eri versioissa.

## Versio 1.0.0

Digitaalitulot 3 ja 4 ovat vaihtaneet paikkaa kortin takapuolen silkkipainossa. Oikea nastajärjestys on:

- DI3: GPIO 27
- DI4: GPIO 26
73 changes: 73 additions & 0 deletions docs/fi/getting-started/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Aloitusopas
translated_from: 75bcdba18bc044c04ce3e220067bf537e069ec82
---

# Aloitusopas

## Kortin kokoaminen

Jotta liittimet voidaan sijoittaa joustavammin pieniin koteloihin, HALMET-kortit toimitetaan ilman 1-Wire- ja GPIO-liittimiä. Jos aiot käyttää kumpaakaan näistä liitännöistä, sinun on juotettava liitin kiinni korttiin.

Jos tarvitset ohjeita nastarimojen juottamiseen, katso SH-ESP32:n [kokoamisohjeet](https://docs.hatlabs.fi/sh-esp32/pages/getting-started/#revision-1-boards).

## Kortin virransyöttö

HALMET saa käyttöjännitteensä NMEA 2000 -liittimen kautta. Jos aiot liittää HALMETin NMEA 2000 -verkkoon, kortin voi syöttää suoraan verkosta. Kytke silloin NMEA 2000 -johtimet 4-napaiseen irrotettavaan riviliittimeen alla olevan kuvan mukaisesti.

<figure markdown="span">
![](halmet_n2k_input.jpg){ width="50%" }
<figcaption>Kytke NMEA 2000 -johtimet liittimeen kuvan mukaisesti.</figcaption>
</figure>

Jos et aio liittää HALMETia NMEA 2000 -verkkoon, käytä samaa liitintä mutta kytke johtimet vain `-`- ja `+`-paikkoihin. Käyttöjännitteen voi ottaa mistä tahansa 5–32 V:n lähteestä. Kortin tyypillinen virrankulutus WiFin ollessa käytössä on 0,07 A 12 V:n jännitteellä.

<figure markdown="span">
![](power_connector.jpg){ width="50%" }
<figcaption>Kytke käyttöjännitejohtimet liittimeen kuvan mukaisesti.</figcaption>
</figure>

## Kotelot

Veneessä HALMET on aina sijoitettava vesitiiviiseen koteloon.
Kortti on suunniteltu sopimaan [SH-ESP32-koteloon](https://shop.hatlabs.fi/products/sh-esp32-enclosure). Alla on esimerkki koteloon asennetusta HALMET-kortista.

<figure markdown="span">
![](halmet_small_enclosure.jpg){ width="50%" }
<figcaption>HALMET asennettuna SH-ESP32-koteloon.</figcaption>
</figure>

SH-ESP32-kotelossa on rajallisesti tilaa liittimille.
Kummallekin pitkälle sivulle mahtuu käytännössä vain 2–3 paneeliliitintä.
Jos aiot kytkeä useampia kuin muutaman tulon, suositellaan suurempaa koteloa.
Esimerkiksi alla näkyvässä Hat Labsin [kompaktissa SH-RPi-kotelossa](https://shop.hatlabs.fi/products/compact-weatherproof-enclosure-for-raspberry-pi-and-sh-rpi-158x90x60-mm) on jo runsaasti tilaa liittimille.

<figure markdown="span">
![](medium_enclosure.jpg){ width="50%" }
<figcaption>Kompakti SH-RPi-kotelo tarjoaa enemmän tilaa paneeliliittimien sijoitteluun.</figcaption>
</figure>


Muita sopivia vesitiiviitä koteloita löytyy helposti mistä tahansa verkkokaupasta. Myös suuremmat ulkokäyttöön tarkoitetut jakorasiat sopivat tarkoitukseen.

### Reikien poraaminen paneeliliittimille

Koteloissa ei yleensä ole valmiiksi porattuja reikiä. Käytä reikiä poratessasi aina kartio- tai porrasterää (sellaista, joka näyttää pieneltä metalliselta joulukuuselta). Tavallinen metalliporanterä puree helposti liian syvälle ja voi halkaista kotelon seinämän.

Kun suunnittelet reikien ja liittimien sijoittelua, jätä riittävästi tilaa liitinmuttereiden kiristämiselle ja liittimen rungolle. Jos aiot asentaa kotelon seinälle, liittimet kannattaa sijoittaa alaspäin, jotta veden pääsy sisään on mahdollisimman epätodennäköistä.

Sopivat reikäkoot eri liittimille:

- PG7-läpivientiholkki ja M12-paneeliliitin (NMEA 2000): 12,5 mm tai 1/2"
- SP13-paneeliliittimet (sinimustat muoviliittimet): 13 mm
- PG9-läpivientiholkki: 16 mm tai 5/8"

Kumiset tai silikoniset läpivientikumit mahdollistavat huomattavasti tiheämmän kaapeloinnin kuin paneeliliittimet tai läpivientiholkit. Ne eivät kuitenkaan ole yhtä vesitiiviitä kuin paneeliliittimet tai läpivientiholkit. Lisäksi ne vaativat kaapelin pysyvän kiinnityksen, mikä voi vaikeuttaa järjestelmän huoltamista.

TODO: Lisää kuva läpivientikumista.

### Paneeliliittimien juottaminen

Kun juotat sisäisiä johtimia paneeliliittimiin, käytä aina kutistesukkaa yksittäisten johtimien päällä.
Muista aina pujottaa kutistesukka johtimeen _ennen_ juottamista...
Yleensä juotostinaa kannattaa ensin lisätä liittimen nastan koloon ja sitten sulattaa tina uudelleen ja työntää johdin paikalleen.
Loading
Loading