Beyond Linting: Why We Switched To Semantic Contracts for A11y and Localization
We arrived at the concept of "Semantic Contracts" not as a top-down decree, but as a survival mechanism. Here's how we implemented the switch.
Join the DZone community and get the full member experience.
Join For FreeI remember sitting in the war room three days before a major release. We were confident; our static accessibility (a11y) linter was passing, and our localization (l10n) unit tests, which performed simple string comparisons, returned green. Then, a beta user reported that the primary_submit_action button was physically impossible to tap because our localization file had grown the button label, pushing the touch target off-screen.
Our tests hadn't failed because they weren't looking at the screen. They were looking at text files. That was the moment I realized our reliance on brittle string-comparison and basic static linting had hit a hard wall at scale. We needed a new approach, one that didn't just check for the presence of a label, but verified the semantic contract between the layout and the user.
The Semantic Contract Insight
We arrived at the concept of "Semantic Contracts" not as a top-down decree, but as a survival mechanism. We were suffering from the "false positive" epidemic: tests that passed while the product was broken. We needed to treat our accessibility tree and layout engine as dynamic data structures rather than static files.
By moving the validation gate into the CI/CD pipeline and using an AI orchestrator to enforce multi-dimensional constraints, we shifted from checking if a button exists to ensuring it functions according to the contract of our design system.

Pillar 1: Accessibility as a Semantic Contract
Our first attempt at accessibility was rule-based. We hardcoded minimum coordinate thresholds (like 44x44 points) into our CI pipeline. It worked until our design system grew to include nested containers that varied by device class. Hardcoded thresholds became a maintenance nightmare, costing us five engineering hours per sprint in manual threshold adjustments. We realized accessibility couldn't be a list of static rules; it had to be a contract validated within the CI environment.
The Tradeoff: Complexity vs. Coverage
We tried standard automated scanners like native native a11y inspectors, but they failed to catch context-dependent issues. For example, an icon-only button is technically "accessible" if the label exists, but it’s contextually confusing if the label isn't descriptive. The cost of manual audit was too high, so we pivoted to an LLM-validator.
Implementation: Enforcing the Contract
We now use automated XCUITest assertions to ensure every primary interactive element meets our physical usability thresholds, followed by an LLM-based pass to ensure the semantic quality of those labels.
func testSubmitButtonMeetsAccessibilityMinimums() {
let app = XCUIApplication()
// Using a standard identifier from our design system
let submitButton = app.buttons["primary_submit_action"]
// Explicitly check for the presence to avoid false positives
XCTAssertTrue(submitButton.exists, "Primary action button not found in hierarchy.")
// Enforcing the 44x44 touch target contract
let frame = submitButton.frame
XCTAssertTrue(frame.size.width >= 44 && frame.size.height >= 44,
"Submit button size \(frame.size) is below the 44x44 accessibility threshold.")
}
Pillar 2: Localization via Visual-Contextual Pipelines
Localization is often treated as a "search and replace" problem. When we were just comparing strings, we missed layout collisions constantly. The tradeoff here was significant: we moved from fast, flaky screenshot comparison testing to a slower, more accurate multi-modal LLM collision detection.
The Failure of Visual Regression
We initially tried pixel-perfect screenshot regression. It failed because our dynamic content—like usernames and feed timestamps—caused thousands of "false failures." Maintaining the baseline screenshots cost us more time than actually writing the localized strings.
Implementation: The Semantic Validator
We now feed our view hierarchy (as JSON) and snapshots into an LLM orchestrator. This allows the system to understand that a button label is not just a collection of pixels, but an interactive semantic component that must not overlap with its parent container.
import openai
def validate_accessibility_labels(accessibility_tree_json):
prompt = f"""
You are an accessibility expert. Analyze the following UI accessibility tree.
Verify that every interactive element has an accessibility label that is descriptive
and not just a repeated image filename or a cryptic ID.
Accessibility Tree: {accessibility_tree_json}
Return a JSON response: {"passed": bool, "issue_description": str}
"""
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
| Architecture Summary | ||
|---|---|---|
|
Validation Engine |
Input Source |
Analytical Method |
|
A11y-Validator |
Accessibility Tree |
Rule-based logic + LLM semantic labeling |
|
L10n-Visualizer |
UI Snapshot + Resource Keys |
LLM-based boundary collision detection |
|
Contract-Gate |
CI/CD Environment |
Infrastructure-as-Policy enforcement |
Moving Forward
The transition to semantic contracts was not cheap in terms of CI time—validating against an LLM is slower than a simple regex check. However, the cost of an emergency patch or a poor user experience for our accessible or international user base is significantly higher.
By treating our UI as data and our accessibility requirements as verifiable contracts, we’ve moved the conversation from "Does this lint?" to "Does this meet our contract?" That shift is what keeps our release process sane, even at scale.
Opinions expressed by DZone contributors are their own.
Comments