Skip to content

Commit fdbc300

Browse files
authored
Improve localized countdown timer display (#110)
1 parent 5ed4519 commit fdbc300

File tree

1 file changed

+91
-52
lines changed

1 file changed

+91
-52
lines changed

src/layouts/Default.astro

Lines changed: 91 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -83,50 +83,90 @@ const languageEntries = Object.entries(languages);
8383
</div>
8484
</footer>
8585
<script>
86+
// Set the date we're counting down to
87+
const countDownDate = new Date("Sep 1, 2026 00:00:00").getTime();
88+
89+
const locale = document.documentElement.lang; // navigator.language; // Detects browser language (e.g., 'en-US');
90+
91+
// Use Intl.RelativeTimeFormat for localized abbreviations
92+
// 'narrow' style gives us 'd', 'h', 'm', 's' in most locales
93+
const formatter = new Intl.RelativeTimeFormat(locale, { style: 'narrow' });
94+
95+
const prefix = new Array(4);
96+
const suffix = new Array(4);
97+
98+
function getOffset(unit) {
99+
switch (unit) {
100+
case 'day':
101+
return 0;
102+
case 'hour':
103+
return 1;
104+
case 'minute':
105+
return 2;
106+
case 'second':
107+
return 3;
108+
}
109+
}
110+
111+
function extractCommon(p, c, reverse) {
112+
let s = 0;
113+
let w = 0;
114+
let i = reverse ? p.length - 1 : 0;
115+
let j = reverse ? c.length - 1 : 0;
116+
const pEnd = reverse ? 0 : p.length;
117+
const cEnd = reverse ? 0 : c.length;
118+
let chr;
119+
while ((reverse ? i >= pEnd : i < pEnd) && (reverse ? j >= cEnd : j < cEnd) && (chr = p[reverse ? i-- : i++]) === c[reverse ? j-- : j++]) {
120+
w = chr === ' ' ? w + 1 : 0;
121+
s++;
122+
}
123+
return s - w;
124+
}
125+
126+
function cacheFormattingInfo(value, unit) {
127+
// FormatToParts lets us extract just the unit identifier
128+
const p = formatter.formatToParts(value, unit);
129+
if (!p.length) return;
130+
const c = formatter.formatToParts(-value, unit);
131+
132+
const offset = getOffset(unit);
133+
if (p[0].type === 'literal') {
134+
if (!c.length || c[0].type !== 'literal') {
135+
prefix[offset] = p[0].value.length;
136+
} else if (!c[0].value.endsWith(p[0].value)) {
137+
prefix[offset] = p[0].value.length - extractCommon(p[0].value, c[0].value, true);
138+
}
139+
}
140+
if (p[p.length - 1].type === 'literal') {
141+
if (!c.length || c[c.length - 1].type !== 'literal') {
142+
suffix[offset] = p[p.length - 1].value.length;
143+
} else if (!c[c.length - 1].value.startsWith(p[p.length - 1].value)) {
144+
suffix[offset] = p[p.length - 1].value.length - extractCommon(p[p.length - 1].value, c[c.length - 1].value, false);
145+
}
146+
}
147+
}
148+
149+
cacheFormattingInfo(1, 'day');
150+
cacheFormattingInfo(2, 'hour');
151+
cacheFormattingInfo(3, 'minute');
152+
cacheFormattingInfo(4, 'second');
153+
86154
/**
87155
* Localizes a duration based on the browser's language settings.
88156
* @param {number} value - The numerical value (e.g., 5)
89157
* @param {string} unit - The unit ('day', 'hour', 'minute', 'second')
90-
* @param {string} locale - The BCP 47 language tag
91158
*/
92-
function getLocalizedUnit(value, unit, locale, trimConjunction) {
93-
// Use Intl.RelativeTimeFormat for localized abbreviations
94-
// 'narrow' style gives us 'd', 'h', 'm', 's' in most locales
95-
96-
const formatter = new Intl.RelativeTimeFormat(locale, { style: 'narrow' });
97-
98-
function findCommonObject(arrays) {
99-
if (!arrays.length) return null;
100-
101-
return arrays.reduce((common, currentArray) => {
102-
return common.filter(objA =>
103-
// Check if the current array contains an object that matches objA
104-
currentArray.some(objB => JSON.stringify(objA) === JSON.stringify(objB))
105-
);
106-
});
107-
}
108-
109-
const p1 = formatter.formatToParts(1, 'day');
110-
const p2 = formatter.formatToParts(2, 'hour');
111-
const p3 = formatter.formatToParts(3, 'minute');
112-
const p4 = formatter.formatToParts(4, 'second');
113-
var prefixParts = findCommonObject([p1, p2, p3, p4]);
114-
var prefix = prefixParts.length == 0 ? "" : prefixParts[0].value;
115-
116-
// FormatToParts lets us extract just the unit identifier
117-
const parts = formatter.formatToParts(value, unit);
118-
119-
const segments = parts
120-
.filter(p => p.type === "integer" || p.type === "literal" || p.type === "unit")
121-
.filter(p => p.value !== prefix)
122-
.map(p => p.value)
123-
.join("");
124-
125-
return `${trimConjunction ? "" : prefix}${segments}`;
159+
function getLocalizedUnit(value, unit, trimConjunction, trimSuffix) {
160+
const offset = getOffset(unit);
161+
const string = formatter.format(value, unit);
162+
const p = prefix[offset];
163+
const s = suffix[offset];
164+
return string.slice(trimConjunction && p || p == 1 && string[0] === '+' ? prefix[offset] : 0, trimSuffix && s ? -suffix[offset] : string.length);
126165
}
127-
128-
// Set the date we're counting down to
129-
var countDownDate = new Date("Sep 1, 2026 00:00:00").getTime();
166+
167+
const remaining = new Array(7);
168+
const separator = ' ';
169+
var timer = null;
130170

131171
function updateBanner() {
132172
// Get today's date and time
@@ -141,28 +181,27 @@ const languageEntries = Object.entries(languages);
141181
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
142182
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
143183

144-
const locale = document.documentElement.lang; // navigator.language; // Detects browser language (e.g., 'en-US');
145-
146-
var remaining = [
147-
days > 0 ? getLocalizedUnit(days, 'day', locale, false) : '',
148-
hours > 0 || days > 0 ? getLocalizedUnit(hours, 'hour', locale, true) : '',
149-
minutes > 0 || hours > 0 || days > 0 ? getLocalizedUnit(minutes, 'minute', locale, true) : '',
150-
getLocalizedUnit(seconds, 'second', locale, true)
151-
].filter(Boolean).join(' ');
152-
remaining = remaining.replace(/^\+\s*/, '');
184+
var parts = 0;
185+
remaining[0] = days > 0 ? getLocalizedUnit(days, 'day', parts++, true) : null;
186+
remaining[1] = parts ? separator : null;
187+
remaining[2] = parts || hours > 0 ? getLocalizedUnit(hours, 'hour', parts++, true) : null;
188+
remaining[3] = parts ? separator : null;
189+
remaining[4] = parts || minutes > 0 ? getLocalizedUnit(minutes, 'minute', parts++, true) : null;
190+
remaining[5] = parts ? separator : null;
191+
remaining[6] = getLocalizedUnit(seconds, 'second', parts++, false);
153192

154193
// Display the result in the element with id="countdown"
155-
document.getElementById("countdown").textContent = remaining;
194+
document.getElementById("countdown").textContent = remaining.join('');
156195

157196
// // If the count down is finished, write some text
158-
// if (distance < 0) {
159-
// clearInterval(x);
197+
if (distance < 0) {
198+
clearInterval(timer);
160199
// document.getElementById("countdown").innerHTML = "EXPIRED";
161-
// }
200+
}
162201
}
163202

164203
// Update the count down every 1 second
165-
setInterval(updateBanner, 1000);
204+
timer = setInterval(updateBanner, 1000);
166205
updateBanner();
167206
</script>
168207
</Base>

0 commit comments

Comments
 (0)