Stop double-encoding translated toolbar labels

i18n() already HTML-encodes its result, so wrapping it in escape() again
turned accented labels into literal entities (e.g. "Schließen") for
screen readers, and .text() did the same for the error message.
This commit is contained in:
jelveh committed 2026-10-01 22:03:03 -07:00
1 parent f1008559ca
commit ffa6b176d0
2 files changed
+17 -8

No files matched your search

@@ -202,9 +202,10 @@ describe('embedded app browser', () => {
describe('embedded toolbar', () => {
async function openToolbar (viewport = { width: 1100, height: 720 }, token = sessionToken) {
async function openToolbar (viewport = { width: 1100, height: 720 }, token = sessionToken, storage = []) {
const context = await browser.newContext({ viewport, storageState: {
cookies: [], origins: [{ origin, localStorage: [
...storage,
{ name: 'auth_token_v2', value: token },
{ name: 'logged_in_users', value: JSON.stringify([
{ uuid: 'first', username: 'First', auth_token: sessionToken },
@@ -266,6 +267,14 @@ describe('embedded toolbar', () => {
} finally { await context.close(); }
});
it('labels controls with translated text encoded once', async () => {
const { context, toolbar } = await openToolbar(undefined, sessionToken, [{ name: 'user_preferences', value: '{"language":"de"}' }]);
try {
await toolbar.getByRole('button', { name: 'Konto', exact: true }).click();
await toolbar.getByRole('button', { name: 'Schließen', exact: true }).waitFor();
} finally { await context.close(); }
});
it('opens upgrade, settings, and add-account in new tabs', async () => {
const { context, page, toolbar } = await openToolbar();
try {
+7 -7
View File
@@ -58,13 +58,13 @@ function showPanel (next) {
closePanel();
panel = next;
$root.find(`[data-panel="${next}"]`).attr('aria-expanded', 'true');
$root.append(`<section class="toolbar-panel dashboard-card" aria-label="${escape(i18n(next === 'apps' ? 'toolbar_apps' : 'account'))}">
<header><strong>${i18n(next === 'apps' ? 'toolbar_apps' : 'account')}</strong><button class="toolbar-close" type="button" aria-label="${escape(i18n('close'))}">×</button></header>
$root.append(`<section class="toolbar-panel dashboard-card" aria-label="${i18n(next === 'apps' ? 'toolbar_apps' : 'account')}">
<header><strong>${i18n(next === 'apps' ? 'toolbar_apps' : 'account')}</strong><button class="toolbar-close" type="button" aria-label="${i18n('close')}">×</button></header>
<div class="toolbar-panel-content"></div></section>`);
$root.find('.toolbar-close').on('click', closePanel);
const $content = $root.find('.toolbar-panel-content');
if ( next === 'apps' ) {
$content.html(`<iframe class="toolbar-apps" title="${escape(i18n('toolbar_apps'))}" src="/embed/apps"></iframe>`);
$content.html(`<iframe class="toolbar-apps" title="${i18n('toolbar_apps')}" src="/embed/apps"></iframe>`);
$content.find('iframe').on('load', event => {
const frameDocument = event.currentTarget.contentDocument;
frameDocument?.addEventListener('keydown', keyEvent => {
@@ -109,7 +109,7 @@ async function accountAction (action) {
busy = true;
$root.find('.toolbar-account, .toolbar-logout, .toolbar-confirm-logout').prop('disabled', true);
try { await action(); }
catch { $root.find('.toolbar-error').text(i18n('toolbar_account_error')).prop('hidden', false); }
catch { $root.find('.toolbar-error').html(i18n('toolbar_account_error')).prop('hidden', false); }
finally {
busy = false;
$root.find('.toolbar-account, .toolbar-logout, .toolbar-confirm-logout').prop('disabled', false);
@@ -148,10 +148,10 @@ async function mount () {
if ( !sessionToken ) throw new Error('Missing session');
user = await (await request('/whoami')).json();
if ( !user.uuid ) throw new Error('Missing account');
$root.html(`<nav class="toolbar-row" aria-label="${escape(i18n('toolbar_label'))}">
$root.html(`<nav class="toolbar-row" aria-label="${i18n('toolbar_label')}">
${shouldShowUpgrade(user) ? `<a class="button button-primary toolbar-upgrade" href="/dashboard?upgrade=1#usage" target="_blank" rel="noopener noreferrer">${i18n('toolbar_upgrade')}</a>` : ''}
<button type="button" class="toolbar-button" data-panel="apps" aria-expanded="false" aria-label="${escape(i18n('toolbar_apps'))}"><span class="toolbar-grid" aria-hidden="true">${'<i></i>'.repeat(9)}</span></button>
<button type="button" class="toolbar-button toolbar-avatar" data-panel="account" aria-expanded="false" aria-label="${escape(i18n('account'))}">${escape(user.username?.[0]?.toUpperCase() || '?')}</button>
<button type="button" class="toolbar-button" data-panel="apps" aria-expanded="false" aria-label="${i18n('toolbar_apps')}"><span class="toolbar-grid" aria-hidden="true">${'<i></i>'.repeat(9)}</span></button>
<button type="button" class="toolbar-button toolbar-avatar" data-panel="account" aria-expanded="false" aria-label="${i18n('account')}">${escape(user.username?.[0]?.toUpperCase() || '?')}</button>
</nav>`);
$root.find('[data-panel]').on('click', event => showPanel(event.currentTarget.dataset.panel));
try {