Locales, Org timestamps, and format-time-string

2019/10/09

C: Emacs

Locales, Org timestamps, and format-time-string

Table of Contents

The problemSetting system-time-localeUse English on my PCSetting $LANG or $LC_TIME for the serverAdvising format-time-string

The problem

Org mode's timestamps include a day-of-week part (like this: <2019-10-09 Wed>). That day-of-week is localized.

When org-todo is rescheduling a repeating task and encounters a timestamp with a day-of-week in a different locale (eg. <2019-10-09 水 .+1d>), it updates the format but does not update the time described by the timestamp. When this happens, I have to run org-todo again, potentially creating a duplicate entry in the heading's logbook. This is normally not an issue, unless you edit the same Org file from two machines in different locales — exactly why I'm bitten by this quirk.

I use Emacs in Termux on my phone, which does not offer locales other than English (not sure if it's an English locale or POSIX). I use Emacs in server mode on my PC (set to the Japanese locale because I'm used to it), which for some reason does not honor system-time-locale and only uses the system-wide locale. Now I have two machines accessing the same Org file, stuck in two different locales.

There are several issues at display. It'd be nice if Termux, or Android, offers more locales. org-todo should update both the format and the time / date of the timestamp. Emacs should honor system-time-locale even when run in server mode.

All of the above is really complicated. For now, I just want a workaround to the problem.

Setting system-time-locale

Before I found out this only works when I'm not using the Emacs daemon, I used this in my init file:

1
2
(setq system-time-locale "en")
(setenv "LANG" "en")

which normally makes format-time-string use English day-of-week names, and makes sure any processes started from this Emacs also do that. This did not work: (format-time-string "%a") still returns "水" on a Wednesday on my PC.

Use English on my PC

Of course this workaround can always be done, but I don't want to change the rest of my system just to work around one issue in Emacs.

Setting $LANG or $LC_TIME for the server

Another workaround is to set $LANG so that Emacs server thinks the system is in the English locale.

As I use Emacs 26's new systemd unit, I have to copy the user unit to my home directory and use it to overwrite the unit in /usr/share.

$HOME/.config/systemd/user/emacs.service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=Emacs text editor
Documentation=info:emacs man:emacs(1) https://gnu.org/software/emacs/

[Service]
Type=simple
ExecStart=/usr/bin/emacs --fg-daemon
ExecStop=/usr/bin/emacsclient --eval "(kill-emacs)"
Environment=SSH_AUTH_SOCK=%t/keyring/ssh
# added by me
Environment=LC_TIME=en
Restart=on-failure

[Install]
WantedBy=default.target

This also does not work. Something with the English locale, or the way I set it, makes it so that Fcitx doesn't work in Emacs anymore. I have to do something else.

Advising format-time-string

Eventually, I decided the best way to do this is probably to advise format-time-string. While the advice will not affect calls to it from C code, since Org is all Emacs Lisp, this should make sure Org timestamps are all in English.

Luckily, calendar.el provides calendar-day-name that returns day name for any date, so we simply have to convert an existing (or current) time value to Calendar's format (month, day, year).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(defun kisaragi/english-dow (&optional time zone abbreviated)
  "Return ABBREVIATED name of the day of week at TIME and ZONE.

If TIME or ZONE is nil, use `current-time' or `current-time-zone'."
  (unless time (setq time (current-time)))
  (unless zone (setq zone (current-time-zone)))
  (calendar-day-name
   (pcase-let ((`(,_ ,_ ,_ ,d ,m ,y . ,_)
                (decode-time time zone)))
     (list m d y))
   abbreviated))

Then we need to parse the format, replace the %a and %A format strings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(defun kisaragi/advice-format-time-string (func format &optional time zone)
  "Pass FORMAT, TIME, and ZONE to FUNC.

Replace \"%A\" in FORMAT with English day of week of today,
\"%a\" with the abbreviated version."
  (let* ((format (replace-regexp-in-string "%a" (kisaragi/english-dow time zone t)
                                           format))
         (format (replace-regexp-in-string "%A" (kisaragi/english-dow time zone nil)
                                           format)))
    (funcall func format time zone)))

Then add the advice:

1
(advice-add 'format-time-string :around #'kisaragi/advice-format-time-string)

Now (format-time-string "%a") always returns the abbreviated English day-of-week, regardless of system locale, and I don’t have to occasionally run org-todo twice anymore. Hopefully.

File in my .emacs.d.