Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | | | |
| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | | | |
| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | | | |
| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | | | |
Expand Down
92 changes: 92 additions & 0 deletions docs/rules/template-require-lang-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# ember/template-require-lang-attribute

<!-- end auto-generated rule header -->

A missing or invalid `lang` attribute can cause an application to fail legal
conformance for digital accessibility requirements.

This rule's objective is to ensure that Ember applications achieve [WCAG
Success Criterion 3.1.1: Language of Page](https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html),
by declaring a valid IETF's BCP 47 language tag for the `lang` attribute. The
state of the `lang` attribute has a usability impact on the experience of users
that require screen-reading assistive technology. When the attribute is
properly assigned:

> "Both assistive technologies and conventional user agents can render text
> more accurately when the language of the Web page is identified. Screen
> readers can load the correct pronunciation rules. Visual browsers can display
> characters and scripts correctly. Media players can show captions correctly.
> **As a result, users with disabilities will be better able to understand the
> content.**"
>
> **Source: [WCAG Success Criterion 3.1.1:
> Intent](https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html#intent)**

When the language of the page cannot be identified, the integrity of the above
information cannot be guaranteed.

Consider any of the following use cases:

- the application developer is unaware that Ember now includes the lang attribute
- the application does not require internationalization
- the application's content is in a language that is not English
- an end-user with a screen reader turned on, whose operating system (OS) is set to a different language, navigates to that page with their screen reader turned on
- the screen reader would attempt to read the page in the language that is defined by the lang attribute on the page, but the supporting element information ("button", "link", etc) is read out in the language that is set by the operating system

## Examples

This rule **forbids** the following:

```gjs
<template>
<html></html>
</template>
```

```gjs
<template>
<html lang=""></html>
</template>
```

```gjs
<template>
<html lang="abracadabra"></html>
</template>
```

This rule **allows** the following:

```gjs
<template>
<html lang="en"></html>
</template>
```

```gjs
<template>
<html lang="en-US"></html>
</template>
```

```gjs
<template>
<html lang={{lang}}></html>
</template>
```

## Migration

Add the `lang` attribute to the `app/index.html` file in your Ember app. If
you use an internationalization addon like `ember-intl`, note that this will
not conflict with that addon.

## Configuration

- boolean -- if `true`, default configuration is applied
- object -- containing the following property:
- boolean -- `validateValues` -- if `true`, the rule checks whether the value in the `lang` attribute is a known IETF's BCP 47 language tag (default: `true`)

## References

- [WCAG Success Criterion 3.1.1: Language of Page](https://www.w3.org/WAI/WCAG21/Understanding/language-of-page.html)
284 changes: 284 additions & 0 deletions lib/rules/template-require-lang-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// Common valid BCP 47 language tags (not exhaustive, but covers the most common)
const COMMON_LANG_CODES = new Set([
'aa',
'ab',
'af',
'ak',
'am',
'an',
'ar',
'as',
'av',
'ay',
'az',
'ba',
'be',
'bg',
'bh',
'bi',
'bm',
'bn',
'bo',
'br',
'bs',
'ca',
'ce',
'ch',
'co',
'cr',
'cs',
'cu',
'cv',
'cy',
'da',
'de',
'dv',
'dz',
'ee',
'el',
'en',
'eo',
'es',
'et',
'eu',
'fa',
'ff',
'fi',
'fj',
'fo',
'fr',
'fy',
'ga',
'gd',
'gl',
'gn',
'gu',
'gv',
'ha',
'he',
'hi',
'ho',
'hr',
'ht',
'hu',
'hy',
'hz',
'ia',
'id',
'ie',
'ig',
'ii',
'ik',
'io',
'is',
'it',
'iu',
'ja',
'jv',
'ka',
'kg',
'ki',
'kj',
'kk',
'kl',
'km',
'kn',
'ko',
'kr',
'ks',
'ku',
'kv',
'kw',
'ky',
'la',
'lb',
'lg',
'li',
'ln',
'lo',
'lt',
'lu',
'lv',
'mg',
'mh',
'mi',
'mk',
'ml',
'mn',
'mr',
'ms',
'mt',
'my',
'na',
'nb',
'nd',
'ne',
'ng',
'nl',
'nn',
'no',
'nr',
'nv',
'ny',
'oc',
'oj',
'om',
'or',
'os',
'pa',
'pi',
'pl',
'ps',
'pt',
'qu',
'rm',
'rn',
'ro',
'ru',
'rw',
'sa',
'sc',
'sd',
'se',
'sg',
'si',
'sk',
'sl',
'sm',
'sn',
'so',
'sq',
'sr',
'ss',
'st',
'su',
'sv',
'sw',
'ta',
'te',
'tg',
'th',
'ti',
'tk',
'tl',
'tn',
'to',
'tr',
'ts',
'tt',
'tw',
'ty',
'ug',
'uk',
'ur',
'uz',
've',
'vi',
'vo',
'wa',
'wo',
'xh',
'yi',
'yo',
'za',
'zh',
'zu',
]);

const DEFAULT_CONFIG = {
validateValues: true,
};

function isValidLangTag(value) {
if (!value || !value.trim()) {
return false;
}
const parts = value.trim().toLowerCase().split('-');

return COMMON_LANG_CODES.has(parts[0]);
}

function parseConfig(config) {
if (config === true || config === undefined) {
return DEFAULT_CONFIG;
}

if (config && typeof config === 'object') {
return {
validateValues:
'validateValues' in config ? config.validateValues : DEFAULT_CONFIG.validateValues,
};
}

return DEFAULT_CONFIG;
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'require lang attribute on html element',
category: 'Accessibility',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-lang-attribute.md',
templateMode: 'both',
},
fixable: null,
schema: [
{
anyOf: [
{ type: 'boolean', enum: [true] },
{
type: 'object',
properties: {
validateValues: { type: 'boolean' },
},
additionalProperties: false,
},
],
},
],
messages: {
invalid: 'The `<html>` element must have the `lang` attribute with a valid value',
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/require-lang-attribute.js',
docs: 'docs/rule/require-lang-attribute.md',
tests: 'test/unit/rules/require-lang-attribute-test.js',
},
},

create(context) {
const config = parseConfig(context.options[0]);

return {
GlimmerElementNode(node) {
if (node.tag !== 'html') {
return;
}

const langAttr = node.attributes?.find((a) => a.name === 'lang');
if (!langAttr) {
context.report({ node, messageId: 'invalid' });
return;
}

if (!langAttr.value) {
context.report({ node, messageId: 'invalid' });
return;
}

if (langAttr.value.type === 'GlimmerTextNode') {
const value = langAttr.value.chars;

if (!value || !value.trim()) {
context.report({ node, messageId: 'invalid' });
} else if (config.validateValues && !isValidLangTag(value)) {
context.report({ node, messageId: 'invalid' });
}
}
},
};
},
};
Loading
Loading