feat: configurable default language and WAI-ARIA accessible name
Release Accessibility Widget / release (push) Waiting to run

widget.js:
- add defaultLanguage config (honored on first visit over browser detect)
- compute screen reader text via ARIA order: aria-labelledby -> aria-label
  -> contents -> alt -> title
- extract getSpeechLang() helper (was duplicated in handleFocus + toggle)
- use optional chaining for ACCESSIBILITY_WIDGET_CONFIG lookup

package.json (fork metadata):
- license: GPL -> GPL-3.0-or-later (valid SPDX)
- engines: node >=12 -> >=14 (optional chaining)
- add homepage, drop empty dep objects and non-standard cdn field
This commit is contained in:
2026-07-01 08:33:50 +08:00
parent f0bc0307c1
commit 6f1f8cdced
2 changed files with 80 additions and 75 deletions
+8 -12
View File
@@ -1,5 +1,5 @@
{
"name": "accessibility-widgets",
"name": "@invisi/accessibility-widgets",
"version": "2.0.14",
"description": "A comprehensive, lightweight accessibility widget that enhances web accessibility for all users. Provides multiple accessibility features including screen reader support, voice control, high contrast mode, and more.",
"main": "widget.js",
@@ -32,26 +32,25 @@
"assistive-technology"
],
"author": {
"name": "sinanisler",
"email": "sinan@sinan.im",
"url": "https://sinan.im"
"name": "Surya Handika Putratama",
"email": "ubunteroz@gmail.com"
},
"license": "GPL",
"license": "GPL-3.0-or-later",
"repository": {
"type": "git",
"url": "git+https://github.com/sinanisler/accessibility-widgets.git"
"url": "git+https://git.invisi.co.id/invisi/accessibility-widgets"
},
"bugs": {
"url": "https://github.com/sinanisler/accessibility-widgets/issues"
"url": "https://git.invisi.co.id/invisi/accessibility-widgets/issues"
},
"homepage": "https://sinan.im",
"homepage": "https://git.invisi.co.id/invisi/accessibility-widgets",
"files": [
"widget.js",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"browserslist": [
"> 1%",
@@ -59,9 +58,6 @@
"not dead",
"not ie 11"
],
"devDependencies": {},
"dependencies": {},
"peerDependencies": {},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/sinanisler"
+69 -60
View File
@@ -1154,7 +1154,10 @@ const savedLanguage = localStorage.getItem('accessibilityWidgetLanguage');
if (savedLanguage && TRANSLATIONS[savedLanguage]) {
currentLanguage = savedLanguage;
} else {
currentLanguage = detectBrowserLanguage();
// Honor configured default language (window.ACCESSIBILITY_WIDGET_CONFIG.defaultLanguage)
// over browser detection when no saved preference exists.
var configDefault = window?.ACCESSIBILITY_WIDGET_CONFIG?.defaultLanguage;
currentLanguage = (configDefault && TRANSLATIONS[configDefault]) ? configDefault : detectBrowserLanguage();
localStorage.setItem('accessibilityWidgetLanguage', currentLanguage);
}
@@ -1234,6 +1237,9 @@ const DEFAULT_WIDGET_CONFIG = {
hoverScale: '1.05'
},
// Applied on first visit when there is no saved preference; overrides browser-language detection.
defaultLanguage: null,
// Language/Text Configuration
lang: {
accessibilityMenu: 'Accessibility Menu',
@@ -2312,10 +2318,10 @@ const pageStyles = `
/* Dyslexia Font */
.snn-dyslexia-font {
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important;
font-family: 'OpenDyslexic', 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important;
}
.snn-dyslexia-font * {
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important;
font-family: 'OpenDyslexic', 'Comic Sans MS', 'Chalkboard SE', 'Bradley Hand', 'Brush Script MT', fantasy !important;
}
/* Line Height - 3 Options */
@@ -3282,6 +3288,48 @@ function handleLineHeight() {
// ACCESSIBILITY FEATURES
// ===========================================
// Resolve the BCP-47 speech-synthesis locale for the active UI language.
function getSpeechLang() {
switch (currentLanguage) {
case 'de': return 'de-DE';
case 'es': return 'es-ES';
case 'it': return 'it-IT';
case 'fr': return 'fr-FR';
case 'ru': return 'ru-RU';
case 'tr': return 'tr-TR';
case 'ar': return 'ar-SA';
case 'hi': return 'hi-IN';
case 'zh-cn': return 'zh-CN';
case 'jp': return 'ja-JP';
case 'pt': return 'pt-PT';
case 'bn': return 'bn-IN';
case 'ko': return 'ko-KR';
case 'vi': return 'vi-VN';
case 'id': return 'id-ID';
case 'th': return 'th-TH';
case 'pl': return 'pl-PL';
case 'nl': return 'nl-NL';
case 'el': return 'el-GR';
case 'sv': return 'sv-SE';
case 'no': return 'no-NO';
case 'da': return 'da-DK';
case 'fi': return 'fi-FI';
case 'cs': return 'cs-CZ';
case 'hu': return 'hu-HU';
case 'ro': return 'ro-RO';
case 'he': return 'he-IL';
case 'fa': return 'fa-IR';
case 'ur': return 'ur-PK';
case 'pa': return 'pa-IN';
case 'mr': return 'mr-IN';
case 'te': return 'te-IN';
case 'ta': return 'ta-IN';
case 'ms': return 'ms-MY';
case 'tl': return 'fil-PH';
default: return 'en-US';
}
}
// Screen reader functionality
const screenReader = {
active: localStorage.getItem('screenReader') === 'true',
@@ -3289,52 +3337,27 @@ const screenReader = {
handleFocus: function (event) {
if (screenReader.active && screenReader.isSupported) {
try {
const content = event.target.innerText || event.target.alt || event.target.title || '';
// WAI-ARIA accessible name computation order: aria-labelledby -> aria-label -> contents -> alt -> title
var target = event.target;
var labelledBy = target.getAttribute && target.getAttribute('aria-labelledby');
var labelledByText = '';
if (labelledBy) {
labelledByText = labelledBy.split(/\s+/).map(function (id) {
var el = document.getElementById(id);
return el ? (el.innerText || el.textContent) : '';
}).join(' ').trim();
}
const content = labelledByText
|| (target.getAttribute && target.getAttribute('aria-label'))
|| target.innerText
|| target.alt
|| target.title
|| '';
if (content.trim() !== '') {
window.speechSynthesis.cancel();
const speech = new SpeechSynthesisUtterance(content);
// Set language based on current interface language
let speechLang = 'en-US'; // default
switch (currentLanguage) {
case 'de': speechLang = 'de-DE'; break;
case 'es': speechLang = 'es-ES'; break;
case 'it': speechLang = 'it-IT'; break;
case 'fr': speechLang = 'fr-FR'; break;
case 'ru': speechLang = 'ru-RU'; break;
case 'tr': speechLang = 'tr-TR'; break;
case 'ar': speechLang = 'ar-SA'; break;
case 'hi': speechLang = 'hi-IN'; break;
case 'zh-cn': speechLang = 'zh-CN'; break;
case 'jp': speechLang = 'ja-JP'; break;
case 'pt': speechLang = 'pt-PT'; break;
case 'bn': speechLang = 'bn-IN'; break;
case 'ko': speechLang = 'ko-KR'; break;
case 'vi': speechLang = 'vi-VN'; break;
case 'id': speechLang = 'id-ID'; break;
case 'th': speechLang = 'th-TH'; break;
case 'pl': speechLang = 'pl-PL'; break;
case 'nl': speechLang = 'nl-NL'; break;
case 'el': speechLang = 'el-GR'; break;
case 'sv': speechLang = 'sv-SE'; break;
case 'no': speechLang = 'no-NO'; break;
case 'da': speechLang = 'da-DK'; break;
case 'fi': speechLang = 'fi-FI'; break;
case 'cs': speechLang = 'cs-CZ'; break;
case 'hu': speechLang = 'hu-HU'; break;
case 'ro': speechLang = 'ro-RO'; break;
case 'he': speechLang = 'he-IL'; break;
case 'fa': speechLang = 'fa-IR'; break;
case 'ur': speechLang = 'ur-PK'; break;
case 'pa': speechLang = 'pa-IN'; break;
case 'mr': speechLang = 'mr-IN'; break;
case 'te': speechLang = 'te-IN'; break;
case 'ta': speechLang = 'ta-IN'; break;
case 'ms': speechLang = 'ms-MY'; break;
case 'tl': speechLang = 'fil-PH'; break;
default: speechLang = 'en-US';
}
speech.lang = speechLang;
speech.lang = getSpeechLang();
speech.onerror = function (event) {
console.warn('Speech synthesis error:', event.error);
@@ -3356,21 +3379,7 @@ const screenReader = {
localStorage.setItem('screenReader', isActive);
try {
// Set language based on current interface language
let speechLang = 'en-US'; // default
switch (currentLanguage) {
case 'de': speechLang = 'de-DE'; break;
case 'es': speechLang = 'es-ES'; break;
case 'it': speechLang = 'it-IT'; break;
case 'fr': speechLang = 'fr-FR'; break;
case 'ru': speechLang = 'ru-RU'; break;
case 'tr': speechLang = 'tr-TR'; break;
case 'ar': speechLang = 'ar-SA'; break;
case 'hi': speechLang = 'hi-IN'; break;
case 'zh-cn': speechLang = 'zh-CN'; break;
case 'jp': speechLang = 'ja-JP'; break;
default: speechLang = 'en-US';
}
let speechLang = getSpeechLang();
if (isActive) {
document.addEventListener('focusin', screenReader.handleFocus);