diff --git a/.gitattributes b/.gitattributes index 190c347..01dd8b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,58 +1,17 @@ -# Git stays passive on line endings by default. -# The .editorconfig end_of_line rules guide what the editor writes. -# The pins below are git's own enforcement for execution-sensitive files. -# - git config --global core.autocrlf false -# - git add --renormalize . -# - git ls-files --eol -* -text +# Normalize every detected text file to LF in the index and on checkout. +# `text=auto` leaves binary files byte-preserved. +* text=auto eol=lf -# This repo declares LF, so .editorconfig already defaults every file to LF. -# That does not make the pins below redundant. -# `* -text` keeps git passive, so .editorconfig binds only the editor. -# An explicit `text eol=lf` is what binds git on checkout and on renormalize. -# Execution-sensitive files therefore get both. +# Windows command scripts require CRLF. +*.bat text eol=crlf +*.cmd text eol=crlf -# A CRLF shebang breaks execution, so scripts stay LF regardless of the default. -*.sh text eol=lf - -# Shebang-executable files carrying no extension, which every rule above matches by -# extension and therefore misses. `ops/vps-backup-pull` is run by systemd on the backup -# host. A named line per file, as with the Python rules below, and `check-eol-pins.py` -# fails if a tracked shebang file ever lands without one. -ops/vps-backup-pull text eol=lf - -# Shebang-executable Python, where a CRLF on line one is a broken interpreter line rather -# than a cosmetic difference. `capture/` is pinned whole, because everything there is a -# script; under `checks/` the two executables are named, because that directory also holds -# lists and fixtures. Neither is a blanket `*.py` rule, so a future non-executable module -# elsewhere is not swept in by accident, and a new executable needs a line here. -capture/*.py text eol=lf -checks/check-url-parity.py text eol=lf -checks/check-env-docs.py text eol=lf -checks/check-eol-pins.py text eol=lf - -# Caddy config is parsed line by line by a daemon rather than by a shell, and a CRLF file -# is rejected or silently mis-parsed. Both files are named: the bundle's `Caddyfile`, and -# the bootstrap that is installed into the container's config directory and is the only -# Caddy file outside the release. -# -# The restricted `authorized_keys` and the forced-command deploy shell were pinned here -# too, and this repository has never carried either: they live on the server, described in -# OPERATIONS.md "Server Hardening". A pin binds nothing for a file that does not exist, -# and the comment claiming to cover "the extensionless shebang script" is what let the -# real one, `ops/vps-backup-pull`, sit unpinned above. `check-eol-pins.py` now fails on a -# pattern matching no tracked file, so neither can come back silently. -deploy/Caddyfile text eol=lf -deploy/bootstrap.Caddyfile text eol=lf - -# Caddy map files are tabular data read by `map` directives. -# They stay LF for the same reason as the Caddyfile. -deploy/maps/*.map text eol=lf +# Repo-specific below: this repo's own additions layered on the fleet baseline above. # Media is 566 MB of images, video, and archives across two trees. -# `* -text` already stops git normalizing them. -# `binary` additionally suppresses textual diff and merge. -# That keeps a `git log -p` or a bad merge from rendering a 4 MB PNG as text. +# `binary` suppresses normalization plus textual diff and merge, so a `git log -p` or a +# bad merge never renders a 4 MB PNG as text, and text=auto's own heuristic is never +# consulted for these paths regardless of what it would guess. # Extensions present are png, jpg, gif, webp, mov, zip, and ico, with nothing textual. static/media/** binary static/external/** binary diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index ebec5b6..e19b23c 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -92,12 +92,6 @@ jobs: - name: Check environment docs step run: python3 checks/check-env-docs.py - # The .gitattributes line-ending pins are hand-maintained, and nothing read them back. - # This is what reads them back: it fails on a tracked shebang file with no LF pin, - # and on a pin naming no tracked file. - - name: Check line-ending pins step - run: python3 checks/check-eol-pins.py - # The pin lives in the action, so validation and the deploy cannot install different generators. - name: Install Hugo step uses: ./.github/actions/install-hugo diff --git a/TODO.md b/TODO.md index c3d923c..093b16a 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,7 @@ The site is built, gated in CI, and deployed to staging by pipeline. It is not y | Media integrity | closed for the files themselves. The conversion's only loss was 19 images, inside five galleries, restored from the capture, and the 98 files with no page links are adjudicated rather than unknown, with `ORPHANED_MEDIA` failing if that count moves either way. How media was *presented* was a separate loss, found later and now gated | | URL contract | done. 328 render, 917 redirect, 778 legacy image URLs, all gated. **Two gates read different subsets of that, which is why two totals appear in this file**: the build gate proves all three against files on disk, and the live check requests 328 + 917 + 8 curated media = **1,253** against a running server, since proving 778 images over the wire on every deploy buys nothing the build gate has not already proven | | Deploy shape | done. Proven on two local mirrors and on the VPS, by hand and by pipeline | -| CI workflows | green. Validation runs on every pull request and feeds the required check. Three gates live in `checks/`: the URL contract, the environment-docs pair, and the line-ending pins | +| CI workflows | green. Validation runs on every pull request and feeds the required check. `checks/` holds the URL contract's build gate (`check-url-parity.py`, live gate `check-live-urls.sh` is deploy-time only) plus `check-env-docs.py`, which checks `ENVIRONMENT.md` against the tree in both directions | | Branches | converged 2026-08-09. [#68][pr-68] promoted `develop` to `main` as a merge commit, `main` carries every commit on `develop`, and the content diff is empty. **The merge itself published nothing**, which is the release model behaving as specified: a human merge never auto-publishes, and the releases named two rows down were cut afterwards by explicit dispatch | | GitHub repo | public, both rulesets active, `configure.sh check` exits 0 | | Release pipeline | proven end to end and cut from both branches 2026-08-09. `1.0.54` is the newest release, from `main` and marked Latest, and `1.0.54-gcd9285a9fe` the newest prerelease, from `develop`. Each tag is pinned to its own branch tip, and the clean-versus-suffixed classification is the gate working rather than a coincidence | @@ -57,7 +57,7 @@ The site is built, gated in CI, and deployed to staging by pipeline. It is not y - **This reverses a deliberate decision, so the comment above the job has to change with the code.** It currently reads "Staging deploys from any ref, since proving a branch before it merges is what staging is for", which is the argument for the present behavior and would be left contradicting the gate. Whoever makes the change states the new reason instead: staging is the rehearsal for what `develop` will promote, so a deploy from a feature branch proves something that is not what ships. Requested by the maintainer 2026-08-09, after a staging deploy was dispatched from `main` here. - Keep the existing comparison style, against the full `github.ref` rather than `ref_name`, for the reason already recorded there: a tag and a branch can share a short name, so a tag called `develop` would satisfy a `ref_name` test while pointing at an arbitrary commit. The new arm inherits that hazard exactly and is not a special case. - Add the weekly non-blocking external-link-check workflow, which is the one gate that cannot be blocking because it fails on other people's outages. -- **[`checks/README.md`](./checks/README.md) names one of the three gates in its own directory.** It is written as the URL contract's document, titled `URL Parity Gate`, and `check-env-docs.py` and `check-eol-pins.py` appear in it nowhere. Each carries its reasoning in its own docstring, which is where the detail belongs, but [#66][pr-66] made a directory's README the authority on that directory and this one currently describes a subset of it without saying so. The small fix is a section naming what else lives there and what each gate answers; the alternative is to state in the README that it is scoped to the contract alone. Either resolves it, and the present state is the one that reads as complete while not being. +- **[`checks/README.md`](./checks/README.md) names one of the two gates in its own directory.** It is written as the URL contract's document, titled `URL Parity Gate`, and `check-env-docs.py` appears in it nowhere. It carries its reasoning in its own docstring, which is where the detail belongs, but [#66][pr-66] made a directory's README the authority on that directory and this one currently describes a subset of it without saying so. The small fix is a section naming what else lives there and what it answers; the alternative is to state in the README that it is scoped to the contract alone. Either resolves it, and the present state is the one that reads as complete while not being. - Decommission WordPress.com only after **30 clean days**, and downgrade to free rather than deleting, which keeps the media reachable as a safety net and preserves the ability to re-export. Do not start sooner: the conversion fetched media over HTTP from the live site. ## Recurring operations @@ -78,7 +78,7 @@ The site is built, gated in CI, and deployed to staging by pipeline. It is not y ## Owed to the hub -**One thing, and it is conditional.** [ProjectTemplate#633][issue-633] reports that the hub's `repo_gate.py --check eol` compares `.gitattributes` and `.editorconfig` to each other and never to the tree, and offers to port this repo's [`checks/check-eol-pins.py`](./checks/check-eol-pins.py) into the hub's `scripts/` with tests in the `test_repo_gate.py` style. That offer is owed only if the hub wants that shape. **Do not start it before the hub rules**, because the open design question changes the code: the dead-pattern direction cannot be lifted unmodified, since a carried baseline forward-declares pins for files its consumers will have and the template itself does not, which is correct there and a finding anywhere else. +**The one open item here closed itself.** [ProjectTemplate#633][issue-633] reported that the hub's `repo_gate.py --check eol` compares `.gitattributes` and `.editorconfig` to each other and never to the tree, citing two shapes this repo's own `checks/check-eol-pins.py` had caught. Rather than porting this repo's script, the hub built the equivalent directly into `repo_gate.py` as `eol-coverage` (`unpinned` plus `dead`), closing #633. This repo has since retired `check-eol-pins.py` and switched `.gitattributes` to the fleet-standard `text=auto eol=lf`, which structurally removes the bug class the script existed to catch (a tracked file with no pin keeping whatever encoding it arrived with, only possible when git stays passive by default) rather than merely fixing an instance of it. The spec update this repo owed the hub has landed: [ProjectTemplate#560][hub-type-pr] authored the `hugo` type, the `self-hosted` target, the `deploy-ssh` mechanism, guarantees D4.6 and D5.6, and a reference leaf pair, all measured from what this repo actually runs rather than from the prediction the intake carried. [#456][hub-issue] and [#558][hub-spec-issue] are closed with it. @@ -124,7 +124,7 @@ Three findings are open at the hub, recorded here rather than only in the issues | [#550][issue-550] | Nothing detects a repo missing from the registry, which is how this repo stayed invisible. Three other repos are still absent. | | [#552][issue-552] | The audit flags any carried `AGENTS.md` naming the template repo, and the byte-locked `Fleet Bootstrap` section names it. Carrying the canonical correctly cannot pass, and it is the one finding the current run cannot clear. | | [#597][issue-597] | Filed from here, after a mandatory pre-merge gate in `OPERATIONS.md` was skipped on [#40][pr-40]. The ruling is that a verification a runner cannot perform needs a declared destination, not a better per-repo pointer. Answered by [#598][pr-598], which this repo owes work against once it is ground truth. | -| [#633][issue-633] | Filed from here, 2026-08-09. The hub's `eol` gate is document-to-document, so it reported `0 issue(s)` against the exact tree carrying both defects [#69][pr-69] fixed. Where a repo sets `[*] end_of_line = lf`, as this one does, that gate is vacuously satisfied by any pin at all, including one naming a file that does not exist. Open on the hub's side; the conditional work it may ask of this repo is under **Owed to the hub**. | +| [#633][issue-633] | Filed from here, 2026-08-09, closed on the hub's side via `eol-coverage` (see **Owed to the hub**). Resolved rather than open; kept in this table as the record of what was filed and why. | **What [#598][pr-598] will ask of this repo, once it is ground truth.** It is merged to the hub's `develop` and not to `main`, so it binds nothing yet, per the trap below about reading `main` as ground truth. It makes `Local Verification` a sixth declared `OPERATIONS.md` heading, leading the file as the only pre-merge one. This repo's `OPERATIONS.md` carries **13 level-two headings and matches none of the five declared today**, so the work is a rename and reorder rather than new prose: `Local Verification Before a Pull Request` becomes `Local Verification`, and `Backup and Restore` is one word from the declared `Backup and Recovery`. Both are near-misses rather than absences, which is the shape a heading check will mostly find in a repo that wrote its operational document before the spec declared headings. Do not start until the hub promotes it. @@ -171,8 +171,8 @@ Each of these was hit or nearly hit, and each is cheap to re-trip. - **A Copilot review hides findings in the review body, where the thread API cannot see them.** The `reviewThreads` query returns line threads only, so a review carrying `Suppressed comments (N)` in a `
` block reports zero unresolved while real findings sit unread. Read the review body itself, not just the threads, before calling a review loop finished. - **`gh pr merge --delete-branch` on a `develop -> main` promotion deletes `develop`.** Use a plain `gh pr merge --merge`. - **A review that exists on the head is not a finished review.** On [#68][pr-68] `pr_review.py wait` returned after one second with `review_on_head=yes unresolved=0 suppressed=0`, an independent thread poll agreed at zero, and the pull request was reported green. Copilot posted a third finding *after* both reads, and the maintainer found it. The re-request had matched a review still being written, so every query was correct and every query was early. Wait and re-sweep all three surfaces, line threads, issue-level comments and the review bodies, before calling a loop finished. A very short `waited=` is the tell. -- **A rule naming a target that does not exist reads as coverage.** `.gitattributes` pinned `deploy/blog-deploy-shell` and `deploy/authorized_keys`, neither ever tracked in this repo, and the comment above them claimed the extensionless-shebang case was handled. It was not, and the one real instance sat unpinned twenty lines up, invisible precisely because the file looked thorough. [`checks/check-eol-pins.py`](./checks/check-eol-pins.py) gates this file now, in both directions. The generalisation is not gated anywhere: a config that names a path, a job, or a host that is absent will read as protection for as long as nobody checks. -- **A gate is only as good as the matcher underneath it, and a wrong matcher fails quietly.** `check-eol-pins.py` shipped with two defects in one function, `git ls-files` pathspec not sharing gitattributes glob semantics and then a leading `/` being stripped before anchoring was decided. Both were found by review rather than by the gate, and each would have made it report a confident wrong answer. [#70][pr-70] fixed them and it now checks its own matching against `git check-attr` on every run, which is the general move: where a check predicts what a tool will do, compare it against the tool rather than against the documentation. +- **A rule naming a target that does not exist reads as coverage.** `.gitattributes` pinned `deploy/blog-deploy-shell` and `deploy/authorized_keys`, neither ever tracked in this repo, and the comment above them claimed the extensionless-shebang case was handled. It was not, and the one real instance sat unpinned twenty lines up, invisible precisely because the file looked thorough. `checks/check-eol-pins.py` gated this file in both directions until this repo retired it and the pin list it audited, per **Owed to the hub**. The generalisation is not gated anywhere: a config that names a path, a job, or a host that is absent will read as protection for as long as nobody checks. +- **A gate is only as good as the matcher underneath it, and a wrong matcher fails quietly.** `check-eol-pins.py` shipped with two defects in one function, `git ls-files` pathspec not sharing gitattributes glob semantics and then a leading `/` being stripped before anchoring was decided. Both were found by review rather than by the gate, and each would have made it report a confident wrong answer. [#70][pr-70] fixed them, and the fixed version checked its own matching against `git check-attr` on every run for as long as the script existed, which is the general move: where a check predicts what a tool will do, compare it against the tool rather than against the documentation. ## Reference @@ -211,7 +211,6 @@ The deploy root is deliberately absent from this table. The rsync destination is [pr-65]: https://github.com/ptr727/Blog/pull/65 [pr-66]: https://github.com/ptr727/Blog/pull/66 [pr-68]: https://github.com/ptr727/Blog/pull/68 -[pr-69]: https://github.com/ptr727/Blog/pull/69 [pr-70]: https://github.com/ptr727/Blog/pull/70 [pr-75]: https://github.com/ptr727/Blog/pull/75 [pr-76]: https://github.com/ptr727/Blog/pull/76 diff --git a/checks/check-eol-pins.py b/checks/check-eol-pins.py deleted file mode 100755 index 8ea0a22..0000000 --- a/checks/check-eol-pins.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -"""Fail if a tracked shebang file has no LF pin, or a .gitattributes pattern matches nothing. - -`.gitattributes` keeps git passive with `* -text` and then names the files whose line -endings are load-bearing. That design is right and it has one weakness: the pins are a -hand-maintained list, so a new execution-sensitive file is pinned only if its author -remembers, and until this gate nothing read the list back. Both directions had already -failed by the time it was written. - - unpinned `ops/vps-backup-pull` is extensionless, so no `*.sh` or `*.py` rule reached - it, and a CRLF checkout would hand systemd a broken interpreter line. - dead `deploy/blog-deploy-shell` and `deploy/authorized_keys` were pinned and have - never been tracked in this repository. That is the worse half: a pin for a - file that does not exist binds nothing while reading as coverage, and its - comment claimed the extensionless case was handled, which is why the file - that actually needed it went unnoticed. - -So this gate reads both directions, and neither is a style rule. A shebang on line one is -the test for the first, because that is exactly the property a CRLF breaks. Executability -is deliberately not the test: the mode bit and the interpreter line move independently, -and it is the interpreter line that fails. - -Text-format files a daemon parses, the Caddy configs and the map tables, are pinned for -the same reason and are not detectable by any property of their contents, so they stay a -named list and only the dead-pattern direction covers them. - -A third direction checks this script's own pattern matching against `git check-attr`, and -it exists because the first two are only as trustworthy as that matching. Two conversion -defects were found by review rather than by this gate, one under-matching and one -over-matching, and each would have made a direction above report the wrong answer while -looking healthy. Comparing against git rather than fixing each shape as it appears is what -stops the next shape being found the same way. - -Read-only. Exit 1 on any finding. -""" -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -REPO = Path(__file__).resolve().parent.parent -ATTRIBUTES = REPO / ".gitattributes" - -# `* -text` is the passive default the pins sit on top of, and it is expected to match -# every tracked file. Reporting it as a pattern that "matches nothing" is impossible, but -# excluding it keeps the dead-pattern check about the named pins. -BASELINE = {"*"} - - -def git(*args: str) -> str: - """Run git in the repository and return stdout, failing loudly rather than silently.""" - result = subprocess.run( - ["git", "-C", str(REPO), *args], - capture_output=True, - text=True, - check=True, - ) - return result.stdout - - -def tracked_files() -> list[str]: - return [line for line in git("ls-files", "-z").split("\0") if line] - - -def patterns() -> list[tuple[int, str]]: - """The pattern from every non-comment, non-blank line, with its line number.""" - found = [] - for number, raw in enumerate(ATTRIBUTES.read_text(encoding="utf-8").splitlines(), 1): - line = raw.strip() - if not line or line.startswith("#"): - continue - found.append((number, line.split()[0])) - return found - - -def lf_patterns() -> list[str]: - """Just the patterns that pin LF, which are the ones with a checkable consequence.""" - found = [] - for raw in ATTRIBUTES.read_text(encoding="utf-8").splitlines(): - line = raw.strip() - if not line or line.startswith("#"): - continue - fields = line.split() - if "eol=lf" in fields[1:]: - found.append(fields[0]) - return found - - -def has_shebang(path: Path) -> bool: - """True if the file opens `#!`, read as bytes so a binary file cannot raise.""" - try: - with path.open("rb") as handle: - return handle.read(2) == b"#!" - except OSError: - return False - - -def pathspec_for(pattern: str) -> str: - """Convert a .gitattributes pattern into a pathspec matching the same files. - - A plain `git ls-files -- ` is the obvious way to ask which files a pin covers - and it is wrong in both directions, because pathspec and gitattributes do not share - glob semantics: - - under a pattern with no slash matches its basename at any depth in gitattributes, - where bare pathspec reads it as a root-relative path. `Dockerfile` covers - `Docker/Dockerfile`, and bare pathspec returns nothing, so a live pin reports - dead and reds CI. This is the direction that produces a false failure. - over `*` does not cross a `/` in gitattributes, and does in bare pathspec, so - `pkg/*.py` wrongly picks up `pkg/sub/nested.py`. Harmless for this check, - since it can only hide a dead pin, never invent one. - - `:(glob)` gives `*` and `**` their gitattributes meaning, and the `**/` prefix supplies - the any-depth match for a slash-free pattern. Verified against `git check-attr`, which - is what git actually applies: on a tree holding `Docker/Dockerfile`, `pkg/mod.py` and - `pkg/sub/nested.py`, check-attr resolves eol=lf for exactly the first two, and this - conversion selects exactly those two where the bare form selects the wrong set both - times. - """ - # Anchoring is decided before the leading slash is removed, because removing it first - # destroys the evidence: `/Dockerfile` is root-anchored and covers only the root file, - # while a bare `Dockerfile` matches at any depth, and the two differ by exactly the - # character being stripped. A trailing slash marks a directory and never anchors. - anchored = pattern.startswith("/") or "/" in pattern.rstrip("/") - body = pattern[1:] if pattern.startswith("/") else pattern - return f":(glob){body}" if anchored else f":(glob)**/{body}" - - -def eol_attribute(paths: list[str]) -> dict[str, str]: - """The resolved `eol` attribute per path, from git rather than by re-implementing the - match rules, because a hand-rolled matcher is a second source of truth that can differ - from the one git actually applies on checkout.""" - if not paths: - return {} - payload = "\0".join(paths) + "\0" - result = subprocess.run( - ["git", "-C", str(REPO), "check-attr", "--stdin", "-z", "eol"], - input=payload, - capture_output=True, - text=True, - check=True, - ) - # -z emits a flat NUL-separated stream of path, attribute, value triples. - fields = [field for field in result.stdout.split("\0") if field != ""] - return {fields[i]: fields[i + 2] for i in range(0, len(fields) - 2, 3)} - - -def main() -> int: - findings: list[str] = [] - files = tracked_files() - - # Direction one: a tracked shebang file whose resolved eol is not lf. - shebangs = sorted(f for f in files if has_shebang(REPO / f)) - if not shebangs: - print("error: no tracked shebang files found, so this gate checked nothing") - return 1 - resolved = eol_attribute(shebangs) - for path in shebangs: - if resolved.get(path) != "lf": - findings.append( - f"unpinned: {path} opens with a shebang and resolves to " - f"eol={resolved.get(path, 'unspecified')}. Add a line to .gitattributes." - ) - - # Direction two: a pin naming a file the repository does not carry. - for number, pattern in patterns(): - if pattern in BASELINE: - continue - if not git("ls-files", "--", pathspec_for(pattern)).strip(): - findings.append( - f"dead: .gitattributes:{number} pattern {pattern!r} matches no tracked " - f"file, so it binds nothing while reading as coverage." - ) - - # Direction three: the matcher above, checked against what git actually applies. - # - # Directions one and two disagree about nothing when the pattern conversion is right, - # and silently report the wrong thing when it is not. Two conversion defects were found - # by review rather than by this gate, one in each direction: a slash-free `Dockerfile` - # under-matched and reported a live pin dead, and a root-anchored `/Dockerfile` - # over-matched into subdirectories. Fixing each shape as it surfaced would leave the - # next shape to be found the same way, so the matcher is checked against `git - # check-attr` instead, which is the thing it is trying to predict. - # - # Both directions are needed and they catch different defects. A missing file means the - # conversion under-matched, which is what produces a false dead report. An extra file - # means it over-matched, which hides a real one. - # - # This holds because no pattern here clears `eol` once another has set it. A future - # pattern that unsets it would need this comparison to account for precedence. - predicted = set() - for pattern in lf_patterns(): - matched = git("ls-files", "-z", "--", pathspec_for(pattern)).split("\0") - predicted.update(path for path in matched if path) - resolved_all = eol_attribute(files) - actual = {path for path in files if resolved_all.get(path) == "lf"} - for path in sorted(actual - predicted): - findings.append( - f"matcher: {path} resolves to eol=lf, and no converted pattern matches it. " - f"The pattern conversion under-matches, so a live pin can report dead." - ) - for path in sorted(predicted - actual): - findings.append( - f"matcher: {path} is matched by a converted eol=lf pattern and resolves to " - f"eol={resolved_all.get(path, 'unspecified')}. The conversion over-matches." - ) - - if findings: - for finding in findings: - print(f"error: {finding}") - print(f"\nFAIL - {len(findings)} line-ending pin finding(s)") - return 1 - - print( - f"PASS - {len(shebangs)} shebang files pinned to LF, " - f"{len(patterns()) - len(BASELINE)} patterns all matching tracked files, " - f"{len(actual)} LF-pinned files agreeing with git check-attr" - ) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/themes/PaperMod/i18n/uz.yaml b/themes/PaperMod/i18n/uz.yaml index c9f2375..6c3c44e 100644 --- a/themes/PaperMod/i18n/uz.yaml +++ b/themes/PaperMod/i18n/uz.yaml @@ -1,20 +1,20 @@ -- id: prev_page - translation: "Oldingi sahifa" - -- id: next_page - translation: "Keyingi sahifa" - -- id: read_time - translation: - one : "Bir daqiqa" - other: "{{ .Count }} daqiqa" - -- id: toc - translation: "Mundarija" - -- id: translations - translation: "Tarjimalar" - -- id: home - translation: "Bosh sahifa" - +- id: prev_page + translation: "Oldingi sahifa" + +- id: next_page + translation: "Keyingi sahifa" + +- id: read_time + translation: + one : "Bir daqiqa" + other: "{{ .Count }} daqiqa" + +- id: toc + translation: "Mundarija" + +- id: translations + translation: "Tarjimalar" + +- id: home + translation: "Bosh sahifa" + diff --git a/themes/PaperMod/i18n/zh-tw.yaml b/themes/PaperMod/i18n/zh-tw.yaml index 48b84d2..99cd15e 100644 --- a/themes/PaperMod/i18n/zh-tw.yaml +++ b/themes/PaperMod/i18n/zh-tw.yaml @@ -1,33 +1,33 @@ -- id: prev_page - translation: "上一頁" - -- id: next_page - translation: "下一頁" - -- id: read_time - translation: - one : "1 分鐘" - other: "{{ .Count }} 分鐘" - -- id: words - translation: - one: "字" - other: "{{ .Count }} 字" - -- id: toc - translation: "目錄" - -- id: translations - translation: "語言" - -- id: home - translation: "首頁" - -- id: edit_post - translation: "編輯" - -- id: code_copy - translation: "複製" - -- id: code_copied - translation: "已複製!" +- id: prev_page + translation: "上一頁" + +- id: next_page + translation: "下一頁" + +- id: read_time + translation: + one : "1 分鐘" + other: "{{ .Count }} 分鐘" + +- id: words + translation: + one: "字" + other: "{{ .Count }} 字" + +- id: toc + translation: "目錄" + +- id: translations + translation: "語言" + +- id: home + translation: "首頁" + +- id: edit_post + translation: "編輯" + +- id: code_copy + translation: "複製" + +- id: code_copied + translation: "已複製!"