Documentación de UXR SEO Analyzer

Introducción

El atributo lang y los lectores de pantalla (WCAG 3.1.1)

Ver contenido

Introducción

El atributo lang de HTML indica a los navegadores y tecnologías asistivas en qué idioma humano está tu contenido. WCAG 3.1.1 (Idioma de la Página) requiere que cada página web declare su idioma principal. Este atributo aparentemente simple tiene impactos profundos en la accesibilidad, pronunciación y experiencia de usuario.

Los lectores de pantalla usan el atributo de idioma para seleccionar las reglas de pronunciación y voz correctas. Sin una identificación adecuada del idioma, un lector de pantalla podría leer texto en francés con pronunciación en inglés, haciendo el contenido incomprensible para los usuarios.

Qué Requiere WCAG

3.1.1 Idioma de la Página (Nivel A)

El idioma humano predeterminado de cada página web puede determinarse programáticamente.

Esto significa:

  • Cada página HTML debe tener un atributo lang en el elemento <html>
  • El valor debe ser una etiqueta de idioma BCP 47 válida (ej., “es”, “en”, “fr”)
  • Esto permite que las tecnologías asistivas carguen reglas de pronunciación apropiadas

3.1.2 Idioma de las Partes (Nivel AA)

El idioma humano de cada pasaje o frase en el contenido puede determinarse programáticamente.

Esto significa:

  • El contenido en un idioma diferente al predeterminado de la página debe tener su propio atributo lang
  • Esto aplica a citas, frases o secciones en idiomas extranjeros
  • Excepciones: nombres propios, términos técnicos, palabras que se han convertido en parte del idioma circundante

Quién Se Beneficia

Tipo de Usuario Beneficio
Usuarios de lectores de pantalla Pronunciación correcta del contenido
Usuarios de herramientas de traducción Traducción precisa y dirigida
Usuarios con discapacidades de lectura Separación silábica y procesamiento de texto apropiados
Motores de búsqueda Mejor comprensión del idioma del contenido
Todos los usuarios Corrección ortográfica y auto-corrección correctas

Implementación Básica

Idioma de la Página

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Título de Página</title>
</head>
<body>
  <!-- Contenido de página en español -->
</body>
</html>

Idioma de las Partes

<p>
  La frase en inglés <span lang="en">that's life</span> significa "así es la vida".
</p>

<blockquote lang="en">
  <p>To be or not to be, that is the question.</p>
  <cite>— William Shakespeare</cite>
</blockquote>

Códigos de Idioma Comunes

Idioma Código Variantes Extendidas
Español es es-ES, es-MX, es-AR
Inglés en en-US, en-GB, en-AU
Francés fr fr-FR, fr-CA
Alemán de de-DE, de-AT, de-CH
Portugués pt pt-BR, pt-PT
Chino zh zh-CN, zh-TW, zh-Hans, zh-Hant
Japonés ja ja-JP
Coreano ko ko-KR
Árabe ar ar-SA, ar-EG
Ruso ru ru-RU

Problemas Comunes

1. Atributo lang Faltante

<!-- MAL: Sin idioma especificado -->
<!DOCTYPE html>
<html>
  <head>...</head>
  <body>...</body>
</html>

<!-- BIEN: Idioma declarado -->
<!DOCTYPE html>
<html lang="es">
  <head>...</head>
  <body>...</body>
</html>

2. Código de Idioma Incorrecto

<!-- MAL: Códigos de idioma inválidos -->
<html lang="espanol">
<html lang="spa">
<html lang="es_MX">

<!-- BIEN: Códigos BCP 47 válidos -->
<html lang="es">
<html lang="es-MX">

3. Frases Extranjeras Sin Etiquetas de Idioma

<!-- MAL: Inglés pronunciado con reglas del español -->
<p>El término marketing es muy usado en negocios.</p>

<!-- BIEN: Palabra inglesa marcada para pronunciación correcta -->
<p>El término <span lang="en">marketing</span> es muy usado en negocios.</p>

4. Desajuste de Idioma

<!-- MAL: Página en español pero marcada como inglés -->
<html lang="en">
  <body>
    <h1>Bienvenido a nuestro sitio</h1>
    <!-- Contenido en español... -->
  </body>
</html>

<!-- BIEN: Declaración de idioma correcta -->
<html lang="es">
  <body>
    <h1>Bienvenido a nuestro sitio</h1>
  </body>
</html>

Cuándo Marcar el Idioma de las Partes

Sí Marcar

  • Palabras extranjeras que podrían pronunciarse mal
  • Citas en otros idiomas
  • Pasajes o secciones en diferentes idiomas
  • Términos técnicos de otros idiomas (cuando la pronunciación importa)
<!-- Cita extranjera -->
<blockquote lang="en">
  <p>To be or not to be.</p>
</blockquote>

<!-- Frase extranjera -->
<p>El concepto de <span lang="de">Weltanschauung</span> es fundamental.</p>

No Necesitas Marcar

  • Nombres propios (como “François” o “München”)
  • Términos técnicos ampliamente usados (como “software” o “internet”)
  • Palabras adoptadas al español (como “fútbol” o “restaurante”)
  • Nombres de marcas
<!-- Estos no necesitan atributos lang -->
<p>Conoce a nuestro CEO, François Martin.</p>
<p>Actualiza el software del sistema.</p>
<p>El internet cambió el mundo.</p>

Impacto en Lectores de Pantalla

Cuando un lector de pantalla encuentra texto, verifica el atributo lang para seleccionar reglas de pronunciación:

Sin lang Con lang
“mar-ket-ing” (pronunciación española) “már-ke-ting” (pronunciación inglesa)
El lector de pantalla adivina el idioma El lector de pantalla sabe el idioma exacto
Potencialmente incomprensible Pronunciación clara y correcta

Probando Atributos de Idioma

Prueba Manual Rápida

  1. Verifica que <html> tenga un atributo lang
  2. Confirma que el código de idioma coincida con el contenido de la página
  3. Busca frases extranjeras y verifica los atributos lang
  4. Usa un lector de pantalla para verificar la pronunciación

Qué Verificar

  • [ ] La página tiene atributo lang en el elemento <html>
  • [ ] El código de idioma tiene formato BCP 47 válido
  • [ ] El idioma coincide con el contenido real de la página
  • [ ] Las frases extranjeras tienen atributos lang apropiados
  • [ ] El lector de pantalla pronuncia el contenido correctamente

Herramientas para Pruebas

  • axe DevTools - Señala idioma de página faltante
  • WAVE - Muestra atributos de idioma
  • DevTools del navegador - Inspeccionar elemento <html>
  • Validador W3C - Valida códigos de idioma

Resumen de Mejores Prácticas

Hacer No Hacer
Siempre incluir lang en <html> Dejar el atributo lang faltante
Usar códigos BCP 47 válidos Usar nombres completos de idioma o códigos inválidos
Marcar frases extranjeras Dejar contenido extranjero sin marcar
Hacer coincidir lang con el contenido real Usar código de idioma incorrecto
Probar con lectores de pantalla Asumir que la pronunciación es correcta

Implementación

Implementación HTML Completa

Plantilla de Idioma de Página

<!DOCTYPE html>
<html lang="es" dir="ltr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- Declaración de idioma en meta (suplementario) -->
  <meta http-equiv="content-language" content="es">

  <!-- Versiones en idiomas alternativos (SEO y accesibilidad) -->
  <link rel="alternate" hreflang="en" href="https://ejemplo.com/en/pagina">
  <link rel="alternate" hreflang="es" href="https://ejemplo.com/es/pagina">
  <link rel="alternate" hreflang="x-default" href="https://ejemplo.com/es/pagina">

  <title>Título de Página</title>
</head>
<body>
  <!-- Contenido en idioma principal (Español) -->

  <!-- Sección en diferente idioma -->
  <section lang="en">
    <h2>Section in English</h2>
    <p>This content is in English.</p>
  </section>

  <!-- Cambios de idioma en línea -->
  <p>
    El concepto de <span lang="de">Weltanschauung</span> se refiere a una
    visión del mundo.
  </p>
</body>
</html>

Estructura de Etiqueta de Idioma BCP 47

idioma-extlang-script-region-variante-extension-uso-privado

Ejemplos:
es          → Español (general)
es-ES       → Español (España)
es-MX       → Español (México)
zh-Hans     → Chino (script Simplificado)
zh-Hant     → Chino (script Tradicional)
zh-CN       → Chino (región China, implica Simplificado)
zh-TW       → Chino (región Taiwán, implica Tradicional)
pt-BR       → Portugués (Brasil)
sr-Latn     → Serbio (script Latino)
sr-Cyrl     → Serbio (script Cirílico)

Referencia Completa de Etiquetas de Idioma

const ETIQUETAS_IDIOMA = {
  // Idiomas Principales
  'es': 'Español',
  'es-ES': 'Español (España)',
  'es-MX': 'Español (México)',
  'es-AR': 'Español (Argentina)',
  'es-CO': 'Español (Colombia)',
  'es-CL': 'Español (Chile)',

  'en': 'Inglés',
  'en-US': 'Inglés (Estados Unidos)',
  'en-GB': 'Inglés (Reino Unido)',
  'en-AU': 'Inglés (Australia)',
  'en-CA': 'Inglés (Canadá)',

  'fr': 'Francés',
  'fr-FR': 'Francés (Francia)',
  'fr-CA': 'Francés (Canadá)',
  'fr-BE': 'Francés (Bélgica)',

  'de': 'Alemán',
  'de-DE': 'Alemán (Alemania)',
  'de-AT': 'Alemán (Austria)',
  'de-CH': 'Alemán (Suiza)',

  'pt': 'Portugués',
  'pt-BR': 'Portugués (Brasil)',
  'pt-PT': 'Portugués (Portugal)',

  // Idiomas Asiáticos
  'zh': 'Chino',
  'zh-Hans': 'Chino (Simplificado)',
  'zh-Hant': 'Chino (Tradicional)',
  'zh-CN': 'Chino (China)',
  'zh-TW': 'Chino (Taiwán)',
  'zh-HK': 'Chino (Hong Kong)',

  'ja': 'Japonés',
  'ja-JP': 'Japonés (Japón)',

  'ko': 'Coreano',
  'ko-KR': 'Coreano (Corea del Sur)',

  // Idiomas RTL
  'ar': 'Árabe',
  'ar-SA': 'Árabe (Arabia Saudita)',
  'ar-EG': 'Árabe (Egipto)',
  'he': 'Hebreo',
  'fa': 'Persa/Farsi',

  // Otros Europeos
  'it': 'Italiano',
  'nl': 'Holandés',
  'ru': 'Ruso',
  'pl': 'Polaco',
  'sv': 'Sueco',
  'da': 'Danés',
  'no': 'Noruego',
  'fi': 'Finlandés',
  'el': 'Griego',
  'tr': 'Turco',

  // Clásicos/Históricos
  'la': 'Latín',
  'grc': 'Griego Antiguo',
};

Implementaciones en Frameworks

React con i18n

import React, { createContext, useContext, useEffect } from 'react';

// Contexto de Idioma
interface LanguageContextType {
  language: string;
  setLanguage: (lang: string) => void;
  dir: 'ltr' | 'rtl';
}

const LanguageContext = createContext<LanguageContextType | null>(null);

// Idiomas RTL
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];

export function LanguageProvider({
  children,
  defaultLanguage = 'es'
}: {
  children: React.ReactNode;
  defaultLanguage?: string;
}) {
  const [language, setLanguageState] = React.useState(defaultLanguage);
  const dir = RTL_LANGUAGES.some(l => language.startsWith(l)) ? 'rtl' : 'ltr';

  const setLanguage = (lang: string) => {
    setLanguageState(lang);
    // Actualizar atributos del documento
    document.documentElement.lang = lang;
    document.documentElement.dir = RTL_LANGUAGES.some(l => lang.startsWith(l))
      ? 'rtl'
      : 'ltr';
  };

  // Establecer idioma inicial del documento
  useEffect(() => {
    document.documentElement.lang = language;
    document.documentElement.dir = dir;
  }, []);

  return (
    <LanguageContext.Provider value={{ language, setLanguage, dir }}>
      {children}
    </LanguageContext.Provider>
  );
}

export function useLanguage() {
  const context = useContext(LanguageContext);
  if (!context) {
    throw new Error('useLanguage debe usarse dentro de LanguageProvider');
  }
  return context;
}

// Componente para cambios de idioma en línea
interface LangSpanProps {
  lang: string;
  children: React.ReactNode;
  className?: string;
}

export function LangSpan({ lang, children, className }: LangSpanProps) {
  const isRtl = RTL_LANGUAGES.some(l => lang.startsWith(l));

  return (
    <span
      lang={lang}
      dir={isRtl ? 'rtl' : undefined}
      className={className}
    >
      {children}
    </span>
  );
}

// Componente para cambios de idioma a nivel de bloque
interface LangBlockProps {
  lang: string;
  children: React.ReactNode;
  as?: 'div' | 'section' | 'article' | 'blockquote';
  className?: string;
}

export function LangBlock({
  lang,
  children,
  as: Component = 'div',
  className
}: LangBlockProps) {
  const isRtl = RTL_LANGUAGES.some(l => lang.startsWith(l));

  return (
    <Component
      lang={lang}
      dir={isRtl ? 'rtl' : 'ltr'}
      className={className}
    >
      {children}
    </Component>
  );
}

// Ejemplo de Uso
function ContenidoMultilingue() {
  return (
    <div>
      <p>
        La palabra alemana <LangSpan lang="de">Schadenfreude</LangSpan> describe
        el placer derivado de la desgracia de otros.
      </p>

      <LangBlock lang="en" as="blockquote">
        <p>To be or not to be, that is the question.</p>
        <cite>— William Shakespeare</cite>
      </LangBlock>

      <LangBlock lang="ar" as="section">
        <h2>مقطع باللغة العربية</h2>
        <p>هذا النص مكتوب باللغة العربية.</p>
      </LangBlock>
    </div>
  );
}

Implementación en Vue 3

<script setup lang="ts">
import { ref, computed, watch, onMounted, provide } from 'vue'

// Composable de idioma
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']

export function useLanguage(defaultLanguage = 'es') {
  const language = ref(defaultLanguage)
  const dir = computed(() =>
    RTL_LANGUAGES.some(l => language.value.startsWith(l)) ? 'rtl' : 'ltr'
  )

  function setLanguage(lang: string) {
    language.value = lang
    document.documentElement.lang = lang
    document.documentElement.dir = RTL_LANGUAGES.some(l => lang.startsWith(l))
      ? 'rtl'
      : 'ltr'
  }

  onMounted(() => {
    document.documentElement.lang = language.value
    document.documentElement.dir = dir.value
  })

  watch(language, (newLang) => {
    document.documentElement.lang = newLang
    document.documentElement.dir = RTL_LANGUAGES.some(l => newLang.startsWith(l))
      ? 'rtl'
      : 'ltr'
  })

  return { language, dir, setLanguage }
}

// Usando en componente
const { language, dir, setLanguage } = useLanguage('es')

// Proporcionar a componentes hijos
provide('language', { language, dir, setLanguage })
</script>

<template>
  <div>
    <!-- Selector de idioma -->
    <select @change="setLanguage(($event.target as HTMLSelectElement).value)">
      <option value="es">Español</option>
      <option value="en">English</option>
      <option value="fr">Français</option>
      <option value="ar">العربية</option>
    </select>

    <!-- Cambio de idioma en línea -->
    <p>
      La frase en inglés <span lang="en">that's life</span> significa "así es la vida".
    </p>

    <!-- Cambio de idioma en bloque con soporte RTL -->
    <blockquote lang="ar" dir="rtl">
      <p>السلام عليكم</p>
    </blockquote>

    <!-- Sección en diferente idioma -->
    <section lang="en">
      <h2>Section in English</h2>
      <p>This content is completely in English.</p>
    </section>
  </div>
</template>

Componente LangSpan para Vue

<!-- components/LangSpan.vue -->
<script setup lang="ts">
import { computed } from 'vue'

const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']

interface Props {
  lang: string
  tag?: 'span' | 'div' | 'section' | 'article' | 'blockquote' | 'p'
}

const props = withDefaults(defineProps<Props>(), {
  tag: 'span'
})

const isRtl = computed(() =>
  RTL_LANGUAGES.some(l => props.lang.startsWith(l))
)

const direction = computed(() => isRtl.value ? 'rtl' : undefined)
</script>

<template>
  <component
    :is="tag"
    :lang="lang"
    :dir="direction"
  >
    <slot />
  </component>
</template>

<!-- Uso -->
<LangSpan lang="de">Kindergarten</LangSpan>
<LangSpan lang="ja" tag="blockquote">
  <p>日本語のテキスト</p>
</LangSpan>

CSS para Estilos Específicos por Idioma

/* Selección de fuentes base por idioma */
:lang(es) {
  font-family: 'Inter', system-ui, sans-serif;
}

:lang(zh) {
  font-family: 'Noto Sans SC', 'PingFang SC', sans-serif;
}

:lang(ja) {
  font-family: 'Noto Sans JP', 'Hiragino Sans', sans-serif;
}

:lang(ko) {
  font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
}

:lang(ar) {
  font-family: 'Noto Sans Arabic', 'Tahoma', sans-serif;
}

/* Soporte para idiomas RTL */
:lang(ar),
:lang(he),
:lang(fa),
:lang(ur) {
  direction: rtl;
  text-align: right;
}

/* Comillas por idioma */
:lang(es) q {
  quotes: '«' '»' '"' '"';
}

:lang(en) q {
  quotes: '"' '"' "'" "'";
}

:lang(fr) q {
  quotes: '« ' ' »' '‹ ' ' ›';
}

:lang(de) q {
  quotes: '„' '"' '‚' ''';
}

:lang(ja) q {
  quotes: '' '' '' '';
}

/* Estilo de frases extranjeras en línea */
[lang]:not(:lang(es)) {
  /* Indicación sutil de cambio de idioma */
  font-style: italic;
}

/* Excepción: No italizar cambios de idioma a nivel de bloque */
section[lang],
article[lang],
blockquote[lang],
div[lang] {
  font-style: normal;
}

/* Aislamiento de texto bidireccional */
[lang] {
  unicode-bidi: isolate;
}

/* Separación silábica por idioma */
:lang(es) {
  hyphens: auto;
  -webkit-hyphens: auto;
}

:lang(de) {
  /* El alemán tiene reglas estrictas de separación silábica */
  hyphens: auto;
  -webkit-hyphens: auto;
}

:lang(zh),
:lang(ja),
:lang(ko) {
  /* Los idiomas CJK no usan separación silábica */
  hyphens: none;
  word-break: keep-all;
}

Detección de Idioma del Lado del Servidor

Implementación Next.js/Node.js

// lib/language.ts
import { NextRequest } from 'next/server';

const SUPPORTED_LANGUAGES = ['es', 'en', 'fr', 'de', 'ar'];
const DEFAULT_LANGUAGE = 'es';

export function detectLanguage(request: NextRequest): string {
  // 1. Verificar parámetro de URL
  const urlLang = request.nextUrl.searchParams.get('lang');
  if (urlLang && SUPPORTED_LANGUAGES.includes(urlLang)) {
    return urlLang;
  }

  // 2. Verificar prefijo de ruta
  const pathLang = request.nextUrl.pathname.split('/')[1];
  if (SUPPORTED_LANGUAGES.includes(pathLang)) {
    return pathLang;
  }

  // 3. Verificar cookie
  const cookieLang = request.cookies.get('preferred-language')?.value;
  if (cookieLang && SUPPORTED_LANGUAGES.includes(cookieLang)) {
    return cookieLang;
  }

  // 4. Verificar encabezado Accept-Language
  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    const languages = acceptLanguage
      .split(',')
      .map(lang => {
        const [code, q = '1'] = lang.trim().split(';q=');
        return { code: code.split('-')[0], quality: parseFloat(q) };
      })
      .sort((a, b) => b.quality - a.quality);

    for (const { code } of languages) {
      if (SUPPORTED_LANGUAGES.includes(code)) {
        return code;
      }
    }
  }

  // 5. Por defecto
  return DEFAULT_LANGUAGE;
}

// middleware.ts
export function middleware(request: NextRequest) {
  const language = detectLanguage(request);

  // Establecer encabezado de idioma para SSR
  const response = NextResponse.next();
  response.headers.set('x-detected-language', language);

  return response;
}

Pruebas Automatizadas

Tests de Playwright para Atributos de Idioma

import { test, expect } from '@playwright/test';

test.describe('Atributos de Idioma', () => {
  test('la página tiene atributo lang válido en elemento html', async ({ page }) => {
    await page.goto('/');

    // Obtener atributo lang del elemento html
    const lang = await page.locator('html').getAttribute('lang');

    // Debe existir
    expect(lang).not.toBeNull();
    expect(lang).not.toBe('');

    // Debe tener formato BCP 47 válido (verificación básica)
    expect(lang).toMatch(/^[a-z]{2,3}(-[A-Za-z]{2,4})?(-[A-Za-z]{2})?$/);
  });

  test('el atributo lang coincide con el idioma del contenido de la página', async ({ page }) => {
    // Probar página en español
    await page.goto('/es/nosotros');
    const esLang = await page.locator('html').getAttribute('lang');
    expect(esLang).toMatch(/^es/);

    // Probar página en inglés
    await page.goto('/en/about');
    const enLang = await page.locator('html').getAttribute('lang');
    expect(enLang).toMatch(/^en/);
  });

  test('las frases extranjeras tienen atributos lang', async ({ page }) => {
    await page.goto('/contenido-multilingue');

    // Encontrar spans/elementos con contenido extranjero
    const foreignPhrases = page.locator('[lang]:not(html)');
    const count = await foreignPhrases.count();

    // Si la página tiene contenido extranjero, debe estar marcado
    if (count > 0) {
      for (let i = 0; i < count; i++) {
        const lang = await foreignPhrases.nth(i).getAttribute('lang');
        expect(lang).not.toBeNull();
        expect(lang).toMatch(/^[a-z]{2,3}/);
      }
    }
  });

  test('los idiomas RTL tienen atributo dir correcto', async ({ page }) => {
    await page.goto('/ar/about');

    // HTML debe tener dir="rtl" para árabe
    const dir = await page.locator('html').getAttribute('dir');
    expect(dir).toBe('rtl');

    // El contenido árabe en línea también debe tener dir
    const arabicContent = page.locator('[lang^="ar"]');
    const count = await arabicContent.count();

    for (let i = 0; i < count; i++) {
      const element = arabicContent.nth(i);
      const isBlock = await element.evaluate(el =>
        ['DIV', 'SECTION', 'ARTICLE', 'BLOCKQUOTE'].includes(el.tagName)
      );

      if (isBlock) {
        const dir = await element.getAttribute('dir');
        expect(dir).toBe('rtl');
      }
    }
  });

  test('los atributos hreflang están presentes para idiomas alternativos', async ({ page }) => {
    await page.goto('/es/nosotros');

    // Verificar enlaces de idiomas alternativos
    const alternateLinks = page.locator('link[rel="alternate"][hreflang]');
    const count = await alternateLinks.count();

    // Debe tener al menos 2 (propio + uno alternativo)
    expect(count).toBeGreaterThanOrEqual(2);

    // Cada uno debe tener hreflang válido
    for (let i = 0; i < count; i++) {
      const hreflang = await alternateLinks.nth(i).getAttribute('hreflang');
      expect(hreflang).toMatch(/^[a-z]{2,3}(-[A-Za-z]{2})?$|^x-default$/);
    }
  });

  test('el selector de idioma cambia el atributo lang', async ({ page }) => {
    await page.goto('/es/nosotros');

    // Verificar idioma inicial
    let lang = await page.locator('html').getAttribute('lang');
    expect(lang).toMatch(/^es/);

    // Hacer clic en selector de idioma
    await page.click('[data-testid="lang-switch-en"]');

    // Esperar navegación
    await page.waitForURL(/\/en\//);

    // Verificar que el idioma cambió
    lang = await page.locator('html').getAttribute('lang');
    expect(lang).toMatch(/^en/);
  });
});

Auditoría de Accesibilidad para Idioma

// language-audit.js
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Auditoría de Accesibilidad de Idioma', () => {
  test('pasa las reglas de idioma de axe', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withRules([
        'html-has-lang',           // elemento html debe tener lang
        'html-lang-valid',         // lang debe ser válido
        'html-xml-lang-mismatch',  // lang y xml:lang deben coincidir
        'valid-lang'               // atributos lang deben ser válidos
      ])
      .analyze();

    expect(results.violations).toHaveLength(0);
  });

  test('todas las páginas tienen declaración de idioma consistente', async ({ page }) => {
    const pages = ['/es/', '/es/nosotros', '/es/servicios', '/es/contacto'];

    for (const url of pages) {
      await page.goto(url);

      const lang = await page.locator('html').getAttribute('lang');
      expect(lang).toMatch(/^es/);

      // Ejecutar axe en cada página
      const results = await new AxeBuilder({ page })
        .withRules(['html-has-lang', 'html-lang-valid'])
        .analyze();

      expect(results.violations).toHaveLength(0);
    }
  });
});

Checklist de Resumen

  • [ ] Cada página tiene atributo lang en el elemento <html>
  • [ ] Los códigos de idioma siguen el formato BCP 47
  • [ ] El idioma coincide con el contenido real de la página
  • [ ] Las frases extranjeras tienen atributos lang apropiados
  • [ ] Los idiomas RTL tienen atributo dir="rtl"
  • [ ] CSS proporciona configuraciones tipográficas específicas por idioma
  • [ ] Las versiones en idiomas alternativos tienen enlaces hreflang
  • [ ] La detección de idioma usa una cadena de respaldo apropiada
  • [ ] Los componentes soportan cambio dinámico de idioma
  • [ ] Las pruebas automatizadas verifican los atributos de idioma

Artículos Relacionados


Referencias

  1. W3C - WCAG 2.2 SC 3.1.1 Language of Page
  2. W3C - WCAG 2.2 SC 3.1.2 Language of Parts
  3. W3C - H57 Using lang attribute
  4. MDN - lang attribute
  5. IETF - BCP 47: Tags for Identifying Languages
  6. W3C - Choosing language tags

Artículos relacionados

Hub de categoría

Hub

Hub de Cumplimiento WCAG 2.2

La accesibilidad web asegura que los sitios web y aplicaciones puedan ser utilizados por todos, incluyendo personas con discapacidades

Última actualización: