Quebec Bill 96 French Website Compliance Auditor
Pricing
from $100.00 / 1,000 page pair compareds
Quebec Bill 96 French Website Compliance Auditor
Batch-audits EN/FR page pairs on business websites for Quebec Bill 96 French-language compliance. No login required - checks a list of URLs you supply, including sites you audit on behalf of clients.
Pricing
from $100.00 / 1,000 page pair compareds
Rating
0.0
(0)
Developer
Barak Eliov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Security
Limited permissionsShare
Batch-audits a list of business websites for Quebec Bill 96 (Charter of the French Language, CQLR c.
C-11) French-content parity. For each target you supply one URL (in either language) - this Actor
discovers its paired language version via <link rel="alternate" hreflang> tags or an in-page
language-switcher link, renders both pages in a real headless browser, and uses an LLM to compare them
for semantic completeness (not a word-for-word diff - two independently-written language versions are
never identical).
No login, no install on the audited site - built for a law firm, translation agency, or franchise/retail chain auditing a LIST of OTHER businesses' or franchise locations' public websites in one run.
What it returns
- Pair discovery, never guessed. Hreflang tags first, in-page switcher-link text as fallback. If
neither signal resolves a pair, the target is reported
discoveryMethod: "failed"and not charged - this Actor never assumes a URL pattern like/en→/fr(confirmed to fail on real sites with non-standard schemes, e.g./fr-cainstead of/fr). - LLM-based semantic completeness comparison, not a textual diff: a 0-100 completeness estimate plus
discrete findings (
missing_content,partial_translation,functional_gap,pricing_gap,navigation_gap,english_only_ui_text). - Screenshot evidence - one full-page screenshot per language version, bundled into the price.
- The single most important correctness rule in this Actor: a page that is bot-blocked, 404s, times
out, or errors is categorized as a fetch failure (
blocked/not_found/timeout/render_error), never charged, and never reported as "French content is missing." A blocked page is evidence of nothing about compliance - the dataset row and the PDF report both say so explicitly. - Optional PDF compliance report - completeness scores, every finding, screenshots, the Charter s.52 obligation, the s.205/207/208/208.1 fine ladder, and a real cited enforcement case.
- URL safety - every URL (the one you supply AND any discovered/pinned paired URL) is checked against private, loopback, and cloud-metadata address ranges before the browser opens it.
- No charge for a failed/blocked/discovery-failed/comparison-failed pair. Only a pair that was actually fetched and actually compared is charged.
Input example
{"targets": [{ "url": "https://www.jeancoutu.com/en", "label": "head-office" },{ "url": "https://garaga.example/nonstandard/ca", "pairedUrl": "https://garaga.example/nonstandard/fr-ca", "label": "non-standard-scheme example" }],"autoDiscoverPair": true,"comparisonScope": "mainContentOnly","generateComplianceReport": true}
Full input fields (all have defaults; see .actor/input_schema.json for the authoritative list):
targets, autoDiscoverPair, englishLocaleHints, frenchLocaleHints, comparisonScope,
includeScreenshots, generateComplianceReport, businessName, waitForSelector, timeoutSecs,
proxyConfiguration, maxConcurrency.
Use pairedUrl whenever you already know a site's paired-language URL, especially on a non-standard
scheme (e.g. /fr-ca instead of /fr) - it skips discovery entirely and guarantees a correct match.
Output example
This dataset has three row shapes, distinguished by rowType.
Pair row (one per target):
{"rowType": "pair","requestedUrl": "https://www.jeancoutu.com/en","label": "head-office","resolvedEnUrl": "https://www.jeancoutu.com/en","resolvedFrUrl": "https://www.jeancoutu.com/fr","discoveryMethod": "hreflang","status": "success","errorCategory": null,"errorMessage": null,"completenessScorePct": 92,"findingCount": 1,"findingsByCategory": { "missing_content": 0, "partial_translation": 1, "functional_gap": 0, "pricing_gap": 0, "navigation_gap": 0, "english_only_ui_text": 0 },"screenshotUrlEn": "https://api.apify.com/v2/key-value-stores/.../records/en-....png","screenshotUrlFr": "https://api.apify.com/v2/key-value-stores/.../records/fr-....png","waitForSelectorTimedOut": false,"comparisonModel": "claude-haiku-4-5-20251001","auditedAt": "2026-10-09T19:00:00.000Z"}
Finding row (one per individual content-parity issue, on a successfully-compared pair):
{"rowType": "finding","pairUrl": "https://www.jeancoutu.com/en","category": "partial_translation","description": "The seasonal promotions section is abbreviated in French compared to the English version.","severityGuess": "medium","sourceSnippetEn": "Save up to 30% on select seasonal items through the end of the month...","sourceSnippetFr": "Profitez de rabais...","screenshotUrl": null}
Summary row (exactly one per run, also saved to the key-value store as batchSummary):
{"rowType": "summary","totalPairsAudited": 1,"totalFindings": 1,"findingsByCategory": { "missing_content": 0, "partial_translation": 1, "functional_gap": 0, "pricing_gap": 0, "navigation_gap": 0, "english_only_ui_text": 0 },"findingsBySeverity": { "high": 0, "medium": 1, "low": 0 },"avgCompletenessScorePct": 92,"complianceReportUrl": "https://api.apify.com/v2/key-value-stores/.../records/compliance-report-....pdf","charterCitation": {"article52": "Charter of the French Language, CQLR c. C-11, s.52 — commercial publications, including website content, must be drawn up in French.","enforcementMechanism": "A complaint or OQLF-initiated investigation can lead to a compliance order under s.177; failure to comply with that order is an offence under s.205.","fineLadder": {"firstOffence": "$3,000-$30,000 (legal person)","secondOffence": "$6,000-$60,000 (doubled, s.207)","thirdOffenceAndBeyond": "$9,000-$90,000 (tripled, s.207)","perDay": "each day a violation continues is a separate offence, s.208.1","directorOfficer": "double the natural-person range if personally liable, s.208"},"citedCase": "8540527 Canada inc. (Comac Corporation): $3,000 fine, Cour du Québec, March 26 2026; infraction detected by OQLF May 15 2024; published by OQLF May 15 2026.","framing": "This is a long-standing, actively-enforced obligation (s.52 predates Bill 96), not a one-time 2025/2026 deadline.","disclaimer": "This report is an automated content-comparison scan, not a legal opinion or an OQLF determination."}}
A failed pair's row has status: "error", errorCategory set to one of invalid_url / not_found /
blocked / pair_discovery_failed / timeout / render_error / comparison_failed, and every
audit-specific field left null. Failed pair rows are never charged.
Errors (never silent, never charged)
| Category | When |
|---|---|
invalid_url | Malformed URL, non-http(s) URL, or the URL (requested, pairedUrl, or discovered) resolves to a private/link-local/loopback/cloud-metadata IP address (SSRF protection, always on) |
not_found | DNS failure, connection refused, 404/410 |
blocked | HTTP 401/403/429 on either language version. Never conflated with a missing-French-content finding - see below |
pair_discovery_failed | No pairedUrl supplied, and no hreflang alternate or switcher link resolved a pair (or autoDiscoverPair is false and no pairedUrl was given); also used when a pair was found but which side is English vs. French could not be confidently determined |
timeout | Navigation/render exceeded timeoutSecs on either side |
render_error | Browser crash, an unclassified render failure, or a 5xx upstream response |
comparison_failed | Both pages were fetched successfully but the LLM comparison call failed or returned a malformed/unusable response, even after one internal retry |
The one rule that matters most: blocked ≠ missing
If the French version of a pair is bot-blocked, times out, or 404s while the English version succeeds
(or vice versa), this is reported as a fetch failure - status: "error", the matching
errorCategory, and an errorMessage that explicitly says "this is a fetch failure, not a finding about
missing or incomplete French content." It is never charged, and the PDF report's banner for that pair
says "FETCH FAILURE — NOT A COMPLIANCE FINDING" rather than implying anything about French content. A
false "non-compliant" conclusion caused by this Actor's own inability to fetch a page would be a serious,
unacceptable error against a real business.
A waitForSelector that never appears is not an error: the pair is still compared and
waitForSelectorTimedOut: true is reported (charged normally - a real comparison was delivered).
Pair discovery
pairedUrlsupplied → used as-is, discovery skipped entirely. Recommended for any site you've already confirmed uses a non-standard scheme.- Hreflang tags →
<link rel="alternate" hreflang="...">on the rendered page. - In-page language-switcher link → anchor text matched against
englishLocaleHints/frenchLocaleHints(e.g. "FR", "Français", "EN", "English"). - Never a guessed URL pattern (e.g. assuming
/en→/fr) - confirmed live to fail on real sites (a real EN/FR pair can live at/fr-ca//ca, with no/en//frpath at all).
Which side is English and which is French is determined from each page's own <html lang> attribute
(cross-checked against the discovery signals). If this cannot be determined with confidence on either
side, the pair is reported pair_discovery_failed rather than guessed - mislabeling the sides would
silently invert this Actor's entire compliance conclusion, which is worse than failing closed.
The LLM comparison
This Actor's core logic calls an LLM (Anthropic's API) to compare the two pages' extracted text for
semantic completeness - never a textual diff, since two independently-written language versions are
never word-for-word identical by nature. The API key is read only from the ANTHROPIC_API_KEY
environment variable - never a user-supplied credential, and never committed to any file. A malformed or
unusable LLM response is retried once, then reported comparison_failed (not charged) - this Actor never
fabricates a finding from a response it couldn't parse.
completenessScorePct and severityGuess are both explicitly heuristic signals from the LLM, not a
legal determination or an official OQLF finding - stated as such in both the dataset and the PDF report.
Screenshots and the PDF compliance report
includeScreenshots (default on) captures one full-page PNG per language version, bundled into the
page-pair-compared price.
generateComplianceReport: true produces one PDF per run: every pair's completeness score and findings,
screenshots, the Charter s.52 obligation, the fine ladder, a real cited enforcement case, and the
corrected "long-standing obligation, not a 2025/2026 deadline" framing. Failed pairs are listed with an
explicit fetch-failure banner - never silently dropped from the report, and never conflated with a
compliance finding. Charged as a separate compliance-report-generated event, and only when it
actually succeeds - if every pair in the batch failed, generation is skipped and nothing is charged.
A per-pair and batch-wide cap bounds how many findings are embedded in full detail in the PDF (cost/size safety, same pattern as this factory's other report-generating Actors); anything held back is disclosed via a count in the report text, never silently dropped - every finding is still a full row in the dataset regardless.
Legal citation (read before citing this report)
- s.52 of the Charter of the French Language is a long-standing obligation that predates Bill 96 - not a new 2025/2026 deadline. The June 2025/2026 dates in Bill 96 coverage apply to other, unrelated provisions.
- The fine ladder and the cited enforcement case (
charterCitationin the summary row) were independently verified againstlegisquebec.gouv.qc.caand a real published OQLF enforcement case. - This Actor cites only article/section numbers plus its own short paraphrase - never verbatim Charter or Gazette officielle text.
- This report is an automated content-comparison scan, not a legal opinion or an official OQLF determination.
Limits
- Max 100 targets per run (internal safety cap; no
maxItemsinput field in v1). - Max 120s
timeoutSecs, minimum 5s, applied independently to each side's navigation. - Extracted page text sent to the LLM is capped at 8,000 characters per side (cost/reliability safety limit) - a truncation note is logged when this triggers; it does not affect which language version was compared, only how much of a very long page's text reaches the LLM call.
- This Actor audits exactly the targets you list. It does not crawl a domain to discover every bilingual page pair on its own.
- Pair discovery relies on hreflang tags or an in-page switcher link. A site with neither (no hreflang,
no visible language-switcher) will be reported
pair_discovery_failed- supplypairedUrlmanually.
Use with AI agents / MCP
Call the Actor with targets as an array of objects (never a comma-separated string). Check rowType
first, then status/errorCategory on pair rows. Only status: "success" pair rows have populated
completenessScorePct/findingCount. Filter rowType: "finding" rows by pairUrl to group findings per
target. The single rowType: "summary" row gives you a batch-level answer, including the static legal
citation, without having to aggregate pair rows yourself.
FAQ
Am I charged for a blocked, failed, or discovery-failed pair? No. Only a pair that was actually fetched and actually compared by the LLM is charged.
If a pair is reported blocked, does that mean the site isn't Bill 96 compliant? No - it means this
Actor couldn't fetch the page. No compliance conclusion can be drawn until the page is reachable.
Is completenessScorePct an official compliance score? No - it is this Actor's own LLM-derived
estimate, stated as a heuristic signal, not a legal determination.
Can this bypass CAPTCHAs or logins? No. It audits publicly-rendered pages only.
Does this store or require my own OpenAI/Anthropic API key? No. The LLM credential is a build-time environment variable on this Actor's own infrastructure - you never supply one.
Not supported yet
Automatic site-wide crawl/discovery of every bilingual page pair on a domain, scheduled recurring re-audits, and a standalone "reverse" FR→EN completeness mode as a headline feature (this Actor already checks both directions internally as a QA signal, but French completeness is the primary output).
Local development
npm installnpx playwright install chromium # one-time, downloads a browser to the shared Playwright cachenpm run typechecknpm testnpm run buildANTHROPIC_API_KEY=sk-... apify run --input-file smoke-input.json --purge
Without ANTHROPIC_API_KEY set, every pair will correctly report comparison_failed (fetch/discovery
still runs normally) rather than crashing - useful for testing the fetch/discovery pipeline without an
LLM credential.
Known limitations
- Some real sites run bot-protection that blocks automated traffic outright (confirmed live during this
build's stress test, e.g.
rona.ca,uniprix.com) - these are correctly reported asblockedand not charged, but cannot be audited at all until the site's protection allows it. - Pair-role labeling (which side is English vs. French) relies on each page's own
<html lang>attribute. A site that omits it on both pages, with no other usable signal, will fail closed (pair_discovery_failed) rather than guess.