myf
myf

Reputation: 11313

CSS :lang() selector for elements in documents of undetermined language

Is it possible to target elements that have no language set nor inherited, i.e. are in unspecified ("unknown") language?

Trivia

HTML document or element language can be set using HTML lang attribute, e.g.:

<html lang="en">
<h1>Dictionary</h1>
<dl>
<dt><abbr lang="en-Mors">-... - .--</abbr>
<dd><i lang="fr-Latn">à propos</i>
</dl>

or using code(s) in the HTTP Content-language header:

HTTP/2 200 OK
[other headers]
Content-language: en,en-Brai,fr-Latn

<html>
<h1>Dictionary</h1>
[rest of document]

or it's long deprecated yet still working <meta http-equiv> counterpart:

<html>
 <head>
  <meta http-equiv="content-language" content="en,en-Brai,fr-Latn">
</head>
<html>
<h1>Dictionary</h1>
[rest of document]

In either case using :lang(en) CSS selector matches main heading from examples and all other elements that has not explicit lang attribute with value not equal or starting with "en".

Goal

In case the document is sent without Content-language HTTP header or <meta> element and without lang attribute, is it possible to match those elements that falls to inevitable "unknown" language?

Plus in document or DOM fragment that has language set by any aforementioned mean, is it possible to use lang() CSS selector to match elements with empty lang="" attribute, that effectively 'opts out' of having language?

HTTP/2 200 OK
[no content-language header nor meta present]

<html>
<p>I Want to select this. <span>And this.</span></p>
<p lang="">And this.</p>
<p lang="en">Not this. <span lang="">But this again.</span></p>

What does not work

Neither :lang(), :lang(unknown), :lang('') nor :not(:lang(*)) works for this purpose. Selectors derived from :not([lang]), [lang=''] would logically give false negative for use-cases with HTTP Content-language header/meta present.

Answer requirements

Seeking answer that either gives solution without false negatives or confirms it is not possible with references to specs (or their absence) and explanation why is it so.


Notes:

When empty lang="" attribute is present, targeting it with [lang=""] attribute selector works, but feels weird considering there is dedicated :lang() pseudo-class for language-related stuff.

Upvotes: 5

Views: 1413

Answers (2)

the Hutt
the Hutt

Reputation: 18418

Edit 2021: This has been accepted as a bug https://bugs.chromium.org/p/chromium/issues/detail?id=1281157


We provide language range in :lang() rule and they are matched against language tags. They've mentioned about supporting asterisks in language ranges:

Language ranges containing asterisks, for example, must be either correctly escaped or quoted as strings, e.g. :lang(*-Latn) or :lang("*-Latn") ref

And in old 2013 draft:

Each language range in :lang() must be a valid CSS identifier [CSS21] or consist of an asterisk (* U+002A) immediately followed by an identifier beginning with an ASCII hyphen (U+002D) for the selector to be valid. ref

But I can't get p:lang(\*-US) to work on Chrome and Firefox on Windows. The rule p:lang(en\002DUS) works thought, but p:lang(en\002D\002A) does not. Not sure about the status of the support for special range "*" in browsers. Also there is no mention of matching undefined by the special range "*" in Matching of Language Tags.


But,p:lang(\*) and p:not(:lang(\*)) work on iPadOs in both Safari and Chrome. Open this jsfiddle on ipad
worksonIOS
I think chromium doesn’t support the full :lang() feature.


Workaround: If a little bit of JavaScript is acceptable then you can try following solution:

document.addEventListener('DOMContentLoaded', init);

function init() {
  if (!document.documentElement.lang) {
    fetchSamePageHeaders(checkHeaderLanguage);
  }
}

//make a lightweight request to the same page to get headers
function fetchSamePageHeaders(callback) {
  var request = new XMLHttpRequest();
  request.onreadystatechange = function() {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (callback && typeof callback === 'function') {
        callback(request.getAllResponseHeaders());
      }
    }
  };

  // The HEAD method asks for a response identical to that 
  // of a GET request, but without the response body.
  //you can also use 'GET', 'POST' method depending on situation      
  request.open('HEAD', document.location, true);
  request.send(null);
}

function checkHeaderLanguage(headers) {
  //console.log(headers);
  headers = headers.split("\n").map(x => x.split(/: */, 2))
    .filter(x => x[0]).reduce((ac, x) => {
      ac[x[0]] = x[1];
      return ac;
    }, {});

  if (!headers['content-language']) {
    console.log('No language in response header. Marking the html tag.');
    let html = document.querySelector('html');
    html.lang = 'dummyLang';
  } else {
    console.log('The response header has language:' + headers['content-language']);
  }
}
p {
  margin: 0;
}

p[lang=""],
p:lang(dummyLang) {
  color: darkgreen;
  font-size: 2em;
}

p:lang(en\2dus)::after {
  content: '<= english';
  font-size: 0.5em;
  color: rebeccapurple;
}
<p>I Want to select this.</p>
<p lang="">And this.</p>
<p lang="en-us">Not this.</p>
<span lang='en-us'>
    <p>Also, not this.</p>
    <p lang="">But, this too.</p>
</span>


Here we are using JavaScript to determine if the language has been mentioned in the html tag or in response header. And assigning the html tag dummyLang language. You may also want to check meta tags.
For detailed explanation about Getting HTTP headers in javascript and pros and cons of this technique, refer this SO discussion.

Upvotes: 3

Esh
Esh

Reputation: 516

I have managed to come up with a work around, first you can run some js to set the lang attribute of every element with no lang attribute to "xyz" and then select that using css.....

document.querySelectorAll("p").forEach(e => {
  if (e.lang == "") e.lang = "xyz";
})
p:lang(xyz) {
  color: red;
}
<p>I Want to select this.</p>
<p lang="">And this.</p>
<p lang="en">Not this.</p>

Upvotes: 0

Related Questions