Krok 13: język polski i angielski ze słownika

Kalendarz ze świętami, notatkami i językami - Kalendarz w JavaScript krok po kroku (HTML/CSS/JS)

Wszystkie napisy strony pochodzą ze słownika `TEXTS`, który dostaje drugi język. Wybrany język to kolejne pole stanu (`lang`), a lista `#lang` zmienia je przez `setState`. `render` bierze słownik `t = TEXTS[state.lang]` i z niego miesiące, dni, nazwy świąt i etykiety. Model i święta się nie zmieniły: identyfikatory świąt są wspólne dla obu języków. W starszym rozwiązaniu słownik przychodził z serwera (AJAX i baza danych); tu jest w kodzie, ale render działa tak samo.

<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="utf-8">
  <title>Kalendarz</title>
  <style>
    body { font-family: sans-serif; max-width: 36em; line-height: 1.4; }
    input, select, button, textarea { font: inherit; padding: 0.2em 0.4em; }
    table { border-collapse: collapse; }
    th, td { border: 1px solid #94a3b8; padding: 0.2em 0.6em; }

    .grid { display: grid; grid-template-columns: repeat(7, 40px); gap: 6px; }
    .grid div { height: 40px; line-height: 40px; text-align: center; background: lightgrey; }
    .grid .day-name { background: darkgrey; color: white; }
    .grid .weekend { background: lightblue; color: white; }
    .grid .none { background: white; }
    .grid .holiday { background: #fecaca; color: #b91c1c; font-weight: bold; }
    .grid .today { background: crimson; color: white; }
    .grid .day { cursor: pointer; }
    .grid .selected { outline: 3px solid #f59e0b; }
    .grid .has-note { text-decoration: underline; }
    .bar { display: flex; align-items: center; gap: 0.5em; width: 316px; }
    .bar strong { flex: 1; text-align: center; }
  </style>
</head>
<body>
  <p><select id="lang"><option value="pl">Polski</option><option value="en">English</option></select></p>

  <p class="bar">
    <button id="prev">&lt;</button>
    <strong id="title"></strong>
    <button id="next">&gt;</button>
  </p>
  <div id="calendar" class="grid"></div>

                  <p id="workdays"></p>
                  <p><span id="chosen"></span> <input id="note"> <button id="save"></button></p>
                  <ul id="holidays"></ul>
  <script>
    const TEXTS = {
      pl: {
        months: ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec',
          'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'],
        days: ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'],
        workdays: 'Dni robocze', choose: 'Kliknij dzień', day: 'Dzień', save: 'Zapisz',
        holidays: {
          newYear: 'Nowy Rok', epiphany: 'Trzech Króli', labour: 'Święto Pracy', constitution: 'Święto Konstytucji 3 Maja',
          assumption: 'Wniebowzięcie NMP', allSaints: 'Wszystkich Świętych', independence: 'Święto Niepodległości',
          christmasEve: 'Wigilia', christmas: 'Boże Narodzenie', boxing: 'Drugi dzień Bożego Narodzenia',
          easter: 'Wielkanoc', easterMonday: 'Poniedziałek Wielkanocny', pentecost: 'Zielone Świątki', corpusChristi: 'Boże Ciało',
        },
      },
      en: {
        months: ['January', 'February', 'March', 'April', 'May', 'June',
          'July', 'August', 'September', 'October', 'November', 'December'],
        days: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
        workdays: 'Working days', choose: 'Click a day', day: 'Day', save: 'Save',
        holidays: {
          newYear: "New Year's Day", epiphany: 'Epiphany', labour: 'Labour Day', constitution: 'Constitution Day',
          assumption: 'Assumption Day', allSaints: "All Saints' Day", independence: 'Independence Day',
          christmasEve: 'Christmas Eve', christmas: 'Christmas Day', boxing: 'Second Day of Christmas',
          easter: 'Easter Sunday', easterMonday: 'Easter Monday', pentecost: 'Pentecost', corpusChristi: 'Corpus Christi',
        },
      },
    };
    // algorytm Meeusa (Jonesa-Butchera): Wielkanoc dla dowolnego roku kalendarza gregoriańskiego
    function easter(year) {
      const a = year % 19;
      const b = Math.floor(year / 100);
      const c = year % 100;
      const d = Math.floor(b / 4);
      const e = b % 4;
      const f = Math.floor((b + 8) / 25);
      const g = Math.floor((b - f + 1) / 3);
      const h = (19 * a + b - d - g + 15) % 30;
      const i = Math.floor(c / 4);
      const k = c % 4;
      const l = (32 + 2 * e + 2 * i - h - k) % 7;
      const m = Math.floor((a + 11 * h + 22 * l) / 451);
      const month = Math.floor((h + l - 7 * m + 114) / 31);
      const day = ((h + l - 7 * m + 114) % 31) + 1;
      return new Date(year, month - 1, day);
    }
    // Date sam przenosi nadmiar dni do kolejnego miesiąca
    const addDays = (date, n) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
    const FIXED_HOLIDAYS = [
      { month: 1, day: 1, id: 'newYear' }, { month: 1, day: 6, id: 'epiphany' },
      { month: 5, day: 1, id: 'labour' }, { month: 5, day: 3, id: 'constitution' },
      { month: 8, day: 15, id: 'assumption' }, { month: 11, day: 1, id: 'allSaints' },
      { month: 11, day: 11, id: 'independence' }, { month: 12, day: 24, id: 'christmasEve', since: 2025 },
      { month: 12, day: 25, id: 'christmas' }, { month: 12, day: 26, id: 'boxing' },
    ];
    const MOVABLE_HOLIDAYS = [
      { offset: 0, id: 'easter' }, { offset: 1, id: 'easterMonday' },
      { offset: 49, id: 'pentecost' }, { offset: 60, id: 'corpusChristi' },
    ];
    const pad = n => String(n).padStart(2, '0');
    const key = date => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;

    function holidaysOf(year) {
      const fixed = FIXED_HOLIDAYS.filter(h => !h.since || year >= h.since)
        .map(h => ({ date: new Date(year, h.month - 1, h.day), id: h.id }));
      const sunday = easter(year);
      const movable = MOVABLE_HOLIDAYS.map(h => ({ date: addDays(sunday, h.offset), id: h.id }));
      return [...fixed, ...movable].sort((x, y) => x.date - y.date);
    }
    // model: tygodnie miesiąca, dzień = { day, key, weekend, today, holiday }
    function monthWeeks(year, month, today) {
      const holidays = new Map(holidaysOf(year).map(h => [key(h.date), h.id]));
      const firstDay = (new Date(year, month - 1, 1).getDay() + 6) % 7;
      const days = new Date(year, month, 0).getDate();
      const cells = Array(firstDay).fill(null);
      for (let d = 1; d <= days; d++) {
        const date = new Date(year, month - 1, d);
        cells.push({
          day: d,
          key: key(date),
          weekend: (firstDay + d - 1) % 7 >= 5,
          today: key(date) === key(today),
          holiday: holidays.get(key(date)) ?? null,
        });
      }
      while (cells.length % 7 !== 0) cells.push(null);
      const weeks = [];
      for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
      return weeks;
    }
    function cell(text, ...classes) {
      const div = document.createElement('div');
      div.textContent = text;
      div.classList.add(...classes.filter(Boolean));
      return div;
    }
    function shift(year, month, delta) {
      const index = year * 12 + (month - 1) + delta;
      return { year: Math.floor(index / 12), month: (index % 12) + 1 };
    }
    function renderGrid(root, weeks, t, selected, notes) {
      const fragment = document.createDocumentFragment();
      t.days.forEach((name, k) => fragment.append(cell(name, 'day-name', k >= 5 ? 'weekend' : '')));
      for (const c of weeks.flat()) {
        if (c === null) {
          fragment.append(cell('', 'none'));
          continue;
        }
        const div = cell(c.day, 'day', c.today ? 'today' : c.weekend ? 'weekend' : '', c.holiday ? 'holiday' : '',
          c.key === selected ? 'selected' : '', notes[c.key] ? 'has-note' : '');
        div.dataset.day = c.day;
        div.dataset.key = c.key;
        div.title = [c.holiday && t.holidays[c.holiday], notes[c.key]].filter(Boolean).join(' - ');
        fragment.append(div);
      }
      root.replaceChildren(fragment);
    }
    function renderHolidays(list, year, month, t) {
      list.replaceChildren(...holidaysOf(year)
        .filter(h => h.date.getMonth() + 1 === month)
        .map(h => {
          const li = document.createElement('li');
          li.textContent = `${pad(h.date.getDate())}.${pad(month)} ${t.holidays[h.id]}`;
          return li;
        }));
    }
    const today = new Date();
    const state = { year: today.getFullYear(), month: today.getMonth() + 1, selected: null, notes: {}, lang: 'pl' };

    function setState(changes) {
      Object.assign(state, changes);
      render();
    }

    function render() {
      const t = TEXTS[state.lang];
      const weeks = monthWeeks(state.year, state.month, today);
      document.getElementById('title').textContent = `${t.months[state.month - 1]} ${state.year}`;
      renderGrid(document.getElementById('calendar'), weeks, t, state.selected, state.notes);
      renderHolidays(document.getElementById('holidays'), state.year, state.month, t);
      const workdays = weeks.flat().filter(c => c && !c.weekend && !c.holiday).length;
      document.getElementById('workdays').textContent = `${t.workdays}: ${workdays}`;
      document.getElementById('chosen').textContent = state.selected ? `${t.day} ${state.selected}:` : t.choose;
      document.getElementById('save').textContent = t.save;
      document.getElementById('note').value = state.notes[state.selected] ?? '';
    }

    document.getElementById('lang').addEventListener('change', e => setState({ lang: e.target.value }));
    document.getElementById('prev').addEventListener('click', () => setState(shift(state.year, state.month, -1)));
    document.getElementById('next').addEventListener('click', () => setState(shift(state.year, state.month, 1)));
    document.getElementById('calendar').addEventListener('click', e => {
      const div = e.target.closest('.day');
      if (div) setState({ selected: div.dataset.key });
    });
    document.getElementById('save').addEventListener('click', () => {
      if (!state.selected) return;
      const notes = { ...state.notes };
      const text = document.getElementById('note').value.trim();
      if (text) notes[state.selected] = text;
      else delete notes[state.selected];
      setState({ notes });
    });
    render();
  </script>
</body>
</html>

Ładowanie interaktywnego edytora... Jeśli się nie pojawi, włącz JavaScript.

Wszystkie lekcje kursu Kalendarz w JavaScript krok po kroku·Zaloguj się, aby zapisać postęp na koncie.