Lời mời lịch và tệp .ics hoạt động thế nào

Bên trong tệp .ics có gì, vì sao lời mời lệch một giờ, quy tắc lặp RRULE thực sự chạy ra sao, và chi tiết mã hóa làm hỏng chữ tiếng Việt.

Trong bài này

Mọi nút "Thêm vào lịch", lời mời hội nghị và xác nhận đặt chỗ cuối cùng đều là cùng một thứ: một tệp văn bản nhỏ theo định dạng iCalendar, quy định trong RFC 5545, thường có đuôi .ics. Nó đủ đơn giản để đọc bằng mắt và đủ chặt chẽ để những lỗi nhỏ sinh ra sự kiện sai giờ, lịch lặp bỏ qua tháng, hay tiêu đề đầy ký tự hỏng. Bài này mở một tệp thật, từng dòng một, rồi nói về ba chỗ hay sai nhất: múi giờ, quy tắc lặp và mã hóa văn bản. Các ví dụ được tạo bằng công cụ tạo tệp lịch ICS.

Bên trong tệp .ics

Một cuộc họp hằng tuần ở TP. Hồ Chí Minh, thứ Ba 09:00 đến 09:30, mười lần, nhắc trước 15 phút (mỗi dòng kết thúc bằng CRLF; dòng thụt vào là dòng nối tiếp):

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//ICS//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:Asia/Ho_Chi_Minh
BEGIN:STANDARD
DTSTART:20260101T070000
TZOFFSETFROM:+0700
TZOFFSETTO:+0700
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20261014T030000Z
DTSTART;TZID=Asia/Ho_Chi_Minh:20261020T090000
DTEND;TZID=Asia/Ho_Chi_Minh:20261020T093000
RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=10
SUMMARY:Họp giao ban tuần
DESCRIPTION:Chương trình: báo cáo tiến độ\, vướng mắc\, kế
  hoạch tuần sau\; mọi người chuẩn bị trước nhé.
LOCATION:Phòng 3\, Tầng 5
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Họp giao ban tuần
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR

Những phần quan trọng:

  • UID định danh sự kiện mãi mãi. Nhập lại tệp đã sửa với cùng UID thì lịch cập nhật sự kiện; đổi UID thì lịch thêm một bản trùng.
  • DTSTAMP là thời điểm tạo tệp, theo UTC.
  • DTSTART/DTEND có TZID là giờ đồng hồ treo tường trong một múi giờ có tên, và khối VTIMEZONE cho trình đọc biết độ lệch của múi giờ đó.
  • RRULE mô tả việc lặp; VALARM là lời nhắc, ở đây trước 15 phút.
  • METHOD:PUBLISH nghĩa là "đây là một sự kiện để thêm". Lời mời thật có theo dõi phản hồi dùng METHOD:REQUEST và được gửi qua máy chủ lịch; nhập một tệp không bao giờ gửi email cho ai.

Múi giờ: vì sao lời mời lệch một giờ

Một thời điểm có thể được ghi theo ba cách: có múi giờ (TZID=America/New_York:...T090000), theo UTC (...T140000Z), hoặc "trôi" (...T090000, không múi giờ). Khác biệt lộ ra khi bắt đầu giờ mùa hè. Lấy cuộc họp 09:00 hằng tuần ở New York vào ngày 3 và 10/3/2026 (Mỹ chỉnh đồng hồ sớm lên ngày 8/3), nhìn từ một đồng nghiệp ở Hà Nội:

TuầnGhi theo múi giờGhi theo UTC (14:00Z)
3/309:00 New York, 21:00 Hà Nội09:00 New York, 21:00 Hà Nội
10/309:00 New York, 20:00 Hà Nội10:00 New York, 21:00 Hà Nội

Có múi giờ, cuộc họp vẫn là 09:00 với người tổ chức, còn giờ Hà Nội dịch một tiếng, và đó là đúng: Việt Nam không có giờ mùa hè, New York thì có. Ghi theo UTC, cuộc họp giữ nguyên thời điểm tuyệt đối, nên người New York thấy cuộc họp 09:00 của mình thành 10:00 từ tháng 3. Đó là lỗi kinh điển "ai cũng đến sớm một tiếng". Giờ trôi còn tệ hơn với cuộc họp: 09:00 là 09:00 theo múi giờ lịch đang đặt, nên người ở các nước khác nhau thấy những thời điểm khác nhau.

Quy tắc dễ nhớ: cuộc họp có địa điểm thì dùng múi giờ, báo thức kiểu "uống thuốc lúc 8:00" thì có thể để trôi, còn UTC ổn cho sự kiện một lần không lặp.

Quy tắc lặp gây bất ngờ

RRULE mạnh và hiểu theo đúng từng chữ. Những điều hay làm người ta vấp:

  • Ngày 31 bỏ qua tháng. FREQ=MONTHLY;BYMONTHDAY=31 bắt đầu 31/1/2026 cho ra 31/1, 31/3, 31/5, 31/7. Chuẩn quy định ngày không hợp lệ bị bỏ qua, không bị dời sang ngày 30.
  • Ngày 29/2 bốn năm mới có một lần. Sự kiện hằng năm vào 29/2/2024 lần tới xuất hiện vào 2028.
  • "Cuối cùng" khác "thứ tư". Quy tắc hằng tháng từ thứ Năm 29/10/2026 theo kiểu "thứ Năm cuối tháng" (BYDAY=-1TH) cho ra 26/11 và 31/12; còn "thứ Năm thứ tư" sẽ cho 26/11 và 24/12.
  • UNTIL có quy tắc riêng. Nếu DTSTART có múi giờ, UNTIL phải theo UTC; với sự kiện cả ngày thì phải là ngày. UNTIL=20261231T165959Z là 23:59:59 ngày 31/12 ở TP. Hồ Chí Minh. Trộn lẫn các dạng là lý do phổ biến khiến Outlook bỏ qua điểm kết thúc.
  • COUNT đếm số lần, không đếm số tuần. FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=6 kết thúc sau hai tuần.

Danh sách các lần diễn ra trong công cụ cho thấy ngày thật trước khi bạn gửi đi.

Mã hóa: vì sao "Họp" thành "H�p"

Hai quy tắc tác động lẫn nhau. Giá trị văn bản phải thoát dấu phẩy, chấm phẩy và gạch chéo ngược, nên "Phòng 3, Tầng 5" được viết Phòng 3\, Tầng 5; quên điều này thì vài trình đọc coi dấu phẩy là dấu phân cách danh sách. Và dòng dài hơn 75 octet phải được gập: một CRLF rồi một khoảng trắng. Octet chứ không phải ký tự: dòng DESCRIPTION đầu tiên ở trên có 58 ký tự nhưng 75 byte, vì phần lớn chữ tiếng Việt chiếm 2 hoặc 3 byte trong UTF-8. Công cụ đếm theo ký tự sẽ viết dòng quá dài; công cụ cắt đúng ở byte thứ 75 có thể chẻ đôi một chữ 3 byte, và trình đọc hiện ký tự thay thế. Chỗ gập phải nằm giữa hai ký tự, như sau chữ "kế" ở trên, và dòng sau bắt đầu bằng khoảng trắng đánh dấu dòng nối tiếp (cộng với khoảng trắng gốc trước "hoạch").

Các lỗi thường gặp

  • Dùng một UID cho nhiều sự kiện khác nhau. Lần nhập sau ghi đè lần trước.
  • Sự kiện cả ngày kết thúc vào chính ngày cuối. DTEND của sự kiện cả ngày không tính ngày đó: sự kiện một ngày 20/10 kết thúc vào 21/10.
  • Gửi tệp và mong nhận phản hồi tham dự. Phản hồi chỉ đi qua máy chủ lịch; tệp .ics đính kèm chỉ là sự kiện để thêm.
  • Dùng UTC cho cuộc họp lặp lại qua đợt đổi giờ mùa hè.
  • Thiếu VTIMEZONE. Phần lớn ứng dụng lịch biết tên múi giờ IANA, nhưng chuẩn yêu cầu múi giờ phải được định nghĩa trong tệp, và vài ứng dụng sẽ coi là giờ trôi nếu thiếu.

Thử ngay

Tạo sự kiện trong công cụ tạo tệp lịch ICS, chọn "theo múi giờ", và xem các lần diễn ra tiếp theo trước khi tải về. Muốn biết 09:00 ở một thành phố là mấy giờ ở nơi khác, hãy dùng công cụ đổi múi giờ.

Công cụ được nhắc trong bài

Về trang blog