Сегодня будем заниматься красотой и анимациями: сделаем универсальные часы с движущимся циферблатом, который покажет нам всё текущее время от года до секунд. А ещё прикрутим сюда выбор языка, чтобы название месяца и дня недели отображалось правильно в разных странах. Внутри много хардкора и кода, а на выходе будет такое:

Логика проекта
Обычно в наших проектах всё происходит только в скрипте, но не в этот раз: от стилей тут тоже много зависит — анимация, расположение элементов и их внешний вид. Поэтому будем делать так:
- Создадим сразу все нужные файлы — index.html, style.css и script.js. На старте они будут пустые, а в процессе мы их наполним.
 - Разместим на странице в блоках два контейнера: один для циферблата, а второй — для выбора языка. Внутри циферблата создадим тоже много контейнеров, каждый из которых будет отвечать за что-то своё: минуты, секунды, дни недели и так далее.
 - Добавим в эти контейнеры элементы с помощью скрипта.
 - Настроим стили, которые превратят то, что выдал скрипт, в круглый циферблат.
 - Добавим анимацию вращения.
 - Наконец, добавим выбор языков (тоже с анимацией).
 
Работы много, поэтому приступим.
Наполняем index.html
Так как у нас будет много данных для вывода, создадим много контейнеров для их структурирования. Чтобы всем этим было проще управлять, данные о времени объединим в один большой контейнер — так мы сможем применять некоторые стили сразу ко всем контейнерам внутри.
В центр часов положим кнопку выбора языка и сделаем её в виде флага (но это чуть позже, в скрипте). Отдельно сделаем контейнер с диалоговым окном выбора языка — там будет кнопка закрытия и блок со всеми языками-флагами.
Из классики: подключение стилей в самом начале и скрипта — в конце. Вроде всё, пишем код. Внешне на странице почти ничего не изменится, но нам главное сейчас — собрать каркас:
<!DOCTYPE html>
<html lang="ru-RU">
<head>
  <!-- Базовые настройки документа -->
  <meta charset="UTF-8"> <!-- Кодировка UTF-8 -->
  <title>Часы на 100 лет вперёд</title> <!-- Заголовок страницы -->
  <link rel="stylesheet" href="style.css"> <!-- Подключение стилей -->
</head>
<body>
<!-- Основная структура часов -->
<div class="clock" data-date="2025-04-55">
  <!-- Контейнеры для каждого типа времени (годы, секунды и так далее) -->
  <div>
    <div data-clock="years" data-numbers="101" class="clock-face"></div> <!-- 100-летний круг -->
  </div>
  <div>
    <div data-clock="seconds" data-numbers="60" class="clock-face"></div> <!-- Секунды -->
  </div>
  <div>
    <div data-clock="minutes" data-numbers="60" class="clock-face"></div> <!-- Минуты -->
  </div>
  <div>
    <div data-clock="hours" data-numbers="24" class="clock-face"></div> <!-- Часы -->
  </div>
  <div>
    <div data-clock="days" data-numbers="31" class="clock-face"></div> <!-- Дни месяца -->
  </div>
  <div>
    <div data-clock="months" data-numbers="12" class="clock-face"></div> <!-- Месяцы -->
  </div>
  <div>
    <div data-clock="day-names" data-numbers="7" class="clock-face"></div> <!-- Дни недели -->
  </div>
  <!-- Кнопка выбора языка -->
  <button type="button" id="current-lang" class="current-lang-display">en</button>
</div>
<!-- Диалоговое окно выбора языка -->
<dialog id="language-dialog">
  <!-- Кнопка закрытия диалога -->
  <button type="button" id="btn-dialog-close" class="btn-dialog-close " autofocus>✕</button>
  <!-- Контейнер для вариантов языков -->
  <div id="language-options" class="language-options"></div>
</dialog>
<!-- Подключение основного скрипта -->
<script src="script.js"></script>
</body>
</html>
Подготавливаем скрипт
Раньше мы приступали к стилям после создания HTML-файла, но не в этот раз. Всё дело в том, что сейчас нам просто нечего оформлять: все данные будут генерироваться в скрипте. А вот когда сгенерируем — тогда и оформим.
Первое, что мы делаем, — создаём константы, которые будут потом часто использоваться в проекте. Самое большое — это локализация и выбор языка, поэтому опишем это в самом начале:
// Массив поддерживаемых языков с флагами
const languageFlags = [
  { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', flag: '🇸🇦' },
  { code: 'cs-CZ', name: 'Czech (Czech Republic)', flag: '🇨🇿' },
  { code: 'da-DK', name: 'Danish (Denmark)', flag: '🇩🇰' },
  { code: 'de-DE', name: 'German (Germany)', flag: '🇩🇪' },
  { code: 'el-GR', name: 'Greek (Greece)', flag: '🇬🇷' },
  { code: 'en-US', name: 'English (US)', flag: '🇺🇸' },
  { code: 'en-GB', name: 'English (UK)', flag: '🇬🇧' },
  { code: 'es-ES', name: 'Spanish (Spain)', flag: '🇪🇸' },
  { code: 'es-MX', name: 'Spanish (Mexico)', flag: '🇲🇽' },
  { code: 'fi-FI', name: 'Finnish (Finland)', flag: '🇫🇮' },
  { code: 'fr-CA', name: 'French (Canada)', flag: '🇨🇦' },
  { code: 'fr-FR', name: 'French (France)', flag: '🇫🇷' },
  { code: 'he-IL', name: 'Hebrew (Israel)', flag: '🇮🇱' },
  { code: 'hi-IN', name: 'Hindi (India)', flag: '🇮🇳' },
  { code: 'hu-HU', name: 'Hungarian (Hungary)', flag: '🇭🇺' },
  { code: 'it-IT', name: 'Italian (Italy)', flag: '🇮🇹' },
  { code: 'ja-JP', name: 'Japanese (Japan)', flag: '🇯🇵' },
  { code: 'ko-KR', name: 'Korean (South Korea)', flag: '🇰🇷' },
  { code: 'nl-NL', name: 'Dutch (Netherlands)', flag: '🇳🇱' },
  { code: 'no-NO', name: 'Norwegian (Norway)', flag: '🇳🇴' },
  { code: 'pl-PL', name: 'Polish (Poland)', flag: '🇵🇱' },
  { code: 'pt-BR', name: 'Portuguese (Brazil)', flag: '🇧🇷' },
  { code: 'pt-PT', name: 'Portuguese (Portugal)', flag: '🇵🇹' },
  { code: 'ro-RO', name: 'Romanian (Romania)', flag: '🇷🇴' },
  { code: 'ru-RU', name: 'Russian (Russia)', flag: '🇷🇺' },
  { code: 'sv-SE', name: 'Swedish (Sweden)', flag: '🇸🇪' },
  { code: 'th-TH', name: 'Thai (Thailand)', flag: '🇹🇭' },
  { code: 'tr-TR', name: 'Turkish (Turkey)', flag: '🇹🇷' },
  { code: 'vi-VN', name: 'Vietnamese (Vietnam)', flag: '🇻🇳' },
  { code: 'zh-CN', name: 'Chinese (Simplified, China)', flag: '🇨🇳' },
];
Дальше зададим радиус круга для выбора языка (это пригодится позже) и получим доступ к разным элементам на странице по их Id (и начнём уже ими пользоваться):
const RADIUS = 140; // Радиус круга для кнопок выбора языка
// Получаем DOM-элементы по их Id
const currentLangDisplay = document.getElementById('current-lang');
const languageDialog = document.getElementById('language-dialog');
const languageOptionsContainer = document.getElementById('language-options');
const closeButton = document.getElementById('btn-dialog-close');
Выбираем стартовый язык
Так как мы решили поддерживать сразу много языков и давать пользователю возможность выбора, нам нужно научиться с ними работать.
Логика тут такая:
- Нам нужно установить язык («локаль» по-программистски), который будет выводиться при запуске проекта.
 - Для этого мы пишем функцию 
getLocale()— она определит стартовый язык из настроек страницы. Иногда функция определения может не срабатывать в некоторых браузерах, поэтому установим язык по умолчанию (английский). - Если язык в настройках страницы указан коротко, то с помощью 
defaultRegionsмы найдём полное название языка и вернём его в функциюgetLocale(). - JavaScript умеет автоматически работать с выбранными нами языками — это значит, что при смене локали скрипт сам подставит нужное название месяца и дня недели на выбранном языке. Вот такая удобная автоматизация.
 
Запишем это на JavaScript:
// Создание карты регионов по умолчанию
const defaultRegions = languageFlags.reduce((map, lang) => {
  const baseLang = lang.code.split('-')[0]; // Извлекаем базовый язык (например, 'en' из 'en-US')
  if (!map[baseLang]) {
    map[baseLang] = lang.code; // Сохраняем язык
  }
  return map;
}, {});
// Функция определения текущей локали
function getLocale() {
  // Получаем основной язык из navigator.languages или navigator.language
  let language = (navigator.languages && navigator.languages[0]) || navigator.language || 'en-US';
  // Если язык указан коротко (например, 'en'), добавляем регион из defaultRegions
  if (language.length === 2) {
    language = defaultRegions[language] || `${language}-${language.toUpperCase()}`;
  }
  // Возвращаем язык
  return language;
}
let locale = getLocale(); // Текущая локаль
На экране всё ещё ничего нет, зато мы определились с языком. Можно двигаться дальше.
Выводим данные для циферблата
Продолжаем работать со скриптом: теперь получим и выведем на экран все данные, которые нам нужны для работы часов. Для этого напишем функцию drawClockFaces(), которая даст нам всё что нужно для дальнейших действий.
Что делает эта функция:
- Получает текущую дату и время во всех подробностях от секунды до года.
 - Получает название дня недели и месяца.
 - Дальше идёт работа с каждым контейнером-циферблатом по отдельности.
 - Получает количество элементов в каждом контейнере (их мы задали в HTML в блоке 
data-numbers). - Задаёт радиус для каждого контейнера и его центр вращения.
 - В каждый контейнер добавляет свои значения в зависимости от циферблата. Если это секунды и минуты — то значения от 0 до 59, если это годы — то от 2000 до 2100 и так далее.
 - Создаёт на странице все элементы из каждого списка.
 - Вычисляет их позицию и угол поворота в зависимости от места элемента и добавляет эти свойства к каждому элементу.
 - Устанавливает все часы в стартовое значение по умолчанию.
 
👉 В скрипте есть сразу неочевидный момент: мы обращаемся к стилям и определяем их свойства, хотя у нас нет ещё заполненного файла style.css. Так можно делать, потому что скрипт создаёт стили на уже загруженной странице — и с ними тоже можно работать: создавать, менять значения и удалять.
// Функция отрисовки циферблатов
function drawClockFaces() {
    // Получаем доступ к элементам циферблата
    const clockFaces = document.querySelectorAll('.clock-face');
    // Получаем текущую дату
    const currentDate = new Date();
    const currentDay = currentDate.getDate();
    const currentMonth = currentDate.getMonth();
    const currentYear = currentDate.getFullYear();
    const currentWeekday = currentDate.getDay();
    const currentHours = currentDate.getHours();
    const currentMinutes = currentDate.getMinutes();
    const currentSeconds = currentDate.getSeconds();
    const totalDaysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
    // Получаем названия дней недели и месяцев для текущей локали
    const weekdayNames = Array.from({ length: 7 }, (_, i) =>
        new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(new Date(2021, 0, i + 3))
    );
    const monthNames = Array.from({ length: 12 }, (_, i) =>
        new Intl.DateTimeFormat(locale, { month: 'long' }).format(new Date(2021, i))
    );
    // Обрабатываем каждый циферблат
    clockFaces.forEach(clockFace => {
        clockFace.innerHTML = ''; // Очищаем содержимое
        const clockType = clockFace.getAttribute('data-clock'); // Тип циферблата
        const numbers = parseInt(clockFace.getAttribute('data-numbers'), 10); // Количество значений
        const RADIUS = (clockFace.offsetWidth / 2) - 20; // Радиус для позиционирования
        const center = clockFace.offsetWidth / 2; // Центр циферблата
        let valueSet; // Набор значений (секунды, минуты и так далее)
        let currentValue; // Текущее значение
        // Определяем набор значений в зависимости от типа циферблата
        switch (clockType) {
            case 'seconds':
                valueSet = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentSeconds).padStart(2, '0');
                break;
            case 'minutes':
                valueSet = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentMinutes).padStart(2, '0');
                break;
            case 'hours':
                valueSet = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentHours).padStart(2, '0');
                break;
            case 'days':
                valueSet = Array.from({ length: totalDaysInMonth }, (_, i) => i + 1);
                currentValue = currentDay;
                break;
            case 'months':
                valueSet = monthNames;
                currentValue = currentMonth;
                break;
            case 'years':
                valueSet = Array.from({ length: 101 }, (_, i) => 2000 + i);
                currentValue = currentYear;
                break;
            case 'day-names':
                valueSet = weekdayNames;
                currentValue = currentWeekday;
                break;
            default:
                return;
        }
        // Создаём элементы для каждого значения
        valueSet.forEach((value, i) => {
            const angle = (i * (360 / numbers)); // Угол для позиционирования
            const x = center + RADIUS * Math.cos((angle * Math.PI) / 180); // X координата
            const y = center + RADIUS * Math.sin((angle * Math.PI) / 180); // Y координата
            const element = document.createElement('span');
            element.classList.add('number');
            element.textContent = value;
            element.style.left = `${x}px`;
            element.style.top = `${y}px`;
            element.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
            clockFace.appendChild(element);
        });
        // Вращаем циферблат, чтобы текущее значение было в позиции "3 часа"
        const currentIndex = valueSet.indexOf(
            typeof valueSet[0] === 'string' ? String(currentValue) : currentValue
        );
        const rotationAngle = -((currentIndex / numbers) * 360);
        clockFace.style.transform = `rotate(${rotationAngle}deg)`;
    });
}
Если мы сейчас вызовем эту функцию, то получим что-то очень странное на странице. Время подключать нормальные стили:

Настраиваем стили
Открываем файл style.css и первое, что делаем, — сбрасываем стандартные стили, задаём переменные и устанавливаем основные стили страницы: шрифт, цвета, отступы и фон:
/* Сброс стандартных стилей */
*,
::before,
::after {
  box-sizing: border-box; /* Упрощает расчёт размеров элементов */
}
/* CSS-переменные для цветов и размеров */
:root {
  --clr-bg: rgb(3 3 3); /* Цвет фона */
  --clock-size: 800px; /* Размер часов */
  --clock-clr: rgb(12, 74, 110); /* Основной цвет часов */
}
/* Основные стили страницы */
body {
  margin: 0;
  min-height: 100svh; /* Минимальная высота = высоте viewport */
  display: grid;
  place-content: center; /* Центрирование содержимого */
  font-family: system-ui; /* Системный шрифт */
  background-color: var(--clr-bg);
  background-image: radial-gradient(rgb(8, 47, 73),rgb(8, 47, 60));
  background-blend-mode: difference; /* Эффект наложения фона */
}

Теперь займёмся основным контейнером со всеми циферблатами. Здесь нам нужно:
- Сделать круг (потому что это классические часы).
 - Поместить его по центру.
 - Сразу предусмотреть то, что проект могут открыть на мобилке.
 - Добавить общие стили для каждого круга (с минутами, секундами и прочим) — радиус, центр и всё такое.
 - Задать индивидуальные размеры для каждого кольца.
 
Запишем это в виде CSS-команд:
/* Основной контейнер часов */
.clock {
  position: fixed;
  inset: 0; /* Растягиваем на весь экран */
  margin: auto; /* Центрирование */
  width: var(--clock-size);
  height: var(--clock-size);
  aspect-ratio: 1; /* Сохраняем форму круга */
  place-content: center;
  background: var(--clock-clr);
  border-radius: 50%; /* Делаем круг */
}
/* Адаптация для мобильных */
@media (width < 800px) {
  .clock {
    left: 0;
    right: auto;
    translate: calc((50% - 2rem) * -1) 0; /* Сдвигаем влево */
  }
}
/* Стили для каждого кольца часов */
.clock > div {
  position: absolute;
  inset: 0;
  margin: auto;
  width: var(--clock-d);
  height: var(--clock-d);
  font-size: var(--f-size, 0.9rem);
  aspect-ratio: 1;
  isolation: isolate; /* Изолируем контекст наложения */
  border-radius: 50%;
}
/* Индивидуальные размеры для каждого кольца */
.clock > div:nth-of-type(1) { --clock-d: calc(var(--clock-size) - 20px); } /* Годы */
.clock > div:nth-of-type(2) { --clock-d: calc(var(--clock-size) - 130px); } /* Секунды */
.clock > div:nth-child(3) { --clock-d: calc(var(--clock-size) - 195px); } /* Минуты */
.clock > div:nth-child(4) { --clock-d: calc(var(--clock-size) - 260px); } /* Часы */
.clock > div:nth-child(5) { --clock-d: calc(var(--clock-size) - 350px); } /* Дни */
.clock > div:nth-child(6) { --clock-d: calc(var(--clock-size) - 470px); } /* Месяцы */
.clock > div:nth-child(7) { --clock-d: calc(var(--clock-size) - 600px); } /* Дни недели */

Теперь делаем основную красоту: добавим стили ко всем причастным контейнерам для того, чтобы они выровняли содержимое каждого блока по кругу. Для этого мы включаем вращение вокруг центра, запрещаем переносить текст и применяем это ко всем нужным контейнерам:
/* Стили циферблатов */
.clock-face {
  position: relative;
  width: 100%;
  height: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  transition: 300ms linear; /* Плавное вращение */
}
/* Стили цифр на циферблате */
.clock-face > * {
  position: absolute;
  transform-origin: center; /* Вращение вокруг центра */
  white-space: nowrap; /* Запрет переноса текста */
  color: white;
  opacity: 0.75;
}
/* Активная цифра (текущее значение) */
.clock-face > *.active {
  opacity: 1;
}

После последних действий стало намного красивее, но это не предел. Сейчас мы видим, что иконка флага выглядит чужеродно: стоит слева, имеет белый фон и явно выбивается по стилю. Поправим это: поместим кнопку с флагом в центр, поменяем фон, добавим круглую рамку и поставим иконку поверх всего циферблата:
/* Кнопка текущего языка */
.clock > .current-lang-display {
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: 100;
  display: grid;
  place-content: center;
  background-color: var(--clock-clr);
  border: 1px solid rgba(255 255 255 / 0.25);
  color: white;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  aspect-ratio: 1/1;
  cursor: pointer;
  transition: 300ms ease-in-out;
  font-size: 1.5rem;
  outline: none;
}

Наконец, добавим красоты, чтобы подсветить текущее время: сделаем простую маску, чтобы она выделяла актуальные данные:
/* Полупрозрачная маска для подсветки текущего времени */
.clock::before {
  content: "";
  position: absolute;
  inset: 1px;
  margin: auto;
  background-color: rgba(0 0 0 / 0.85);
  clip-path: polygon(
    0 0,
    100% 0,
    100% 48%,
    50% 48%,
    50% 52%,
    100% 52%,
    100% 100%,
    0 100%
  ); /* Создаём "разрез" в маске */
  border-radius: 50%;
  z-index: 20; /* Поверх других элементов */
}

Работаем с языками
Перед тем как добавлять анимации, сделаем крутую фичу — выбор языка. JavaScript умеет это делать автоматически, поэтому наша задача — объяснить, что и где заменить.
Первое, что для этого сделаем, — скрытие названия текущего языка, чтобы при выборе других он нам не мешал. Причём сделаем относительно абсурдно: сначала выведем название языка, потом предусмотрим, чтобы при наведении на другой флаг язык всё-таки показывался, и сделаем функцию скрытия названия для языка, чтобы не показывался текущий.
Запишем это на JS:
// Показ названия языка в центре
let titleDisplay = null;
function showTitle(languageName) {
  if (titleDisplay) {
    titleDisplay.remove();
  }
  titleDisplay = document.createElement('div');
  titleDisplay.classList.add('language-title');
  titleDisplay.textContent = languageName;
  languageOptionsContainer.appendChild(titleDisplay);
}
// Скрытие названия языка
function hideTitle() {
  if (titleDisplay) {
    titleDisplay.textContent = '';
  }
}
// Обновление отображения текущего языка
function setCurrentLangDisplay(lang) {
  currentLangDisplay.textContent = lang.flag;
  currentLangDisplay.title = lang.name;
  showTitle(lang.name);
}
Теперь добавим функции управления диалоговым окном, которое появится при нажатии на флаг в центре. У нас ещё нет обработчика этого события, поэтому это тоже предусмотрим в коде:
// Управление диалоговым окном
function openDialog() {
  languageDialog.showModal();
  createLanguageOptions();
  addDialogCloseListener();
}
// Закрыть диалоговое окно
function closeDialog() {
  languageDialog.close();
  removeLanguageOptions();
  removeDialogCloseListener();
}
// Убрать опции выбора языка
function removeLanguageOptions() {
  languageOptionsContainer.innerHTML = '';
}
// Добавляем обработчики событий для кликов снаружи поля выбора 
function addDialogCloseListener() {
  languageDialog.addEventListener('click', closeDialogOnClickOutside);
}
function removeDialogCloseListener() {
  languageDialog.removeEventListener('click', closeDialogOnClickOutside);
}
// Закрытие диалога при клике вне его области
function closeDialogOnClickOutside(e) {
  if (e.target === languageDialog) {
    closeDialog();
  }
}
Чтобы наши механики работали, добавим обработчики событий по кликам:
// Обработчики событий
closeButton.addEventListener(‘click’, closeDialog);
currentLangDisplay.addEventListener(‘click’, openDialog);
Наконец, напишем большую функцию, которая загрузит все флаги в диалоговое окно и даст пользователю возможность выбора языка. Логика такая:
- Загружаем все флаги из стартового массива.
 - Считаем относительно круга позицию каждого флага (сейчас мы этого не увидим, потому что нет стилей, но в будущем пригодится).
 - Каждый флаг добавляем на страницу в элемент label.
 - Добавляем радиокнопку к каждому флагу, чтобы можно было выбрать только один язык из предложенных.
 - Добавляем обработчики событий, если пользователь передумал выбирать язык.
 - Пишем обработчик, который при выборе языка даст команду обновить надписи в нужных элементах.
 
Вот как это выглядит в коде:
// Создание кнопок выбора языка
function createLanguageOptions() {
  const centerX = languageOptionsContainer.offsetWidth / 2;
  const centerY = languageOptionsContainer.offsetHeight / 2;
  // Создаём кнопки для каждого языка
  languageFlags.forEach((lang, index, arr) => {
    const angle = (index / arr.length) * 2 * Math.PI; // Угол для позиционирования
    const x = centerX + RADIUS * Math.cos(angle); // X координата
    const y = centerY + RADIUS * Math.sin(angle); // Y координата
    // Создаём элемент label
    const radioWrapper = document.createElement('label');
    radioWrapper.title = lang.name;
    radioWrapper.style.left = `${x}px`;
    radioWrapper.style.top = `${y}px`;
    // Создаём радиокнопку
    const radioInput = document.createElement('input');
    radioInput.type = 'radio';
    radioInput.name = 'language';
    radioInput.value = lang.code;
    // Помечаем текущий язык
    if (lang.code === locale) {
      radioInput.checked = true;
      radioWrapper.classList.add('active');
    }
    // Создаём элемент флага
    const flag = document.createElement('span');
    flag.classList.add('flag-icon');
    flag.innerText = lang.flag;
    // Собираем структуру
    radioWrapper.appendChild(radioInput);
    radioWrapper.appendChild(flag);
    languageOptionsContainer.appendChild(radioWrapper);
    // Обработчики событий
    radioWrapper.addEventListener('mouseover', () => showTitle(lang.name, radioWrapper));
    radioWrapper.addEventListener('mouseleave', hideTitle);
  
    // Смена языка
    radioInput.addEventListener('change', () => {
      locale = radioInput.value;
      setCurrentLangDisplay(lang);
      drawClockFaces();
      document.querySelector('label.active')?.classList.remove('active');
      radioWrapper.classList.add('active');
      closeDialog();
    });
  });
}
После всех обновлений при клике на флаг получаем такую картинку — выглядит некрасиво, поэтому снова возвращаемся к стилям.

Добавляем стили к окну с языками
Возвращаемся к файлу style.css, чтобы настроить красивый внешний вид диалогового окна выбора языков. Вот что нам нужно тут сделать:
- Прописать общий стиль окна — сделать его круглым, как и всё остальное на странице.
 - Добавить анимацию открытия и эффектов при наведении на другой флаг.
 - Настроить внешний вид кнопки закрытия окна (если пользователь передумал выбирать язык) и убрать её из центра, чтобы она не закрывала текущий флаг.
 - Оформить красиво флаги и добавить анимацию наведения на них.
 - Написать название языка при наведении на флаг.
 - Скрыть радиокнопки, чтобы они не мешали интерфейсу.
 
Как обычно, прокомментировали каждый блок, чтобы было проще понять, что происходит в каждом:
/* Стили диалогового окна */
dialog {
  width: min(calc(100% - 2rem), 380px); /* Адаптивная ширина */
  padding: 1rem;
  border: none;
  border-radius: 999px; /* Округлая форма */
  background: rgba(0 0 0 / 0.25);
  text-align: center;
  aspect-ratio: 1;
  overflow: visible;
}
/* Анимация открытия */
@starting-style {
  opacity: 0;
  scale: 0;
}
transition: opacity 500ms ease-in,
  scale 500ms cubic-bezier(0.28, -0.55, 0.27, 1.55);
/* Затемнение фона */
dialog[open]::backdrop {
  background-color: rgba(from black r g b / 0.5);
  backdrop-filter: blur(3px); /* Размытие фона */
  opacity: 1;
}
/* Кнопка закрытия диалога */
dialog .btn-dialog-close {
  position: absolute;
  top: 0rem;
  right: 25%;
  aspect-ratio: 1;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: black;
  font-size: 1.2rem;
  color: white;
  border: none;
  outline: none;
  cursor: pointer;
  transition: rotate 300ms ease-in-out;
  z-index: 11;
}
/* Эффекты кнопки закрытия */
.btn-dialog-close:focus-visible,
.btn-dialog-close:hover {
  rotate: 90deg;
}
/* Контейнер вариантов языков */
.language-options {
  position: absolute;
  inset: 0;
  margin: auto;
  border-radius: 50%;
  aspect-ratio: 1/1;
  overflow: hidden;
}
/* Элементы выбора языка */
.language-options > label {
  position: absolute;
  transform: translate(-50%, -50%);
  cursor: pointer;
  font-size: 0.9rem;
  aspect-ratio: 1/1;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  transition: 300ms ease-in-out;
  display: grid;
  place-content: center;
  transform-origin: center;
}
/* Активный язык */
.language-options > label.active {
  color: white;
  background: var(--clock-clr);
}
/* Эффекты при наведении */
.language-options > label:focus-visible,
.language-options > label:hover {
  scale: 1.1;
  z-index: 2;
}
/* Название языка в центре */
.language-title {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none; /* Игнорирует клики */
  color: white;
  font-size: 1.2rem;
}
/* Анимация названия */
@starting-style {
  opacity: 0;
}
transition: opacity 300ms ease-in-out;
/* Иконки флагов */
.flag-icon {
  font-size: 1.5rem;
  display: grid;
  place-content: center;
}
/* Скрытие радиокнопок */
.language-options input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0, 0, 0, 0);
  overflow: hidden;
}
Смотрите, что получилось в итоге:

Добавляем разделители в циферблат
Сейчас визуально всё хорошо, но не хватает привычного двоеточия между минутами и секундами. Добавим это тоже в стилях:
/* Разделители времени (двоеточия) */
.current-lang-display::before,
.current-lang-display::after {
  content: ": ";
  color: white;
  position: absolute;
  z-index: 199;
  top: 50%;
  right: 0;
  font-size: 0.9rem;
  translate: 283px -10px;
}

Финал — добавляем анимацию
Последнее, что нам осталось сделать в проекте, — добавить анимацию вращения, чтобы часы обновлялись и всегда показывали верное время.
Что будем здесь делать:
- Получаем доступ ко всем циферблатам.
 - Получаем текущие значения времени.
 - Находим количество элементов в каждом циферблате.
 - Вычисляем угол поворота при смене времени для каждого циферблата.
 - Считаем углы поворотов для того, чтобы анимация получалась плавной, а не дёрганой и резкой.
 - Сохраняем новый угол поворота.
 - Обновляем активные элементы в каждом циферблате, чтобы знать, на сколько сейчас повёрнуто каждое поле.
 - Обновляем кадры анимации.
 
Эта механика — сложная, и чтобы в ней разобраться, надо держать в уме две ключевые вещи: активный элемент и угол поворота. Но если вы помните немного из школьного курса геометрии, то собрать всё вместе будет не очень сложно.
Запишем код на JavaScript:
// Функция анимации вращения циферблатов
function rotateClockFaces() {
    const clockFaces = document.querySelectorAll('.clock-face');
    const lastAngles = {}; // Хранит последние углы для каждого циферблата
    
    function updateRotations() {
        const now = new Date();
        // Получаем текущие значения времени
        const currentSecond = now.getSeconds();
        const currentMinute = now.getMinutes();
        const currentHour = now.getHours();
        const currentDay = now.getDate();
        const currentMonth = now.getMonth();
        const currentYear = now.getFullYear();
        const currentWeekday = now.getDay();
        // Обрабатываем каждый элемент циферблата
        clockFaces.forEach(clockFace => {
            const clockType = clockFace.getAttribute('data-clock');
            // Получаем количество делений в каждом циферблате
            const totalNumbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
            let currentValue;
            switch (clockType) {
                case 'seconds':
                    currentValue = currentSecond;
                    break;
                case 'minutes':
                    currentValue = currentMinute;
                    break;
                case 'hours':
                    currentValue = currentHour;
                    break;
                case 'days':
                    currentValue = currentDay - 1;
                    break;
                case 'months':
                    currentValue = currentMonth;
                    break;
                case 'years':
                    currentValue = currentYear - 2000;
                    break;
                case 'day-names':
                    currentValue = currentWeekday; // 0 = Sunday
                    break;
                default:
                    return;
            }
            // Вычисляем угол поворота на каждом шаге для каждого циферблата
            const targetAngle = (360 / totalNumbers) * currentValue;
            // Получаем последний угол для плавного вращения
            const clockId = clockFace.id || clockType;
            const lastAngle = lastAngles[clockId] || 0;
            // Вычисляем минимальный угол для плавного перехода
            const delta = targetAngle - lastAngle;
            const shortestDelta = ((delta + 540) % 360) - 180;
            // Обновляем вращение
            const newAngle = lastAngle + shortestDelta;
            // Обновляем стиль элемента
            clockFace.style.transform = `rotate(${newAngle * -1}deg)`;
            // Сохраняем новый угол
            lastAngles[clockId] = newAngle;
            // Обновляем активный элемент
            const numbers = clockFace.querySelectorAll('.number');
            numbers.forEach((number, index) => {
                if (index === currentValue) {
                    number.classList.add('active');
                } else {
                    number.classList.remove('active');
                }
            });
        });
        
        // Запрашиваем следующий кадр анимации
        requestAnimationFrame(updateRotations);
    }
    updateRotations();
}
Уф! Справились! Результат — на гифке ниже. Или можно посмотреть, как всё это работает, на странице проекта.
<!DOCTYPE html>
<html lang="ru-RU">
<head>
  <!-- Базовые настройки документа -->
  <meta charset="UTF-8"> <!-- Кодировка UTF-8 -->
  <title>Часы на 100 лет вперёд</title> <!-- Заголовок страницы -->
  <link rel="stylesheet" href="style.css"> <!-- Подключение стилей -->
</head>
<body>
<!-- Основная структура часов -->
<div class="clock" data-date="2025-04-55">
  <!-- Контейнеры для каждого типа времени (годы, секунды и так далее) -->
  <div>
    <div data-clock="years" data-numbers="101" class="clock-face"></div> <!-- 100-летний круг -->
  </div>
  <div>
    <div data-clock="seconds" data-numbers="60" class="clock-face"></div> <!-- Секунды -->
  </div>
  <div>
    <div data-clock="minutes" data-numbers="60" class="clock-face"></div> <!-- Минуты -->
  </div>
  <div>
    <div data-clock="hours" data-numbers="24" class="clock-face"></div> <!-- Часы -->
  </div>
  <div>
    <div data-clock="days" data-numbers="31" class="clock-face"></div> <!-- Дни месяца -->
  </div>
  <div>
    <div data-clock="months" data-numbers="12" class="clock-face"></div> <!-- Месяцы -->
  </div>
  <div>
    <div data-clock="day-names" data-numbers="7" class="clock-face"></div> <!-- Дни недели -->
  </div>
  <!-- Кнопка выбора языка -->
  <button type="button" id="current-lang" class="current-lang-display">en</button>
</div>
<!-- Диалоговое окно выбора языка -->
<dialog id="language-dialog">
  <!-- Кнопка закрытия диалога -->
  <button type="button" id="btn-dialog-close" class="btn-dialog-close " autofocus>✕</button>
  <!-- Контейнер для вариантов языков -->
  <div id="language-options" class="language-options"></div>
</dialog>
<!-- Подключение основного скрипта -->
<script src="script.js"></script>
</body>
</html>
/* Сброс стандартных стилей */
*,
::before,
::after {
  box-sizing: border-box; /* Упрощает расчёт размеров элементов */
}
/* CSS-переменные для цветов и размеров */
:root {
  --clr-bg: rgb(3 3 3); /* Цвет фона */
  --clock-size: 800px; /* Размер часов */
  --clock-clr: rgb(12, 74, 110); /* Основной цвет часов */
}
/* Основные стили страницы */
body {
  margin: 0;
  min-height: 100svh; /* Минимальная высота = высоте viewport */
  display: grid;
  place-content: center; /* Центрирование содержимого */
  font-family: system-ui; /* Системный шрифт */
  background-color: var(--clr-bg);
  background-image: radial-gradient(rgb(8, 47, 73),rgb(8, 47, 60));
  background-blend-mode: difference; /* Эффект наложения фона */
}
/* Основной контейнер часов */
.clock {
  position: fixed;
  inset: 0; /* Растягиваем на весь экран */
  margin: auto; /* Центрирование */
  width: var(--clock-size);
  height: var(--clock-size);
  aspect-ratio: 1; /* Сохраняем квадратную форму */
  place-content: center;
  background: var(--clock-clr);
  border-radius: 50%; /* Делаем круг */
}
/* Адаптация для мобильных */
@media (width < 800px) {
  .clock {
    left: 0;
    right: auto;
    translate: calc((50% - 2rem) * -1) 0; /* Сдвигаем влево */
  }
}
/* Полупрозрачная маска для подсветки текущего времени */
.clock::before {
  content: "";
  position: absolute;
  inset: 1px;
  margin: auto;
  background-color: rgba(0 0 0 / 0.85);
  clip-path: polygon(
    0 0,
    100% 0,
    100% 48%,
    50% 48%,
    50% 52%,
    100% 52%,
    100% 100%,
    0 100%
  ); /* Создаём "разрез" в маске */
  border-radius: 50%;
  z-index: 20; /* Поверх других элементов */
}
/* Стили для каждого кольца часов */
.clock > div {
  position: absolute;
  inset: 0;
  margin: auto;
  width: var(--clock-d);
  height: var(--clock-d);
  font-size: var(--f-size, 0.9rem);
  aspect-ratio: 1;
  isolation: isolate; /* Изолируем контекст наложения */
  border-radius: 50%;
}
/* Индивидуальные размеры для каждого кольца */
.clock > div:nth-of-type(1) { --clock-d: calc(var(--clock-size) - 20px); } /* Годы */
.clock > div:nth-of-type(2) { --clock-d: calc(var(--clock-size) - 130px); } /* Секунды */
.clock > div:nth-child(3) { --clock-d: calc(var(--clock-size) - 195px); } /* Минуты */
.clock > div:nth-child(4) { --clock-d: calc(var(--clock-size) - 260px); } /* Часы */
.clock > div:nth-child(5) { --clock-d: calc(var(--clock-size) - 350px); } /* Дни */
.clock > div:nth-child(6) { --clock-d: calc(var(--clock-size) - 470px); } /* Месяцы */
.clock > div:nth-child(7) { --clock-d: calc(var(--clock-size) - 600px); } /* Дни недели */
/* Стили циферблатов */
.clock-face {
  position: relative;
  width: 100%;
  height: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  transition: 300ms linear; /* Плавное вращение */
}
/* Стили цифр на циферблате */
.clock-face > * {
  position: absolute;
  transform-origin: center; /* Вращение вокруг центра */
  white-space: nowrap; /* Запрет переноса текста */
  color: white;
  opacity: 0.75;
}
/* Активная цифра (текущее значение) */
.clock-face > *.active {
  opacity: 1;
}
/* Кнопка текущего языка */
.clock > .current-lang-display {
  position: absolute;
  inset: 0;
  margin: auto;
  z-index: 100;
  display: grid;
  place-content: center;
  background-color: var(--clock-clr);
  border: 1px solid rgba(255 255 255 / 0.25);
  color: white;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  aspect-ratio: 1/1;
  cursor: pointer;
  transition: 300ms ease-in-out;
  font-size: 1.5rem;
  outline: none;
}
/* Стили диалогового окна */
dialog {
  width: min(calc(100% - 2rem), 380px); /* Адаптивная ширина */
  padding: 1rem;
  border: none;
  border-radius: 999px; /* Округлая форма */
  background: rgba(0 0 0 / 0.25);
  text-align: center;
  aspect-ratio: 1;
  overflow: visible;
}
/* Анимация открытия */
@starting-style {
  opacity: 0;
  scale: 0;
}
transition: opacity 500ms ease-in,
  scale 500ms cubic-bezier(0.28, -0.55, 0.27, 1.55);
/* Затемнение фона */
dialog[open]::backdrop {
  background-color: rgba(from black r g b / 0.5);
  backdrop-filter: blur(3px); /* Размытие фона */
  opacity: 1;
}
/* Кнопка закрытия диалога */
dialog .btn-dialog-close {
  position: absolute;
  top: 0rem;
  right: 25%;
  aspect-ratio: 1;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: black;
  font-size: 1.2rem;
  color: white;
  border: none;
  outline: none;
  cursor: pointer;
  transition: rotate 300ms ease-in-out;
  z-index: 11;
}
/* Эффекты кнопки закрытия */
.btn-dialog-close:focus-visible,
.btn-dialog-close:hover {
  rotate: 90deg;
}
/* Контейнер вариантов языков */
.language-options {
  position: absolute;
  inset: 0;
  margin: auto;
  border-radius: 50%;
  aspect-ratio: 1/1;
  overflow: hidden;
}
/* Элементы выбора языка */
.language-options > label {
  position: absolute;
  transform: translate(-50%, -50%);
  cursor: pointer;
  font-size: 0.9rem;
  aspect-ratio: 1/1;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  transition: 300ms ease-in-out;
  display: grid;
  place-content: center;
  transform-origin: center;
}
/* Активный язык */
.language-options > label.active {
  color: white;
  background: var(--clock-clr);
}
/* Эффекты при наведении */
.language-options > label:focus-visible,
.language-options > label:hover {
  scale: 1.1;
  z-index: 2;
}
/* Название языка в центре */
.language-title {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none; /* Игнорирует клики */
  color: white;
  font-size: 1.2rem;
}
/* Анимация названия */
@starting-style {
  opacity: 0;
}
transition: opacity 300ms ease-in-out;
/* Иконки флагов */
.flag-icon {
  font-size: 1.5rem;
  display: grid;
  place-content: center;
}
/* Скрытие радиокнопок */
.language-options input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0, 0, 0, 0);
  overflow: hidden;
}
/* Разделители времени (двоеточия) */
.current-lang-display::before,
.current-lang-display::after {
  content: ": ";
  color: white;
  position: absolute;
  z-index: 199;
  top: 50%;
  right: 0;
  font-size: 0.9rem;
  translate: 283px -10px;
}
// Массив поддерживаемых языков с флагами
const languageFlags = [
  { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', flag: '🇸🇦' },
  { code: 'cs-CZ', name: 'Czech (Czech Republic)', flag: '🇨🇿' },
  { code: 'da-DK', name: 'Danish (Denmark)', flag: '🇩🇰' },
  { code: 'de-DE', name: 'German (Germany)', flag: '🇩🇪' },
  { code: 'el-GR', name: 'Greek (Greece)', flag: '🇬🇷' },
  { code: 'en-US', name: 'English (US)', flag: '🇺🇸' },
  { code: 'en-GB', name: 'English (UK)', flag: '🇬🇧' },
  { code: 'es-ES', name: 'Spanish (Spain)', flag: '🇪🇸' },
  { code: 'es-MX', name: 'Spanish (Mexico)', flag: '🇲🇽' },
  { code: 'fi-FI', name: 'Finnish (Finland)', flag: '🇫🇮' },
  { code: 'fr-CA', name: 'French (Canada)', flag: '🇨🇦' },
  { code: 'fr-FR', name: 'French (France)', flag: '🇫🇷' },
  { code: 'he-IL', name: 'Hebrew (Israel)', flag: '🇮🇱' },
  { code: 'hi-IN', name: 'Hindi (India)', flag: '🇮🇳' },
  { code: 'hu-HU', name: 'Hungarian (Hungary)', flag: '🇭🇺' },
  { code: 'it-IT', name: 'Italian (Italy)', flag: '🇮🇹' },
  { code: 'ja-JP', name: 'Japanese (Japan)', flag: '🇯🇵' },
  { code: 'ko-KR', name: 'Korean (South Korea)', flag: '🇰🇷' },
  { code: 'nl-NL', name: 'Dutch (Netherlands)', flag: '🇳🇱' },
  { code: 'no-NO', name: 'Norwegian (Norway)', flag: '🇳🇴' },
  { code: 'pl-PL', name: 'Polish (Poland)', flag: '🇵🇱' },
  { code: 'pt-BR', name: 'Portuguese (Brazil)', flag: '🇧🇷' },
  { code: 'pt-PT', name: 'Portuguese (Portugal)', flag: '🇵🇹' },
  { code: 'ro-RO', name: 'Romanian (Romania)', flag: '🇷🇴' },
  { code: 'ru-RU', name: 'Russian (Russia)', flag: '🇷🇺' },
  { code: 'sv-SE', name: 'Swedish (Sweden)', flag: '🇸🇪' },
  { code: 'th-TH', name: 'Thai (Thailand)', flag: '🇹🇭' },
  { code: 'tr-TR', name: 'Turkish (Turkey)', flag: '🇹🇷' },
  { code: 'vi-VN', name: 'Vietnamese (Vietnam)', flag: '🇻🇳' },
  { code: 'zh-CN', name: 'Chinese (Simplified, China)', flag: '🇨🇳' },
];
const RADIUS = 140; // Радиус круга для кнопок выбора языка
// Получаем DOM-элементы по их Id
const currentLangDisplay = document.getElementById('current-lang');
const languageDialog = document.getElementById('language-dialog');
const languageOptionsContainer = document.getElementById('language-options');
const closeButton = document.getElementById('btn-dialog-close');
// Создание карты регионов по умолчанию
const defaultRegions = languageFlags.reduce((map, lang) => {
  const baseLang = lang.code.split('-')[0]; // Извлекаем базовый язык (например, 'en' из 'en-US')
  if (!map[baseLang]) {
    map[baseLang] = lang.code; // Сохраняем язык
  }
  return map;
}, {});
// Функция определения текущей локали
function getLocale() {
  // Получаем основной язык из navigator.languages или navigator.language
  let language = (navigator.languages && navigator.languages[0]) || navigator.language || 'en-US';
  // Если язык указан коротко (например, 'en'), добавляем регион из defaultRegions
  if (language.length === 2) {
    language = defaultRegions[language] || `${language}-${language.toUpperCase()}`;
  }
  // Возвращаем язык
  return language;
}
let locale = getLocale(); // Текущая локаль
// Функция отрисовки циферблатов
function drawClockFaces() {
    // Получаем доступ к элементам циферблата
    const clockFaces = document.querySelectorAll('.clock-face');
    // Получаем текущую дату
    const currentDate = new Date();
    const currentDay = currentDate.getDate();
    const currentMonth = currentDate.getMonth();
    const currentYear = currentDate.getFullYear();
    const currentWeekday = currentDate.getDay();
    const currentHours = currentDate.getHours();
    const currentMinutes = currentDate.getMinutes();
    const currentSeconds = currentDate.getSeconds();
    const totalDaysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
    // Получаем названия дней недели и месяцев для текущей локали
    const weekdayNames = Array.from({ length: 7 }, (_, i) =>
        new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(new Date(2021, 0, i + 3))
    );
    const monthNames = Array.from({ length: 12 }, (_, i) =>
        new Intl.DateTimeFormat(locale, { month: 'long' }).format(new Date(2021, i))
    );
    // Обрабатываем каждый циферблат
    clockFaces.forEach(clockFace => {
        clockFace.innerHTML = ''; // Очищаем содержимое
        const clockType = clockFace.getAttribute('data-clock'); // Тип циферблата
        const numbers = parseInt(clockFace.getAttribute('data-numbers'), 10); // Количество значений
        const RADIUS = (clockFace.offsetWidth / 2) - 20; // Радиус для позиционирования
        const center = clockFace.offsetWidth / 2; // Центр циферблата
        let valueSet; // Набор значений (секунды, минуты и так далее)
        let currentValue; // Текущее значение
        // Определяем набор значений в зависимости от типа циферблата
        switch (clockType) {
            case 'seconds':
                valueSet = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentSeconds).padStart(2, '0');
                break;
            case 'minutes':
                valueSet = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentMinutes).padStart(2, '0');
                break;
            case 'hours':
                valueSet = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
                currentValue = String(currentHours).padStart(2, '0');
                break;
            case 'days':
                valueSet = Array.from({ length: totalDaysInMonth }, (_, i) => i + 1);
                currentValue = currentDay;
                break;
            case 'months':
                valueSet = monthNames;
                currentValue = currentMonth;
                break;
            case 'years':
                valueSet = Array.from({ length: 101 }, (_, i) => 2000 + i);
                currentValue = currentYear;
                break;
            case 'day-names':
                valueSet = weekdayNames;
                currentValue = currentWeekday;
                break;
            default:
                return;
        }
        // Создаём элементы для каждого значения
        valueSet.forEach((value, i) => {
            const angle = (i * (360 / numbers)); // Угол для позиционирования
            const x = center + RADIUS * Math.cos((angle * Math.PI) / 180); // X координата
            const y = center + RADIUS * Math.sin((angle * Math.PI) / 180); // Y координата
            const element = document.createElement('span');
            element.classList.add('number');
            element.textContent = value;
            element.style.left = `${x}px`;
            element.style.top = `${y}px`;
            element.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
            clockFace.appendChild(element);
        });
        // Вращаем циферблат, чтобы текущее значение было в позиции "3 часа"
        const currentIndex = valueSet.indexOf(
            typeof valueSet[0] === 'string' ? String(currentValue) : currentValue
        );
        const rotationAngle = -((currentIndex / numbers) * 360);
        clockFace.style.transform = `rotate(${rotationAngle}deg)`;
    });
}
// Функция анимации вращения циферблатов
function rotateClockFaces() {
    const clockFaces = document.querySelectorAll('.clock-face');
    const lastAngles = {}; // Хранит последние углы для каждого циферблата
    
    function updateRotations() {
        const now = new Date();
        // Получаем текущие значения времени
        const currentSecond = now.getSeconds();
        const currentMinute = now.getMinutes();
        const currentHour = now.getHours();
        const currentDay = now.getDate();
        const currentMonth = now.getMonth();
        const currentYear = now.getFullYear();
        const currentWeekday = now.getDay();
        // Обрабатываем каждый элемент циферблата
        clockFaces.forEach(clockFace => {
            const clockType = clockFace.getAttribute('data-clock');
            // Получаем количество делений в каждом циферблате
            const totalNumbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
            let currentValue;
            switch (clockType) {
                case 'seconds':
                    currentValue = currentSecond;
                    break;
                case 'minutes':
                    currentValue = currentMinute;
                    break;
                case 'hours':
                    currentValue = currentHour;
                    break;
                case 'days':
                    currentValue = currentDay - 1;
                    break;
                case 'months':
                    currentValue = currentMonth;
                    break;
                case 'years':
                    currentValue = currentYear - 2000;
                    break;
                case 'day-names':
                    currentValue = currentWeekday; // 0 = Sunday
                    break;
                default:
                    return;
            }
            // Вычисляем угол поворота на каждом шаге для каждого циферблата
            const targetAngle = (360 / totalNumbers) * currentValue;
            // Получаем последний угол для плавного вращения
            const clockId = clockFace.id || clockType;
            const lastAngle = lastAngles[clockId] || 0;
            // Вычисляем минимальный угол для плавного перехода
            const delta = targetAngle - lastAngle;
            const shortestDelta = ((delta + 540) % 360) - 180;
            // Обновляем вращение
            const newAngle = lastAngle + shortestDelta;
            // Обновляем стиль элемента
            clockFace.style.transform = `rotate(${newAngle * -1}deg)`;
            // Сохраняем новый угол
            lastAngles[clockId] = newAngle;
            // Обновляем активный элемент
            const numbers = clockFace.querySelectorAll('.number');
            numbers.forEach((number, index) => {
                if (index === currentValue) {
                    number.classList.add('active');
                } else {
                    number.classList.remove('active');
                }
            });
        });
        
        // Запрашиваем следующий кадр анимации
        requestAnimationFrame(updateRotations);
    }
    updateRotations();
}
// Создание кнопок выбора языка
function createLanguageOptions() {
  const centerX = languageOptionsContainer.offsetWidth / 2;
  const centerY = languageOptionsContainer.offsetHeight / 2;
  // Создаём кнопки для каждого языка
  languageFlags.forEach((lang, index, arr) => {
    const angle = (index / arr.length) * 2 * Math.PI; // Угол для позиционирования
    const x = centerX + RADIUS * Math.cos(angle); // X координата
    const y = centerY + RADIUS * Math.sin(angle); // Y координата
    // Создаём элемент label
    const radioWrapper = document.createElement('label');
    radioWrapper.title = lang.name;
    radioWrapper.style.left = `${x}px`;
    radioWrapper.style.top = `${y}px`;
    // Создаём радиокнопку
    const radioInput = document.createElement('input');
    radioInput.type = 'radio';
    radioInput.name = 'language';
    radioInput.value = lang.code;
    // Помечаем текущий язык
    if (lang.code === locale) {
      radioInput.checked = true;
      radioWrapper.classList.add('active');
    }
    // Создаём элемент флага
    const flag = document.createElement('span');
    flag.classList.add('flag-icon');
    flag.innerText = lang.flag;
    // Собираем структуру
    radioWrapper.appendChild(radioInput);
    radioWrapper.appendChild(flag);
    languageOptionsContainer.appendChild(radioWrapper);
    // Обработчики событий
    radioWrapper.addEventListener('mouseover', () => showTitle(lang.name, radioWrapper));
    radioWrapper.addEventListener('mouseleave', hideTitle);
  
    // Смена языка
    radioInput.addEventListener('change', () => {
      locale = radioInput.value;
      setCurrentLangDisplay(lang);
      drawClockFaces();
      document.querySelector('label.active')?.classList.remove('active');
      radioWrapper.classList.add('active');
      closeDialog();
    });
  });
}
// Показ названия языка в центре
let titleDisplay = null;
function showTitle(languageName) {
  if (titleDisplay) {
    titleDisplay.remove();
  }
  titleDisplay = document.createElement('div');
  titleDisplay.classList.add('language-title');
  titleDisplay.textContent = languageName;
  languageOptionsContainer.appendChild(titleDisplay);
}
// Скрытие названия языка
function hideTitle() {
  if (titleDisplay) {
    titleDisplay.textContent = '';
  }
}
// Обновление отображения текущего языка
function setCurrentLangDisplay(lang) {
  currentLangDisplay.textContent = lang.flag;
  currentLangDisplay.title = lang.name;
  showTitle(lang.name);
}
// Управление диалоговым окном
function openDialog() {
  languageDialog.showModal();
  createLanguageOptions();
  addDialogCloseListener();
}
// Закрыть диалоговое окно
function closeDialog() {
  languageDialog.close();
  removeLanguageOptions();
  removeDialogCloseListener();
}
// Убрать опции выбора языка
function removeLanguageOptions() {
  languageOptionsContainer.innerHTML = '';
}
// Добавляем обработчики событий для кликов снаружи поля выбора 
function addDialogCloseListener() {
  languageDialog.addEventListener('click', closeDialogOnClickOutside);
}
function removeDialogCloseListener() {
  languageDialog.removeEventListener('click', closeDialogOnClickOutside);
}
// Закрытие диалога при клике вне его области
function closeDialogOnClickOutside(e) {
  if (e.target === languageDialog) {
    closeDialog();
  }
}
// Обработчики событий
closeButton.addEventListener('click', closeDialog);
currentLangDisplay.addEventListener('click', openDialog);
// Инициализация
drawClockFaces();
rotateClockFaces();
setCurrentLangDisplay(languageFlags.find(lang => lang.code === locale));
Вам слово
Приходите к нам в соцсети поделиться своим мнением о проекте и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте: шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
						
