From 517f69f17aa55fdee1dcea55a862f3a7c9e0a9be Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:13:51 +0000 Subject: [PATCH 1/7] docs(roadmap): the plan for 0.8.0, and the way to 1.0 0.8.0 is reach: four more countries, one place a country is defined, a command that lists them, init that stops turning a typo into Austria, and audit --watch. 0.9.0 is the first ten minutes, 1.0.0 the contracts frozen. The four countries' citations were established from secondary sources only -- the official gazettes were not reachable from the session that wrote them -- and checking them against the primary text is written in as a release blocker rather than left implicit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- ROADMAP.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index bd3387f..45fa435 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,6 +9,109 @@ here is allowed to make a run look more complete than it was. A section for a released version is left in place with what landed marked on it, so the record shows what was planned as well as what shipped. +## Towards 1.0 + +1.0 is the release where somebody who has never read this file can install the tool, answer +a few questions, and end up with an audit in CI and a statement they can publish, without +having to learn the tool first. Three releases get there: + +- **0.8.0 — reach.** More of the EU single market, and the loop between editing and seeing + a result made short enough to use while working rather than after. +- **0.9.0 — the first ten minutes.** Everything a new user meets before their first useful + result: `init` that sets up CI and a baseline as well as a config, errors that say what to + type next, and a German rendering for Belgium's third language community. The countries + after this release's four — Sweden, Denmark, Finland, Czechia — go here, with the same rule. +- **1.0.0 — the promise.** The JSON report, the review record, the baseline and the config + file frozen as documented contracts under semver, with a migration note for anything that + changed on the way. No new surface: 1.0 is 0.9 with the guarantees written down. + +## 0.8.0 — reach + +0.7.0 made a run cost what it should. 0.8.0 spends that on two things: getting the tool to +more of the people the EAA applies to, and getting a result to them while they are still +looking at the code that caused it. + +### 1. Four more countries: Belgium, Ireland, Poland, Portugal + +On 0.5.0's rule: a statement is written under its country's own law, not translated from +another's, in the language that law is administered in plus English. + +| | Statute | Supervision named | Languages | +| --- | --- | --- | --- | +| `BE` | Loi du 5 novembre 2023 / wet van 5 november 2023, amending the Code de droit économique | SPF Économie, Direction générale de l'Inspection économique | `fr`, `nl`, `en` | +| `IE` | European Union (Accessibility Requirements of Products and Services) Regulations 2023 (S.I. No. 636 of 2023) | CCPC for e-commerce, ComReg and the Central Bank for their sectors | `en` | +| `PL` | Ustawa z dnia 26 kwietnia 2024 r. o zapewnianiu spełniania wymagań dostępności niektórych produktów i usług przez podmioty gospodarcze (Dz.U. 2024 poz. 731) | Prezes Zarządu PFRON, who receives every report; the minister for digital affairs supervises e-commerce | `pl`, `en` | +| `PT` | Decreto-Lei n.º 82/2022, de 6 de dezembro | ANACOM for e-commerce; supervisors report to INR | `pt`, `en` | + +Belgium is the case the roadmap has been waiting for. Supervision is split, and the +templates say so rather than naming one body as if it owned the subject. Belgium also has +three official languages. The German-speaking community gets its rendering in 0.9: the +federal law is published in French and Dutch, and a German document written without a +German source text would be exactly the translation this rule forbids. + +**What this release could not do, written down rather than hidden:** the session that wrote +these templates could not reach the official gazettes (irishstatutebook.ie, +isap.sejm.gov.pl, dre.pt, ejustice.just.fgov.be). Every citation above is corroborated by +several independent secondary sources: regulators' own pages, law firms and government +portals, as indexed. Each one still has to be checked against the primary text before 0.8.0 +is tagged. That check is a release blocker, listed under *Done means* below. The templates +also cite less than the older seven do: no article numbers and no fine amounts, because +those are the details a secondary source gets wrong. + +### 2. One place a country is defined + +A country today is spread across `COUNTRIES`, `STATEMENT_LOCALES`, `init`'s locale table, +the date formats, the docs table and the snapshot list. Adding four more at once would mean +changing all of them eleven times over. The facts move into one registry (name, languages, +statute, authority, default site locale) that everything else reads from. A test fails when +a template exists without its registry entry, or the other way round. + +### 3. `eaa-kit countries` + +What the statement can be written for, from the terminal: code, name, languages, statute +and the authority each template names. Today that information lives in a docs table, so +nobody finds out that `--country PT` exists until they have read the docs. + +### 4. `audit --watch` + +[Deferred from 0.7.0](#not-in-070). A run over an unchanged build costs about as much as +starting the process, so it is worth repeating on every save. `--watch` re-audits the +build directory whenever something in it changes. The page cache means only the changed +pages are audited again, and the report is printed again after each run. + +The refusals: + +- **Directories only.** A running site under `--url` changes without writing anything this + process can watch, so a watch over it would be a poll that looks like a watch. It is an + error, not a quiet fallback. +- **No exit code on the way.** A watch never exits 1 because a run found something. Its + job is to show the result, and CI has the one-shot run for failing a build. +- **A run is never skipped because another one is in progress.** A change that arrives + mid-run starts a new run as soon as the current one ends. A report never shows a build + the files on disk have already moved past. + +### 5. `init` stops guessing + +`init` turns an unrecognised country into `AT` without saying so. That is an Austrian legal +document for somebody who typed `pl`. An answer it does not recognise is now asked again, +with the list, and the prompt names each country in full. + +### Not in 0.8.0 + +- **Belgium in German, and the Nordic and Czech statements.** 0.9, for the reason above. +- **Watching a running site.** See the refusal above. +- **A score, Level AAA, anything model-generated, a hosted dashboard.** As before, and not + later. + +### Done means + +- `lint`, `typecheck`, `test`, `smoke` and the packaged-CLI run green across the CI matrix. +- **Every citation in the four new countries' templates checked against the primary + text**, and the checking recorded in the changelog: who checked it, against which + consolidated version. Until then 0.8.0 is not tagged. +- `examples/` regenerated and drift-checked. +- A changelog entry saying what was given up as well as what was added. + ## 0.7.0 — the run that costs nothing, and the loop that closes 0.6.0 added the half no engine can do. 0.7.0 is about three things a user feels: how long a From 8ee2af6ef194b8b569caf419de6bb31949082026 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:13:51 +0000 Subject: [PATCH 2/7] feat: Belgium, Ireland, Poland and Portugal, and eaa-kit countries Four more countries, each written under its own law: BE in French, Dutch and English; IE in English; PL in Polish and English; PT in Portuguese and English. Belgium and Portugal say supervision is split rather than naming one body as the whole of it, which is what the roadmap asked of Belgium. What each country is now lives in one registry, src/config/countries.ts, that init, the new `eaa-kit countries` command and a test read. The test fails when a template exists without its entry or the other way round. The four are marked unverified in the registry and in `eaa-kit countries`: their citations come from regulators' pages and law firms, not from the gazettes, and cite less than the older seven -- no article numbers, no fine amounts -- for that reason. init no longer turns an answer it does not recognise into AT without a word. It asks again, accepts a country's English name, and when it does fall back after three tries it says so. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- src/cli/countries.ts | 47 ++++++ src/cli/index.ts | 9 ++ src/cli/init.ts | 56 ++++--- src/config/countries.ts | 136 ++++++++++++++++ src/config/define.ts | 9 +- src/statement/render.ts | 2 + src/statement/templates/be.en.md | 142 +++++++++++++++++ src/statement/templates/be.fr.md | 146 +++++++++++++++++ src/statement/templates/be.nl.md | 147 ++++++++++++++++++ src/statement/templates/ie.en.md | 144 +++++++++++++++++ src/statement/templates/pl.en.md | 143 +++++++++++++++++ src/statement/templates/pl.pl.md | 144 +++++++++++++++++ src/statement/templates/pt.en.md | 142 +++++++++++++++++ src/statement/templates/pt.pt.md | 145 +++++++++++++++++ tests/cli/countries.test.ts | 83 ++++++++++ tests/cli/init.test.ts | 30 +++- .../__snapshots__/statement.be.en.html | 84 ++++++++++ .../__snapshots__/statement.be.en.md | 87 +++++++++++ .../__snapshots__/statement.be.fr.html | 84 ++++++++++ .../__snapshots__/statement.be.fr.md | 89 +++++++++++ .../__snapshots__/statement.be.nl.html | 84 ++++++++++ .../__snapshots__/statement.be.nl.md | 89 +++++++++++ .../__snapshots__/statement.ie.en.html | 85 ++++++++++ .../__snapshots__/statement.ie.en.md | 89 +++++++++++ .../__snapshots__/statement.pl.en.html | 84 ++++++++++ .../__snapshots__/statement.pl.en.md | 88 +++++++++++ .../__snapshots__/statement.pl.pl.html | 84 ++++++++++ .../__snapshots__/statement.pl.pl.md | 88 +++++++++++ .../__snapshots__/statement.pt.en.html | 84 ++++++++++ .../__snapshots__/statement.pt.en.md | 87 +++++++++++ .../__snapshots__/statement.pt.pt.html | 84 ++++++++++ .../__snapshots__/statement.pt.pt.md | 89 +++++++++++ tests/statement/render.test.ts | 44 +++++- tests/statement/snapshot.test.ts | 8 + 34 files changed, 2932 insertions(+), 24 deletions(-) create mode 100644 src/cli/countries.ts create mode 100644 src/config/countries.ts create mode 100644 src/statement/templates/be.en.md create mode 100644 src/statement/templates/be.fr.md create mode 100644 src/statement/templates/be.nl.md create mode 100644 src/statement/templates/ie.en.md create mode 100644 src/statement/templates/pl.en.md create mode 100644 src/statement/templates/pl.pl.md create mode 100644 src/statement/templates/pt.en.md create mode 100644 src/statement/templates/pt.pt.md create mode 100644 tests/cli/countries.test.ts create mode 100644 tests/statement/__snapshots__/statement.be.en.html create mode 100644 tests/statement/__snapshots__/statement.be.en.md create mode 100644 tests/statement/__snapshots__/statement.be.fr.html create mode 100644 tests/statement/__snapshots__/statement.be.fr.md create mode 100644 tests/statement/__snapshots__/statement.be.nl.html create mode 100644 tests/statement/__snapshots__/statement.be.nl.md create mode 100644 tests/statement/__snapshots__/statement.ie.en.html create mode 100644 tests/statement/__snapshots__/statement.ie.en.md create mode 100644 tests/statement/__snapshots__/statement.pl.en.html create mode 100644 tests/statement/__snapshots__/statement.pl.en.md create mode 100644 tests/statement/__snapshots__/statement.pl.pl.html create mode 100644 tests/statement/__snapshots__/statement.pl.pl.md create mode 100644 tests/statement/__snapshots__/statement.pt.en.html create mode 100644 tests/statement/__snapshots__/statement.pt.en.md create mode 100644 tests/statement/__snapshots__/statement.pt.pt.html create mode 100644 tests/statement/__snapshots__/statement.pt.pt.md diff --git a/src/cli/countries.ts b/src/cli/countries.ts new file mode 100644 index 0000000..962e4b0 --- /dev/null +++ b/src/cli/countries.ts @@ -0,0 +1,47 @@ +import pc from 'picocolors' +import { COUNTRY_INFO } from '../config/countries.ts' +import { COUNTRIES } from '../config/define.ts' + +/** + * `eaa-kit countries`. + * + * What a statement can be written for, without opening the docs: each + * country's code, the languages it has, the statute it is written under and the + * body a reader is sent to. Before this, nobody found out that `--country PT` + * existed until they had read a table on another page. + */ + +export interface CountriesCommandOptions { + /** Machine-readable, for anything that wants to build a picker from it. */ + json?: boolean + /** Colour. Defaults to whatever picocolors detects for this terminal. */ + color?: boolean +} + +export function formatCountries(options: CountriesCommandOptions = {}): string { + if (options.json) { + const entries = COUNTRIES.map((code) => ({ code, ...COUNTRY_INFO[code] })) + return `${JSON.stringify(entries, null, 2)}\n` + } + + const colors = pc.createColors(options.color ?? pc.isColorSupported) + const lines: string[] = [] + for (const code of COUNTRIES) { + const info = COUNTRY_INFO[code] + lines.push( + `${colors.bold(code)} ${info.name} ${colors.dim(`(${info.languages.join(', ')})`)}`, + ` Statute: ${info.statute}`, + ` Authority: ${info.authority}`, + ) + // Said on the list itself, and not only in the docs, because this list is + // where somebody picks the country they will publish a document under. + if (info.unverified) { + lines.push( + colors.yellow(' New in 0.8: citations not yet checked against the primary text'), + ) + } + lines.push('') + } + lines.push('eaa-kit statement --country --lang ') + return `${lines.join('\n')}\n` +} diff --git a/src/cli/index.ts b/src/cli/index.ts index afd44f3..bfefef5 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -281,6 +281,15 @@ program process.exitCode = exitCode }) +program + .command('countries') + .description('List the countries a statement can be written for, and what each one names') + .option('--json', 'print the list as JSON') + .action(async (flags: { json?: true }) => { + const { formatCountries } = await import('./countries.ts') + process.stdout.write(formatCountries(flags)) + }) + program .command('diff') .description('Compare two JSON reports: what a change made worse, and what it fixed') diff --git a/src/cli/init.ts b/src/cli/init.ts index ed00e6c..b4c4750 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -3,6 +3,7 @@ import path from 'node:path' import { createInterface } from 'node:readline/promises' import pc from 'picocolors' import { DEFAULT_FAIL_ON } from '../audit/impact.ts' +import { COUNTRY_INFO, findCountry } from '../config/countries.ts' import { COUNTRIES, type Country } from '../config/define.ts' import { CONFIG_FILENAMES } from '../config/load.ts' import { exists } from '../fs.ts' @@ -53,16 +54,6 @@ interface Detected { country?: Country } -const COUNTRY_LOCALES: Record = { - AT: 'de-AT', - DE: 'de-DE', - CH: 'de-CH', - ES: 'es-ES', - FR: 'fr-FR', - IT: 'it-IT', - NL: 'nl-NL', -} - /** * Everything the project already says about itself. * @@ -144,10 +135,8 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< const name = await ask('Site name', detected.name ?? '') const url = await ask('Site URL', detected.url ?? 'https://example.com') - const country = normaliseCountry( - await ask(`Country whose law applies (${COUNTRIES.join('/')})`, 'AT'), - ) - const locale = await ask('Language of the site', COUNTRY_LOCALES[country]) + const country = await askCountry(ask, rl !== undefined) + const locale = await ask('Language of the site', COUNTRY_INFO[country].siteLocale) const legalName = await ask('Legal entity answerable for the site', name) const email = await ask('Feedback email', '') const feedbackUrl = await ask('Feedback or contact form URL (optional)', '') @@ -208,8 +197,39 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< return { file: target, exitCode: 0 } } -/** Falls back rather than failing: a typo should not throw away the answers. */ -function normaliseCountry(value: string): Country { - const upper = value.trim().toUpperCase() - return (COUNTRIES as readonly string[]).includes(upper) ? (upper as Country) : 'AT' +/** How often an answer that is not a country is asked again before giving up on it. */ +const COUNTRY_ATTEMPTS = 3 + +/** + * The country whose law the statement is written under. + * + * An answer that is not a country is asked again, with the list. It used to + * become Austria without a word, which is how somebody who typed `pl` got an + * Austrian legal document. Once the attempts run out the default is used, + * because a typo should not throw away every other answer, but it is said out + * loud, so the file is not left looking like the answer somebody gave. + */ +async function askCountry( + ask: (question: string, fallback: string) => Promise, + interactive: boolean, +): Promise { + const fallback: Country = 'AT' + const choices = COUNTRIES.map((code) => `${code} ${COUNTRY_INFO[code].name}`).join(', ') + if (interactive) process.stderr.write(pc.dim(`Countries: ${choices}\n`)) + + let answer = '' + for (let attempt = 0; attempt < COUNTRY_ATTEMPTS; attempt += 1) { + answer = await ask('Country whose law applies', fallback) + const country = findCountry(answer) + if (country !== undefined) return country + warn( + `${answer} is not one of the countries a statement can be written for: ${COUNTRIES.join(', ')}`, + ) + } + + warn( + `enforcement.country is set to ${fallback} because ${answer} was not recognised. ` + + 'Change it before generating a statement.', + ) + return fallback } diff --git a/src/config/countries.ts b/src/config/countries.ts new file mode 100644 index 0000000..33ed85d --- /dev/null +++ b/src/config/countries.ts @@ -0,0 +1,136 @@ +import type { Country, StatementLocale } from './define.ts' + +/** + * What this tool knows about each country, in one place. + * + * Before 0.8.0 a country was spread across the enum, `init`'s locale table, the + * docs and the snapshot list, and adding one meant finding all of them. This is + * the list everything else reads, and `Record` makes a country added + * to the enum without an entry here a type error rather than a gap somebody + * discovers from a user. + * + * `statute` and `authority` are summaries for `eaa-kit countries` and the docs. + * The wording a published statement uses lives in that country's templates, + * where it is written in the language of the document. A test asserts that + * `languages` matches the template files, so this list cannot claim a document + * nobody wrote. + */ +export interface CountryInfo { + /** In English, for the one list every reader shares. */ + name: string + /** Statement languages, in the order they are listed. The law's own language comes first. */ + languages: readonly StatementLocale[] + /** What `init` offers as the site's language. */ + siteLocale: string + statute: string + /** Who the enforcement section tells a reader to go to. */ + authority: string + /** + * Set where the citations were established from secondary sources only. + * `eaa-kit countries` shows it, so nobody takes these templates to have had the + * same checking as the others. It is removed once somebody checks the + * citations against the primary text. + */ + unverified?: true +} + +export const COUNTRY_INFO: Record = { + AT: { + name: 'Austria', + languages: ['de', 'en'], + siteLocale: 'de-AT', + statute: 'Barrierefreiheitsgesetz (BaFG)', + authority: 'Sozialministeriumservice', + }, + BE: { + name: 'Belgium', + languages: ['fr', 'nl', 'en'], + siteLocale: 'fr-BE', + statute: 'Loi du 5 novembre 2023 / wet van 5 november 2023 (Code de droit économique)', + authority: 'SPF Économie / FOD Economie, Economic Inspection', + unverified: true, + }, + CH: { + name: 'Switzerland', + languages: ['de', 'en'], + siteLocale: 'de-CH', + statute: 'Behindertengleichstellungsgesetz (BehiG), not an EAA transposition', + authority: 'the courts; there is no supervisory body', + }, + DE: { + name: 'Germany', + languages: ['de', 'en'], + siteLocale: 'de-DE', + statute: 'Barrierefreiheitsstärkungsgesetz (BFSG)', + authority: 'Marktüberwachungsstelle der Länder (MLBF)', + }, + ES: { + name: 'Spain', + languages: ['es', 'en'], + siteLocale: 'es-ES', + statute: 'Ley 11/2023, de 8 de mayo', + authority: + "the competent market surveillance authority, usually the autonomous community's consumer body", + }, + FR: { + name: 'France', + languages: ['fr', 'en'], + siteLocale: 'fr-FR', + statute: 'Ordonnance n° 2023-859, and art. 47 of loi n° 2005-102', + authority: 'Défenseur des droits, and Arcom', + }, + IE: { + name: 'Ireland', + languages: ['en'], + siteLocale: 'en-IE', + statute: 'S.I. No. 636 of 2023', + authority: 'Competition and Consumer Protection Commission (CCPC)', + unverified: true, + }, + IT: { + name: 'Italy', + languages: ['it', 'en'], + siteLocale: 'it-IT', + statute: 'D.lgs. 27 maggio 2022, n. 82 (legge Stanca)', + authority: 'AgID', + }, + NL: { + name: 'Netherlands', + languages: ['nl', 'en'], + siteLocale: 'nl-NL', + statute: 'Implementatiewet toegankelijkheidsvoorschriften producten en diensten', + authority: 'ACM for services, RDI for products', + }, + PL: { + name: 'Poland', + languages: ['pl', 'en'], + siteLocale: 'pl-PL', + statute: 'Ustawa z dnia 26 kwietnia 2024 r. (Dz.U. 2024 poz. 731)', + authority: 'Prezes Zarządu PFRON', + unverified: true, + }, + PT: { + name: 'Portugal', + languages: ['pt', 'en'], + siteLocale: 'pt-PT', + statute: 'Decreto-Lei n.º 82/2022, de 6 de dezembro', + authority: 'ANACOM for e-commerce services', + unverified: true, + }, +} + +/** + * A country from what somebody typed: the code in any case, or the English name. + * + * Undefined rather than a default. A statement is a legal document under one + * country's law, and quietly substituting another country for a typo is how + * somebody ends up publishing an Austrian statement for a Polish shop. + */ +export function findCountry(value: string): Country | undefined { + const wanted = value.trim().toLowerCase() + if (wanted === '') return undefined + for (const [code, info] of Object.entries(COUNTRY_INFO) as Array<[Country, CountryInfo]>) { + if (code.toLowerCase() === wanted || info.name.toLowerCase() === wanted) return code + } + return undefined +} diff --git a/src/config/define.ts b/src/config/define.ts index 7d3db89..fa31555 100644 --- a/src/config/define.ts +++ b/src/config/define.ts @@ -1,8 +1,11 @@ import { IMPACT_LEVELS } from '../audit/impact.ts' import * as s from '../schema.ts' -/** Countries with their own supervisory body and statute text. */ -export const COUNTRIES = ['AT', 'DE', 'CH', 'ES', 'FR', 'IT', 'NL'] as const +/** + * Countries with their own supervisory body and statute text. What is known + * about each one is in `countries.ts`. + */ +export const COUNTRIES = ['AT', 'BE', 'CH', 'DE', 'ES', 'FR', 'IE', 'IT', 'NL', 'PL', 'PT'] as const export type Country = (typeof COUNTRIES)[number] /** @@ -13,7 +16,7 @@ export type Country = (typeof COUNTRIES)[number] * has the language it is published in and English. `renderStatement` says which * ones a country has when asked for one it does not. */ -export const STATEMENT_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'nl'] as const +export const STATEMENT_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'nl', 'pl', 'pt'] as const export type StatementLocale = (typeof STATEMENT_LOCALES)[number] /** diff --git a/src/statement/render.ts b/src/statement/render.ts index 76c953d..71f2862 100644 --- a/src/statement/render.ts +++ b/src/statement/render.ts @@ -273,6 +273,8 @@ const DATE_LOCALES: Record = { fr: 'fr-FR', it: 'it-IT', nl: 'nl-NL', + pl: 'pl-PL', + pt: 'pt-PT', } /** diff --git a/src/statement/templates/be.en.md b/src/statement/templates/be.en.md new file mode 100644 index 0000000..75e6ee9 --- /dev/null +++ b/src/statement/templates/be.en.md @@ -0,0 +1,142 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the Code of Economic Law as amended by the Act of 5 November 2023, which +transposes Directive (EU) 2019/882 (the European Accessibility Act) into Belgian law for +e-commerce and consumer banking services. Its obligations apply from 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of these obligations. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the matter to the FPS Economy, +whose Directorate-General for Economic Inspection supervises compliance by e-commerce +services. + +FPS Economy, S.M.E.s, Self-employed and Energy +https://economie.fgov.be + +Supervision in Belgium is split: other services the Directive covers, such as electronic +communications, are supervised by other authorities. Websites and mobile applications of +public sector bodies fall under a separate regime, which requires an accessibility +statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/be.fr.md b/src/statement/templates/be.fr.md new file mode 100644 index 0000000..a3c3656 --- /dev/null +++ b/src/statement/templates/be.fr.md @@ -0,0 +1,146 @@ +# Déclaration d'accessibilité + +{{ provider.legalName }} s'engage à rendre le site {{ site.name }} accessible, conformément +au Code de droit économique tel que modifié par la loi du 5 novembre 2023, qui transpose en +droit belge la directive (UE) 2019/882 (European Accessibility Act) pour les services de +commerce électronique et les services bancaires aux consommateurs. Ces obligations +s'appliquent depuis le 28 juin 2025. + +Cette déclaration d'accessibilité s'applique à {{ site.url }}. + +## État de conformité + +{{#if compliance.isCompliant}} +Ce site est totalement conforme à {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Ce site est partiellement conforme à {{ compliance.standard }}. Les contenus énumérés +ci-dessous ne sont pas accessibles, pour les motifs indiqués. +{{/if}} +{{#if compliance.isNonCompliant}} +Ce site n'est pas conforme à {{ compliance.standard }}. Les contenus énumérés ci-dessous ne +sont pas accessibles, pour les motifs indiqués. +{{/if}} + +## Contenus non accessibles + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Exigence concernée : {{ standards }} +{{/if}} +{{#if pageList}} + Pages concernées : {{ pageList }}{{#if hasMorePages}} et {{ morePages }} autres{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Motif : charge disproportionnée. +{{/if}} +{{#if isOutOfScope}} + Motif : le contenu n'entre pas dans le champ d'application de ces obligations. +{{/if}} +{{#if isFixPlanned}} + Motif : la barrière est connue et sa correction est engagée. +{{/if}} +{{#if remedyByFormatted}} + Correction prévue avant le : {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Détecté par un test automatisé (axe-core, règle {{ ruleId }}) ; à reformuler dans vos propres mots. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Aucun contenu non accessible n'était connu au moment de l'évaluation. +{{/if}} + +## Établissement de cette déclaration + +Cette déclaration a été établie le {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Elle repose sur une auto-évaluation réalisée par {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Elle repose sur une évaluation réalisée par un tiers. +{{/if}} + +{{#if review.isSingle}} +Un des {{ review.total }} critères de succès des WCAG 2.2 (niveaux A et AA) a été vérifié +manuellement. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} des {{ review.total }} critères de succès des WCAG 2.2 (niveaux A et +AA) ont été vérifiés manuellement. +{{/if}} +{{#if review.hasDate}} +La plus récente de ces vérifications manuelles a été effectuée le +{{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Le test automatisé du {{ audit.checkedOnFormatted }} a porté sur une page de ce site. +{{/if}} +{{#if audit.isMultiPage}} +Le test automatisé du {{ audit.checkedOnFormatted }} a porté sur {{ audit.pages }} pages de +ce site. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Une autre vérification de règle demande une appréciation humaine. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} autres vérifications de règles demandent une appréciation humaine. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Une vérification de règle n'a pu être tranchée par l'outil utilisé ; elle n'est pas +présentée comme satisfaite. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} vérifications de règles n'ont pu être tranchées par l'outil +utilisé ; elles ne sont pas présentées comme satisfaites. +{{/if}} +{{#if hasAudit}} + +{{/if}} +L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne +détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un +test avec des technologies d'assistance. + +## Retour d'information et contact + +Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme +accessible ? Écrivez-nous : + +- Courriel : {{ provider.email }} +{{#if provider.feedbackUrl}} +- Formulaire de contact : {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Téléphone : {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adresse : {{ provider.address }} +{{/if}} + +Nous nous efforçons de répondre à votre message dans les meilleurs délais. + +## Voies de recours + +Si votre demande reste sans réponse satisfaisante, vous pouvez signaler le problème au +SPF Économie, dont la Direction générale de l'Inspection économique contrôle le respect de +ces obligations par les services de commerce électronique. + +SPF Économie, P.M.E., Classes moyennes et Énergie +https://economie.fgov.be + +En Belgique, le contrôle est réparti : d'autres services visés par la directive, comme les +communications électroniques, relèvent d'autres autorités. Les sites internet et +applications mobiles des organismes du secteur public relèvent d'un régime distinct, qui +prévoit sa propre déclaration d'accessibilité ; le présent document n'en tient pas lieu. + +--- + +Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. +Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute. diff --git a/src/statement/templates/be.nl.md b/src/statement/templates/be.nl.md new file mode 100644 index 0000000..29c58d7 --- /dev/null +++ b/src/statement/templates/be.nl.md @@ -0,0 +1,147 @@ +# Toegankelijkheidsverklaring + +{{ provider.legalName }} zet zich in om de website {{ site.name }} toegankelijk te maken, +in overeenstemming met het Wetboek van economisch recht zoals gewijzigd door de wet van +5 november 2023, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) voor +diensten op het gebied van elektronische handel en bankdiensten voor consumenten in +Belgisch recht is omgezet. Deze verplichtingen gelden sinds 28 juni 2025. + +Deze toegankelijkheidsverklaring geldt voor {{ site.url }}. + +## Nalevingsstatus + +{{#if compliance.isCompliant}} +Deze website voldoet volledig aan {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Deze website voldoet gedeeltelijk aan {{ compliance.standard }}. De inhoud die hieronder +staat is niet toegankelijk, om de genoemde redenen. +{{/if}} +{{#if compliance.isNonCompliant}} +Deze website voldoet niet aan {{ compliance.standard }}. De inhoud die hieronder staat is +niet toegankelijk, om de genoemde redenen. +{{/if}} + +## Niet-toegankelijke inhoud + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Betrokken eis: {{ standards }} +{{/if}} +{{#if pageList}} + Betrokken pagina's: {{ pageList }}{{#if hasMorePages}} en {{ morePages }} andere{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reden: onevenredige last. +{{/if}} +{{#if isOutOfScope}} + Reden: de inhoud valt buiten het toepassingsgebied van deze verplichtingen. +{{/if}} +{{#if isFixPlanned}} + Reden: de drempel is bekend en wordt verholpen. +{{/if}} +{{#if remedyByFormatted}} + Verwacht verholpen op: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Vastgesteld met een geautomatiseerde test (axe-core, regel {{ ruleId }}); beschrijf dit in eigen woorden. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Op het moment van de beoordeling was er geen niet-toegankelijke inhoud bekend. +{{/if}} + +## Opstelling van deze verklaring + +Deze verklaring is opgesteld op {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Zij berust op een zelfbeoordeling door {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Zij berust op een beoordeling door een derde partij. +{{/if}} + +{{#if review.isSingle}} +Eén van de {{ review.total }} succescriteria van WCAG 2.2 (niveaus A en AA) is handmatig +gecontroleerd. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} van de {{ review.total }} succescriteria van WCAG 2.2 (niveaus A en +AA) zijn handmatig gecontroleerd. +{{/if}} +{{#if review.hasDate}} +De meest recente van deze handmatige controles is uitgevoerd op +{{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +De geautomatiseerde test van {{ audit.checkedOnFormatted }} betrof één pagina van deze +website. +{{/if}} +{{#if audit.isMultiPage}} +De geautomatiseerde test van {{ audit.checkedOnFormatted }} betrof {{ audit.pages }} +pagina's van deze website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Voor één andere regelcontrole is een menselijke beoordeling nodig. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Voor {{ audit.needsReview }} andere regelcontroles is een menselijke beoordeling nodig. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Bij één regelcontrole kwam het gebruikte gereedschap niet tot een uitkomst; die wordt niet +als voldaan gepresenteerd. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Bij {{ audit.notEvaluated }} regelcontroles kwam het gebruikte gereedschap niet tot een +uitkomst; die worden niet als voldaan gepresenteerd. +{{/if}} +{{#if hasAudit}} + +{{/if}} +De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen +vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen +test met hulptechnologie. + +## Reactie en contact + +Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons +weten: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contactformulier: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefoon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adres: {{ provider.address }} +{{/if}} + +Wij streven ernaar snel te reageren. + +## Handhavingsprocedure + +Bent u niet tevreden met onze reactie, dan kunt u het probleem melden bij de FOD Economie, +waarvan de Algemene Directie Economische Inspectie toeziet op de naleving door diensten op +het gebied van elektronische handel. + +FOD Economie, K.M.O., Middenstand en Energie +https://economie.fgov.be + +Het toezicht is in België verdeeld: voor andere diensten waarop de richtlijn van toepassing +is, zoals elektronische communicatie, zijn andere toezichthouders bevoegd. Websites en +mobiele applicaties van overheidsinstanties vallen onder een afzonderlijke regeling, met een +eigen toegankelijkheidsverklaring; dit document vervangt die niet. + +--- + +Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u +haar publiceert en laat haar bij twijfel juridisch toetsen. diff --git a/src/statement/templates/ie.en.md b/src/statement/templates/ie.en.md new file mode 100644 index 0000000..5f263b6 --- /dev/null +++ b/src/statement/templates/ie.en.md @@ -0,0 +1,144 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the European Union (Accessibility Requirements of Products and Services) +Regulations 2023 (S.I. No. 636 of 2023), which transpose Directive (EU) 2019/882 (the +European Accessibility Act) into Irish law. They apply from 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of these Regulations. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the Competition and +Consumer Protection Commission (CCPC), which enforces the accessibility requirements for +e-commerce services and e-books. + +Competition and Consumer Protection Commission +https://www.ccpc.ie + +Supervision is split by sector: electronic communications services are overseen by the +Commission for Communications Regulation (ComReg), and consumer banking services by the +Central Bank of Ireland. + +Websites and mobile applications of public sector bodies fall under a separate regime, +which requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/pl.en.md b/src/statement/templates/pl.en.md new file mode 100644 index 0000000..643af9e --- /dev/null +++ b/src/statement/templates/pl.en.md @@ -0,0 +1,143 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the Act of 26 April 2024 on ensuring compliance with the accessibility +requirements of certain products and services by economic operators (Journal of Laws 2024, +item 731), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Polish law. It has been in force since 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of this Act. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the lack of accessibility to the +President of the Management Board of the State Fund for Rehabilitation of Disabled Persons +(PFRON). The President receives reports about every product and service the Act covers, and +either deals with a report or passes it to the competent market surveillance authority, +which for e-commerce services is the minister responsible for digital affairs. + +Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych (PFRON) +https://www.pfron.org.pl + +Websites and mobile applications of public bodies fall under separate legislation, which +requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/pl.pl.md b/src/statement/templates/pl.pl.md new file mode 100644 index 0000000..6addcf1 --- /dev/null +++ b/src/statement/templates/pl.pl.md @@ -0,0 +1,144 @@ +# Deklaracja dostępności + +{{ provider.legalName }} zobowiązuje się zapewnić dostępność strony internetowej +{{ site.name }} zgodnie z ustawą z dnia 26 kwietnia 2024 r. o zapewnianiu spełniania +wymagań dostępności niektórych produktów i usług przez podmioty gospodarcze +(Dz.U. 2024 poz. 731), która wdraża do prawa polskiego dyrektywę (UE) 2019/882 +(Europejski akt o dostępności). Ustawa obowiązuje od 28 czerwca 2025 r. + +Niniejsza deklaracja dostępności dotyczy strony {{ site.url }}. + +## Status zgodności + +{{#if compliance.isCompliant}} +Ta strona internetowa jest w pełni zgodna z {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Ta strona internetowa jest częściowo zgodna z {{ compliance.standard }}. Treści wymienione +poniżej nie są dostępne z podanych przyczyn. +{{/if}} +{{#if compliance.isNonCompliant}} +Ta strona internetowa nie jest zgodna z {{ compliance.standard }}. Treści wymienione +poniżej nie są dostępne z podanych przyczyn. +{{/if}} + +## Treści niedostępne + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Dotyczy wymagania: {{ standards }} +{{/if}} +{{#if pageList}} + Dotyczy stron: {{ pageList }}{{#if hasMorePages}} oraz innych (liczba: {{ morePages }}){{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Przyczyna: nieproporcjonalne obciążenie. +{{/if}} +{{#if isOutOfScope}} + Przyczyna: treść nie jest objęta zakresem stosowania ustawy. +{{/if}} +{{#if isFixPlanned}} + Przyczyna: bariera jest znana i trwa jej usuwanie. +{{/if}} +{{#if remedyByFormatted}} + Przewidywany termin usunięcia: {{ remedyByFormatted }} r. +{{/if}} +{{#if isFromAudit}} + Wykryto w teście automatycznym (axe-core, reguła {{ ruleId }}); opisz to własnymi słowami. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +W chwili oceny nie były znane żadne treści niedostępne. +{{/if}} + +## Sporządzenie deklaracji + +Deklarację sporządzono dnia {{ compliance.assessedOnFormatted }} r. + +{{#if compliance.isSelfAssessment}} +Deklaracja opiera się na samoocenie przeprowadzonej przez {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Deklaracja opiera się na ocenie przeprowadzonej przez podmiot zewnętrzny. +{{/if}} + +{{#if review.isSingle}} +Jedno z {{ review.total }} kryteriów sukcesu WCAG 2.2 (poziomy A i AA) zostało sprawdzone +ręcznie. +{{/if}} +{{#if review.isPlural}} +Liczba kryteriów sukcesu WCAG 2.2 (poziomy A i AA) sprawdzonych ręcznie: +{{ review.answered }} z {{ review.total }}. +{{/if}} +{{#if review.hasDate}} +Ostatnie z tych ręcznych sprawdzeń odnotowano dnia {{ review.checkedOnFormatted }} r. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Test automatyczny z dnia {{ audit.checkedOnFormatted }} r. objął jedną stronę tej witryny. +{{/if}} +{{#if audit.isMultiPage}} +Test automatyczny z dnia {{ audit.checkedOnFormatted }} r. objął następującą liczbę stron +tej witryny: {{ audit.pages }}. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Jedna dalsza kontrola reguły wymaga oceny przez człowieka. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Liczba dalszych kontroli reguł wymagających oceny przez człowieka: {{ audit.needsReview }}. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Jednej kontroli reguły użyte narzędzie nie mogło rozstrzygnąć; nie jest ona przedstawiana +jako spełniona. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Liczba kontroli reguł, których użyte narzędzie nie mogło rozstrzygnąć: +{{ audit.notEvaluated }}; nie są one przedstawiane jako spełnione. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Ocena opiera się częściowo na testach automatycznych. Narzędzia automatyczne wykrywają +tylko część możliwych barier; nie zastępują testów ręcznych ani testów z użyciem +technologii wspomagających. + +## Informacje zwrotne i dane kontaktowe + +Napotkali Państwo barierę lub potrzebują informacji w dostępnej formie? Prosimy o kontakt: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Formularz kontaktowy: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adres: {{ provider.address }} +{{/if}} + +Staramy się odpowiadać niezwłocznie. + +## Zgłoszenie braku dostępności + +Jeśli nie są Państwo zadowoleni z naszej odpowiedzi, mogą Państwo zgłosić brak dostępności +Prezesowi Zarządu Państwowego Funduszu Rehabilitacji Osób Niepełnosprawnych (PFRON). Prezes +Zarządu PFRON przyjmuje zgłoszenia dotyczące wszystkich produktów i usług objętych ustawą +i rozpatruje je sam albo przekazuje właściwemu organowi nadzoru rynku; dla usług handlu +elektronicznego jest nim minister właściwy do spraw informatyzacji. + +Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych +https://www.pfron.org.pl + +Strony internetowe i aplikacje mobilne podmiotów publicznych podlegają odrębnym przepisom, +które przewidują własną deklarację dostępności. Niniejszy dokument jej nie zastępuje. + +--- + +Niniejsza deklaracja została wygenerowana za pomocą eaa-kit i nie stanowi porady prawnej. +Przed publikacją należy ją przejrzeć, a w razie wątpliwości skonsultować z prawnikiem. diff --git a/src/statement/templates/pt.en.md b/src/statement/templates/pt.en.md new file mode 100644 index 0000000..0db4909 --- /dev/null +++ b/src/statement/templates/pt.en.md @@ -0,0 +1,142 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Decree-Law No. 82/2022 of 6 December, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into Portuguese law. Its obligations for products +and services apply from 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of this Decree-Law. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the competent supervisory +authority. Supervision is split between several authorities: for e-commerce services it is +the National Communications Authority (ANACOM), while banking services, for example, are +supervised by the Banco de Portugal. The supervisory authorities report non-compliant +products and services to the National Institute for Rehabilitation (INR, I.P.). + +Autoridade Nacional de Comunicações (ANACOM) +https://www.anacom.pt + +Websites and mobile applications of public sector bodies fall under a separate regime, +which requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/pt.pt.md b/src/statement/templates/pt.pt.md new file mode 100644 index 0000000..1cf51f0 --- /dev/null +++ b/src/statement/templates/pt.pt.md @@ -0,0 +1,145 @@ +# Declaração de Acessibilidade + +{{ provider.legalName }} compromete-se a tornar o sítio web {{ site.name }} acessível, nos +termos do Decreto-Lei n.º 82/2022, de 6 de dezembro, que transpõe para a ordem jurídica +portuguesa a Diretiva (UE) 2019/882 (Ato Europeu da Acessibilidade). As suas obrigações +relativas a produtos e serviços aplicam-se desde 28 de junho de 2025. + +A presente declaração de acessibilidade aplica-se a {{ site.url }}. + +## Estado de conformidade + +{{#if compliance.isCompliant}} +Este sítio web está totalmente conforme com {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Este sítio web está parcialmente conforme com {{ compliance.standard }}. Os conteúdos +indicados abaixo não são acessíveis, pelos motivos apresentados. +{{/if}} +{{#if compliance.isNonCompliant}} +Este sítio web não está conforme com {{ compliance.standard }}. Os conteúdos indicados +abaixo não são acessíveis, pelos motivos apresentados. +{{/if}} + +## Conteúdos não acessíveis + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requisito afetado: {{ standards }} +{{/if}} +{{#if pageList}} + Páginas afetadas: {{ pageList }}{{#if hasMorePages}} e mais {{ morePages }}{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Motivo: encargo desproporcionado. +{{/if}} +{{#if isOutOfScope}} + Motivo: o conteúdo não se enquadra no âmbito de aplicação deste decreto-lei. +{{/if}} +{{#if isFixPlanned}} + Motivo: a barreira é conhecida e está a ser corrigida. +{{/if}} +{{#if remedyByFormatted}} + Correção prevista até: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detetado por um teste automático (axe-core, regra {{ ruleId }}); descreva-o por palavras suas. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No momento da avaliação não era conhecido nenhum conteúdo não acessível. +{{/if}} + +## Elaboração desta declaração + +A presente declaração foi elaborada em {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Baseia-se numa autoavaliação realizada por {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Baseia-se numa avaliação realizada por terceiros. +{{/if}} + +{{#if review.isSingle}} +Um dos {{ review.total }} critérios de sucesso das WCAG 2.2 (níveis A e AA) foi verificado +manualmente. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} dos {{ review.total }} critérios de sucesso das WCAG 2.2 (níveis A e +AA) foram verificados manualmente. +{{/if}} +{{#if review.hasDate}} +A mais recente dessas verificações manuais foi registada em {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +O teste automático de {{ audit.checkedOnFormatted }} abrangeu uma página deste sítio web. +{{/if}} +{{#if audit.isMultiPage}} +O teste automático de {{ audit.checkedOnFormatted }} abrangeu {{ audit.pages }} páginas +deste sítio web. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Uma outra verificação de regra requer uma decisão humana. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} outras verificações de regras requerem uma decisão humana. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Uma verificação de regra não pôde ser decidida pela ferramenta utilizada; não é apresentada +como cumprida. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} verificações de regras não puderam ser decididas pela ferramenta +utilizada; não são apresentadas como cumpridas. +{{/if}} +{{#if hasAudit}} + +{{/if}} +A avaliação baseia-se em parte em testes automáticos. As ferramentas automáticas detetam +apenas uma parte das barreiras possíveis; não substituem testes manuais nem testes com +tecnologias de apoio. + +## Comentários e contactos + +Encontrou uma barreira ou precisa de informação num formato acessível? Contacte-nos: + +- Correio eletrónico: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Formulário de contacto: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Morada: {{ provider.address }} +{{/if}} + +Procuramos responder com a maior brevidade possível. + +## Procedimento de reclamação + +Se não ficar satisfeito com a nossa resposta, pode apresentar uma reclamação à entidade +fiscalizadora competente. A fiscalização está repartida por várias entidades: para os +serviços de comércio eletrónico é a Autoridade Nacional de Comunicações (ANACOM), e os +serviços bancários, por exemplo, são fiscalizados pelo Banco de Portugal. As entidades +fiscalizadoras comunicam os produtos e serviços que não cumprem os requisitos de +acessibilidade ao Instituto Nacional para a Reabilitação (INR, I.P.). + +Autoridade Nacional de Comunicações (ANACOM) +https://www.anacom.pt + +Os sítios web e as aplicações móveis dos organismos do setor público estão sujeitos a um +regime próprio, que prevê a sua própria declaração de acessibilidade. O presente documento +não a substitui. + +--- + +Esta declaração foi gerada com o eaa-kit e não constitui aconselhamento jurídico. Reveja-a +antes de a publicar e, em caso de dúvida, peça a um jurista que a verifique. diff --git a/tests/cli/countries.test.ts b/tests/cli/countries.test.ts new file mode 100644 index 0000000..2d1d156 --- /dev/null +++ b/tests/cli/countries.test.ts @@ -0,0 +1,83 @@ +import { readdir } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { formatCountries } from '../../src/cli/countries.ts' +import { COUNTRY_INFO, findCountry } from '../../src/config/countries.ts' +import { COUNTRIES, STATEMENT_LOCALES } from '../../src/config/define.ts' + +const TEMPLATES = path.join(import.meta.dirname, '../../src/statement/templates') + +describe('the country registry', () => { + it('lists exactly the templates that exist', async () => { + // Both directions: a template nobody registered is a document the CLI will + // not offer, and a registered language with no template is a promise the + // statement command breaks with an error. + const onDisk = (await readdir(TEMPLATES)) + .filter((file) => file.endsWith('.md')) + .map((file) => file.replace(/\.md$/, '')) + .sort() + const registered = COUNTRIES.flatMap((code) => + COUNTRY_INFO[code].languages.map((locale) => `${code.toLowerCase()}.${locale}`), + ).sort() + + expect(registered).toEqual(onDisk) + }) + + it('gives every country English, and its own language first', () => { + for (const code of COUNTRIES) { + const { languages } = COUNTRY_INFO[code] + expect(languages).toContain('en') + expect(STATEMENT_LOCALES).toEqual(expect.arrayContaining([...languages])) + } + }) + + it('offers a site locale in a language the country has a statement in', () => { + for (const code of COUNTRIES) { + const { languages, siteLocale } = COUNTRY_INFO[code] + expect(languages).toContain(siteLocale.split('-')[0]) + } + }) +}) + +describe('findCountry', () => { + it.each([ + ['pl', 'PL'], + ['PT', 'PT'], + [' Belgium ', 'BE'], + ['ireland', 'IE'], + ])('reads %j as %s', (typed, expected) => { + expect(findCountry(typed)).toBe(expected) + }) + + it.each(['', 'Narnia', 'EU', 'GB'])('does not guess at %j', (typed) => { + expect(findCountry(typed)).toBeUndefined() + }) +}) + +describe('eaa-kit countries', () => { + it('names every country, its languages, statute and authority', () => { + const text = formatCountries({ color: false }) + + for (const code of COUNTRIES) { + const info = COUNTRY_INFO[code] + expect(text).toContain(`${code} ${info.name} (${info.languages.join(', ')})`) + expect(text).toContain(info.statute) + expect(text).toContain(info.authority) + } + }) + + it('says which countries have not had their citations checked', () => { + const text = formatCountries({ color: false }) + const flagged = text.split('\n\n').filter((block) => block.includes('not yet checked')) + + expect(flagged.map((block) => block.slice(0, 2)).sort()).toEqual( + COUNTRIES.filter((code) => COUNTRY_INFO[code].unverified).sort(), + ) + }) + + it('prints JSON with the code on every entry', () => { + const parsed = JSON.parse(formatCountries({ json: true })) as Array<{ code: string }> + + expect(parsed.map((entry) => entry.code)).toEqual([...COUNTRIES]) + }) +}) diff --git a/tests/cli/init.test.ts b/tests/cli/init.test.ts index cfefe23..c161784 100644 --- a/tests/cli/init.test.ts +++ b/tests/cli/init.test.ts @@ -117,11 +117,37 @@ describe('runInitCommand', () => { expect((await written(cwd)).site).toMatchObject({ locale: 'de-CH' }) }) - it('falls back rather than throwing away the answers on a bad country', async () => { + it('takes a country by its English name, in any case', async () => { const cwd = await project() - await runInitCommand({ cwd, ask: answers('S', 'https://s.at', 'Austria', '', 'S', 'a@s.at') }) + await runInitCommand({ cwd, ask: answers('S', 'https://s.pl', 'poland', '', 'S', 'a@s.pl') }) + + expect((await written(cwd)).enforcement).toMatchObject({ country: 'PL' }) + expect((await written(cwd)).site).toMatchObject({ locale: 'pl-PL' }) + }) + + it('asks again rather than turning an unknown country into Austria', async () => { + const cwd = await project() + await runInitCommand({ + cwd, + ask: answers('S', 'https://s.pt', 'Narnia', 'pt', '', 'S', 'a@s.pt'), + }) + + expect((await written(cwd)).enforcement).toMatchObject({ country: 'PT' }) + expect(stderr.join('')).toContain('Narnia is not one of the countries') + }) + + it('falls back out loud rather than throwing away the answers', async () => { + // Three wrong answers in a row, and the rest are still worth keeping. The + // default is written, and said to be a default rather than an answer. + const cwd = await project() + await runInitCommand({ + cwd, + ask: answers('S', 'https://s.at', 'x', 'y', 'z', '', 'S', 'a@s.at'), + }) expect((await written(cwd)).enforcement).toMatchObject({ country: 'AT' }) + expect((await written(cwd)).provider).toMatchObject({ email: 'a@s.at' }) + expect(stderr.join('')).toContain('enforcement.country is set to AT because z') }) it('leaves the optional feedback URL out rather than writing an empty one', async () => { diff --git a/tests/statement/__snapshots__/statement.be.en.html b/tests/statement/__snapshots__/statement.be.en.html new file mode 100644 index 0000000..acdaee8 --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the Code of Economic Law as amended by the Act of 5 November 2023, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Belgian law for e-commerce and consumer banking services. Its obligations apply from 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can report the matter to the FPS Economy, whose Directorate-General for Economic Inspection supervises compliance by e-commerce services.

+

FPS Economy, S.M.E.s, Self-employed and Energy https://economie.fgov.be

+

Supervision in Belgium is split: other services the Directive covers, such as electronic communications, are supervised by other authorities. Websites and mobile applications of public sector bodies fall under a separate regime, which requires an accessibility statement of its own. This document is not that statement.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.be.en.md b/tests/statement/__snapshots__/statement.be.en.md new file mode 100644 index 0000000..ef1fa62 --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.en.md @@ -0,0 +1,87 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the Code of Economic Law as amended by the Act of 5 November 2023, which +transposes Directive (EU) 2019/882 (the European Accessibility Act) into Belgian law for +e-commerce and consumer banking services. Its obligations apply from 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the matter to the FPS Economy, +whose Directorate-General for Economic Inspection supervises compliance by e-commerce +services. + +FPS Economy, S.M.E.s, Self-employed and Energy +https://economie.fgov.be + +Supervision in Belgium is split: other services the Directive covers, such as electronic +communications, are supervised by other authorities. Websites and mobile applications of +public sector bodies fall under a separate regime, which requires an accessibility +statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.be.fr.html b/tests/statement/__snapshots__/statement.be.fr.html new file mode 100644 index 0000000..ad4f14e --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.fr.html @@ -0,0 +1,84 @@ + + + + + + +Déclaration d'accessibilité + + + +
+

Déclaration d'accessibilité

+

Musterbetrieb GmbH s'engage à rendre le site Musterbetrieb accessible, conformément au Code de droit économique tel que modifié par la loi du 5 novembre 2023, qui transpose en droit belge la directive (UE) 2019/882 (European Accessibility Act) pour les services de commerce électronique et les services bancaires aux consommateurs. Ces obligations s'appliquent depuis le 28 juin 2025.

+

Cette déclaration d'accessibilité s'applique à https://example.at.

+

État de conformité

+

Ce site est partiellement conforme à EN 301 549 V3.2.1 (WCAG 2.2 AA). Les contenus énumérés ci-dessous ne sont pas accessibles, pour les motifs indiqués.

+

Contenus non accessibles

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Exigence concernée : WCAG 4.1.2, EN 301 549 9.4.1.2
    + Motif : la barrière est connue et sa correction est engagée.
    + Correction prévue avant le : 31 décembre 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Motif : charge disproportionnée.
  • +
  • Form field must not have multiple label elements
    + Pages concernées : index.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle form-field-multiple-labels) ; à reformuler dans vos propres mots.
  • +
  • Images must have alternative text
    + Exigence concernée : WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages concernées : index.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle image-alt) ; à reformuler dans vos propres mots.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Exigence concernée : WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages concernées : blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html et 2 autres
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle color-contrast) ; à reformuler dans vos propres mots.
  • +
  • Document should have one main landmark
    + Exigence concernée : WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages concernées : kontakt.html, team.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle landmark-one-main) ; à reformuler dans vos propres mots.
  • +
+

Établissement de cette déclaration

+

Cette déclaration a été établie le 21 août 2026.

+

Elle repose sur une auto-évaluation réalisée par Musterbetrieb GmbH.

+

Le test automatisé du 21 août 2026 a porté sur 8 pages de ce site. 2 autres vérifications de règles demandent une appréciation humaine. 12 vérifications de règles n'ont pu être tranchées par l'outil utilisé ; elles ne sont pas présentées comme satisfaites.

+

L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un test avec des technologies d'assistance.

+

Retour d'information et contact

+

Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme accessible ? Écrivez-nous :

+ +

Nous nous efforçons de répondre à votre message dans les meilleurs délais.

+

Voies de recours

+

Si votre demande reste sans réponse satisfaisante, vous pouvez signaler le problème au SPF Économie, dont la Direction générale de l'Inspection économique contrôle le respect de ces obligations par les services de commerce électronique.

+

SPF Économie, P.M.E., Classes moyennes et Énergie https://economie.fgov.be

+

En Belgique, le contrôle est réparti : d'autres services visés par la directive, comme les communications électroniques, relèvent d'autres autorités. Les sites internet et applications mobiles des organismes du secteur public relèvent d'un régime distinct, qui prévoit sa propre déclaration d'accessibilité ; le présent document n'en tient pas lieu.

+
+

Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute.

+
+ + diff --git a/tests/statement/__snapshots__/statement.be.fr.md b/tests/statement/__snapshots__/statement.be.fr.md new file mode 100644 index 0000000..b960846 --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.fr.md @@ -0,0 +1,89 @@ +# Déclaration d'accessibilité + +Musterbetrieb GmbH s'engage à rendre le site Musterbetrieb accessible, conformément +au Code de droit économique tel que modifié par la loi du 5 novembre 2023, qui transpose en +droit belge la directive (UE) 2019/882 (European Accessibility Act) pour les services de +commerce électronique et les services bancaires aux consommateurs. Ces obligations +s'appliquent depuis le 28 juin 2025. + +Cette déclaration d'accessibilité s'applique à https://example.at. + +## État de conformité + +Ce site est partiellement conforme à EN 301 549 V3.2.1 (WCAG 2.2 AA). Les contenus énumérés +ci-dessous ne sont pas accessibles, pour les motifs indiqués. + +## Contenus non accessibles + +- Die eingebettete Karte hat keinen Titel. + Exigence concernée : WCAG 4.1.2, EN 301 549 9.4.1.2 + Motif : la barrière est connue et sa correction est engagée. + Correction prévue avant le : 31 décembre 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Motif : charge disproportionnée. +- Form field must not have multiple label elements + Pages concernées : index.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle form-field-multiple-labels) ; à reformuler dans vos propres mots. +- Images must have alternative text + Exigence concernée : WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages concernées : index.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle image-alt) ; à reformuler dans vos propres mots. +- Elements must meet minimum color contrast ratio thresholds + Exigence concernée : WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages concernées : blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html et 2 autres + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle color-contrast) ; à reformuler dans vos propres mots. +- Document should have one main landmark + Exigence concernée : WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages concernées : kontakt.html, team.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle landmark-one-main) ; à reformuler dans vos propres mots. + +## Établissement de cette déclaration + +Cette déclaration a été établie le 21 août 2026. + +Elle repose sur une auto-évaluation réalisée par Musterbetrieb GmbH. + +Le test automatisé du 21 août 2026 a porté sur 8 pages de +ce site. +2 autres vérifications de règles demandent une appréciation humaine. +12 vérifications de règles n'ont pu être tranchées par l'outil +utilisé ; elles ne sont pas présentées comme satisfaites. + +L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne +détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un +test avec des technologies d'assistance. + +## Retour d'information et contact + +Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme +accessible ? Écrivez-nous : + +- Courriel : office@example.at +- Formulaire de contact : https://example.at/kontakt +- Téléphone : +43 1 2345678 +- Adresse : Hauptstraße 1, 1010 Wien + +Nous nous efforçons de répondre à votre message dans les meilleurs délais. + +## Voies de recours + +Si votre demande reste sans réponse satisfaisante, vous pouvez signaler le problème au +SPF Économie, dont la Direction générale de l'Inspection économique contrôle le respect de +ces obligations par les services de commerce électronique. + +SPF Économie, P.M.E., Classes moyennes et Énergie +https://economie.fgov.be + +En Belgique, le contrôle est réparti : d'autres services visés par la directive, comme les +communications électroniques, relèvent d'autres autorités. Les sites internet et +applications mobiles des organismes du secteur public relèvent d'un régime distinct, qui +prévoit sa propre déclaration d'accessibilité ; le présent document n'en tient pas lieu. + +--- + +Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. +Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute. diff --git a/tests/statement/__snapshots__/statement.be.nl.html b/tests/statement/__snapshots__/statement.be.nl.html new file mode 100644 index 0000000..0df286a --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.nl.html @@ -0,0 +1,84 @@ + + + + + + +Toegankelijkheidsverklaring + + + +
+

Toegankelijkheidsverklaring

+

Musterbetrieb GmbH zet zich in om de website Musterbetrieb toegankelijk te maken, in overeenstemming met het Wetboek van economisch recht zoals gewijzigd door de wet van 5 november 2023, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) voor diensten op het gebied van elektronische handel en bankdiensten voor consumenten in Belgisch recht is omgezet. Deze verplichtingen gelden sinds 28 juni 2025.

+

Deze toegankelijkheidsverklaring geldt voor https://example.at.

+

Nalevingsstatus

+

Deze website voldoet gedeeltelijk aan EN 301 549 V3.2.1 (WCAG 2.2 AA). De inhoud die hieronder staat is niet toegankelijk, om de genoemde redenen.

+

Niet-toegankelijke inhoud

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Betrokken eis: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reden: de drempel is bekend en wordt verholpen.
    + Verwacht verholpen op: 31 december 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reden: onevenredige last.
  • +
  • Form field must not have multiple label elements
    + Betrokken pagina's: index.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel form-field-multiple-labels); beschrijf dit in eigen woorden.
  • +
  • Images must have alternative text
    + Betrokken eis: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Betrokken pagina's: index.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel image-alt); beschrijf dit in eigen woorden.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Betrokken eis: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Betrokken pagina's: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html en 2 andere
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel color-contrast); beschrijf dit in eigen woorden.
  • +
  • Document should have one main landmark
    + Betrokken eis: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Betrokken pagina's: kontakt.html, team.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel landmark-one-main); beschrijf dit in eigen woorden.
  • +
+

Opstelling van deze verklaring

+

Deze verklaring is opgesteld op 21 augustus 2026.

+

Zij berust op een zelfbeoordeling door Musterbetrieb GmbH.

+

De geautomatiseerde test van 21 augustus 2026 betrof 8 pagina's van deze website. Voor 2 andere regelcontroles is een menselijke beoordeling nodig. Bij 12 regelcontroles kwam het gebruikte gereedschap niet tot een uitkomst; die worden niet als voldaan gepresenteerd.

+

De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen test met hulptechnologie.

+

Reactie en contact

+

Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons weten:

+ +

Wij streven ernaar snel te reageren.

+

Handhavingsprocedure

+

Bent u niet tevreden met onze reactie, dan kunt u het probleem melden bij de FOD Economie, waarvan de Algemene Directie Economische Inspectie toeziet op de naleving door diensten op het gebied van elektronische handel.

+

FOD Economie, K.M.O., Middenstand en Energie https://economie.fgov.be

+

Het toezicht is in België verdeeld: voor andere diensten waarop de richtlijn van toepassing is, zoals elektronische communicatie, zijn andere toezichthouders bevoegd. Websites en mobiele applicaties van overheidsinstanties vallen onder een afzonderlijke regeling, met een eigen toegankelijkheidsverklaring; dit document vervangt die niet.

+
+

Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u haar publiceert en laat haar bij twijfel juridisch toetsen.

+
+ + diff --git a/tests/statement/__snapshots__/statement.be.nl.md b/tests/statement/__snapshots__/statement.be.nl.md new file mode 100644 index 0000000..e1dee88 --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.nl.md @@ -0,0 +1,89 @@ +# Toegankelijkheidsverklaring + +Musterbetrieb GmbH zet zich in om de website Musterbetrieb toegankelijk te maken, +in overeenstemming met het Wetboek van economisch recht zoals gewijzigd door de wet van +5 november 2023, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) voor +diensten op het gebied van elektronische handel en bankdiensten voor consumenten in +Belgisch recht is omgezet. Deze verplichtingen gelden sinds 28 juni 2025. + +Deze toegankelijkheidsverklaring geldt voor https://example.at. + +## Nalevingsstatus + +Deze website voldoet gedeeltelijk aan EN 301 549 V3.2.1 (WCAG 2.2 AA). De inhoud die hieronder +staat is niet toegankelijk, om de genoemde redenen. + +## Niet-toegankelijke inhoud + +- Die eingebettete Karte hat keinen Titel. + Betrokken eis: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reden: de drempel is bekend en wordt verholpen. + Verwacht verholpen op: 31 december 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reden: onevenredige last. +- Form field must not have multiple label elements + Betrokken pagina's: index.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel form-field-multiple-labels); beschrijf dit in eigen woorden. +- Images must have alternative text + Betrokken eis: WCAG 1.1.1, EN 301 549 9.1.1.1 + Betrokken pagina's: index.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel image-alt); beschrijf dit in eigen woorden. +- Elements must meet minimum color contrast ratio thresholds + Betrokken eis: WCAG 1.4.3, EN 301 549 9.1.4.3 + Betrokken pagina's: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html en 2 andere + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel color-contrast); beschrijf dit in eigen woorden. +- Document should have one main landmark + Betrokken eis: WCAG 1.3.1, EN 301 549 9.1.3.1 + Betrokken pagina's: kontakt.html, team.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel landmark-one-main); beschrijf dit in eigen woorden. + +## Opstelling van deze verklaring + +Deze verklaring is opgesteld op 21 augustus 2026. + +Zij berust op een zelfbeoordeling door Musterbetrieb GmbH. + +De geautomatiseerde test van 21 augustus 2026 betrof 8 +pagina's van deze website. +Voor 2 andere regelcontroles is een menselijke beoordeling nodig. +Bij 12 regelcontroles kwam het gebruikte gereedschap niet tot een +uitkomst; die worden niet als voldaan gepresenteerd. + +De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen +vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen +test met hulptechnologie. + +## Reactie en contact + +Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons +weten: + +- E-mail: office@example.at +- Contactformulier: https://example.at/kontakt +- Telefoon: +43 1 2345678 +- Adres: Hauptstraße 1, 1010 Wien + +Wij streven ernaar snel te reageren. + +## Handhavingsprocedure + +Bent u niet tevreden met onze reactie, dan kunt u het probleem melden bij de FOD Economie, +waarvan de Algemene Directie Economische Inspectie toeziet op de naleving door diensten op +het gebied van elektronische handel. + +FOD Economie, K.M.O., Middenstand en Energie +https://economie.fgov.be + +Het toezicht is in België verdeeld: voor andere diensten waarop de richtlijn van toepassing +is, zoals elektronische communicatie, zijn andere toezichthouders bevoegd. Websites en +mobiele applicaties van overheidsinstanties vallen onder een afzonderlijke regeling, met een +eigen toegankelijkheidsverklaring; dit document vervangt die niet. + +--- + +Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u +haar publiceert en laat haar bij twijfel juridisch toetsen. diff --git a/tests/statement/__snapshots__/statement.ie.en.html b/tests/statement/__snapshots__/statement.ie.en.html new file mode 100644 index 0000000..1db34c7 --- /dev/null +++ b/tests/statement/__snapshots__/statement.ie.en.html @@ -0,0 +1,85 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the European Union (Accessibility Requirements of Products and Services) Regulations 2023 (S.I. No. 636 of 2023), which transpose Directive (EU) 2019/882 (the European Accessibility Act) into Irish law. They apply from 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can complain to the Competition and Consumer Protection Commission (CCPC), which enforces the accessibility requirements for e-commerce services and e-books.

+

Competition and Consumer Protection Commission https://www.ccpc.ie

+

Supervision is split by sector: electronic communications services are overseen by the Commission for Communications Regulation (ComReg), and consumer banking services by the Central Bank of Ireland.

+

Websites and mobile applications of public sector bodies fall under a separate regime, which requires an accessibility statement of its own. This document is not that statement.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.ie.en.md b/tests/statement/__snapshots__/statement.ie.en.md new file mode 100644 index 0000000..f0d5982 --- /dev/null +++ b/tests/statement/__snapshots__/statement.ie.en.md @@ -0,0 +1,89 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the European Union (Accessibility Requirements of Products and Services) +Regulations 2023 (S.I. No. 636 of 2023), which transpose Directive (EU) 2019/882 (the +European Accessibility Act) into Irish law. They apply from 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the Competition and +Consumer Protection Commission (CCPC), which enforces the accessibility requirements for +e-commerce services and e-books. + +Competition and Consumer Protection Commission +https://www.ccpc.ie + +Supervision is split by sector: electronic communications services are overseen by the +Commission for Communications Regulation (ComReg), and consumer banking services by the +Central Bank of Ireland. + +Websites and mobile applications of public sector bodies fall under a separate regime, +which requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.pl.en.html b/tests/statement/__snapshots__/statement.pl.en.html new file mode 100644 index 0000000..ec09591 --- /dev/null +++ b/tests/statement/__snapshots__/statement.pl.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the Act of 26 April 2024 on ensuring compliance with the accessibility requirements of certain products and services by economic operators (Journal of Laws 2024, item 731), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Polish law. It has been in force since 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can report the lack of accessibility to the President of the Management Board of the State Fund for Rehabilitation of Disabled Persons (PFRON). The President receives reports about every product and service the Act covers, and either deals with a report or passes it to the competent market surveillance authority, which for e-commerce services is the minister responsible for digital affairs.

+

Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych (PFRON) https://www.pfron.org.pl

+

Websites and mobile applications of public bodies fall under separate legislation, which requires an accessibility statement of its own. This document is not that statement.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.pl.en.md b/tests/statement/__snapshots__/statement.pl.en.md new file mode 100644 index 0000000..9b11136 --- /dev/null +++ b/tests/statement/__snapshots__/statement.pl.en.md @@ -0,0 +1,88 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the Act of 26 April 2024 on ensuring compliance with the accessibility +requirements of certain products and services by economic operators (Journal of Laws 2024, +item 731), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Polish law. It has been in force since 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the lack of accessibility to the +President of the Management Board of the State Fund for Rehabilitation of Disabled Persons +(PFRON). The President receives reports about every product and service the Act covers, and +either deals with a report or passes it to the competent market surveillance authority, +which for e-commerce services is the minister responsible for digital affairs. + +Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych (PFRON) +https://www.pfron.org.pl + +Websites and mobile applications of public bodies fall under separate legislation, which +requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.pl.pl.html b/tests/statement/__snapshots__/statement.pl.pl.html new file mode 100644 index 0000000..a06da2b --- /dev/null +++ b/tests/statement/__snapshots__/statement.pl.pl.html @@ -0,0 +1,84 @@ + + + + + + +Deklaracja dostępności + + + +
+

Deklaracja dostępności

+

Musterbetrieb GmbH zobowiązuje się zapewnić dostępność strony internetowej Musterbetrieb zgodnie z ustawą z dnia 26 kwietnia 2024 r. o zapewnianiu spełniania wymagań dostępności niektórych produktów i usług przez podmioty gospodarcze (Dz.U. 2024 poz. 731), która wdraża do prawa polskiego dyrektywę (UE) 2019/882 (Europejski akt o dostępności). Ustawa obowiązuje od 28 czerwca 2025 r.

+

Niniejsza deklaracja dostępności dotyczy strony https://example.at.

+

Status zgodności

+

Ta strona internetowa jest częściowo zgodna z EN 301 549 V3.2.1 (WCAG 2.2 AA). Treści wymienione poniżej nie są dostępne z podanych przyczyn.

+

Treści niedostępne

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Dotyczy wymagania: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Przyczyna: bariera jest znana i trwa jej usuwanie.
    + Przewidywany termin usunięcia: 31 grudnia 2026 r.
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Przyczyna: nieproporcjonalne obciążenie.
  • +
  • Form field must not have multiple label elements
    + Dotyczy stron: index.html
    + Przyczyna: bariera jest znana i trwa jej usuwanie.
    + Wykryto w teście automatycznym (axe-core, reguła form-field-multiple-labels); opisz to własnymi słowami.
  • +
  • Images must have alternative text
    + Dotyczy wymagania: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Dotyczy stron: index.html
    + Przyczyna: bariera jest znana i trwa jej usuwanie.
    + Wykryto w teście automatycznym (axe-core, reguła image-alt); opisz to własnymi słowami.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Dotyczy wymagania: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Dotyczy stron: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html oraz innych (liczba: 2)
    + Przyczyna: bariera jest znana i trwa jej usuwanie.
    + Wykryto w teście automatycznym (axe-core, reguła color-contrast); opisz to własnymi słowami.
  • +
  • Document should have one main landmark
    + Dotyczy wymagania: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Dotyczy stron: kontakt.html, team.html
    + Przyczyna: bariera jest znana i trwa jej usuwanie.
    + Wykryto w teście automatycznym (axe-core, reguła landmark-one-main); opisz to własnymi słowami.
  • +
+

Sporządzenie deklaracji

+

Deklarację sporządzono dnia 21 sierpnia 2026 r.

+

Deklaracja opiera się na samoocenie przeprowadzonej przez Musterbetrieb GmbH.

+

Test automatyczny z dnia 21 sierpnia 2026 r. objął następującą liczbę stron tej witryny: 8. Liczba dalszych kontroli reguł wymagających oceny przez człowieka: 2. Liczba kontroli reguł, których użyte narzędzie nie mogło rozstrzygnąć: 12; nie są one przedstawiane jako spełnione.

+

Ocena opiera się częściowo na testach automatycznych. Narzędzia automatyczne wykrywają tylko część możliwych barier; nie zastępują testów ręcznych ani testów z użyciem technologii wspomagających.

+

Informacje zwrotne i dane kontaktowe

+

Napotkali Państwo barierę lub potrzebują informacji w dostępnej formie? Prosimy o kontakt:

+ +

Staramy się odpowiadać niezwłocznie.

+

Zgłoszenie braku dostępności

+

Jeśli nie są Państwo zadowoleni z naszej odpowiedzi, mogą Państwo zgłosić brak dostępności Prezesowi Zarządu Państwowego Funduszu Rehabilitacji Osób Niepełnosprawnych (PFRON). Prezes Zarządu PFRON przyjmuje zgłoszenia dotyczące wszystkich produktów i usług objętych ustawą i rozpatruje je sam albo przekazuje właściwemu organowi nadzoru rynku; dla usług handlu elektronicznego jest nim minister właściwy do spraw informatyzacji.

+

Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych https://www.pfron.org.pl

+

Strony internetowe i aplikacje mobilne podmiotów publicznych podlegają odrębnym przepisom, które przewidują własną deklarację dostępności. Niniejszy dokument jej nie zastępuje.

+
+

Niniejsza deklaracja została wygenerowana za pomocą eaa-kit i nie stanowi porady prawnej. Przed publikacją należy ją przejrzeć, a w razie wątpliwości skonsultować z prawnikiem.

+
+ + diff --git a/tests/statement/__snapshots__/statement.pl.pl.md b/tests/statement/__snapshots__/statement.pl.pl.md new file mode 100644 index 0000000..3fef4ef --- /dev/null +++ b/tests/statement/__snapshots__/statement.pl.pl.md @@ -0,0 +1,88 @@ +# Deklaracja dostępności + +Musterbetrieb GmbH zobowiązuje się zapewnić dostępność strony internetowej +Musterbetrieb zgodnie z ustawą z dnia 26 kwietnia 2024 r. o zapewnianiu spełniania +wymagań dostępności niektórych produktów i usług przez podmioty gospodarcze +(Dz.U. 2024 poz. 731), która wdraża do prawa polskiego dyrektywę (UE) 2019/882 +(Europejski akt o dostępności). Ustawa obowiązuje od 28 czerwca 2025 r. + +Niniejsza deklaracja dostępności dotyczy strony https://example.at. + +## Status zgodności + +Ta strona internetowa jest częściowo zgodna z EN 301 549 V3.2.1 (WCAG 2.2 AA). Treści wymienione +poniżej nie są dostępne z podanych przyczyn. + +## Treści niedostępne + +- Die eingebettete Karte hat keinen Titel. + Dotyczy wymagania: WCAG 4.1.2, EN 301 549 9.4.1.2 + Przyczyna: bariera jest znana i trwa jej usuwanie. + Przewidywany termin usunięcia: 31 grudnia 2026 r. +- Ältere PDF-Dokumente sind nicht barrierefrei. + Przyczyna: nieproporcjonalne obciążenie. +- Form field must not have multiple label elements + Dotyczy stron: index.html + Przyczyna: bariera jest znana i trwa jej usuwanie. + Wykryto w teście automatycznym (axe-core, reguła form-field-multiple-labels); opisz to własnymi słowami. +- Images must have alternative text + Dotyczy wymagania: WCAG 1.1.1, EN 301 549 9.1.1.1 + Dotyczy stron: index.html + Przyczyna: bariera jest znana i trwa jej usuwanie. + Wykryto w teście automatycznym (axe-core, reguła image-alt); opisz to własnymi słowami. +- Elements must meet minimum color contrast ratio thresholds + Dotyczy wymagania: WCAG 1.4.3, EN 301 549 9.1.4.3 + Dotyczy stron: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html oraz innych (liczba: 2) + Przyczyna: bariera jest znana i trwa jej usuwanie. + Wykryto w teście automatycznym (axe-core, reguła color-contrast); opisz to własnymi słowami. +- Document should have one main landmark + Dotyczy wymagania: WCAG 1.3.1, EN 301 549 9.1.3.1 + Dotyczy stron: kontakt.html, team.html + Przyczyna: bariera jest znana i trwa jej usuwanie. + Wykryto w teście automatycznym (axe-core, reguła landmark-one-main); opisz to własnymi słowami. + +## Sporządzenie deklaracji + +Deklarację sporządzono dnia 21 sierpnia 2026 r. + +Deklaracja opiera się na samoocenie przeprowadzonej przez Musterbetrieb GmbH. + +Test automatyczny z dnia 21 sierpnia 2026 r. objął następującą liczbę stron +tej witryny: 8. +Liczba dalszych kontroli reguł wymagających oceny przez człowieka: 2. +Liczba kontroli reguł, których użyte narzędzie nie mogło rozstrzygnąć: +12; nie są one przedstawiane jako spełnione. + +Ocena opiera się częściowo na testach automatycznych. Narzędzia automatyczne wykrywają +tylko część możliwych barier; nie zastępują testów ręcznych ani testów z użyciem +technologii wspomagających. + +## Informacje zwrotne i dane kontaktowe + +Napotkali Państwo barierę lub potrzebują informacji w dostępnej formie? Prosimy o kontakt: + +- E-mail: office@example.at +- Formularz kontaktowy: https://example.at/kontakt +- Telefon: +43 1 2345678 +- Adres: Hauptstraße 1, 1010 Wien + +Staramy się odpowiadać niezwłocznie. + +## Zgłoszenie braku dostępności + +Jeśli nie są Państwo zadowoleni z naszej odpowiedzi, mogą Państwo zgłosić brak dostępności +Prezesowi Zarządu Państwowego Funduszu Rehabilitacji Osób Niepełnosprawnych (PFRON). Prezes +Zarządu PFRON przyjmuje zgłoszenia dotyczące wszystkich produktów i usług objętych ustawą +i rozpatruje je sam albo przekazuje właściwemu organowi nadzoru rynku; dla usług handlu +elektronicznego jest nim minister właściwy do spraw informatyzacji. + +Państwowy Fundusz Rehabilitacji Osób Niepełnosprawnych +https://www.pfron.org.pl + +Strony internetowe i aplikacje mobilne podmiotów publicznych podlegają odrębnym przepisom, +które przewidują własną deklarację dostępności. Niniejszy dokument jej nie zastępuje. + +--- + +Niniejsza deklaracja została wygenerowana za pomocą eaa-kit i nie stanowi porady prawnej. +Przed publikacją należy ją przejrzeć, a w razie wątpliwości skonsultować z prawnikiem. diff --git a/tests/statement/__snapshots__/statement.pt.en.html b/tests/statement/__snapshots__/statement.pt.en.html new file mode 100644 index 0000000..2c5f2bd --- /dev/null +++ b/tests/statement/__snapshots__/statement.pt.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Decree-Law No. 82/2022 of 6 December, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Portuguese law. Its obligations for products and services apply from 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can complain to the competent supervisory authority. Supervision is split between several authorities: for e-commerce services it is the National Communications Authority (ANACOM), while banking services, for example, are supervised by the Banco de Portugal. The supervisory authorities report non-compliant products and services to the National Institute for Rehabilitation (INR, I.P.).

+

Autoridade Nacional de Comunicações (ANACOM) https://www.anacom.pt

+

Websites and mobile applications of public sector bodies fall under a separate regime, which requires an accessibility statement of its own. This document is not that statement.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.pt.en.md b/tests/statement/__snapshots__/statement.pt.en.md new file mode 100644 index 0000000..c4b3302 --- /dev/null +++ b/tests/statement/__snapshots__/statement.pt.en.md @@ -0,0 +1,87 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Decree-Law No. 82/2022 of 6 December, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into Portuguese law. Its obligations for products +and services apply from 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the competent supervisory +authority. Supervision is split between several authorities: for e-commerce services it is +the National Communications Authority (ANACOM), while banking services, for example, are +supervised by the Banco de Portugal. The supervisory authorities report non-compliant +products and services to the National Institute for Rehabilitation (INR, I.P.). + +Autoridade Nacional de Comunicações (ANACOM) +https://www.anacom.pt + +Websites and mobile applications of public sector bodies fall under a separate regime, +which requires an accessibility statement of its own. This document is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.pt.pt.html b/tests/statement/__snapshots__/statement.pt.pt.html new file mode 100644 index 0000000..c4955a5 --- /dev/null +++ b/tests/statement/__snapshots__/statement.pt.pt.html @@ -0,0 +1,84 @@ + + + + + + +Declaração de Acessibilidade + + + +
+

Declaração de Acessibilidade

+

Musterbetrieb GmbH compromete-se a tornar o sítio web Musterbetrieb acessível, nos termos do Decreto-Lei n.º 82/2022, de 6 de dezembro, que transpõe para a ordem jurídica portuguesa a Diretiva (UE) 2019/882 (Ato Europeu da Acessibilidade). As suas obrigações relativas a produtos e serviços aplicam-se desde 28 de junho de 2025.

+

A presente declaração de acessibilidade aplica-se a https://example.at.

+

Estado de conformidade

+

Este sítio web está parcialmente conforme com EN 301 549 V3.2.1 (WCAG 2.2 AA). Os conteúdos indicados abaixo não são acessíveis, pelos motivos apresentados.

+

Conteúdos não acessíveis

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requisito afetado: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Motivo: a barreira é conhecida e está a ser corrigida.
    + Correção prevista até: 31 de dezembro de 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Motivo: encargo desproporcionado.
  • +
  • Form field must not have multiple label elements
    + Páginas afetadas: index.html
    + Motivo: a barreira é conhecida e está a ser corrigida.
    + Detetado por um teste automático (axe-core, regra form-field-multiple-labels); descreva-o por palavras suas.
  • +
  • Images must have alternative text
    + Requisito afetado: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Páginas afetadas: index.html
    + Motivo: a barreira é conhecida e está a ser corrigida.
    + Detetado por um teste automático (axe-core, regra image-alt); descreva-o por palavras suas.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requisito afetado: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Páginas afetadas: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html e mais 2
    + Motivo: a barreira é conhecida e está a ser corrigida.
    + Detetado por um teste automático (axe-core, regra color-contrast); descreva-o por palavras suas.
  • +
  • Document should have one main landmark
    + Requisito afetado: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Páginas afetadas: kontakt.html, team.html
    + Motivo: a barreira é conhecida e está a ser corrigida.
    + Detetado por um teste automático (axe-core, regra landmark-one-main); descreva-o por palavras suas.
  • +
+

Elaboração desta declaração

+

A presente declaração foi elaborada em 21 de agosto de 2026.

+

Baseia-se numa autoavaliação realizada por Musterbetrieb GmbH.

+

O teste automático de 21 de agosto de 2026 abrangeu 8 páginas deste sítio web. 2 outras verificações de regras requerem uma decisão humana. 12 verificações de regras não puderam ser decididas pela ferramenta utilizada; não são apresentadas como cumpridas.

+

A avaliação baseia-se em parte em testes automáticos. As ferramentas automáticas detetam apenas uma parte das barreiras possíveis; não substituem testes manuais nem testes com tecnologias de apoio.

+

Comentários e contactos

+

Encontrou uma barreira ou precisa de informação num formato acessível? Contacte-nos:

+ +

Procuramos responder com a maior brevidade possível.

+

Procedimento de reclamação

+

Se não ficar satisfeito com a nossa resposta, pode apresentar uma reclamação à entidade fiscalizadora competente. A fiscalização está repartida por várias entidades: para os serviços de comércio eletrónico é a Autoridade Nacional de Comunicações (ANACOM), e os serviços bancários, por exemplo, são fiscalizados pelo Banco de Portugal. As entidades fiscalizadoras comunicam os produtos e serviços que não cumprem os requisitos de acessibilidade ao Instituto Nacional para a Reabilitação (INR, I.P.).

+

Autoridade Nacional de Comunicações (ANACOM) https://www.anacom.pt

+

Os sítios web e as aplicações móveis dos organismos do setor público estão sujeitos a um regime próprio, que prevê a sua própria declaração de acessibilidade. O presente documento não a substitui.

+
+

Esta declaração foi gerada com o eaa-kit e não constitui aconselhamento jurídico. Reveja-a antes de a publicar e, em caso de dúvida, peça a um jurista que a verifique.

+
+ + diff --git a/tests/statement/__snapshots__/statement.pt.pt.md b/tests/statement/__snapshots__/statement.pt.pt.md new file mode 100644 index 0000000..b3d3a04 --- /dev/null +++ b/tests/statement/__snapshots__/statement.pt.pt.md @@ -0,0 +1,89 @@ +# Declaração de Acessibilidade + +Musterbetrieb GmbH compromete-se a tornar o sítio web Musterbetrieb acessível, nos +termos do Decreto-Lei n.º 82/2022, de 6 de dezembro, que transpõe para a ordem jurídica +portuguesa a Diretiva (UE) 2019/882 (Ato Europeu da Acessibilidade). As suas obrigações +relativas a produtos e serviços aplicam-se desde 28 de junho de 2025. + +A presente declaração de acessibilidade aplica-se a https://example.at. + +## Estado de conformidade + +Este sítio web está parcialmente conforme com EN 301 549 V3.2.1 (WCAG 2.2 AA). Os conteúdos +indicados abaixo não são acessíveis, pelos motivos apresentados. + +## Conteúdos não acessíveis + +- Die eingebettete Karte hat keinen Titel. + Requisito afetado: WCAG 4.1.2, EN 301 549 9.4.1.2 + Motivo: a barreira é conhecida e está a ser corrigida. + Correção prevista até: 31 de dezembro de 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Motivo: encargo desproporcionado. +- Form field must not have multiple label elements + Páginas afetadas: index.html + Motivo: a barreira é conhecida e está a ser corrigida. + Detetado por um teste automático (axe-core, regra form-field-multiple-labels); descreva-o por palavras suas. +- Images must have alternative text + Requisito afetado: WCAG 1.1.1, EN 301 549 9.1.1.1 + Páginas afetadas: index.html + Motivo: a barreira é conhecida e está a ser corrigida. + Detetado por um teste automático (axe-core, regra image-alt); descreva-o por palavras suas. +- Elements must meet minimum color contrast ratio thresholds + Requisito afetado: WCAG 1.4.3, EN 301 549 9.1.4.3 + Páginas afetadas: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html e mais 2 + Motivo: a barreira é conhecida e está a ser corrigida. + Detetado por um teste automático (axe-core, regra color-contrast); descreva-o por palavras suas. +- Document should have one main landmark + Requisito afetado: WCAG 1.3.1, EN 301 549 9.1.3.1 + Páginas afetadas: kontakt.html, team.html + Motivo: a barreira é conhecida e está a ser corrigida. + Detetado por um teste automático (axe-core, regra landmark-one-main); descreva-o por palavras suas. + +## Elaboração desta declaração + +A presente declaração foi elaborada em 21 de agosto de 2026. + +Baseia-se numa autoavaliação realizada por Musterbetrieb GmbH. + +O teste automático de 21 de agosto de 2026 abrangeu 8 páginas +deste sítio web. +2 outras verificações de regras requerem uma decisão humana. +12 verificações de regras não puderam ser decididas pela ferramenta +utilizada; não são apresentadas como cumpridas. + +A avaliação baseia-se em parte em testes automáticos. As ferramentas automáticas detetam +apenas uma parte das barreiras possíveis; não substituem testes manuais nem testes com +tecnologias de apoio. + +## Comentários e contactos + +Encontrou uma barreira ou precisa de informação num formato acessível? Contacte-nos: + +- Correio eletrónico: office@example.at +- Formulário de contacto: https://example.at/kontakt +- Telefone: +43 1 2345678 +- Morada: Hauptstraße 1, 1010 Wien + +Procuramos responder com a maior brevidade possível. + +## Procedimento de reclamação + +Se não ficar satisfeito com a nossa resposta, pode apresentar uma reclamação à entidade +fiscalizadora competente. A fiscalização está repartida por várias entidades: para os +serviços de comércio eletrónico é a Autoridade Nacional de Comunicações (ANACOM), e os +serviços bancários, por exemplo, são fiscalizados pelo Banco de Portugal. As entidades +fiscalizadoras comunicam os produtos e serviços que não cumprem os requisitos de +acessibilidade ao Instituto Nacional para a Reabilitação (INR, I.P.). + +Autoridade Nacional de Comunicações (ANACOM) +https://www.anacom.pt + +Os sítios web e as aplicações móveis dos organismos do setor público estão sujeitos a um +regime próprio, que prevê a sua própria declaração de acessibilidade. O presente documento +não a substitui. + +--- + +Esta declaração foi gerada com o eaa-kit e não constitui aconselhamento jurídico. Reveja-a +antes de a publicar e, em caso de dúvida, peça a um jurista que a verifique. diff --git a/tests/statement/render.test.ts b/tests/statement/render.test.ts index 08ddbc9..2a99ef1 100644 --- a/tests/statement/render.test.ts +++ b/tests/statement/render.test.ts @@ -114,7 +114,7 @@ describe('template selection', () => { await expect(renderStatement(config(), missing)).rejects.toThrow(StatementError) await expect(renderStatement(config(), missing)).rejects.toThrow( - /Available: at\.de, at\.en, ch\.de, ch\.en, de\.de, de\.en, es\.en/, + /Available: at\.de, at\.en, be\.en, be\.fr, be\.nl, ch\.de, ch\.en, de\.de/, ) }) }) @@ -283,6 +283,40 @@ describe('enforcement body', () => { expect(statement.markdown).toContain('Marktüberwachungsstelle der Länder') expect(statement.markdown).not.toContain('Sozialministeriumservice') }) + + it.each([ + ['BE', 'fr', 'SPF Économie'], + ['BE', 'nl', 'FOD Economie'], + ['BE', 'en', 'FPS Economy'], + ['IE', 'en', 'Competition and Consumer Protection Commission (CCPC)'], + ['PL', 'pl', 'Prezesowi Zarządu Państwowego Funduszu Rehabilitacji'], + ['PL', 'en', 'State Fund for Rehabilitation of Disabled Persons (PFRON)'], + ['PT', 'pt', 'Autoridade Nacional de Comunicações (ANACOM)'], + ['PT', 'en', 'National Communications Authority (ANACOM)'], + ] as Array<[Country, StatementLocale, string]>)( + 'names the %s authority in %s', + async (country, locale, authority) => { + const statement = await renderStatement(config(), { country, locale }) + + expect(flat(statement.markdown)).toContain(authority) + expect(statement.markdown).not.toContain('Sozialministeriumservice') + }, + ) + + it.each([ + ['BE', 'fr'], + ['BE', 'nl'], + ['BE', 'en'], + ['PT', 'pt'], + ['PT', 'en'], + ] as Array<[Country, StatementLocale]>)( + 'says supervision is split in %s/%s rather than naming one body as the whole of it', + async (country, locale) => { + const statement = await renderStatement(config(), { country, locale }) + + expect(flat(statement.markdown)).toMatch(/réparti|verdeeld|split|repartida/) + }, + ) }) describe('every template', () => { @@ -686,6 +720,14 @@ describe('what a person checked', () => { ['IT', 'en', 'success criteria in WCAG 2.2'], ['NL', 'nl', 'succescriteria van WCAG 2.2'], ['NL', 'en', 'success criteria in WCAG 2.2'], + ['BE', 'fr', 'critères de succès des WCAG 2.2'], + ['BE', 'nl', 'succescriteria van WCAG 2.2'], + ['BE', 'en', 'success criteria in WCAG 2.2'], + ['IE', 'en', 'success criteria in WCAG 2.2'], + ['PL', 'pl', 'kryteriów sukcesu WCAG 2.2'], + ['PL', 'en', 'success criteria in WCAG 2.2'], + ['PT', 'pt', 'critérios de sucesso das WCAG 2.2'], + ['PT', 'en', 'success criteria in WCAG 2.2'], ] as Array<[Country, StatementLocale, string]>)( 'reaches the document in %s/%s', async (country, locale, phrase) => { diff --git a/tests/statement/snapshot.test.ts b/tests/statement/snapshot.test.ts index ee06dce..b4870d8 100644 --- a/tests/statement/snapshot.test.ts +++ b/tests/statement/snapshot.test.ts @@ -38,6 +38,9 @@ function stable(html: string): string { const COMBINATIONS = [ { country: 'AT', locale: 'de' }, { country: 'AT', locale: 'en' }, + { country: 'BE', locale: 'en' }, + { country: 'BE', locale: 'fr' }, + { country: 'BE', locale: 'nl' }, { country: 'CH', locale: 'de' }, { country: 'CH', locale: 'en' }, { country: 'DE', locale: 'de' }, @@ -46,10 +49,15 @@ const COMBINATIONS = [ { country: 'ES', locale: 'es' }, { country: 'FR', locale: 'en' }, { country: 'FR', locale: 'fr' }, + { country: 'IE', locale: 'en' }, { country: 'IT', locale: 'en' }, { country: 'IT', locale: 'it' }, { country: 'NL', locale: 'en' }, { country: 'NL', locale: 'nl' }, + { country: 'PL', locale: 'en' }, + { country: 'PL', locale: 'pl' }, + { country: 'PT', locale: 'en' }, + { country: 'PT', locale: 'pt' }, ] as const async function fixtures() { From 2a8121688a2d97b9486f143db44e73549fa63dd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:15:48 +0000 Subject: [PATCH 3/7] feat(audit): --watch, which audits again whenever the build changes A run over an unchanged build costs about what starting the process does, so it is worth repeating on every save. --watch runs once, then watches the directory that run read and audits again when anything in it changes; the page cache means only the pages whose markup moved are audited. The refusals the roadmap set: --url is an error rather than a poll that looks like a watch; the watch exits 0 on Ctrl-C whatever the last run found, because failing a build is the one-shot run's job; and a change that lands mid-run starts another run when that one ends instead of being dropped. Later runs are pointed at the directory the first one found rather than repeating auto-detection, which may run the project's build into the very directory being watched. The page cache and an --output inside the build are ignored for the same reason. A named directory that does not exist yet is waited for, since a build in another terminal is the usual way it appears. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- src/cli/audit.ts | 17 ++- src/cli/index.ts | 27 ++++- src/cli/watch.ts | 214 ++++++++++++++++++++++++++++++++++++++ tests/cli/watch.test.ts | 223 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 src/cli/watch.ts create mode 100644 tests/cli/watch.test.ts diff --git a/src/cli/audit.ts b/src/cli/audit.ts index ebd0778..075c918 100644 --- a/src/cli/audit.ts +++ b/src/cli/audit.ts @@ -85,6 +85,12 @@ export interface AuditCommandOptions extends CrawlCommandOptions { export interface AuditCommandResult { audits: PageAudit[] + /** + * The directory the pages were read from, as the run found it — which under + * auto-detection is the only place anybody learns it. Undefined for a crawl + * and for a run that stopped before it found anything. `--watch` watches it. + */ + directory?: string /** * 0 clean, 1 violations at or above the --fail-on threshold, 2 the audit * could not run or could not finish. @@ -112,6 +118,7 @@ export async function runAuditCommand( const resolved = await resolvePages(dir, options) if (!resolved) return { audits: [], exitCode: 2 } const { pages, origin, label, cleanup, directory, completeness: collection } = resolved + const found = directory === undefined ? {} : { directory } // A credential handed to a run that never makes a request is not a credential // anybody needed, and silently ignoring it would leave somebody believing a @@ -172,7 +179,7 @@ export async function runAuditCommand( // they came from, not served back out of a copy on disk. ...(directory === undefined ? {} : { directory }), }) - if (!fresh) return { audits: [], exitCode: 2 } + if (!fresh) return { audits: [], ...found, exitCode: 2 } // Recorded before anything downstream touches them: a baseline moves // violations into `accepted`, and storing that would freeze one project's @@ -191,7 +198,7 @@ export async function runAuditCommand( if (options.baseline) { const applied = await acceptBaseline(audits, options) - if (!applied) return { audits, exitCode: 2 } + if (!applied) return { audits, ...found, exitCode: 2 } audits = applied } @@ -200,7 +207,7 @@ export async function runAuditCommand( const review = await loadReview(options) // A review asked for and not readable is exit 2 for the same reason a // missing baseline is: the run did not report what it was told to report. - if (review === FAILED) return { audits, exitCode: 2 } + if (review === FAILED) return { audits, ...found, exitCode: 2 } // label, not dir: it is what the run actually audited. dir is undefined // under auto-detection, and was the unused ./dist default under --url, @@ -213,10 +220,10 @@ export async function runAuditCommand( const unaudited = audits.filter((audit) => audit.error) if (unaudited.length > 0) { fail(`${unaudited.length} of ${audits.length} pages could not be audited`) - return { audits, exitCode: 2 } + return { audits, ...found, exitCode: 2 } } - return { audits, exitCode: countAtOrAbove(audits, failOn) > 0 ? 1 : 0 } + return { audits, ...found, exitCode: countAtOrAbove(audits, failOn) > 0 ? 1 : 0 } } finally { await cleanup?.() } diff --git a/src/cli/index.ts b/src/cli/index.ts index bfefef5..7c5246f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -215,11 +215,32 @@ program .option('--review ', 'what a person checked, from eaa-kit checklist') .option('--review-max-age ', 'stop counting review entries older than this', parseDepth) .option('--config ', 'take defaults from this config file, otherwise it is searched for') - .action(async (dir: string | undefined, flags: AuditFlags) => { + .option('--watch', 'audit again whenever the build directory changes, until Ctrl-C') + .action(async (dir: string | undefined, { watch, ...flags }: AuditFlags & { watch?: true }) => { const defaults = await auditDefaults({ ...(flags.config ? { config: flags.config } : {}) }) const invocation = auditInvocation(dir, defaults, flags) - const { exitCode } = await runAuditCommand(invocation.dir, invocation.options) - process.exitCode = exitCode + if (!watch) { + const { exitCode } = await runAuditCommand(invocation.dir, invocation.options) + process.exitCode = exitCode + return + } + + // Refused rather than turned into a poll: a running site changes without + // writing anything this process can see. + if (invocation.options.url !== undefined) { + fail('--watch watches a build directory, and --url audits a running site.') + process.exitCode = 2 + return + } + const { watchAudit } = await import('./watch.ts') + process.exitCode = await watchAudit({ + run: (directory) => runAuditCommand(directory, invocation.options), + directory: invocation.dir, + cwd: process.cwd(), + // What a run writes into the directory it may be watching: the page + // cache, and the report when --output points inside the build. + ignore: ['.eaa-kit', ...(invocation.options.output ? [invocation.options.output] : [])], + }) }) // The page-selection and credential flags below repeat `audit`'s word for word, diff --git a/src/cli/watch.ts b/src/cli/watch.ts new file mode 100644 index 0000000..7c062e9 --- /dev/null +++ b/src/cli/watch.ts @@ -0,0 +1,214 @@ +import { type FSWatcher, watch as fsWatch } from 'node:fs' +import path from 'node:path' +import pc from 'picocolors' +import { isDirectory } from '../fs.ts' +import { note, warn } from './command.ts' + +/** + * `eaa-kit audit --watch`. + * + * A run over a build where nothing changed costs about what starting the + * process costs, because the page cache reuses every result and never loads an + * engine. That makes the run worth repeating on every save, and this repeats + * it: when anything in the build directory changes, the audit runs again, and + * only the pages whose markup changed are audited again. + * + * Three refusals, from the roadmap: + * + * - Directories only. A running site changes without writing anything this + * process can see, and a watch over one would really be a poll. The command + * refuses `--url` before this is reached. + * - No verdict on the way out. A watch shows results. Failing a build is the + * one-shot run's job, so a watch that ends because somebody pressed Ctrl-C + * exits 0 whatever the last run found. + * - Never skip a run because one is already going. A change that lands during + * a run starts another run as soon as it finishes. Otherwise the report on + * screen would describe a build that the files on disk have already + * replaced. + */ + +export interface AuditRun { + exitCode: number + directory?: string +} + +/** The part of `fs.watch` this uses, so a test can hand in its own. */ +export type Watch = ( + directory: string, + onChange: (file: string | null) => void, + onError: (error: Error) => void, +) => { close: () => void } + +export interface WatchAuditOptions { + /** One audit of `directory`, or of whatever auto-detection finds when it is undefined. */ + run: (directory: string | undefined) => Promise + /** The directory asked for, if one was. */ + directory: string | undefined + cwd: string + /** + * Paths under the watched directory that a run writes itself: the report + * under `--output`, and the page cache. A change there is the watch seeing + * its own output, and reacting to it would run for ever. + */ + ignore?: readonly string[] + /** How long to wait for a build to stop writing before auditing it. */ + debounceMs?: number + /** How often to look for a directory that is not there yet. */ + pollMs?: number + /** Ends the watch. Defaults to SIGINT and SIGTERM. */ + signal?: AbortSignal + watch?: Watch +} + +const DEFAULT_DEBOUNCE_MS = 150 +const DEFAULT_POLL_MS = 1000 + +/** + * Run, then run again on every change, until the signal fires. + * + * Resolves with 0 once the watch is ended, or with 2 when there is nothing it + * could watch: a run that found its pages over HTTP, or found none at all with + * no directory named to wait for. + */ +export async function watchAudit(options: WatchAuditOptions): Promise { + const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS + const pollMs = options.pollMs ?? DEFAULT_POLL_MS + const watch = options.watch ?? nodeWatch + const { signal, dispose } = stopSignal(options.signal) + + const first = await options.run(options.directory) + + // The directory the first run actually read is the one to watch, and the one + // every later run is pointed at. Pointing them at it rather than repeating + // auto-detection matters: detection may run the project's build, and a build + // writes into the directory being watched. + const found = first.directory ?? options.directory + if (found === undefined) { + dispose() + warn( + '--watch needs a build directory to watch, and this run did not read one. ' + + 'Name the directory, e.g. eaa-kit audit ./dist --watch.', + ) + return 2 + } + const target = path.resolve(options.cwd, found) + const ignored = (options.ignore ?? []).map((entry) => path.resolve(options.cwd, entry)) + + let running: Promise | undefined + let pending = false + let timer: ReturnType | undefined + let watcher: { close: () => void } | undefined + let poller: ReturnType | undefined + + const runAgain = async (): Promise => { + do { + pending = false + process.stderr.write(`\n${pc.dim(`─── ${new Date().toLocaleTimeString()} ───`)}\n`) + await options.run(found) + } while (pending && !signal.aborted) + running = undefined + if (!signal.aborted) waiting() + } + + const schedule = (): void => { + if (signal.aborted) return + if (running !== undefined) { + pending = true + return + } + clearTimeout(timer) + timer = setTimeout(() => { + running = runAgain() + }, debounceMs) + } + + const isIgnored = (file: string | null): boolean => { + if (file === null) return false + const absolute = path.resolve(target, file) + return ignored.some((entry) => absolute === entry || absolute.startsWith(`${entry}${path.sep}`)) + } + + const arm = async (): Promise => { + if (signal.aborted) return + if (!(await isDirectory(target))) { + // Not there yet, or removed by a build that empties its output by + // deleting it. Wait for it rather than giving up: `vite build` in another + // terminal is the whole reason somebody runs a watch. + poller ??= setInterval(() => { + void isDirectory(target).then((present) => { + if (!present || signal.aborted) return + clearInterval(poller) + poller = undefined + void arm() + schedule() + }) + }, pollMs) + return + } + watcher = watch( + target, + (file) => { + if (!isIgnored(file)) schedule() + }, + () => { + watcher?.close() + watcher = undefined + void arm() + }, + ) + } + + const waiting = (): void => { + note(`Watching ${path.relative(options.cwd, target) || '.'} for changes. Ctrl-C to stop.`) + } + + await arm() + waiting() + + await new Promise((resolve) => { + if (signal.aborted) resolve() + else signal.addEventListener('abort', () => resolve(), { once: true }) + }) + + clearTimeout(timer) + clearInterval(poller) + watcher?.close() + dispose() + // A run in progress finishes rather than being abandoned halfway through a + // write to the cache or to --output. + await running + return 0 +} + +/** Recursive `fs.watch`, which Node supports on every platform this tool does. */ +function nodeWatch( + directory: string, + onChange: (file: string | null) => void, + onError: (error: Error) => void, +): { close: () => void } { + const watcher: FSWatcher = fsWatch(directory, { recursive: true }, (_event, file) => + onChange(file === null ? null : String(file)), + ) + watcher.on('error', onError) + return watcher +} + +/** The caller's signal, or one that Ctrl-C and SIGTERM fire. */ +function stopSignal(given: AbortSignal | undefined): { + signal: AbortSignal + dispose: () => void +} { + if (given !== undefined) return { signal: given, dispose: () => {} } + + const controller = new AbortController() + const stop = (): void => controller.abort() + process.once('SIGINT', stop) + process.once('SIGTERM', stop) + return { + signal: controller.signal, + dispose: () => { + process.off('SIGINT', stop) + process.off('SIGTERM', stop) + }, + } +} diff --git a/tests/cli/watch.test.ts b/tests/cli/watch.test.ts new file mode 100644 index 0000000..ef6f77a --- /dev/null +++ b/tests/cli/watch.test.ts @@ -0,0 +1,223 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { type AuditRun, type Watch, watchAudit } from '../../src/cli/watch.ts' + +const dirs: string[] = [] +let stderr: string[] = [] + +beforeEach(() => { + stderr = [] + vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + stderr.push(String(chunk)) + return true + }) +}) + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +async function directory(): Promise { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-watch-')) + dirs.push(dir) + return dir +} + +/** A watcher the test fires by hand, instead of waiting on the filesystem. */ +function fakeWatch(): { + watch: Watch + change: (file: string | null) => void + closed: () => boolean +} { + let listener: ((file: string | null) => void) | undefined + let isClosed = false + return { + watch: (_directory, onChange) => { + listener = onChange + return { + close: () => { + isClosed = true + }, + } + }, + change: (file) => listener?.(file), + closed: () => isClosed, + } +} + +const settle = (ms = 30) => new Promise((resolve) => setTimeout(resolve, ms)) + +describe('audit --watch', () => { + it('runs once, then again on a change, pointed at the directory it found', async () => { + const dir = await directory() + const calls: Array = [] + const run = async (given: string | undefined): Promise => { + calls.push(given) + return { exitCode: 1, directory: dir } + } + const fake = fakeWatch() + const controller = new AbortController() + + const done = watchAudit({ + run, + directory: undefined, + cwd: dir, + debounceMs: 5, + signal: controller.signal, + watch: fake.watch, + }) + await settle() + fake.change('index.html') + await settle() + controller.abort() + + // Auto-detection happens once. Every later run is told the directory, so + // a detection that would run the build never writes into what is watched. + expect(calls).toEqual([undefined, dir]) + // A watch shows results; failing a build is the one-shot run's job. + expect(await done).toBe(0) + expect(fake.closed()).toBe(true) + }) + + it('folds a burst of changes into one run', async () => { + const dir = await directory() + let runs = 0 + const fake = fakeWatch() + const controller = new AbortController() + + const done = watchAudit({ + run: async () => { + runs += 1 + return { exitCode: 0, directory: dir } + }, + directory: dir, + cwd: dir, + debounceMs: 10, + signal: controller.signal, + watch: fake.watch, + }) + await settle() + for (const file of ['a.html', 'b.html', 'c.html']) fake.change(file) + await settle(50) + controller.abort() + await done + + expect(runs).toBe(2) + }) + + it('runs again after a change that arrives mid-run, rather than dropping it', async () => { + const dir = await directory() + let runs = 0 + let release: (() => void) | undefined + const fake = fakeWatch() + const controller = new AbortController() + + const done = watchAudit({ + run: async () => { + runs += 1 + // The second run is held open until the test lets it go. + if (runs === 2) { + await new Promise((resolve) => { + release = resolve + }) + } + return { exitCode: 0, directory: dir } + }, + directory: dir, + cwd: dir, + debounceMs: 5, + signal: controller.signal, + watch: fake.watch, + }) + await settle() + fake.change('index.html') + await settle() + expect(runs).toBe(2) + + fake.change('about.html') + release?.() + await settle() + controller.abort() + await done + + expect(runs).toBe(3) + }) + + it('does not react to what a run writes itself', async () => { + const dir = await directory() + let runs = 0 + const fake = fakeWatch() + const controller = new AbortController() + + const done = watchAudit({ + run: async () => { + runs += 1 + return { exitCode: 0, directory: dir } + }, + directory: dir, + cwd: dir, + ignore: ['.eaa-kit', 'report.html'], + debounceMs: 5, + signal: controller.signal, + watch: fake.watch, + }) + await settle() + fake.change(path.join('.eaa-kit', 'cache', 'pages.json')) + fake.change('report.html') + await settle() + controller.abort() + await done + + expect(runs).toBe(1) + }) + + it('refuses when the run read no directory and none was named', async () => { + const fake = fakeWatch() + + const code = await watchAudit({ + run: async () => ({ exitCode: 2 }), + directory: undefined, + cwd: process.cwd(), + signal: new AbortController().signal, + watch: fake.watch, + }) + + expect(code).toBe(2) + expect(stderr.join('')).toContain('--watch needs a build directory') + }) + + it('waits for a named directory that does not exist yet', async () => { + const parent = await directory() + const missing = path.join(parent, 'dist') + let runs = 0 + const fake = fakeWatch() + const controller = new AbortController() + + const done = watchAudit({ + run: async () => { + runs += 1 + return { exitCode: 2 } + }, + directory: missing, + cwd: parent, + debounceMs: 5, + pollMs: 10, + signal: controller.signal, + watch: fake.watch, + }) + await settle() + expect(runs).toBe(1) + + const { mkdir } = await import('node:fs/promises') + await mkdir(missing) + await settle(60) + controller.abort() + await done + + // The build that created it is the change worth auditing. + expect(runs).toBe(2) + }) +}) From 3fe72f2ffb394ab41802c9cfb2211040110eff15 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:19:24 +0000 Subject: [PATCH 4/7] docs: the eleven countries, eaa-kit countries and --watch The statement docs gain the four countries, with a note on each that its citations come from secondary sources and cite less for that reason. The audit docs describe --watch and its refusals, the README and the package description say eleven, and the changelog opens 0.8.0 with what was given up as well as what was added. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ README.md | 20 +++++++++++++++----- docs/audit.md | 22 ++++++++++++++++++++++ docs/statement.md | 24 +++++++++++++++++++++--- package.json | 2 +- 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d399d..5eaedf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,42 @@ move: the JSON report's `schemaVersion` and the baseline file's. Both are bumped a field is removed, renamed, or changes meaning — new fields may appear without one, so consumers must ignore what they do not recognise. +## Unreleased — 0.8.0 + +### Added + +- **Four more countries: Belgium, Ireland, Poland and Portugal.** `BE` in French, Dutch and + English, `IE` in English, `PL` in Polish and English, `PT` in Portuguese and English — + eighteen templates to twenty-six, each written under its own country's law rather than + translated from another's. Belgium and Portugal say that supervision is split instead of + naming one body as if it owned the subject. `pl` and `pt` are new `--lang` values. + + **Given up, and said so:** the statute, the authority and the enforcement route for these + four were established from regulators' pages, government portals and law firms. The + official gazettes could not be reached from where the templates were written. So they + cite less than the older seven, with no article numbers and no fine amounts. They are + marked in `eaa-kit countries` and in the docs until somebody has checked them against + the primary text, and that check blocks the 0.8.0 tag. Belgium has no German rendering; + that is 0.9. +- **`eaa-kit countries`** lists every country a statement can be written for, with its + languages, statute and the authority its template names. `--json` prints the same list + for other tools. +- **`audit --watch`** audits again whenever the build directory changes. Unchanged pages + are reused from the cache, so a save costs about as much as the pages it touched. + `--watch` with `--url` is an error rather than a poll dressed up as a watch. A watch + exits 0 when stopped, whatever it last found. A change that lands mid-run starts another + run rather than being dropped. + +### Changed + +- **`init` no longer turns an unrecognised country into Austria without a word.** It asks + again, with the list, and accepts a country's English name as well as its code. After + three unrecognised answers it still writes the file, since a typo should not cost every + other answer, but it says the country is a default and not what was typed. +- What the tool knows about each country lives in one registry, `src/config/countries.ts`. + A test fails when a template exists without its registry entry, or an entry without its + template. + ## 0.7.0 — 2026-09-10 ### Added diff --git a/README.md b/README.md index facdd95..1db8f7f 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ Build-time WCAG 2.2 AA auditor and EU accessibility statement generator for stat built for the freelancers and small agencies who have to comply with the European Accessibility Act (in force since 28 June 2025) without an accessibility budget. It started in the DACH region — the BFSG in Germany, the BaFG in Austria — and the statement now names -the statute and supervisory body of **seven countries**: Austria, Germany, Switzerland, -Spain, France, Italy and the Netherlands, each in its own language as well as English. +the statute and supervisory body of **eleven countries**: Austria, Belgium, Germany, +Switzerland, Spain, France, Ireland, Italy, the Netherlands, Poland and Portugal, each in +its own language as well as English. 0.7.0 makes a run cost what it should. A page that has not changed byte for byte is not audited again, and a run with nothing to re-audit never loads an engine at all: twenty @@ -27,7 +28,8 @@ Sites behind a login or a preview protection are auditable too. npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed npx eaa-kit init # write an eaa.config.json -npx eaa-kit statement # accessibility statement, in one of seven countries +npx eaa-kit statement # accessibility statement, in one of eleven countries +npx eaa-kit countries # which ones, in which languages, under which law npx eaa-kit checklist # the manual review no engine can do for you ``` @@ -72,7 +74,7 @@ listing the barriers a real audit found. ```bash eaa-kit statement --output src/content/a11y.md -eaa-kit statement --country FR --lang fr # or ES, IT, NL, AT, DE, CH +eaa-kit statement --country PL --lang pl # eaa-kit countries lists all eleven ``` Each country's statement is a document under its own law rather than a translation of @@ -130,6 +132,14 @@ your site. **Says what it did not measure.** A crawl that stopped at its page limit, or could not fetch forty URLs, no longer produces a report that looks like a complete one. +**Keeps up while you work.** `--watch` audits again every time the build changes. Because +of the cache below, only the pages you touched are audited again, so the report is ready +by the time you switch windows. + +```bash +eaa-kit audit ./dist --watch +``` + **Audits only what changed.** A page whose markup is byte-identical to the last run's keeps the result it already had, and a run with nothing to re-audit never loads an engine — twenty pages in ~230 ms instead of ~2,520 ms, which is about what starting the process @@ -182,7 +192,7 @@ eaa-kit audit --url https://preview.example.com --basic-auth user:password | --- | --- | | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you | | [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them | -| [The statement command](docs/statement.md) | The config file, the seven countries, and filling a statement from audit results | +| [The statement command](docs/statement.md) | The config file, the eleven countries, and filling a statement from audit results | | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations | | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed | | [Coverage of WCAG](docs/audit.md#how-much-of-wcag-a-run-reaches) | What an automated engine can reach at all, and what it cannot | diff --git a/docs/audit.md b/docs/audit.md index 8701425..d00fd94 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -239,6 +239,7 @@ chatter coming along. | `--review ` | — | [What a person checked](review.md), for the criteria no engine reaches | | `--review-max-age ` | — | Stop counting review entries older than this | | `--config ` | searched for | Take defaults from this config file rather than the one found by searching | +| `--watch` | off | [Audit again](#watching-a-build) whenever the build directory changes, until Ctrl-C | Dot directories such as build caches are skipped by default. `--include` and `--exclude` replace the defaults rather than adding to them. @@ -530,6 +531,27 @@ not be unsafe, but it would be a large directory of facts about somebody else's baseline is a file somebody commits and then lives with for months, and it is worth the 1.2 seconds to build one from a run that looked at every page itself. +### Watching a build + +```bash +eaa-kit audit ./dist --watch +``` + +Because unchanged pages come from the cache, a run is cheap enough to repeat on every save. +`--watch` audits once, then audits again whenever anything in the build directory changes, +and prints the report each time. Only the pages whose markup changed are audited again. +Keep your build tool's own watch running in another terminal; this watches what it writes. + +- **Directories only.** `--watch` with `--url` is an error. A running site changes without + writing anything this process can see, so watching it would really mean polling it. +- **No verdict.** A watch exits 0 when you stop it, whatever the last run found. Failing a + build is what the one-shot run in CI is for. +- **No change is dropped.** A change that lands during a run starts another run as soon + as that one finishes, so the report on screen is never for a build that has already + been replaced. +- A directory that does not exist yet is waited for, and the page cache and an `--output` + inside the build are not treated as changes. + ## The Issues section The console report leads with the violations grouped by the element that causes them: diff --git a/docs/statement.md b/docs/statement.md index 2cef96d..f3742b6 100644 --- a/docs/statement.md +++ b/docs/statement.md @@ -17,8 +17,8 @@ eaa-kit statement --review eaa-review.json # say what a person checked | Flag | Default | Meaning | | --- | --- | --- | | `--config ` | searched for | Path to the config file | -| `--lang ` | from `site.locale` | `de`, `en`, `es`, `fr`, `it` or `nl` — see the table below for which countries have which | -| `--country ` | from `enforcement.country` | `AT`, `CH`, `DE`, `ES`, `FR`, `IT` or `NL` | +| `--lang ` | from `site.locale` | `de`, `en`, `es`, `fr`, `it`, `nl`, `pl` or `pt` — see the table below for which countries have which | +| `--country ` | from `enforcement.country` | `AT`, `BE`, `CH`, `DE`, `ES`, `FR`, `IE`, `IT`, `NL`, `PL` or `PT` — `eaa-kit countries` lists them | | `--audit ` | — | A report from `eaa-kit audit --format json`; its violations are listed as non-accessible content | | `--review ` | — | A [review record](review.md); the statement says how many criteria a person checked, and refuses a false claim of conformance | | `--format ` | from `--output` | `markdown` or `html` | @@ -107,7 +107,7 @@ export default defineConfig({ ], }, enforcement: { - country: 'AT', // AT, CH, DE, ES, FR, IT or NL + country: 'AT', // AT, BE, CH, DE, ES, FR, IE, IT, NL, PL or PT }, }) ``` @@ -162,6 +162,24 @@ is the French statement, and there is no French rendering of the Austrian one. | `FR` | `fr`, `en` | Ordonnance n° 2023-859 du 6 septembre 2023, and art. 47 of loi n° 2005-102 | the Défenseur des droits, and [Arcom](https://www.arcom.fr) | | `IT` | `it`, `en` | D.lgs. 27 maggio 2022, n. 82, amending the legge Stanca (l. 4/2004) | [AgID](https://www.agid.gov.it) | | `NL` | `nl`, `en` | Implementatiewet toegankelijkheidsvoorschriften producten en diensten | [ACM](https://www.acm.nl) for services, RDI for products | +| `BE` † | `fr`, `nl`, `en` | Code de droit économique / Wetboek van economisch recht, as amended by the law of 5 November 2023 | [SPF Économie / FOD Economie](https://economie.fgov.be), Economic Inspection, and says supervision is split | +| `IE` † | `en` | European Union (Accessibility Requirements of Products and Services) Regulations 2023 (S.I. No. 636 of 2023) | [CCPC](https://www.ccpc.ie); ComReg and the Central Bank for their sectors | +| `PL` † | `pl`, `en` | Ustawa z dnia 26 kwietnia 2024 r. (Dz.U. 2024 poz. 731) | [Prezes Zarządu PFRON](https://www.pfron.org.pl), who passes e-commerce reports to the minister for digital affairs | +| `PT` † | `pt`, `en` | Decreto-Lei n.º 82/2022, de 6 de dezembro | [ANACOM](https://www.anacom.pt) for e-commerce, and says supervision is split | + +`eaa-kit countries` prints the same list in the terminal, and `--json` prints it for +anything that wants to build on it. + +† **New in 0.8.0, and checked less than the others.** The statute, the authority and the +enforcement route for these four were established from regulators' own pages, government +portals and law firms. The official gazettes could not be reached when these templates were +written. For that reason these templates cite less than the older seven: no article +numbers and no fine amounts, which are the details a secondary source most often gets wrong. +They will be checked against the primary text before 0.8.0 is released. Until then, read +the enforcement section with that in mind, and if you find something wrong, +[open an issue](https://github.com/likeBloodMoon/eaa-kit/issues). +Belgium has no German rendering yet: the federal law is published in French and Dutch, and +a German one written without a German source text would be a translation. Asking for a language a country does not have is an error naming the ones it does, rather than a fall back to another language: a legal document silently published in a language diff --git a/package.json b/package.json index 48aefa0..6a667fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "eaa-kit", "version": "0.7.0", - "description": "WCAG 2.2 AA auditor and EU accessibility statement generator for seven countries (AT, DE, CH, ES, FR, IT, NL). Audits a static build or a running site from the command line.", + "description": "WCAG 2.2 AA auditor and EU accessibility statement generator for eleven countries (AT, BE, CH, DE, ES, FR, IE, IT, NL, PL, PT). Audits a static build or a running site from the command line.", "type": "module", "license": "MIT", "repository": { From c301b963f049505f93e19fb471803e2f38d04e36 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:19:51 +0000 Subject: [PATCH 5/7] docs(changelog): fourteen templates to twenty-two, not eighteen to twenty-six Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eaedf3..32f1144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ consumers must ignore what they do not recognise. - **Four more countries: Belgium, Ireland, Poland and Portugal.** `BE` in French, Dutch and English, `IE` in English, `PL` in Polish and English, `PT` in Portuguese and English — - eighteen templates to twenty-six, each written under its own country's law rather than + fourteen templates to twenty-two, each written under its own country's law rather than translated from another's. Belgium and Portugal say that supervision is split instead of naming one body as if it owned the subject. `pl` and `pt` are new `--lang` values. From 8658be41a6a1412bb515f5eaf160c37df9a62ab0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:35:41 +0000 Subject: [PATCH 6/7] feat: eaa-kit on its own is the whole first run, with nothing to set up `eaa-kit` with no command printed the help and exited 2, and it is the first thing anybody types. Now it finds the site the way `audit` does, audits it, writes the HTML report to .eaa-kit/report.html, and ends with what it found out about the project and the command to run next: init without a config, statement with one, baseline when there are findings and no baseline, then --watch and checklist. Detection closes two gaps on the way. A folder of hand-written HTML with no package.json is the site, and is audited where it stands. init reads and the canonical link from the built site and offers them as defaults, with the country a language tag points at: pl is Poland, fr-BE is Belgium, en and fr-CA suggest nothing, because a site's language does not settle whose law applies. The first run writes nothing outside .eaa-kit/, gives that directory a .gitignore of its own rather than editing the project's, leaves a folder with no site in it untouched, and keeps audit's exit codes. It is decided before commander sees the arguments, so `eaa-kit adit` still gets "did you mean audit?" rather than "too many arguments". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- CHANGELOG.md | 13 +++ README.md | 1 + ROADMAP.md | 20 +++++ docs/audit.md | 34 ++++++++ src/audit/project.ts | 17 +++- src/audit/site.ts | 117 ++++++++++++++++++++++++++ src/cli/audit.ts | 19 +++++ src/cli/index.ts | 13 ++- src/cli/init.ts | 43 +++++++++- src/cli/start.ts | 162 ++++++++++++++++++++++++++++++++++++ src/config/countries.ts | 23 +++++ tests/audit/project.test.ts | 29 +++++++ tests/audit/site.test.ts | 75 +++++++++++++++++ tests/cli/init.test.ts | 28 +++++++ tests/cli/start.test.ts | 90 ++++++++++++++++++++ 15 files changed, 678 insertions(+), 6 deletions(-) create mode 100644 src/audit/site.ts create mode 100644 src/cli/start.ts create mode 100644 tests/audit/site.test.ts create mode 100644 tests/cli/start.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f1144..1cf32b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,19 @@ consumers must ignore what they do not recognise. ### Added +- **`eaa-kit` on its own is now the whole first run.** It used to print the help and exit 2. + With nothing set up (no config, no flags), it finds the site, audits it, prints the + report, writes the HTML report to `.eaa-kit/report.html`, and ends with what it found + out about the project and which command to run next. `.eaa-kit/` gets its own + `.gitignore`. A folder with no site in it is left untouched. The exit codes are + `audit`'s: a first look at a site with critical barriers does not exit 0. +- **A folder of hand-written HTML is found.** No `package.json` and HTML at the top level + means the folder is the site, and `audit` with no arguments audits it where it stands. +- **`init` reads what the built site says about itself.** `` becomes the + default language and, where it points at one, the default country: `pl` is Poland, + `fr-BE` is Belgium, `en` and `fr-CA` suggest nothing. The canonical link becomes the + default address. These are only defaults: `init` still asks, because a site's language + does not settle which country's law applies. - **Four more countries: Belgium, Ireland, Poland and Portugal.** `BE` in French, Dutch and English, `IE` in English, `PL` in Polish and English, `PT` in Portuguese and English — fourteen templates to twenty-two, each written under its own country's law rather than diff --git a/README.md b/README.md index 1db8f7f..0e886d2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ through, `audit --review` reads the answers back beside what the run measured, a Sites behind a login or a preview protection are auditable too. ```bash +npx eaa-kit # nothing to set up: finds your site, audits it, writes a report npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed npx eaa-kit init # write an eaa.config.json diff --git a/ROADMAP.md b/ROADMAP.md index 45fa435..4b967af 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -96,6 +96,26 @@ The refusals: document for somebody who typed `pl`. An answer it does not recognise is now asked again, with the list, and the prompt names each country in full. +### 6. The first run needs no setup + +`eaa-kit` on its own printed the help and exited 2, and that is the first command anybody +types. Now it is the whole first run. It finds the site the way `audit` already does, +audits it, writes the HTML report to `.eaa-kit/report.html`, and then says what it found +out about the project and which command comes next, depending on what the project already +has: `init` if there is no config, `baseline` if there are findings and no baseline. + +Two gaps in the detection close with it. A folder of hand-written HTML with no +`package.json` is audited where it stands. `init` now reads what the built site states +about itself, `` and its canonical address, and offers those as defaults. It +still only offers what the site states outright: a language tag suggests a country, it +does not decide one, and `init` still asks. + +The refusals: the first run writes nothing into the project outside `.eaa-kit/`, and gives +that directory a `.gitignore` of its own instead of editing the project's. A folder with no +site in it is left exactly as it was found. The first run also keeps `audit`'s exit codes. +Exiting 0 on a site with critical barriers, because this happened to be somebody's first +look, would tell them it was clean. + ### Not in 0.8.0 - **Belgium in German, and the Nordic and Czech statements.** 0.9, for the reason above. diff --git a/docs/audit.md b/docs/audit.md index d00fd94..ee8ba06 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -4,10 +4,41 @@ with axe-core. This page covers the command, both engines, and how to read what it returns. ```bash +eaa-kit # first run: find the site, audit it, write a report eaa-kit audit # works out what to audit eaa-kit audit ./dist # or say so ``` +## The first run + +`eaa-kit` with no command needs no config file, no flags and no setup. It finds the site +the way `audit` does (below), audits it, and prints the console report. It also writes the +HTML report to `.eaa-kit/report.html`, a page you can open, keep, and send to whoever owns +the site. Then it says what it worked out about the project and what to run next: + +``` +What eaa-kit found about this project + Site dist/ (Astro), 12 pages + Language pl-PL → a statement under Poland's law + Address https://sklep.pl + Report file:///…/.eaa-kit/report.html + +Next + eaa-kit init write the config for your statement, with Poland filled in + eaa-kit baseline accept today's findings; later runs fail only on new ones + eaa-kit audit --watch check again on every build while you fix things + eaa-kit checklist the 34 criteria no automated test can check +``` + +The language and address come from the site itself: `` and the canonical link on +its home page. `eaa-kit init` offers them as its defaults. A language tag suggests a country +and does not decide one. `de` could be Germany, Austria or Switzerland, and the country +whose law applies depends on where you sell, so `init` still asks. + +Nothing is written into the project except under `.eaa-kit/`, which also gets a `.gitignore` +so the report and the cache stay out of version control. A folder where no site is found is +left untouched. Exit codes are those of `audit`. + ## With no arguments `eaa-kit audit` on its own works out what this project needs, in three steps: @@ -24,6 +55,9 @@ eaa-kit audit ./dist # or say so cannot be exported. It starts `start`, `preview` or `serve`, crawls what that serves, and stops it again afterwards. +A folder with no `package.json` and HTML files at its top level is a site written by hand, +and is audited where it stands. + Naming a directory or passing `--url` skips all of it, and `--no-build` stops it running anything, leaving step 1 only. diff --git a/src/audit/project.ts b/src/audit/project.ts index e66a5ef..3e31da1 100644 --- a/src/audit/project.ts +++ b/src/audit/project.ts @@ -83,6 +83,12 @@ export async function findBuildOutput(cwd: string): Promise return undefined } +/** Whether a folder has an HTML file directly in it, not only somewhere below. */ +async function holdsTopLevelHtml(cwd: string): Promise { + const found = await glob(['*.html', '*.htm'], { cwd, onlyFiles: true, dot: false }) + return found.length > 0 +} + /** * How to invoke a package-manager script on this platform. * @@ -285,7 +291,16 @@ export async function autoDetectSource( return { steps } } - if (pkg === undefined) return undefined + if (pkg === undefined) { + // No package.json and no framework: a site written by hand, whose source is + // the site. Only the top level is looked at, so a home directory full of + // saved web pages somewhere below it is not mistaken for a project. + if (detected === undefined && (await holdsTopLevelHtml(cwd))) { + step('Found hand-written HTML in this folder') + return { directory: cwd, steps } + } + return undefined + } const scripts = pkg.scripts ?? {} if (options.noBuild) return undefined diff --git a/src/audit/site.ts b/src/audit/site.ts new file mode 100644 index 0000000..1391b51 --- /dev/null +++ b/src/audit/site.ts @@ -0,0 +1,117 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { glob } from 'tinyglobby' + +/** + * What a built site says about itself, read from its home page. + * + * `init` used to know only what package.json says: a package name and, if + * somebody filled it in, a homepage. The built site usually says more, and + * says it outright. `` gives the site's language, the canonical + * link gives its address, and `` gives what it calls itself. These are + * things the site states, not guesses, which is the only kind of default + * `init` offers. + * + * Read with patterns rather than parsed. This runs before any engine is + * loaded, and loading jsdom to read three attributes would cost more than the + * whole first run is meant to take. + */ +export interface SiteFacts { + /** The BCP 47 tag on `<html lang>`, e.g. `pl-PL`. */ + lang?: string + /** The origin of `<link rel="canonical">` or `og:url`. */ + url?: string + title?: string +} + +/** The home page, or failing that the first page in path order. */ +async function homePage(directory: string): Promise<string | undefined> { + for (const name of ['index.html', 'index.htm']) { + const candidate = path.join(directory, name) + try { + return await readFile(candidate, 'utf8') + } catch { + // not this one + } + } + const pages = ( + await glob(['**/*.html', '**/*.htm'], { + cwd: directory, + ignore: ['**/node_modules/**'], + onlyFiles: true, + dot: false, + }) + ).sort() + const first = pages[0] + if (first === undefined) return undefined + try { + return await readFile(path.join(directory, first), 'utf8') + } catch { + return undefined + } +} + +export async function readSiteFacts(directory: string): Promise<SiteFacts> { + const html = await homePage(directory) + if (html === undefined) return {} + return siteFactsFrom(html) +} + +export function siteFactsFrom(html: string): SiteFacts { + const facts: SiteFacts = {} + + const htmlTag = /<html\b[^>]*>/i.exec(html)?.[0] + const lang = htmlTag === undefined ? undefined : attribute(htmlTag, 'lang') + if (lang !== undefined && /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i.test(lang)) facts.lang = lang + + const url = canonicalUrl(html) + if (url !== undefined) facts.url = url + + const title = /<title\b[^>]*>([^<]*)<\/title>/i.exec(html)?.[1]?.trim() + if (title !== undefined && title !== '') facts.title = decodeEntities(title) + + return facts +} + +/** + * The address the site gives for itself. Only an absolute http(s) URL counts: + * a relative canonical tells you nothing about where the site lives. + */ +function canonicalUrl(html: string): string | undefined { + const candidates: string[] = [] + for (const tag of html.match(/<link\b[^>]*>/gi) ?? []) { + const rel = attribute(tag, 'rel')?.toLowerCase().split(/\s+/) + const href = attribute(tag, 'href') + if (rel?.includes('canonical') && href !== undefined) candidates.push(href) + } + for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) { + const content = attribute(tag, 'content') + if (attribute(tag, 'property') === 'og:url' && content !== undefined) candidates.push(content) + } + + for (const candidate of candidates) { + try { + const url = new URL(decodeEntities(candidate)) + if (url.protocol === 'http:' || url.protocol === 'https:') return url.origin + } catch { + // not absolute + } + } + return undefined +} + +function attribute(tag: string, name: string): string | undefined { + const match = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(tag) + const value = match?.[1] ?? match?.[2] ?? match?.[3] + return value === undefined ? undefined : value.trim() +} + +/** The five entities a title or URL realistically carries. */ +function decodeEntities(text: string): string { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'|'/g, "'") +} diff --git a/src/cli/audit.ts b/src/cli/audit.ts index 075c918..0371028 100644 --- a/src/cli/audit.ts +++ b/src/cli/audit.ts @@ -81,6 +81,12 @@ export interface AuditCommandOptions extends CrawlCommandOptions { * has to be able to say it looked at everything itself. */ noCache?: boolean + /** + * Also write the HTML report here, beside whatever `format` produces. The + * first run uses it: the console report is for whoever ran the command, and + * the HTML page is the one they can open, keep, and send on. + */ + htmlReport?: string } export interface AuditCommandResult { @@ -417,6 +423,19 @@ async function emit( // a caller that says where relative paths start means it for all of them. await emitDocument(body, options.output, options.cwd ?? process.cwd()) if (options.output !== undefined) note(`Report written to ${options.output}`) + + if (options.htmlReport !== undefined) { + const html = await render( + audits, + dir, + failOn, + completeness, + true, + { ...options, format: 'html' }, + review, + ) + await emitDocument(html, options.htmlReport, options.cwd ?? process.cwd()) + } } async function render( diff --git a/src/cli/index.ts b/src/cli/index.ts index 7c5246f..ae4f144 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -153,6 +153,7 @@ program .name('eaa-kit') .description( 'WCAG 2.2 AA auditor and EU accessibility statement generator for static sites.\n' + + 'Run it with no command to find your site, audit it and write a report.\n' + 'Not legal advice.', ) .version(TOOL_VERSION, '-v, --version') @@ -372,7 +373,17 @@ program }) try { - await program.parseAsync(process.argv) + // `eaa-kit` on its own: the first thing anybody types, so it does the useful + // thing rather than printing the help and exiting 2. Decided before commander + // sees the arguments, rather than as a program action, because an action on + // the program turns `eaa-kit adit` into "too many arguments" instead of + // "unknown command, did you mean audit?". + if (process.argv.length <= 2) { + const { runFirstRun } = await import('./start.ts') + process.exitCode = (await runFirstRun()).exitCode + } else { + await program.parseAsync(process.argv) + } } catch (cause) { // A config file that exists and cannot be read: the same report `statement` // gives, since it is the same file and the same mistake. diff --git a/src/cli/init.ts b/src/cli/init.ts index b4c4750..9dc84e9 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -3,7 +3,7 @@ import path from 'node:path' import { createInterface } from 'node:readline/promises' import pc from 'picocolors' import { DEFAULT_FAIL_ON } from '../audit/impact.ts' -import { COUNTRY_INFO, findCountry } from '../config/countries.ts' +import { COUNTRY_INFO, countryForLocale, findCountry } from '../config/countries.ts' import { COUNTRIES, type Country } from '../config/define.ts' import { CONFIG_FILENAMES } from '../config/load.ts' import { exists } from '../fs.ts' @@ -78,9 +78,35 @@ export async function detectDefaults(cwd: string): Promise<Detected> { } catch { // no package.json, or not JSON: nothing to read } + + // The built site, when there is one, states more than package.json does: + // its language on <html lang> and its address on the canonical link. Both + // win over package.json, whose homepage is as often the repository as the + // site. Nothing is built or started to find them; a project with no build + // yet simply gets the package.json answers. + const site = await builtSite(cwd) + if (site !== undefined) { + const { readSiteFacts } = await import('../audit/site.ts') + const facts = await readSiteFacts(site) + if (facts.url !== undefined) detected.url = facts.url + if (facts.title !== undefined && detected.name === undefined) detected.name = facts.title + if (facts.lang !== undefined) { + detected.locale = facts.lang + const country = countryForLocale(facts.lang) + if (country !== undefined) detected.country = country + } + } return detected } +/** The built site's directory, from the same search `audit` makes, without building. */ +async function builtSite(cwd: string): Promise<string | undefined> { + const { autoDetectSource } = await import('../audit/project.ts') + const found = await autoDetectSource(cwd, { noBuild: true }) + await found?.cleanup?.() + return found?.directory +} + /** Whether a config is already there, so init never overwrites one silently. */ export async function existingConfig(cwd: string): Promise<string | undefined> { for (const name of CONFIG_FILENAMES) { @@ -135,8 +161,16 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< const name = await ask('Site name', detected.name ?? '') const url = await ask('Site URL', detected.url ?? 'https://example.com') - const country = await askCountry(ask, rl !== undefined) - const locale = await ask('Language of the site', COUNTRY_INFO[country].siteLocale) + const country = await askCountry(ask, rl !== undefined, detected.country) + const locale = await ask( + 'Language of the site', + // The site's own tag, when the country chosen is the one it points at. + // Somebody who picks another country is saying the site is not what it + // looks like, and gets that country's language instead. + detected.locale !== undefined && detected.country === country + ? detected.locale + : COUNTRY_INFO[country].siteLocale, + ) const legalName = await ask('Legal entity answerable for the site', name) const email = await ask('Feedback email', '') const feedbackUrl = await ask('Feedback or contact form URL (optional)', '') @@ -212,8 +246,9 @@ const COUNTRY_ATTEMPTS = 3 async function askCountry( ask: (question: string, fallback: string) => Promise<string>, interactive: boolean, + suggested: Country | undefined, ): Promise<Country> { - const fallback: Country = 'AT' + const fallback: Country = suggested ?? 'AT' const choices = COUNTRIES.map((code) => `${code} ${COUNTRY_INFO[code].name}`).join(', ') if (interactive) process.stderr.write(pc.dim(`Countries: ${choices}\n`)) diff --git a/src/cli/start.ts b/src/cli/start.ts new file mode 100644 index 0000000..a358837 --- /dev/null +++ b/src/cli/start.ts @@ -0,0 +1,162 @@ +import { access, mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import pc from 'picocolors' +import { DEFAULT_BASELINE_FILE } from '../audit/baseline.ts' +import { COUNTRY_INFO, countryForLocale } from '../config/countries.ts' +import { runAuditCommand } from './audit.ts' +import { auditDefaults, auditInvocation, fail } from './command.ts' + +/** + * `eaa-kit`, with nothing after it. + * + * The first thing anybody types is the name of the tool, and until 0.8 that + * printed the help and exited 2. This makes that first command do the useful + * thing instead. It finds the site the way `audit` does: an existing build, the + * project's own build, its server, or a folder of hand-written HTML. It audits + * the site, writes the HTML report where it can be opened, and then says what it + * found about the project and which command to run next, based on what the + * project already has. + * + * Nothing is asked and nothing is written into the project except under + * `.eaa-kit/`, which the run's own cache already uses. A first look should be + * safe to take on somebody else's checkout. + */ + +/** Where the first run leaves the report, relative to the project. */ +export const FIRST_RUN_REPORT = path.join('.eaa-kit', 'report.html') + +export interface FirstRunOptions { + cwd?: string +} + +export async function runFirstRun(options: FirstRunOptions = {}): Promise<{ exitCode: number }> { + const cwd = options.cwd ?? process.cwd() + process.stderr.write( + `${pc.bold('eaa-kit')} ${pc.dim('· WCAG 2.2 AA · nothing to set up, finding your site…')}\n\n`, + ) + + // A project that has written its defaults down gets them here too: the first + // run is the same audit as `eaa-kit audit`, with a report written beside it. + const defaults = await auditDefaults({ cwd }) + const invocation = auditInvocation(undefined, defaults, {}) + + const result = await runAuditCommand(invocation.dir, { + ...invocation.options, + cwd, + htmlReport: FIRST_RUN_REPORT, + }) + + if (result.audits.length === 0) { + // Detection has already said what it tried and how to point the tool at + // the site by hand. Repeating it would bury that under a second copy. + fail('No site found to audit, so no report was written.') + return { exitCode: result.exitCode } + } + + // After the run, not before: a folder with no site in it is left exactly as + // it was found. + await ensureToolDirectory(cwd) + process.stderr.write(await summary(cwd, result)) + return { exitCode: result.exitCode } +} + +/** + * `.eaa-kit/`, with a `.gitignore` that keeps everything in it out of version + * control. + * + * The docs have always said the directory belongs in `.gitignore`, which is + * advice nobody running the tool for the first time has read. The first run is + * about to put a report there as well as the cache. So it looks after that + * itself, without touching the project's own `.gitignore`. + */ +async function ensureToolDirectory(cwd: string): Promise<void> { + const directory = path.join(cwd, '.eaa-kit') + const ignore = path.join(directory, '.gitignore') + try { + await mkdir(directory, { recursive: true }) + if (!(await exists(ignore))) { + await writeFile(ignore, '# Written by eaa-kit: the cache and reports are not source.\n*\n') + } + } catch { + // A read-only checkout still got its audit; there is nothing to protect. + } +} + +async function summary( + cwd: string, + result: Awaited<ReturnType<typeof runAuditCommand>>, +): Promise<string> { + const lines: string[] = ['', pc.bold('What eaa-kit found about this project')] + + const where = + result.directory === undefined + ? 'a running server' + : path.relative(cwd, result.directory) === '' + ? 'this folder' + : `${path.relative(cwd, result.directory)}${path.sep}` + const { detectFramework } = await import('../audit/frameworks.ts') + const { readPackageJson } = await import('../audit/project.ts') + const framework = (await detectFramework(cwd, await readPackageJson(cwd)))?.framework.name + const pages = result.audits.length === 1 ? '1 page' : `${result.audits.length} pages` + lines.push(` Site ${where}${framework === undefined ? '' : ` (${framework})`}, ${pages}`) + + const facts = + result.directory === undefined + ? {} + : await (await import('../audit/site.ts')).readSiteFacts(result.directory) + const country = facts.lang === undefined ? undefined : countryForLocale(facts.lang) + if (facts.lang !== undefined) { + const law = + country === undefined + ? '' + : pc.dim(` → a statement under ${COUNTRY_INFO[country].name}'s law`) + lines.push(` Language ${facts.lang}${law}`) + } else { + lines.push(` Language ${pc.yellow('none declared')} ${pc.dim('(<html lang> is missing)')}`) + } + if (facts.url !== undefined) lines.push(` Address ${facts.url}`) + + const report = path.resolve(cwd, FIRST_RUN_REPORT) + if (await exists(report)) { + lines.push(` Report ${pathToFileURL(report).href}`) + } + + lines.push('', pc.bold('Next')) + const next = (command: string, why: string): void => { + lines.push(` ${pc.cyan(command.padEnd(24))}${why}`) + } + + const { existingConfig } = await import('./init.ts') + const config = await existingConfig(cwd) + if (config === undefined) { + next( + 'eaa-kit init', + country === undefined + ? 'write the config your accessibility statement is made from' + : `write the config for your statement, with ${COUNTRY_INFO[country].name} filled in`, + ) + } else { + next('eaa-kit statement', `write your accessibility statement from ${config}`) + } + + const failing = result.audits.some((audit) => audit.violations.length > 0) + if (failing && !(await exists(path.join(cwd, DEFAULT_BASELINE_FILE)))) { + next('eaa-kit baseline', "accept today's findings; later runs fail only on new ones") + } + if (result.directory !== undefined) { + next('eaa-kit audit --watch', 'check again on every build while you fix things') + } + next('eaa-kit checklist', 'the 34 criteria no automated test can check') + + return `${lines.join('\n')}\n` +} + +async function exists(target: string): Promise<boolean> { + try { + await access(target) + return true + } catch { + return false + } +} diff --git a/src/config/countries.ts b/src/config/countries.ts index 33ed85d..c93420b 100644 --- a/src/config/countries.ts +++ b/src/config/countries.ts @@ -134,3 +134,26 @@ export function findCountry(value: string): Country | undefined { } return undefined } + +/** + * The country a site's language tag points at, when it points at one. + * + * The region decides where there is one: `de-AT` is Austria, `fr-BE` is + * Belgium, and a region with no country listed here gives nothing. A bare language counts only where that language's own country is + * listed, so `pl` is Poland and `fr` is France. English has no such country, so + * `en` gives nothing. This is a default for `init` to offer, not an answer. + * Which country's law applies depends on where the business sells, and a site's + * language only suggests that. + */ +export function countryForLocale(tag: string): Country | undefined { + const [language, region] = tag.toLowerCase().split('-') + if (language === undefined) return undefined + const codes = Object.keys(COUNTRY_INFO) as Country[] + + // A region this tool has no country for is a site aimed somewhere else, and + // `fr-CA` is not a reason to offer France. + if (region !== undefined) return codes.find((code) => code.toLowerCase() === region) + return codes.find( + (code) => COUNTRY_INFO[code].siteLocale.toLowerCase() === `${language}-${language}`, + ) +} diff --git a/tests/audit/project.test.ts b/tests/audit/project.test.ts index b3b6aa7..59adce0 100644 --- a/tests/audit/project.test.ts +++ b/tests/audit/project.test.ts @@ -131,3 +131,32 @@ describe('a CMS is detected and then left alone', () => { expect(await autoDetectSource(dir, { noBuild: true })).toBeUndefined() }) }) + +describe('a site written by hand', () => { + it('is audited where it stands when there is no package.json', async () => { + // No build, no framework: the folder somebody uploads is the site. + const dir = await project({ 'index.html': '<html></html>', 'about/index.html': '' }) + + const detected = await autoDetectSource(dir) + + expect(detected?.directory).toBe(dir) + expect(detected?.steps.join(' ')).toContain('hand-written HTML') + }) + + it('is not assumed from HTML nested somewhere below', async () => { + const dir = await project({ 'notes/saved/page.html': '' }) + + expect(await autoDetectSource(dir)).toBeUndefined() + }) + + it('is not assumed in a project with a package.json', async () => { + // A Vite project has an index.html at its root, and it is the source the + // build reads, not a page anybody visits. + const dir = await project({ + 'index.html': '<html></html>', + 'package.json': JSON.stringify({ scripts: {} }), + }) + + expect(await autoDetectSource(dir, { noBuild: true })).toBeUndefined() + }) +}) diff --git a/tests/audit/site.test.ts b/tests/audit/site.test.ts new file mode 100644 index 0000000..c1865f1 --- /dev/null +++ b/tests/audit/site.test.ts @@ -0,0 +1,75 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { readSiteFacts, siteFactsFrom } from '../../src/audit/site.ts' +import { countryForLocale } from '../../src/config/countries.ts' + +const dirs: string[] = [] + +afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +describe('siteFactsFrom', () => { + it('reads the language, the canonical address and the title', () => { + const facts = siteFactsFrom( + '<!doctype html><html class="x" lang="pl-PL"><head><title> Sklep & Kawiarnia ' + + '', + ) + + expect(facts).toEqual({ lang: 'pl-PL', url: 'https://sklep.pl', title: 'Sklep & Kawiarnia' }) + }) + + it('falls back to og:url for the address', () => { + const facts = siteFactsFrom( + '', + ) + + expect(facts.url).toBe('https://example.pt') + }) + + it('states nothing a page does not state', () => { + // A relative canonical says nothing about where the site lives, and a lang + // that is not a language tag is not a language. + expect( + siteFactsFrom(''), + ).toEqual({}) + }) +}) + +describe('readSiteFacts', () => { + it('reads the home page, or the first page when there is none', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-site-')) + dirs.push(dir) + await mkdir(path.join(dir, 'b')) + await writeFile(path.join(dir, 'b', 'page.html'), '') + await writeFile(path.join(dir, 'a.html'), '') + + expect((await readSiteFacts(dir)).lang).toBe('fr-BE') + + await writeFile(path.join(dir, 'index.html'), '') + expect((await readSiteFacts(dir)).lang).toBe('de-AT') + }) +}) + +describe('countryForLocale', () => { + it.each([ + ['de-AT', 'AT'], + ['fr-BE', 'BE'], + ['nl-be', 'BE'], + ['de', 'DE'], + ['fr', 'FR'], + ['pl', 'PL'], + ['pt-PT', 'PT'], + ['en-IE', 'IE'], + ])('reads %s as %s', (tag, country) => { + expect(countryForLocale(tag)).toBe(country) + }) + + it.each(['en', 'en-GB', 'fr-CA', 'pt-BR', 'sv-SE'])('offers nothing for %s', (tag) => { + // English has no country of its own here, and a region this tool has no + // country for is a site aimed somewhere else. + expect(countryForLocale(tag)).toBeUndefined() + }) +}) diff --git a/tests/cli/init.test.ts b/tests/cli/init.test.ts index c161784..951cd09 100644 --- a/tests/cli/init.test.ts +++ b/tests/cli/init.test.ts @@ -42,6 +42,34 @@ async function written(dir: string): Promise> { } describe('detectDefaults', () => { + it('reads the language, country and address the built site states', async () => { + const cwd = await project({ 'package.json': JSON.stringify({ name: 'sklep' }) }) + const { mkdir } = await import('node:fs/promises') + await mkdir(path.join(cwd, 'dist')) + await writeFile( + path.join(cwd, 'dist', 'index.html'), + '', + ) + + expect(await detectDefaults(cwd)).toEqual({ + name: 'sklep', + url: 'https://sklep.pl', + locale: 'pl-PL', + country: 'PL', + }) + }) + + it('offers the detected country and language as the defaults', async () => { + const cwd = await project() + await writeFile(path.join(cwd, 'index.html'), 'Loja') + + await runInitCommand({ cwd, ask: answers('', '', '', '', '', 'a@loja.pt') }) + + const config = await written(cwd) + expect(config.enforcement).toMatchObject({ country: 'PT' }) + expect(config.site).toMatchObject({ name: 'Loja', locale: 'pt-PT' }) + }) + it('takes the name and homepage a project states outright', async () => { const cwd = await project({ 'package.json': JSON.stringify({ name: 'gradska-web', homepage: 'https://gradska.at' }), diff --git a/tests/cli/start.test.ts b/tests/cli/start.test.ts new file mode 100644 index 0000000..421fd82 --- /dev/null +++ b/tests/cli/start.test.ts @@ -0,0 +1,90 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { FIRST_RUN_REPORT, runFirstRun } from '../../src/cli/start.ts' + +const dirs: string[] = [] +let stderr: string[] = [] + +beforeEach(() => { + stderr = [] + vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + stderr.push(String(chunk)) + return true + }) + vi.spyOn(process.stdout, 'write').mockImplementation(() => true) +}) + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +async function folder(files: Record): Promise { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-start-')) + dirs.push(dir) + for (const [name, body] of Object.entries(files)) await writeFile(path.join(dir, name), body) + return dir +} + +const PAGE = + 'Sklep' + + '

Sklep

' + +describe('eaa-kit with no command', () => { + it('finds the site, audits it and leaves an HTML report, with nothing set up', async () => { + const cwd = await folder({ 'index.html': PAGE }) + + const { exitCode } = await runFirstRun({ cwd }) + + // The image has no alt text, which is critical: the first run keeps the + // audit's exit codes rather than softening them. + expect(exitCode).toBe(1) + const report = await readFile(path.join(cwd, FIRST_RUN_REPORT), 'utf8') + expect(report).toContain('') + expect(report).toContain('image-alt') + }) + + it('says what it detected, and which command comes next', async () => { + const cwd = await folder({ 'index.html': PAGE }) + + await runFirstRun({ cwd }) + const said = stderr.join('') + + expect(said).toContain('this folder, 1 page') + expect(said).toContain("pl-PL → a statement under Poland's law") + expect(said).toContain('eaa-kit init') + expect(said).toContain('with Poland filled in') + expect(said).toContain('eaa-kit baseline') + }) + + it('points at the statement once a config exists', async () => { + const cwd = await folder({ 'index.html': PAGE, 'eaa.config.json': '{}' }) + + await runFirstRun({ cwd }) + + expect(stderr.join('')).toContain('eaa-kit statement') + expect(stderr.join('')).not.toContain('eaa-kit init') + }) + + it('keeps what it writes out of version control', async () => { + const cwd = await folder({ 'index.html': PAGE }) + + await runFirstRun({ cwd }) + + expect(await readFile(path.join(cwd, '.eaa-kit', '.gitignore'), 'utf8')).toMatch(/^\*$/m) + }) + + it('exits 2 and writes no report when there is no site to find', async () => { + const cwd = await folder({}) + + const { exitCode } = await runFirstRun({ cwd }) + + expect(exitCode).toBe(2) + expect(stderr.join('')).toContain('No site found to audit') + // Left exactly as it was found: not even the tool's own directory. + await expect(readFile(path.join(cwd, FIRST_RUN_REPORT), 'utf8')).rejects.toThrow() + await expect(readFile(path.join(cwd, '.eaa-kit', '.gitignore'), 'utf8')).rejects.toThrow() + }) +}) From 47a287d0706070b8d9ae2987cbfabc2ce548a165 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 20:38:54 +0000 Subject: [PATCH 7/7] test(start): read the first-run summary without its colour codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI forces colour, which puts an escape between "pl-PL" and the dimmed "→ a statement under Poland's law", so the assertion looked for text that was there but split. Reproduced with FORCE_COLOR=1; the test now strips the codes before comparing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013BXnvSeRtgZoTXe4j753gM --- tests/cli/start.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/cli/start.test.ts b/tests/cli/start.test.ts index 421fd82..24c9846 100644 --- a/tests/cli/start.test.ts +++ b/tests/cli/start.test.ts @@ -1,6 +1,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' +import { stripVTControlCharacters } from 'node:util' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { FIRST_RUN_REPORT, runFirstRun } from '../../src/cli/start.ts' @@ -50,7 +51,9 @@ describe('eaa-kit with no command', () => { const cwd = await folder({ 'index.html': PAGE }) await runFirstRun({ cwd }) - const said = stderr.join('') + // Without the colour codes, which CI turns on and which split a line like + // "pl-PL → …" in two. + const said = stripVTControlCharacters(stderr.join('')) expect(said).toContain('this folder, 1 page') expect(said).toContain("pl-PL → a statement under Poland's law")