Hereâs a GPT-5.2 Pro matter-of-fact rewrite of the above Calculatrice de durĂ©e page from its source in one âautonomousâ go. The code should be cleaner, standalone (no longer living in WordPress) ⊠and for those of us that donât have fluency in French, internationalized strings in English and Spanish are also employable via the UI.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Duration Calculator</title>
<style>
:root {
--bg: #ffffff;
--surface: #ffffff;
--surface-muted: #f8fafc;
--text: #2d3748;
--text-strong: #1a202c;
--text-muted: #718096;
--text-faint: #a0aec0;
--border: #e2e8f0;
--border-strong: #cbd5e0;
--accent: #805ad5;
--btn-add: #10b981;
--btn-sub: #475569;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 16px;
--font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: var(--font);
font-size: 17px;
line-height: 1.6;
background: var(--bg);
color: var(--text);
}
button, input, select { font: inherit; }
button { cursor: pointer; }
:focus-visible {
outline: 2px solid rgba(128, 90, 213, 0.45);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.app {
min-height: 100%;
display: grid;
place-items: start center;
padding: 28px 12px;
}
.calculator {
width: 100%;
max-width: 480px;
background: var(--surface);
padding: 10px;
}
.toolbar {
display: flex;
justify-content: flex-end;
padding: 0 10px 8px 10px;
}
.lang-select {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
}
.lang-select select {
border: 1px solid var(--border-strong);
border-radius: 999px;
padding: 6px 10px;
background: #fff;
color: var(--text);
}
.header-row {
display: flex;
padding: 0 10px;
margin-bottom: 8px;
gap: 10px;
}
.header-col {
flex: 1;
font-size: 11px;
font-weight: 800;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: center;
}
.header-spacer { width: 30px; }
.rows {
display: grid;
gap: 10px;
}
.row {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 10px;
display: flex;
align-items: center;
box-shadow: var(--shadow);
}
.inputs {
display: flex;
flex: 1;
gap: 10px;
}
.field {
flex: 1;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
position: relative;
height: 55px;
background: #fff;
}
.field-label {
position: absolute;
top: 4px;
left: 8px;
font-size: 10px;
font-weight: 700;
color: var(--accent);
text-transform: uppercase;
}
.field-input {
width: 100%;
height: 100%;
border: none;
background: transparent;
text-align: center;
font-size: 20px;
font-weight: 700;
color: var(--text);
padding-top: 12px;
outline: none;
}
.field-input::placeholder { color: var(--border); }
.field-input::-webkit-inner-spin-button,
.field-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.field-input { -moz-appearance: textfield; }
.delete {
width: 30px;
margin-left: 8px;
border: 0;
background: transparent;
color: var(--border-strong);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.delete svg { width: 20px; height: 20px; }
.add-line-wrap {
text-align: center;
margin: 15px 0 25px 0;
}
.btn-outline {
background: #fff;
border: 1px solid var(--border-strong);
border-radius: 999px;
padding: 8px 24px;
color: var(--text-muted);
font-weight: 700;
font-size: 11px;
display: inline-flex;
align-items: center;
gap: 6px;
outline: none;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.main-btn {
flex: 1;
padding: 14px;
border: none;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-add { background: var(--btn-add); color: #fff; }
.btn-sub { background: var(--btn-sub); color: #fff; }
.reset-wrap {
text-align: center;
margin-bottom: 25px;
}
.reset svg { width: 14px; height: 14px; }
.result {
background: var(--surface-muted);
border: 1px solid #edf2f7;
border-radius: var(--radius-xl);
padding: 25px;
text-align: center;
}
.result-title {
color: var(--text-faint);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 10px;
}
.result-value {
font-size: 42px;
font-weight: 800;
color: var(--text-strong);
line-height: 1;
display: flex;
justify-content: center;
align-items: baseline;
}
.unit {
font-size: 20px;
color: var(--border-strong);
font-weight: 700;
margin-left: 2px;
margin-right: 12px;
}
.unit:last-child { margin-right: 0; }
@media (max-width: 600px) {
.app { padding: 18px 8px; }
.calculator { max-width: 100%; padding: 10px 5px; }
}
</style>
</head>
<body>
<main class="app">
<section class="calculator" id="dc-app" aria-label="Duration calculator">
<div class="toolbar">
<div class="lang-select">
<label for="dc-lang" data-i18n="languageLabel">Language</label>
<select id="dc-lang" name="lang" data-role="lang">
<option value="en">English</option>
<option value="fr">Français</option>
<option value="es">Español</option>
</select>
</div>
</div>
<div class="header-row" role="row">
<div class="header-col" role="columnheader" data-i18n="hours">HOURS</div>
<div class="header-col" role="columnheader" data-i18n="minutes">MINUTES</div>
<div class="header-col" role="columnheader" data-i18n="seconds">SECONDS</div>
<div class="header-spacer" aria-hidden="true"></div>
</div>
<div class="rows" id="dc-rows" aria-live="polite"></div>
<div class="add-line-wrap">
<button class="btn-outline" id="dc-add-row" type="button">
<span aria-hidden="true">+</span>
<span data-i18n="addRow">ADD A ROW</span>
</button>
</div>
<div class="actions">
<button class="main-btn btn-add" id="dc-add" type="button">
<span aria-hidden="true">+</span>
<span data-i18n="add">ADD</span>
</button>
<button class="main-btn btn-sub" id="dc-sub" type="button">
<span aria-hidden="true">â</span>
<span data-i18n="subtract">SUBTRACT</span>
</button>
</div>
<div class="reset-wrap">
<button class="btn-outline reset" id="dc-reset" type="button">
<svg aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span data-i18n="reset">RESET</span>
</button>
</div>
<div class="result" role="status" aria-live="polite">
<div class="result-title" id="dc-mode"></div>
<div class="result-value" data-i18n-aria-label="result" aria-label="Result">
<span id="dc-res-h">00</span><span class="unit" id="dc-unit-h">h</span>
<span id="dc-res-m">00</span><span class="unit" id="dc-unit-m">m</span>
<span id="dc-res-s">00</span><span class="unit" id="dc-unit-s">s</span>
</div>
</div>
<template id="dc-row-template">
<div class="row">
<div class="inputs">
<div class="field">
<span class="field-label" aria-hidden="true">H</span>
<input class="field-input dc-h" type="number" inputmode="numeric" pattern="[0-9]*" min="0" placeholder="00" autocomplete="off" data-i18n-placeholder="placeholder" data-i18n-aria-label="ariaHours">
</div>
<div class="field">
<span class="field-label" aria-hidden="true">M</span>
<input class="field-input dc-m" type="number" inputmode="numeric" pattern="[0-9]*" min="0" placeholder="00" autocomplete="off" data-i18n-placeholder="placeholder" data-i18n-aria-label="ariaMinutes">
</div>
<div class="field">
<span class="field-label" aria-hidden="true">S</span>
<input class="field-input dc-s" type="number" inputmode="numeric" pattern="[0-9]*" min="0" placeholder="00" autocomplete="off" data-i18n-placeholder="placeholder" data-i18n-aria-label="ariaSeconds">
</div>
</div>
<button class="delete" type="button" data-action="delete" data-i18n-title="deleteRow" data-i18n-aria-label="deleteRow" title="Delete row" aria-label="Delete row">
<span class="sr-only" data-i18n="deleteRow">Delete row</span>
<svg aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</template>
</section>
</main>
<script>
(() => {
'use strict';
// i18n : dictionnaire central, détection de langue, et application des textes/attributs.
const STRINGS = {
en: {
appTitle: 'Duration Calculator (add or subtract)',
languageLabel: 'Language',
hours: 'HOURS',
minutes: 'MINUTES',
seconds: 'SECONDS',
addRow: 'ADD A ROW',
add: 'ADD',
subtract: 'SUBTRACT',
reset: 'RESET',
deleteRow: 'Delete row',
placeholder: '00',
result: 'Result',
ariaHours: 'Hours',
ariaMinutes: 'Minutes',
ariaSeconds: 'Seconds',
modeAdd: 'ADDITION',
modeSub: 'SUBTRACTION',
totalMode: 'TOTAL ({mode})',
unitH: 'h',
unitM: 'm',
unitS: 's'
},
fr: {
appTitle: 'Calculatrice de durée (addition ou soustraction)',
languageLabel: 'Langue',
hours: 'HEURES',
minutes: 'MINUTES',
seconds: 'SECONDES',
addRow: 'AJOUTER UNE LIGNE',
add: 'AJOUTER',
subtract: 'SOUSTRAIRE',
reset: 'RĂINITIALISER',
deleteRow: 'Supprimer la ligne',
placeholder: '00',
result: 'Résultat',
ariaHours: 'Heures',
ariaMinutes: 'Minutes',
ariaSeconds: 'Secondes',
modeAdd: 'ADDITION',
modeSub: 'SOUSTRACTION',
totalMode: 'TOTAL ({mode})',
unitH: 'h',
unitM: 'm',
unitS: 's'
},
es: {
appTitle: 'Calculadora de duraciĂłn (sumar o restar)',
languageLabel: 'Idioma',
hours: 'HORAS',
minutes: 'MINUTOS',
seconds: 'SEGUNDOS',
addRow: 'AĂADIR UNA FILA',
add: 'SUMAR',
subtract: 'RESTAR',
reset: 'REINICIAR',
deleteRow: 'Eliminar fila',
placeholder: '00',
result: 'Resultado',
ariaHours: 'Horas',
ariaMinutes: 'Minutos',
ariaSeconds: 'Segundos',
modeAdd: 'SUMA',
modeSub: 'RESTA',
totalMode: 'TOTAL ({mode})',
unitH: 'h',
unitM: 'm',
unitS: 's'
}
};
const STORAGE_KEY = 'durationCalculator.locale';
const DEFAULT_LOCALE = 'en';
function normalizeLocale(tag) {
const base = String(tag || '').toLowerCase().split('-')[0];
return STRINGS[base] ? base : DEFAULT_LOCALE;
}
function detectLocale() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && STRINGS[saved]) return saved;
const candidates = Array.isArray(navigator.languages) && navigator.languages.length ? navigator.languages : [navigator.language];
for (const candidate of candidates) {
const normalized = normalizeLocale(candidate);
if (STRINGS[normalized]) return normalized;
}
return DEFAULT_LOCALE;
}
function formatTemplate(text, vars) {
return String(text).replace(/\{(\w+)\}/g, (_, key) => {
if (!vars) return `{${key}}`;
return key in vars ? String(vars[key]) : `{${key}}`;
});
}
const i18n = {
locale: detectLocale(),
t(key, vars) {
const dict = STRINGS[this.locale] || STRINGS[DEFAULT_LOCALE];
const fallback = STRINGS[DEFAULT_LOCALE];
const raw = key in dict ? dict[key] : (fallback[key] || key);
return formatTemplate(raw, vars);
},
apply(root) {
const setText = (el, key) => { el.textContent = this.t(key); };
const setAttr = (el, attr, key) => { el.setAttribute(attr, this.t(key)); };
root.querySelectorAll('[data-i18n]').forEach(el => setText(el, el.getAttribute('data-i18n')));
root.querySelectorAll('[data-i18n-title]').forEach(el => setAttr(el, 'title', el.getAttribute('data-i18n-title')));
root.querySelectorAll('[data-i18n-placeholder]').forEach(el => setAttr(el, 'placeholder', el.getAttribute('data-i18n-placeholder')));
root.querySelectorAll('[data-i18n-aria-label]').forEach(el => setAttr(el, 'aria-label', el.getAttribute('data-i18n-aria-label')));
document.title = this.t('appTitle');
document.documentElement.lang = this.locale;
const unitH = document.getElementById('dc-unit-h');
const unitM = document.getElementById('dc-unit-m');
const unitS = document.getElementById('dc-unit-s');
if (unitH) unitH.textContent = this.t('unitH');
if (unitM) unitM.textContent = this.t('unitM');
if (unitS) unitS.textContent = this.t('unitS');
},
setLocale(locale, { persist = true } = {}) {
const next = normalizeLocale(locale);
this.locale = next;
if (persist) localStorage.setItem(STORAGE_KEY, next);
this.apply(document);
document.dispatchEvent(new CustomEvent('i18n:changed', { detail: { locale: next } }));
}
};
window.DurationCalculator = window.DurationCalculator || {};
window.DurationCalculator.i18n = i18n;
})();
</script>
<script>
(() => {
'use strict';
// Calculatrice : gestion des lignes dynamiques et calculs (addition / soustraction).
document.addEventListener('DOMContentLoaded', () => {
const i18n = window.DurationCalculator.i18n;
const els = {
rows: document.getElementById('dc-rows'),
tpl: document.getElementById('dc-row-template'),
lang: document.getElementById('dc-lang'),
addRow: document.getElementById('dc-add-row'),
add: document.getElementById('dc-add'),
sub: document.getElementById('dc-sub'),
reset: document.getElementById('dc-reset'),
mode: document.getElementById('dc-mode'),
resH: document.getElementById('dc-res-h'),
resM: document.getElementById('dc-res-m'),
resS: document.getElementById('dc-res-s')
};
let lastMode = 'add';
function pad2(n) {
return n < 10 ? '0' + n : String(n);
}
function secsFromRow(row) {
const h = parseInt(row.querySelector('.dc-h').value, 10) || 0;
const m = parseInt(row.querySelector('.dc-m').value, 10) || 0;
const s = parseInt(row.querySelector('.dc-s').value, 10) || 0;
return (h * 3600) + (m * 60) + s;
}
function setResult(totalSecs) {
const isNeg = totalSecs < 0;
const abs = Math.abs(totalSecs);
const h = Math.floor(abs / 3600);
const rem = abs % 3600;
const m = Math.floor(rem / 60);
const s = rem % 60;
els.resH.textContent = (isNeg ? '-' : '') + pad2(h);
els.resM.textContent = pad2(m);
els.resS.textContent = pad2(s);
}
function renderMode(mode) {
lastMode = mode;
const modeLabel = mode === 'sub' ? i18n.t('modeSub') : i18n.t('modeAdd');
els.mode.textContent = i18n.t('totalMode', { mode: modeLabel });
}
function addRow() {
const fragment = els.tpl.content.cloneNode(true);
els.rows.appendChild(fragment);
i18n.apply(document);
}
function resetRows() {
els.rows.innerHTML = '';
addRow();
addRow();
}
function calculateAdd() {
let total = 0;
els.rows.querySelectorAll('.row').forEach((row) => {
total += secsFromRow(row);
});
setResult(total);
renderMode('add');
}
function calculateSub() {
const rows = Array.from(els.rows.querySelectorAll('.row'));
if (!rows.length) {
setResult(0);
renderMode('sub');
return;
}
let total = secsFromRow(rows[0]);
for (let i = 1; i < rows.length; i += 1) {
total -= secsFromRow(rows[i]);
}
setResult(total);
renderMode('sub');
}
els.lang.addEventListener('change', () => {
i18n.setLocale(els.lang.value);
els.lang.value = i18n.locale;
renderMode(lastMode);
});
els.addRow.addEventListener('click', addRow);
els.rows.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('[data-action="delete"]');
if (!deleteBtn) return;
const row = deleteBtn.closest('.row');
const remaining = els.rows.querySelectorAll('.row').length;
if (remaining > 1) {
row.remove();
} else {
row.querySelectorAll('input').forEach((input) => { input.value = ''; });
}
});
els.reset.addEventListener('click', () => {
resetRows();
setResult(0);
renderMode('add');
});
els.add.addEventListener('click', calculateAdd);
els.sub.addEventListener('click', calculateSub);
i18n.apply(document);
i18n.setLocale(i18n.locale, { persist: true });
els.lang.value = i18n.locale;
resetRows();
setResult(0);
renderMode('add');
});
})();
</script>
</body>
</html>
The style and structure was also prompted. The original rather opaque âsubtractionâ was preserved, where a better UI might be to have a (+) or (-) per-entry.
106k â 18k
Just offered here instead of promoting a host location.
After this task, another calculation doneâŠ
