Operator screen
Localization
The same panels in five languages, switched at run time.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Localization - Smart.Industrial</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<div class="hmi-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">LOCALIZATION</span>
<span class="hmi-area">One screen, five languages, no reload</span>
</div>
<div class="hmi-header-actions">
<span class="hmi-resolution-label" id="localeLabel">English</span>
<button class="hmi-button" id="toEnglish" type="button" aria-pressed="true">English</button>
<button class="hmi-button" id="toGerman" type="button" aria-pressed="false">Deutsch</button>
<button class="hmi-button" id="toFrench" type="button" aria-pressed="false">Français</button>
<button class="hmi-button" id="toSpanish" type="button" aria-pressed="false">Español</button>
<button class="hmi-button" id="toChinese" type="button" aria-pressed="false">中文</button>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-panel loc-explain" aria-label="How this works">
<div class="hmi-panel-header">The mechanism, and its one missing piece</div>
<div class="loc-body">
<p>
Every component in the suite has a <code>messages</code> object, a <code>locale</code> property that selects the language, and a <code>localize(key, values)</code> method that replaces <code>{{token}}</code> placeholders.
</p>
<p>
German, French, Spanish and Simplified Chinese packs are included. Each pack is keyed by tag name, covers all 54 Industrial components and is applied with <code>Smart.Industrial.applyLocale(document.body, 'fr')</code>. A test reads the English keys of every component from a live instance and fails if a pack is missing a key, contains a stale key or drops a <code>{{token}}</code>.
</p>
<p>
<b>Note.</b> Assigning <code>element.messages = { de: … }</code> replaces the whole object, including the built-in <code>en</code> pack, so switching back to English would show raw message keys. <code>applyLocale</code> merges the languages instead. A pack is only loaded when it is requested.
</p>
<p class="loc-status" id="status"></p>
</div>
</section>
<div class="loc-grid">
<section class="hmi-panel" aria-label="Alarm banner and annunciator">
<div class="hmi-panel-header">Alarms</div>
<div class="loc-panel-body" tabindex="0">
<smart-alarm-banner id="banner"></smart-alarm-banner>
<smart-annunciator id="annunciator" columns="3"></smart-annunciator>
</div>
</section>
<section class="hmi-panel" aria-label="Alarm summary">
<div class="hmi-panel-header">Alarm summary</div>
<div class="loc-panel-body loc-panel-fill" tabindex="0">
<smart-alarm-grid id="alarmGrid"></smart-alarm-grid>
</div>
</section>
<section class="hmi-panel" aria-label="Test sequence">
<div class="hmi-panel-header">Test sequence</div>
<div class="loc-panel-body loc-panel-fill" tabindex="0">
<smart-sequence-editor id="sequence" show-results></smart-sequence-editor>
</div>
</section>
<section class="hmi-panel" aria-label="Operator controls">
<div class="hmi-panel-header">Operator controls</div>
<div class="loc-panel-body loc-controls">
<smart-faceplate id="faceplate" tag="FIC-101" description="Feed flow" unit="m³/h" min="0" max="120" process-value="78.4" setpoint="80" output="46" mode="auto"></smart-faceplate>
<div class="loc-stack">
<smart-selector-switch id="hoa" label="P-101A" value="auto"></smart-selector-switch>
<div class="loc-row">
<smart-momentary-button id="start" label="Start" mode="hold" hold-duration="1000" kind="start" state="stopped"></smart-momentary-button>
<smart-momentary-button id="stop" label="Stop" mode="confirm" kind="stop" interlocked interlock-reason="P-101A in Auto"></smart-momentary-button>
</div>
<smart-permissive id="permissive" mode="interlock"></smart-permissive>
</div>
</div>
</section>
<section class="hmi-panel" aria-label="Accessible names">
<div class="hmi-panel-header">What a screen reader is told</div>
<div class="loc-panel-body" tabindex="0">
<p class="loc-note">
The accessible names are translated together with the visible text. The values below are read from the live components.
</p>
<dl class="loc-names" id="names"></dl>
</div>
</section>
</div>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.locale.de.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.locale.fr.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.locale.es.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.locale.zh.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.faceplate.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.selectorswitch.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.momentarybutton.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.permissive.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmbanner.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.annunciator.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmgrid.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.sequenceeditor.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Localization - the mechanism was always there; the second language was not.
*
* Nothing on this page translates anything. The data below is in neither language: it is
* tags, numbers and states. Every word that changes when the toggle is pressed comes out
* of an element's own `messages` block, which is the point - a host that has to translate
* its components' vocabulary is a host doing the component's job. */
/* The state is not a string on the record - the element derives it from the ISA-18.2
condition pair (is the alarm still active, has it been acknowledged) plus the operator
overrides. That is deliberate: a state field would let a feed assert "acknowledged"
without anyone having acknowledged anything. Spread across the machine on purpose, so
every state name in the pack has something to translate. */
const ALARMS = [
{ id: 'a1', tag: 'TK-2100.LAHH', message: 'Feed tank level high-high', priority: 1,
active: true, acknowledged: false, area: 'Utilities', timestamp: Date.now() - 42000 },
{ id: 'a2', tag: 'P-101A.FAULT', message: 'Transfer pump A drive fault', priority: 1,
active: true, acknowledged: false, area: 'Transfer', timestamp: Date.now() - 190000 },
{ id: 'a3', tag: 'HX-204.TAH', message: 'Exchanger outlet temperature high', priority: 2,
active: true, acknowledged: true, area: 'Process', timestamp: Date.now() - 640000 },
//Gone away without anyone acknowledging it - the case hand-rolled lists all miss.
{ id: 'a4', tag: 'PT-330.COMM', message: 'Transmitter communication lost', priority: 2,
active: false, acknowledged: false, area: 'Process', timestamp: Date.now() - 900000 },
{ id: 'a5', tag: 'CV-118.DEV', message: 'Control valve deviation', priority: 3,
active: true, acknowledged: false, shelvedUntil: Date.now() + 3600000,
area: 'Process', timestamp: Date.now() - 1500000 },
{ id: 'a6', tag: 'FT-090.MAINT', message: 'Flow transmitter under calibration', priority: 3,
active: true, acknowledged: false, outOfService: true,
area: 'Utilities', timestamp: Date.now() - 2400000 }
];
const TILES = [
{ label: 'TK-2100', state: 'alarm' },
{ label: 'P-101A', state: 'alarm' },
{ label: 'HX-204', state: 'ack' },
{ label: 'PT-330', state: 'ringback' },
{ label: 'CV-118', state: 'normal' },
{ label: 'FT-090', state: 'normal' }
];
/* The result is not asserted either - the element compares the measured value against the
limit and decides. VREF below is 2.507 against 2.5 plus or minus 0.005, which fails; put
the tolerance at 0.01 and the same reading passes. That is the point of keeping limits as
data rather than as a claimed outcome. */
const STEPS = [
{ id: 's1', name: 'Supply rail', action: 'Measure 3V3 rail', min: 3.25, max: 3.35,
value: 3.29 },
{ id: 's2', name: 'Quiescent current', action: 'Measure Iq at idle', max: 12, value: 9.4 },
{ id: 's3', name: 'Reference', action: 'Measure VREF', nominal: 2.5, tolerance: 0.005,
value: 2.507 },
{ id: 's4', name: 'Gain', action: 'Measure closed-loop gain', min: 19.5, max: 20.5,
value: null },
{ id: 's5', name: 'Noise floor', action: 'Measure RMS noise', max: 45, value: null }
];
function el(id) {
return document.getElementById(id);
}
let locale = 'en';
/* Read the accessible names back off the elements rather than printing what we think
they should be. The whole claim being made here is that these follow the locale, and a
hardcoded expectation would demonstrate nothing. */
function showNames() {
const list = el('names'),
rows = [
['smart-alarm-banner', el('banner')],
['smart-annunciator', el('annunciator')],
['smart-alarm-grid', el('alarmGrid')],
['smart-sequence-editor', el('sequence')],
['smart-faceplate', el('faceplate')],
['smart-selector-switch', el('hoa')],
['smart-momentary-button', el('stop').querySelector('.smart-momentary') || el('stop')],
['smart-permissive', el('permissive')]
];
list.innerHTML = '';
rows.forEach(function (row) {
const element = row[1];
/* The name may sit on the host or on the real table inside it, depending on
whether the element carries its own semantics or delegates to a <table>. */
const name = element.getAttribute('aria-label') ||
(element.$ && element.$.tableContainer &&
element.$.tableContainer.getAttribute('aria-label')) ||
(element.$ && element.$.container &&
element.$.container.getAttribute('aria-label')) || '—';
const dt = document.createElement('dt'),
dd = document.createElement('dd');
dt.textContent = row[0];
dd.textContent = name;
list.appendChild(dt);
list.appendChild(dd);
});
}
const LANGUAGES = {
en: { button: 'toEnglish', name: 'English' },
de: { button: 'toGerman', name: 'Deutsch' },
fr: { button: 'toFrench', name: 'Français' },
es: { button: 'toSpanish', name: 'Español' },
zh: { button: 'toChinese', name: '中文' }
};
function apply(next) {
locale = next;
const changed = Smart.Industrial.applyLocale(document.body, locale);
el('localeLabel').textContent = LANGUAGES[locale].name;
Object.keys(LANGUAGES).forEach(function (code) {
el(LANGUAGES[code].button).setAttribute('aria-pressed', String(locale === code));
});
el('status').textContent = 'applyLocale(document.body, \'' + locale + '\') updated ' +
changed + ' element' + (changed === 1 ? '' : 's') + ' in place. No reload, and the ' +
'data was never touched.';
//The names are read after the elements have repainted, not before.
setTimeout(showNames, 120);
}
window.onload = function () {
el('banner').alarms = ALARMS;
el('annunciator').tiles = TILES;
el('alarmGrid').alarms = ALARMS;
el('sequence').steps = STEPS;
Object.keys(LANGUAGES).forEach(function (code) {
el(LANGUAGES[code].button).addEventListener('click', function () {
apply(code);
});
});
/* The controls are data too: an interlock reason and permissive labels are the plant's
words, and stay in the plant's language whatever the chrome speaks. */
el('permissive').conditions = [
{ id: 'c1', label: 'Suction valve open', satisfied: true },
{ id: 'c2', label: 'Seal water pressure', satisfied: false, timestamp: Date.now() - 95000 },
{ id: 'c3', label: 'Motor overload reset', satisfied: true }
];
const toggle = el('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
setTimeout(function () {
apply('en');
}, 400);
};
styles.css
/* Sequence of events. Colour is used for exactly one thing on this screen: marking the
rows whose order stopped being a fact. Everything else is neutral, which is what makes
that marking readable. */
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 13px;
}
.hmi-screen {
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 10px;
padding: 10px;
/* at least the screen: it fills a display, and grows past a short one rather than squeezing
every panel until its content is cut */
min-height: 100%;
box-sizing: border-box;
}
/* ── Header ──────────────────────────────────────────────────────────── */
.hmi-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 10px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.hmi-identity {
display: flex;
flex-direction: column;
gap: 2px;
}
.hmi-plant {
font-size: 16px;
font-weight: 600;
letter-spacing: .08em;
}
.hmi-area {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .04em;
}
.hmi-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.hmi-resolution-label {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .06em;
text-transform: uppercase;
}
.hmi-button,
.hmi-theme-toggle {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
height: 30px;
letter-spacing: .06em;
padding: 0 14px;
text-transform: uppercase;
}
.hmi-button:hover,
.hmi-theme-toggle:hover {
border-color: var(--smart-border-hover);
color: var(--smart-text-color);
}
.hmi-button:focus-visible,
.hmi-theme-toggle:focus-visible {
outline: 2px solid var(--smart-industrial-focus);
outline-offset: 2px;
box-shadow: 0 0 0 4px var(--smart-industrial-focus-halo);
}
/* ── Panels ──────────────────────────────────────────────────────────── */
.hmi-panel {
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.hmi-panel-header {
align-items: center;
background: var(--smart-industrial-panel-header-background);
border-bottom: 1px solid var(--smart-industrial-panel-border);
color: var(--smart-text-secondary-color);
display: flex;
font-size: 11px;
gap: 16px;
justify-content: space-between;
letter-spacing: .08em;
padding: 7px 12px;
text-transform: uppercase;
}
/* ── Localization screen ─────────────────────────────────────────────── */
.hmi-screen {
grid-template-rows: auto auto 1fr;
}
.loc-body {
padding: 10px 14px;
overflow: auto;
}
.loc-body p {
margin: 0 0 8px;
max-width: 105ch;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.loc-body p:last-child {
margin-bottom: 0;
}
.loc-body code {
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
color: var(--smart-industrial-accent-text);
}
.loc-status {
border-left: 2px solid var(--smart-industrial-accent-text);
padding-left: 10px;
font-family: Consolas, Monaco, monospace;
font-size: 11.5px;
}
/* Three by two, so a language change can be seen happening in five places at once
rather than one panel at a time down a scroll. */
.loc-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
/* each row as tall as its tallest panel, and never less than 300px; on a display with room
to spare, the rows are stretched to fill it (auto tracks stretch, fr tracks would make
every row as tall as the tallest one in the grid) */
grid-template-rows: repeat(2, minmax(300px, auto));
gap: 10px;
min-height: 0;
}
.loc-panel-body {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
min-height: 0;
overflow: auto;
}
.loc-panel-fill {
padding: 0;
overflow: hidden;
}
.loc-panel-fill smart-alarm-grid,
.loc-panel-fill smart-sequence-editor {
width: 100%;
height: 100%;
}
.loc-note {
margin: 0;
color: var(--smart-text-tertiary-color);
font-size: 11.5px;
line-height: 1.5;
}
/* Term and definition side by side: the pair is the unit being read, and stacking
them doubles the vertical space for no gain. */
.loc-names {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 14px;
margin: 0;
align-content: start;
}
.loc-names dt {
font-family: Consolas, Monaco, monospace;
font-size: 11px;
color: var(--smart-text-tertiary-color);
white-space: nowrap;
}
.loc-names dd {
margin: 0;
font-size: 12px;
}
smart-annunciator {
width: 100%;
}
.loc-controls {
display: grid;
grid-template-columns: auto 1fr;
gap: 16px;
align-items: start;
}
.loc-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.loc-row {
display: flex;
gap: 12px;
align-items: flex-start;
}
@media (max-width: 1100px) {
.loc-controls {
grid-template-columns: 1fr;
}
.loc-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(3, minmax(300px, auto));
}
}
Components on this screen
Alarm BannerAlarm GridAnnunciatorFaceplateMomentary ButtonPermissiveSelector SwitchSequence Editor