Skip to content

W3C Accessibility Metadata Display Guide #574

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions BuildTools/Scripts/convert-a11y-display-guide-localizations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* Copyright 2025 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*
* This script can be used to convert the localized files from https://github.com/w3c/publ-a11y-display-guide-localizations
* into other output formats for various platforms.
*/

const fs = require('fs');
const path = require('path');
const [inputFolder, outputFormat, outputFolder, keyPrefix = ''] = process.argv.slice(2);

/**
* Ends the script with the given error message.
*/
function fail(message) {
console.error(`Error: ${message}`);
process.exit(1);
}

/**
* Converter for Apple localized strings.
*/
function convertApple(lang, version, keys, keyPrefix, write) {
let disclaimer = `DO NOT EDIT. File generated automatically from v${version} of the ${lang} JSON strings.`;

let stringsOutput = `// ${disclaimer}\n\n`;
for (const [key, value] of Object.entries(keys)) {
stringsOutput += `"${keyPrefix}${key}" = "${value}";\n`;
}
let stringsFile = path.join(`Resources/${lang}.lproj`, 'W3CAccessibilityMetadataDisplayGuide.strings');
write(stringsFile, stringsOutput);

// Using the "base" language, we will generate a static list of string keys to validate them at compile time.
if (lang == 'en-US') {
writeSwiftExtensions(disclaimer, keys, keyPrefix, write);
}
}

/**
* Generates a static list of string keys to validate them at compile time.
*/
function writeSwiftExtensions(disclaimer, keys, keyPrefix, write) {
let keysOutput = `//
// Copyright 2025 Readium Foundation. All rights reserved.
// Use of this source code is governed by the BSD-style license
// available in the top-level LICENSE file of the project.
//

// ${disclaimer}\n\npublic extension AccessibilityDisplayString {\n`
let keysList = Object.keys(keys)
.filter((k) => !k.endsWith("-descriptive"))
.map((k) => removeSuffix(k, "-compact"));
for (const key of keysList) {
keysOutput += ` static let ${convertKebabToCamelCase(key)}: Self = "${keyPrefix}${key}"\n`;
}
keysOutput += "}\n"
write("Publication/Accessibility/AccessibilityDisplayString+Generated.swift", keysOutput);
}

const converters = {
apple: convertApple
};

if (!inputFolder || !outputFormat || !outputFolder) {
console.error('Usage: node convert.js <input-folder> <output-format> <output-folder> [key-prefix]');
process.exit(1);
}

const langFolder = path.join(inputFolder, 'lang');
if (!fs.existsSync(langFolder)) {
fail(`the specified input folder does not contain a 'lang' directory`);
}

const convert = converters[outputFormat];
if (!convert) {
fail(`unrecognized output format: ${outputFormat}, try: ${Object.keys(converters).join(', ')}.`);
}

fs.readdir(langFolder, (err, langDirs) => {
if (err) {
fail(`reading directory: ${err.message}`);
}

langDirs.forEach(langDir => {
const langDirPath = path.join(langFolder, langDir);

fs.readdir(langDirPath, (err, files) => {
if (err) {
fail(`reading language directory ${langDir}: ${err.message}`);
}

files.forEach(file => {
const filePath = path.join(langDirPath, file);
if (path.extname(file) === '.json') {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(`Error reading file ${file}: ${err.message}`);
return;
}

try {
const jsonData = JSON.parse(data);
const version = jsonData["metadata"]["version"];
convert(langDir, version, parseJsonKeys(jsonData), keyPrefix, write);
} catch (err) {
fail(`parsing JSON from file ${file}: ${err.message}`);
}
});
}
});
});
});
});

/**
* Writes the given content to the file path relative to the outputFolder provided in the CLI arguments.
*/
function write(relativePath, content) {
const outputPath = path.join(outputFolder, relativePath);
const outputDir = path.dirname(outputPath);

if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFile(outputPath, content, 'utf8', err => {
if (err) {
fail(`writing file ${outputPath}: ${err.message}`);
} else {
console.log(`Wrote ${outputPath}`);
}
});
}

/**
* Collects the JSON translation keys.
*/
function parseJsonKeys(obj) {
const keys = {};
for (const key in obj) {
if (key === 'metadata') continue; // Ignore the metadata key
if (typeof obj[key] === 'object') {
for (const subKey in obj[key]) {
if (typeof obj[key][subKey] === 'object') {
for (const innerKey in obj[key][subKey]) {
const fullKey = `${subKey}-${innerKey}`;
keys[fullKey] = obj[key][subKey][innerKey];
}
} else {
keys[subKey] = obj[key][subKey];
}
}
}
}
return keys;
}

function convertKebabToCamelCase(string) {
return string
.split('-')
.map((word, index) => {
if (index === 0) {
return word;
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}

function removeSuffix(str, suffix) {
if (str.endsWith(suffix)) {
return str.slice(0, -suffix.length);
}
return str;
}
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file. Take a look

**Warning:** Features marked as *alpha* may change or be removed in a future release without notice. Use with caution.

<!-- ## [Unreleased] -->
## [Unreleased]

### Added

#### Shared

* Implementation of the [W3C Accessibility Metadata Display Guide](https://w3c.github.io/publ-a11y/a11y-meta-display-guide/2.0/guidelines/) specification to facilitate displaying accessibility metadata to users. [See the dedicated user guide](docs/Guides/Accessibility.md).


## [3.2.0]

Expand Down
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ SCRIPTS_PATH := Sources/Navigator/EPUB/Scripts

help:
@echo "Usage: make <target>\n\n\
carthage-proj\tGenerate the Carthage Xcode project\n\
carthage-proj\t\tGenerate the Carthage Xcode project\n\
scripts\t\tBundle the Navigator EPUB scripts\n\
test\t\t\tRun unit tests\n\
lint-format\tVerify formatting\n\
lint-format\t\tVerify formatting\n\
format\t\tFormat sources\n\
update-a11y-l10n\tUpdate the Accessibility Metadata Display Guide localization files\n\
"

.PHONY: carthage-project
Expand Down Expand Up @@ -44,3 +45,12 @@ lint-format:
f: format
format:
swift run --package-path BuildTools swiftformat .

.PHONY: update-a11y-l10n
update-a11y-l10n:
@which node >/dev/null 2>&1 || (echo "ERROR: node is required, please install it first"; exit 1)
rm -rf publ-a11y-display-guide-localizations
git clone https://github.com/w3c/publ-a11y-display-guide-localizations.git
node BuildTools/Scripts/convert-a11y-display-guide-localizations.js publ-a11y-display-guide-localizations apple Sources/Shared readium.a11y.
rm -rf publ-a11y-display-guide-localizations

3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ let package = Package(
.product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"),
],
path: "Sources/Shared",
resources: [
.process("Resources"),
],
linkerSettings: [
.linkedFramework("CoreServices"),
.linkedFramework("UIKit"),
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Guides are available to help you make the most of the toolkit.
* [Opening a publication](docs/Guides/Open%20Publication.md) – parse a publication package (EPUB, PDF, etc.) or manifest (RWPM) into Readium `Publication` models
* [Extracting the content of a publication](docs/Guides/Content.md) – API to extract the text content of a publication for searching or indexing it
* [Text-to-speech](docs/Guides/TTS.md) – read aloud the content of a textual publication using speech synthesis
* [Accessibility](docs/Guides/Accessibility.md) – inspect accessibility metadata and present it to users


### Navigator

Expand Down
11 changes: 11 additions & 0 deletions Sources/Internal/Extensions/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import Foundation

public extension Array {
init(builder: (inout Self) -> Void) {
self.init()
builder(&self)
}

/// Creates a new `Array` from the given `elements`, if they are not nil.
init(ofNotNil elements: Element?...) {
self = elements.compactMap { $0 }
Expand Down Expand Up @@ -37,6 +42,12 @@ public extension Array {
}
}

public extension Array where Element: Equatable {
@inlinable func containsAny(_ elements: Element...) -> Bool {
contains { elements.contains($0) }
}
}

public extension Array where Element: Hashable {
/// Creates a new `Array` after removing all the element duplicates.
func removingDuplicates() -> Array {
Expand Down
Loading