feat: rule jsxA11yHTMLInvalidLang
MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -106,6 +106,7 @@ type LintDiagnosticCategory =
| "lint/jsxA11yAnchorHasContent"
| "lint/jsxA11yHeadingHasContent"
| "lint/jsxA11yHTMLHasLang"
+ | "lint/jsxA11yHTMLInvalidLang"
| "lint/jsxA11yIframeHasTitle"
| "lint/jsxA11yImgRedundantAlt"
| "lint/jsxA11yNoAccessKey"

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -300,6 +300,10 @@ export const descriptions = createMessages({
category: "lint/jsxA11yAnchorHasContent",
message: "Anchor must have content and the content must be accessible by a screen reader.",
},
+ JSX_A11Y_HTML_INVALID_LANG: {
+ category: "lint/jsxA11yHTMLInvalidLang",
+ message: `the <emphasis>html</emphasis> attribute <emphasis>lang</emphasis> is not supported.`,
+ },
NO_DID_UPDATE_SET_STATE: {
category: "lint/noDidUpdateSetState",
message: "Avoid <emphasis>this.setState</emphasis> in <emphasis>componentDidUpdate</emphasis>",

MODIFY
packages/@romejs/js-compiler/lint/rules/index.ts
packages/@romejs/js-compiler/lint/rules/index.ts
@@ -20,6 +20,7 @@ import jsxA11YAltText from "./react/jsxA11yAltText";
import jsxA11YAnchorHasContent from "./react/jsxA11yAnchorHasContent";
import jsxA11YHeadingHasContent from "./react/jsxA11yHeadingHasContent";
import jsxA11YHTMLHasLang from "./react/jsxA11yHTMLHasLang";
+import jsxA11YHTMLInvalidLang from "./react/jsxA11yHTMLInvalidLang";
import jsxA11YIframeHasTitle from "./react/jsxA11yIframeHasTitle";
import jsxA11YImgRedundantAlt from "./react/jsxA11yImgRedundantAlt";
import jsxA11YNoAccessKey from "./react/jsxA11yNoAccessKey";
@@ -94,6 +95,7 @@ export const lintTransforms = [
jsxA11YAnchorHasContent,
jsxA11YHeadingHasContent,
jsxA11YHTMLHasLang,
+ jsxA11YHTMLInvalidLang,
jsxA11YIframeHasTitle,
jsxA11YImgRedundantAlt,
jsxA11YNoAccessKey,

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yHTMLInvalidLang.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "should warn when an invalid lang is provided",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ '<html lang="foo"></html>',
+ '<html lang="foo-bar"></html>',
+ '<html lang="aa-zz"></html>',
+ '<html lang="zz-AA"></html>',
+ // VALID
+ '<html lang="en-US"></html>',
+ ],
+ {category: "lint/jsxA11yHTMLInvalidLang"},
+ );
+ },
+);

ADD
packages/@romejs/js-compiler/lint/rules/react/jsxA11yHTMLInvalidLang.ts
+import {Path, TransformExitResult} from "@romejs/js-compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {JSXElement} from "@romejs/js-ast";
+import {
+ getJSXAttribute,
+ hasJSXAttribute,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+
+const ISO = {
+ "countries": [
+ "AF",
+ "AL",
+ "DZ",
+ "AS",
+ "AD",
+ "AO",
+ "AI",
+ "AQ",
+ "AG",
+ "AR",
+ "AM",
+ "AW",
+ "AU",
+ "AT",
+ "AZ",
+ "BS",
+ "BH",
+ "BD",
+ "BB",
+ "BY",
+ "BE",
+ "BZ",
+ "BJ",
+ "BM",
+ "BT",
+ "BO",
+ "BA",
+ "BW",
+ "BR",
+ "IO",
+ "VG",
+ "BN",
+ "BG",
+ "BF",
+ "MM",
+ "BI",
+ "KH",
+ "CM",
+ "CA",
+ "CV",
+ "KY",
+ "CF",
+ "TD",
+ "CL",
+ "CN",
+ "CX",
+ "CC",
+ "CO",
+ "KM",
+ "CK",
+ "CR",
+ "HR",
+ "CU",
+ "CY",
+ "CZ",
+ "CD",
+ "DK",
+ "DJ",
+ "DM",
+ "DO",
+ "EC",
+ "EG",
+ "SV",
+ "GQ",
+ "ER",
+ "EE",
+ "ET",
+ "FK",
+ "FO",
+ "FJ",
+ "FI",
+ "FR",
+ "PF",
+ "GA",
+ "GM",
+ "GE",
+ "DE",
+ "GH",
+ "GI",
+ "GR",
+ "GL",
+ "GD",
+ "GU",
+ "GT",
+ "GN",
+ "GW",
+ "GY",
+ "HT",
+ "VA",
+ "HN",
+ "HK",
+ "HU",
+ "IS",
+ "IN",
+ "ID",
+ "IR",
+ "IQ",
+ "IE",
+ "IM",
+ "IL",
+ "IT",
+ "CI",
+ "JM",
+ "JP",
+ "JE",
+ "JO",
+ "KZ",
+ "KE",
+ "KI",
+ "KW",
+ "KG",
+ "LA",
+ "LV",
+ "LB",
+ "LS",
+ "LR",
+ "LY",
+ "LI",
+ "LT",
+ "LU",
+ "MO",
+ "MK",
+ "MG",
+ "MW",
+ "MY",
+ "MV",
+ "ML",
+ "MT",
+ "MH",
+ "MR",
+ "MU",
+ "YT",
+ "MX",
+ "FM",
+ "MD",
+ "MC",
+ "MN",
+ "ME",
+ "MS",
+ "MA",
+ "MZ",
+ "NA",
+ "NR",
+ "NP",
+ "NL",
+ "AN",
+ "NC",
+ "NZ",
+ "NI",
+ "NE",
+ "NG",
+ "NU",
+ "KP",
+ "MP",
+ "NO",
+ "OM",
+ "PK",
+ "PW",
+ "PA",
+ "PG",
+ "PY",
+ "PE",
+ "PH",
+ "PN",
+ "PL",
+ "PT",
+ "PR",
+ "QA",
+ "CG",
+ "RO",
+ "RU",
+ "RW",
+ "BL",
+ "SH",
+ "KN",
+ "LC",
+ "MF",
+ "PM",
+ "VC",
+ "WS",
+ "SM",
+ "ST",
+ "SA",
+ "SN",
+ "RS",
+ "SC",
+ "SL",
+ "SG",
+ "SK",
+ "SI",
+ "SB",
+ "SO",
+ "ZA",
+ "KR",
+ "ES",
+ "LK",
+ "SD",
+ "SR",
+ "SJ",
+ "SZ",
+ "SE",
+ "CH",
+ "SY",
+ "TW",
+ "TJ",
+ "TZ",
+ "TH",
+ "TL",
+ "TG",
+ "TK",
+ "TO",
+ "TT",
+ "TN",
+ "TR",
+ "TM",
+ "TC",
+ "TV",
+ "UG",
+ "UA",
+ "AE",
+ "GB",
+ "US",
+ "UY",
+ "VI",
+ "UZ",
+ "VU",
+ "VE",
+ "VN",
+ "WF",
+ "EH",
+ "YE",
+ "ZM",
+ "ZW",
+ ],
+ "languages": [
+ "ab",
+ "aa",
+ "af",
+ "sq",
+ "am",
+ "ar",
+ "an",
+ "hy",
+ "as",
+ "ay",
+ "az",
+ "ba",
+ "eu",
+ "bn",
+ "dz",
+ "bh",
+ "bi",
+ "br",
+ "bg",
+ "my",
+ "be",
+ "km",
+ "ca",
+ "zh",
+ "zh-Hans",
+ "zh-Hant",
+ "co",
+ "hr",
+ "cs",
+ "da",
+ "nl",
+ "en",
+ "eo",
+ "et",
+ "fo",
+ "fa",
+ "fj",
+ "fi",
+ "fr",
+ "fy",
+ "gl",
+ "gd",
+ "gv",
+ "ka",
+ "de",
+ "el",
+ "kl",
+ "gn",
+ "gu",
+ "ht",
+ "ha",
+ "he",
+ "iw",
+ "hi",
+ "hu",
+ "is",
+ "io",
+ "id",
+ "in",
+ "ia",
+ "ie",
+ "iu",
+ "ik",
+ "ga",
+ "it",
+ "ja",
+ "jv",
+ "kn",
+ "ks",
+ "kk",
+ "rw",
+ "ky",
+ "rn",
+ "ko",
+ "ku",
+ "lo",
+ "la",
+ "lv",
+ "li",
+ "ln",
+ "lt",
+ "mk",
+ "mg",
+ "ms",
+ "ml",
+ "mt",
+ "mi",
+ "mr",
+ "mo",
+ "mn",
+ "na",
+ "ne",
+ "no",
+ "oc",
+ "or",
+ "om",
+ "ps",
+ "pl",
+ "pt",
+ "pa",
+ "qu",
+ "rm",
+ "ro",
+ "ru",
+ "sm",
+ "sg",
+ "sa",
+ "sr",
+ "sh",
+ "st",
+ "tn",
+ "sn",
+ "ii",
+ "sd",
+ "si",
+ "ss",
+ "sk",
+ "sl",
+ "so",
+ "es",
+ "su",
+ "sw",
+ "sv",
+ "tl",
+ "tg",
+ "ta",
+ "tt",
+ "te",
+ "th",
+ "bo",
+ "ti",
+ "to",
+ "ts",
+ "tr",
+ "tk",
+ "tw",
+ "ug",
+ "uk",
+ "ur",
+ "uz",
+ "vi",
+ "vo",
+ "wa",
+ "cy",
+ "wo",
+ "xh",
+ "yi",
+ "ji",
+ "yo",
+ "zu",
+ ],
+};
+
+const LANG_REGEX = new RegExp(/([a-z]{2})-([A-Z]{2})/);
+
+function jsxSupportedLang(node: JSXElement): boolean | undefined {
+ const attr = getJSXAttribute(node, "lang");
+
+ return (
+ attr !== undefined &&
+ attr.value &&
+ attr.value.type === "StringLiteral" &&
+ LANG_REGEX.test(attr.value.value) &&
+ langSupported(attr.value.value)
+ );
+}
+
+function langSupported(lang: string): boolean {
+ const matches = LANG_REGEX.exec(lang);
+ if (matches && matches.length > 0) {
+ return (
+ ISO.languages.includes(matches[1]) && ISO.countries.includes(matches[2])
+ );
+ }
+ return false;
+}
+export default {
+ name: "jsxA11yHTMLInvalidLang",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (isJSXElement(node, "html") && hasJSXAttribute(node, "lang")) {
+ if (jsxSupportedLang(node) === false) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_HTML_INVALID_LANG,
+ );
+ }
+ }
+
+ return node;
+ },
+};

feat: rule jsxA11yHTMLInvalidLang