Files
Max Gramser a3a5d11c54 Start a new language with two-letter weather days, which check accepts
Following docs/TRANSLATING.md for Swedish: `i18n.py new` guessed three letters (sön mån tis) and `check` then failed
the new file on it. The guess is now two letters from the abbreviation (sö må ti, the CLDR's short width here).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 20:12:34 +02:00

56 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Day and month names, date orders, the decimal mark and the clock of languages, from the Unicode CLDR that Node's Intl
// carries (app 0.2.90); tools/i18n.py cldr writes them into the translations. Prints JSON: {code: {...}}.
const codes = process.argv.slice(2);
const space = (text) => text.replace(/[   ]/g, ' ');
// 2026-03-01 is a Sunday: day i of that week is Sunday + i, as the screens count (ESPHome's day_of_week 1 is Sunday).
const day = (i) => new Date(Date.UTC(2026, 2, 1 + i, 12));
const month = (m) => new Date(Date.UTC(2026, m, 14, 12));
const pattern = (code, options, names) => {
const parts = new Intl.DateTimeFormat(code, { ...options, timeZone: 'UTC' }).formatToParts(new Date(Date.UTC(2026, 8, 14, 14, 5)));
let out = '';
for (const part of parts) {
if (names[part.type]) out += `{${names[part.type]}}`;
else if (part.type === 'literal') out += space(part.value);
}
return out.replace(/\{hour\}:\{minute\}/, '{time}').replace(/\{hour\}\.\{minute\}/, '{time}');
};
const result = {};
for (const code of codes) {
const short = new Intl.DateTimeFormat(code, { weekday: 'short', timeZone: 'UTC' });
const long = new Intl.DateTimeFormat(code, { weekday: 'long', timeZone: 'UTC' });
const monthLong = (m) => new Intl.DateTimeFormat(code, { day: 'numeric', month: 'long', timeZone: 'UTC' }).formatToParts(month(m)).find((p) => p.type === 'month').value;
const monthShort = (m) => new Intl.DateTimeFormat(code, { day: 'numeric', month: 'short', timeZone: 'UTC' }).formatToParts(month(m)).find((p) => p.type === 'month').value;
const numbers = new Intl.NumberFormat(code);
const parts = numbers.formatToParts(12345.5);
const decimal = parts.find((p) => p.type === 'decimal').value;
const group = space(parts.find((p) => p.type === 'group').value);
const period = (hour) => (new Intl.DateTimeFormat(code, { hour: 'numeric', hour12: true, timeZone: 'UTC' })
.formatToParts(new Date(Date.UTC(2026, 0, 1, hour))).find((p) => p.type === 'dayPeriod') || { value: hour < 12 ? 'AM' : 'PM' }).value;
const cycle = new Intl.DateTimeFormat(code, { hour: 'numeric', timeZone: 'UTC' }).resolvedOptions().hourCycle;
// A locale that writes a short date with the month as a number (Portugal: "14/09") gets the standalone short names
// and "day month" instead: the screens' calendar shows a month's name beside the big day.
const numericMonth = /^\d+$/.test(monthShort(8));
const standalone = (m) => space(new Intl.DateTimeFormat(code, { month: 'short', timeZone: 'UTC' }).format(month(m)));
const withNames = (text) => (numericMonth ? text.replace(/\{day\}\W*\{month\}/, '{day} {month}') : text);
result[code] = {
clock: cycle === 'h12' || cycle === 'h11' ? '12' : '24',
number: { decimal, group, group_min: numbers.format(1234).includes(parts.find((p) => p.type === 'group').value) ? '1' : '2' },
time: { am: space(period(9)), pm: space(period(21)) },
date: {
weekdays: [0, 1, 2, 3, 4, 5, 6].map((i) => long.format(day(i))),
weekdays_short: [0, 1, 2, 3, 4, 5, 6].map((i) => short.format(day(i))),
// The narrow columns of the weather card hold two letters: a first guess from the short name without its dot
// ("sö", "må" in Swedish, as the CLDR's short width), which the language's translator checks (tools/i18n.py keeps
// the list once a language has it).
weekdays_min: [0, 1, 2, 3, 4, 5, 6].map((i) => [...short.format(day(i)).replace(/\.$/, '')].slice(0, 2).join('')),
months: [...Array(12).keys()].map(monthLong),
months_short: [...Array(12).keys()].map(numericMonth ? standalone : monthShort),
top_bar: withNames(pattern(code, { weekday: 'short', day: 'numeric', month: 'short' }, { weekday: 'weekday', day: 'day', month: 'month' })),
full: pattern(code, { weekday: 'long', day: 'numeric', month: 'long' }, { weekday: 'weekday', day: 'day', month: 'month' }),
day_month: withNames(pattern(code, { day: 'numeric', month: 'short' }, { day: 'day', month: 'month' })),
weekday_time: pattern(code, { weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }, { weekday: 'weekday', hour: 'hour', minute: 'minute' }),
},
};
}
console.log(JSON.stringify(result, null, 1));