Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 55e52e7

Browse files
authoredApr 22, 2025··
W3C Accessibility Metadata Display Guide (#574)
1 parent d501744 commit 55e52e7

25 files changed

+3306
-206
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Copyright 2025 Readium Foundation. All rights reserved.
3+
* Use of this source code is governed by the BSD-style license
4+
* available in the top-level LICENSE file of the project.
5+
*
6+
* This script can be used to convert the localized files from https://github.com/w3c/publ-a11y-display-guide-localizations
7+
* into other output formats for various platforms.
8+
*/
9+
10+
const fs = require('fs');
11+
const path = require('path');
12+
const [inputFolder, outputFormat, outputFolder, keyPrefix = ''] = process.argv.slice(2);
13+
14+
/**
15+
* Ends the script with the given error message.
16+
*/
17+
function fail(message) {
18+
console.error(`Error: ${message}`);
19+
process.exit(1);
20+
}
21+
22+
/**
23+
* Converter for Apple localized strings.
24+
*/
25+
function convertApple(lang, version, keys, keyPrefix, write) {
26+
let disclaimer = `DO NOT EDIT. File generated automatically from v${version} of the ${lang} JSON strings.`;
27+
28+
let stringsOutput = `// ${disclaimer}\n\n`;
29+
for (const [key, value] of Object.entries(keys)) {
30+
stringsOutput += `"${keyPrefix}${key}" = "${value}";\n`;
31+
}
32+
let stringsFile = path.join(`Resources/${lang}.lproj`, 'W3CAccessibilityMetadataDisplayGuide.strings');
33+
write(stringsFile, stringsOutput);
34+
35+
// Using the "base" language, we will generate a static list of string keys to validate them at compile time.
36+
if (lang == 'en-US') {
37+
writeSwiftExtensions(disclaimer, keys, keyPrefix, write);
38+
}
39+
}
40+
41+
/**
42+
* Generates a static list of string keys to validate them at compile time.
43+
*/
44+
function writeSwiftExtensions(disclaimer, keys, keyPrefix, write) {
45+
let keysOutput = `//
46+
// Copyright 2025 Readium Foundation. All rights reserved.
47+
// Use of this source code is governed by the BSD-style license
48+
// available in the top-level LICENSE file of the project.
49+
//
50+
51+
// ${disclaimer}\n\npublic extension AccessibilityDisplayString {\n`
52+
let keysList = Object.keys(keys)
53+
.filter((k) => !k.endsWith("-descriptive"))
54+
.map((k) => removeSuffix(k, "-compact"));
55+
for (const key of keysList) {
56+
keysOutput += ` static let ${convertKebabToCamelCase(key)}: Self = "${keyPrefix}${key}"\n`;
57+
}
58+
keysOutput += "}\n"
59+
write("Publication/Accessibility/AccessibilityDisplayString+Generated.swift", keysOutput);
60+
}
61+
62+
const converters = {
63+
apple: convertApple
64+
};
65+
66+
if (!inputFolder || !outputFormat || !outputFolder) {
67+
console.error('Usage: node convert.js <input-folder> <output-format> <output-folder> [key-prefix]');
68+
process.exit(1);
69+
}
70+
71+
const langFolder = path.join(inputFolder, 'lang');
72+
if (!fs.existsSync(langFolder)) {
73+
fail(`the specified input folder does not contain a 'lang' directory`);
74+
}
75+
76+
const convert = converters[outputFormat];
77+
if (!convert) {
78+
fail(`unrecognized output format: ${outputFormat}, try: ${Object.keys(converters).join(', ')}.`);
79+
}
80+
81+
fs.readdir(langFolder, (err, langDirs) => {
82+
if (err) {
83+
fail(`reading directory: ${err.message}`);
84+
}
85+
86+
langDirs.forEach(langDir => {
87+
const langDirPath = path.join(langFolder, langDir);
88+
89+
fs.readdir(langDirPath, (err, files) => {
90+
if (err) {
91+
fail(`reading language directory ${langDir}: ${err.message}`);
92+
}
93+
94+
files.forEach(file => {
95+
const filePath = path.join(langDirPath, file);
96+
if (path.extname(file) === '.json') {
97+
fs.readFile(filePath, 'utf8', (err, data) => {
98+
if (err) {
99+
console.error(`Error reading file ${file}: ${err.message}`);
100+
return;
101+
}
102+
103+
try {
104+
const jsonData = JSON.parse(data);
105+
const version = jsonData["metadata"]["version"];
106+
convert(langDir, version, parseJsonKeys(jsonData), keyPrefix, write);
107+
} catch (err) {
108+
fail(`parsing JSON from file ${file}: ${err.message}`);
109+
}
110+
});
111+
}
112+
});
113+
});
114+
});
115+
});
116+
117+
/**
118+
* Writes the given content to the file path relative to the outputFolder provided in the CLI arguments.
119+
*/
120+
function write(relativePath, content) {
121+
const outputPath = path.join(outputFolder, relativePath);
122+
const outputDir = path.dirname(outputPath);
123+
124+
if (!fs.existsSync(outputDir)) {
125+
fs.mkdirSync(outputDir, { recursive: true });
126+
}
127+
128+
fs.writeFile(outputPath, content, 'utf8', err => {
129+
if (err) {
130+
fail(`writing file ${outputPath}: ${err.message}`);
131+
} else {
132+
console.log(`Wrote ${outputPath}`);
133+
}
134+
});
135+
}
136+
137+
/**
138+
* Collects the JSON translation keys.
139+
*/
140+
function parseJsonKeys(obj) {
141+
const keys = {};
142+
for (const key in obj) {
143+
if (key === 'metadata') continue; // Ignore the metadata key
144+
if (typeof obj[key] === 'object') {
145+
for (const subKey in obj[key]) {
146+
if (typeof obj[key][subKey] === 'object') {
147+
for (const innerKey in obj[key][subKey]) {
148+
const fullKey = `${subKey}-${innerKey}`;
149+
keys[fullKey] = obj[key][subKey][innerKey];
150+
}
151+
} else {
152+
keys[subKey] = obj[key][subKey];
153+
}
154+
}
155+
}
156+
}
157+
return keys;
158+
}
159+
160+
function convertKebabToCamelCase(string) {
161+
return string
162+
.split('-')
163+
.map((word, index) => {
164+
if (index === 0) {
165+
return word;
166+
}
167+
return word.charAt(0).toUpperCase() + word.slice(1);
168+
})
169+
.join('');
170+
}
171+
172+
function removeSuffix(str, suffix) {
173+
if (str.endsWith(suffix)) {
174+
return str.slice(0, -suffix.length);
175+
}
176+
return str;
177+
}

‎CHANGELOG.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file. Take a look
44

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

7-
<!-- ## [Unreleased] -->
7+
## [Unreleased]
8+
9+
### Added
10+
11+
#### Shared
12+
13+
* 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).
14+
815

916
## [3.2.0]
1017

‎Makefile

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ SCRIPTS_PATH := Sources/Navigator/EPUB/Scripts
22

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

1213
.PHONY: carthage-project
@@ -44,3 +45,12 @@ lint-format:
4445
f: format
4546
format:
4647
swift run --package-path BuildTools swiftformat .
48+
49+
.PHONY: update-a11y-l10n
50+
update-a11y-l10n:
51+
@which node >/dev/null 2>&1 || (echo "ERROR: node is required, please install it first"; exit 1)
52+
rm -rf publ-a11y-display-guide-localizations
53+
git clone https://github.com/w3c/publ-a11y-display-guide-localizations.git
54+
node BuildTools/Scripts/convert-a11y-display-guide-localizations.js publ-a11y-display-guide-localizations apple Sources/Shared readium.a11y.
55+
rm -rf publ-a11y-display-guide-localizations
56+

‎Package.swift

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ let package = Package(
4343
.product(name: "ReadiumZIPFoundation", package: "ZIPFoundation"),
4444
],
4545
path: "Sources/Shared",
46+
resources: [
47+
.process("Resources"),
48+
],
4649
linkerSettings: [
4750
.linkedFramework("CoreServices"),
4851
.linkedFramework("UIKit"),

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Guides are available to help you make the most of the toolkit.
6565
* [Opening a publication](docs/Guides/Open%20Publication.md) – parse a publication package (EPUB, PDF, etc.) or manifest (RWPM) into Readium `Publication` models
6666
* [Extracting the content of a publication](docs/Guides/Content.md) – API to extract the text content of a publication for searching or indexing it
6767
* [Text-to-speech](docs/Guides/TTS.md) – read aloud the content of a textual publication using speech synthesis
68+
* [Accessibility](docs/Guides/Accessibility.md) – inspect accessibility metadata and present it to users
69+
6870

6971
### Navigator
7072

‎Sources/Internal/Extensions/Array.swift

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
import Foundation
88

99
public extension Array {
10+
init(builder: (inout Self) -> Void) {
11+
self.init()
12+
builder(&self)
13+
}
14+
1015
/// Creates a new `Array` from the given `elements`, if they are not nil.
1116
init(ofNotNil elements: Element?...) {
1217
self = elements.compactMap { $0 }
@@ -37,6 +42,12 @@ public extension Array {
3742
}
3843
}
3944

45+
public extension Array where Element: Equatable {
46+
@inlinable func containsAny(_ elements: Element...) -> Bool {
47+
contains { elements.contains($0) }
48+
}
49+
}
50+
4051
public extension Array where Element: Hashable {
4152
/// Creates a new `Array` after removing all the element duplicates.
4253
func removingDuplicates() -> Array {

0 commit comments

Comments
 (0)
Please sign in to comment.