Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,35 @@ async function _loadLang(lang) {

// ── Core API ──────────────────────────────────────────────────────────────────

/**
* Look up a translation key in the current language, falling back to English.
* Replace {placeholder} tokens with values from `params`.
*/
export function t(key, params = {}) {
function _interpolate(key, params, escape) {
const strings = _cache[_currentLang] ?? _cache.en ?? {};
const fallback = _cache.en ?? {};
let str = strings[key] ?? fallback[key] ?? key;

for (const [k, v] of Object.entries(params)) {
str = str.replaceAll(`{${k}}`, v);
str = str.replaceAll(`{${k}}`, escape ? _escapeHtml(v) : v);
}

return str;
}

const _escapeHtml = (v) => String(v)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');

/** Translate key + interpolate {placeholder}s. Text sinks only; innerHTML uses tHtml(). */
export function t(key, params = {}) {
return _interpolate(key, params, false);
}

/** t() with params HTML-escaped (templates stay raw). For innerHTML sinks. */
export function tHtml(key, params = {}) {
return _interpolate(key, params, true);
}

export function getLang() {
return _currentLang;
}
Expand Down Expand Up @@ -101,9 +114,9 @@ export function applyTranslations() {
el.textContent = t(el.dataset.i18n);
});

// innerHTML (safe: all values are hardcoded in translation files, not user input)
// innerHTML — templates trusted, params escaped
document.querySelectorAll('[data-i18n-html]').forEach(el => {
el.innerHTML = t(el.dataset.i18nHtml);
el.innerHTML = tHtml(el.dataset.i18nHtml);
});

// title attribute
Expand Down
8 changes: 4 additions & 4 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { buildAdjacency, bucketFill,
buildExclusionOverlayGeo, buildFaceWeights } from './exclusion.js';
import { runFastDiagnostics, runExpensiveDiagnostics,
getEdgePositions, getShellAssignments } from './meshValidation.js';
import { t, initLang, setLang, getLang, applyTranslations, TRANSLATIONS } from './i18n.js';
import { t, tHtml, initLang, setLang, getLang, applyTranslations, TRANSLATIONS } from './i18n.js';
import { QuantizedPointMap } from './meshIndex.js';
import { zipSync, unzipSync, strToU8, strFromU8 } from 'fflate';

Expand Down Expand Up @@ -3138,7 +3138,7 @@ function renderFastDiag(diag) {
meshDiagFast.appendChild(makeDiagLine(t('diag.multipleShells', { n: diag.shellCount }), 'shells'));
const tip = document.createElement('div');
tip.style.cssText = 'margin-top:4px;opacity:0.8;font-size:10px';
tip.innerHTML = t('diag.recommendFix');
tip.innerHTML = tHtml('diag.recommendFix');
meshDiagFast.appendChild(tip);
}
applyDiagSeverity();
Expand All @@ -3156,7 +3156,7 @@ function renderAdvancedDiag(results) {
meshDiagAdvanced.appendChild(makeDiagLine(t('diag.overlappingTris', { n: results.overlappingPairs }), 'overlaps'));
const tip = document.createElement('div');
tip.style.cssText = 'margin-top:4px;opacity:0.8;font-size:10px';
tip.innerHTML = t('diag.recommendFix');
tip.innerHTML = tHtml('diag.recommendFix');
meshDiagAdvanced.appendChild(tip);
}
applyDiagSeverity();
Expand Down Expand Up @@ -3225,7 +3225,7 @@ function applySmartResolution() {
const clampedNote = d.budgetClamped
? ` <span class="clamped">[${t('ui.smartResBudgetCapped')}]</span>`
: '';
smartResInfo.innerHTML = t('ui.smartResInfo', {
smartResInfo.innerHTML = tHtml('ui.smartResInfo', {
edge: result.edge.toFixed(2),
ppe: d.pixelsPerEdge.toFixed(1),
pix: d.pixMm.toFixed(3),
Expand Down