Stephen Chenney's Professional Ramblings

Explaining (some of) the Web Platform

Canvas Localization Support

The Web must support all users, with a multitude of languages and writing styles. HTML supports localization through the HTTP content language representation header, while HTML defines the lang and dir attributes. Yet the canvas element has no direct way of specifying the language for text drawing, nor good support for controlling the writing direction. Such localization controls the choice of characters from a given font and the bidi ordering.

Canvas Localization Today #

In browsers at the time of writing, a <canvas> element in a web page uses the HTML lang attribute of the element for glyph selection, and uses the CSS direction property for the text direction. Note that in all browsers the CSS direction property will have the HTML dir value unless otherwise set. In addition, there is an explicit direction attribute on the <canvas> element’s CanvasRenderingContext2D to control the text drawing direction. That attribute accepts the special "inherit" value, the default, by which the direction is taken from the element.

This example demonstrates the current behavior for language in canvas text.

<div>
<h3>Canvas lang="en"</h3>
<canvas id="en" width="200px" height="60px" lang="en"></canvas>
</div>
<div>
<h3>Canvas lang="tr"</h3>
<canvas id="tr" width="200px" height="60px" lang="tr"></canvas>
</div>
<script>
let en_context = document.getElementById('en').getContext('2d');
let tr_context = document.getElementById('tr').getContext('2d');

function drawText(context) {
context.font = '20px Lato-Medium';
context.color = 'black';
context.fillText('finish', 50, 20);
}

function run_tests() {
drawText(en_context);
drawText(tr_context);
};

// See the example for the code to load the font
</script>

The language comes from the lang attribute on the canvas element, and here it controls the use of ligatures.

OffscreenCanvas provides no such localization support. The "inherit" text direction is always left-to-right and the language is always the default browser language. As a result, in many locales text content will be drawn differently in offscreen vs. DOM canvases. This is a significant problem given offscreen canvas is the preferred method for high performance applications.

The new canvas lang attribute #

The first step to improving localization is giving authors explicit control over the content language for canvas text metrics and drawing. The HTML WHATWG standards group has agreed to add the lang attribute to CanvasRenderingContext2D and OffscreenCanvasRenderingContext2D. The attribute takes any value that an HTML lang attribute can take, or the "inherit" value specifying that the language be taken from the <canvas> element.

This example shows the simplest use of the new attribute:

<script>
function run_test(language_string) {
let context = document.getElementById(language_string).getContext('2d');
context.lang = language_string;
context.font = '20px Lato-Medium';
context.color = 'black';
context.fillText('finish', 50, 20);
}

let latoMediumFontFace = new FontFace(
// Lato-Medium is a font with language specific ligatures.
'Lato-Medium',
'url(../fonts/Lato-Medium.ttf)'
);

latoMediumFontFace.load().then((font) => {
document.fonts.add(font);
run_test('en');
run_test('tr');
});
</script>

The line context.lang = language_string; sets the language for text to the given string, which can be any valid BC47 locale. In this case we use generic English (“en”) and Turkish (“tr”).

The default value for the lang attribute is "inherit" whereby the text uses the language of the canvas. The first example works as it always has, where the canvas text inherits the language of the element.

Offscreen canvas, however, may now use the lang attribute to localize the language. This was not possible before.

Let’s explore these situations, starting with the explicit setting.

offscreen_ctx.lang = 'tr';
offscreen_ctx.font = '20px Lato-Medium';
offscreen_ctx.color = 'black';
offscreen_ctx.fillText('finish', 50, 20);

The language for the offscreen context is explicitly set to Turkish, and the font uses that language to render appropriate glyphs (ligatures or not, in this case).

When the offscreen is created from a canvas element, through the transferControlToOffscreen() method, the language is taken from the canvas element. Here is an example:

<!DOCTYPE html>
<meta charset="utf-8">
<div>
<h3>Canvas lang="tr"</h3>
<canvas lang="tr" id="tr" width="200px" height="60px"></canvas>
</div>
<script>
let context = document.getElementById("tr").transferControlToOffscreen().getContext("2d");

// The default value for lang is "inherit". The canvas that transferred this
// offscreen context has a lang="tr" attribute, so the language used for text
// is "tr".
context.font = '20px Lato-Medium';
context.color = 'black';
context.fillText('finish', 50, 20);
</script>

The value used for "inherit" is captured when control is transferred. If the language on the original canvas element is subsequently changed, the language used for "inherit" on the offscreen does not change.

When the offscreen canvas is created directly in script, there is no canvas element to use, so "inherit" takes the value from the document element for the realm the script is running in. Here’s an example:

<!DOCTYPE html>
<htnl lang="tr">
<meta charset="utf-8">
<div>
<h3>Canvas lang="tr"</h3>
<canvas id="tr" width="200px" height="60px"></canvas>
</div>
<script>
// This is the output canvas.
var canvas = document.getElementById('tr');
var bitmap_ctx = canvas.getContext('bitmaprenderer');

// A newly constructed offscreen canvas.
// Note the lang attribute on the document element, in this case "tr"
// is stored in the offscreen when it is created.
var offscreen = new OffscreenCanvas(canvas.width, canvas.height);
var offscreen_ctx = offscreen.getContext('2d');

offscreen_ctx.font = '20px Lato-Medium';
offscreen_ctx.color = 'black';
offscreen_ctx.fillText('finish', 50, 20);

// Transfer the OffscreenCanvas image to the output canvas.
const bitmap = offscreen.transferToImageBitmap();
bitmap_ctx.transferFromImageBitmap(bitmap);
</script>
</htnl>

Finally, when the offscreen canvas is transferred to a worker, the worker will use the offscreen’s "inherit" language for it’s own ‘“inherit”’ value. This is a complete example, but the key snippet is:

// Create a canvas to serve as the placeholder for the offscreen worker.
const placeholder_canvas = document.createElement('canvas');
placeholder_canvas.setAttribute('width', '200');
placeholder_canvas.setAttribute('height', '60');
// Set the `lang` attribute on the placeholder canvas`
placeholder_canvas.setAttribute('lang', 'tr');

// Create the offscreen from the placeholder. The offscreen will get the
// language from this placeholder, in this case 'tr'.
const offscreen = placeholder_canvas.transferControlToOffscreen();

// Create the worker and transfer the offscreen to it. The language is
// transferred along with the offscreen, so content rendered in the
// offscreen will use 'tr' for the language (unless the `lang` is
// set explicitly on the context in the worker).
const worker = new Worker('canvas-lang-inherit-worker.js');
worker.postMessage({canvas: offscreen}, [offscreen]);

The lang canvas text attribute is implemented in Chrome 135 and later when “Experimental Web Platform Features” is enabled or the command line flag “–enable-blink-features=CanvasTextLang” is used.

Fixing direction for offscreen canvas #

The problem of direction = "inherit" for OffscreenCanvas is solved along the same lines as lang: Capture the inherited value at creation from the placeholder <canvas> element or the document hosting the script that created the offscreen canvas.

All of the examples above for lang work equally well for direction, where the HTML element attribute is dir.

Thanks #

Canvas lang attribute support in Chromium was implemented by Igalia S.L. funded by Bloomberg L.P.