agama/doc/locale_api.md
2023-08-31 11:45:56 +01:00

9.9 KiB

Legacy-free Locale Service

Problem statement: (2023-03)

Agama currently has a separate Language service, although it's rather simplistic. It just allows to set the language of the installed system using Yast::Language.Set. And it's quite memory demanding for such an unimpressive task.

That service would be a nice candidate to be rewritten from scratch with no dependencies on YaST or Ruby. It's small enough and could give us a good overview on how much can we save.

Original Plan:

  1. take the systemd APIs as a sensible starting point.
  2. deviate only where we add value

The problem with the original plan is that the installer runs in one system (inst-sys, /) and operates on another (target, /mnt) and we cannot use the full systemd API. We may use systemd-firstboot instead but its API is much more limited.

Localization

This design includes localized labels in the API. In other contexts that would be a responsibility of the frontend, but here the backend has the information, provided by langtable.

(Languages, Territories and Timezones have localized names. Keyboards do not.)

(Possible alternative: still include localized labels, but in a supplemental method while the main method only provides the IDs (and English labels))

Proposal

A Proposal is what the installer proposes to the user as settings to be applied to the target system.

For example, when selecting the "German (Germany)" locale, the timezone will be proposed to "Europe/Berlin".

Design decision: put the proposal logic to the antecedent object, that is, the Locale object will know how to change the Timezone object, not the other way around (Timezone reacting to Locale value).

Overriding the User's Choice?

If setting the locale proposes the keyboard, what do we do if the user first changes the keyboard and _then_ the locale?

When Agama UI first shows up, it may show default choices like:

Locale: English (US), Keyboard: US

Then we change the locale to Czech, and the keyboard is adjusted automatically:

Locale: Czech, Keyboard: Czech

We tune the keyboard:

Locale: Czech, Keyboard: Czech (qwerty)

When we then change the locale, the keyboard could stay the same, as we have already touched it:

Locale: German, Keyboard: Czech (qwerty)

Simple Design: Always Repropose

We can easily afford throwing away the user's choice of keyboard layout and simply set what we consider a good default for a newly set locale, because:

  1. it is just one setting (as opposed to whole partitioning layout)
  2. the change will be visible in the UI, I assume

Detailed Design: Prioritize

But other cases may not be as simple, so here's a generic design (NOT YET USED):

All settings are wrapped in a Priority<T> generic type (an Enum in Rust), meaning, what is the source and importance of the setting:

  • Machine(data) means the system has proposed it
  • Human(data) means the user has made the choice

In D-Bus, it is represented by wrapping the data in a struct, with a leading byte* tagging the priority. For ease of recognition when watching bus traffic, special numbers are used:

  • 23 means Human, for the number of chromosome pairs
  • 42 means Machine, as the famous Answer was given by Deep Thought, a machine

In the following dump, we see that the locale was set by the user and the system has adjusted the keyboard.

node ...Agama1/Locale {
  interface ...Agama1.Locale {
    properties:
      readwrite (yas)   Locale = (23, ['cs_CZ.UTF-8', 'de_DE.UTF-8']);
      readwrite (y(ss)) X11Keyboard = (42, ('cz','qwerty));
  };
};

You may know a similar settings in libzypp where it has 4 levels.

*: maybe this is a crazy optimization? I am not too opposed to use strings for this on the bus.

Interfaces

Language and Keyboard

  • when setting the locale, adjust the proposed package selection and keyboard accordingly. And timezone.

The general design of the proposal layer is

  • declarative, using read-write properties
  • setting some properties will make changes in the proposal layer of other properties of other objects

I don't know: should the proposal be adjusted automatically as part of the property setter, or should it be explicit?

So here, setting Locale below will set also VConsoleKeyboard here and

  • Agama...Software...todo(...)
  • Agama...Timezone...todo(...)

For the first version of the API, let's keep things simple:

LocaleType is just one string, the value for the LANG variable, like "cs_CZ.UTF-8".

VConsoleKeyboardType is a string, for example "cz-qwerty" or "us".

systemd-firstboot only has an option for the console keymap, but we have a way to propagate it to X11, see bsc#1046436

We don't expose the X11 keyboard, instead letting systemd do it via the convert parameter.

(The other systemd keyboard settings are X11Model and X11Options, we don't have UI or data for that)

NOTE: langtable on the other hand only deals with the X11 keyboards, linking them to languages and territories.

# this is gdbus syntax BTW
node /org/opensuse/Agama1/Locale {
  interface org.opensuse.Agama1.Locale {
    methods:
      # In the same order as in SupportedLocales, pairs of
      # (english_labels, native_labels), where foo_labels
      # is a pair of (language, territory)
      LabelsForLocales(
        out a((ss)(ss)) id_english_native # [(("Spanish", "Spain"), ("Español", "España")), (('English', 'United States'), ('English', 'United States'))]
      )
      ListVConsoleKeyboards(
        out as    ids  # like ["cz", "cz-qwerty", "gb-intl", "us", "us-dvorak",…]
      )

      # ProposeKeyboard(); # not needed? adjusted automatically, same object
      # Sets Agama/TimeDate1's Timezone (but not LocalRTC, that's for Storage to say?)
      ProposeTimeDate(); # different object but same service
      ProposeSoftware(); # different service

      Commit();
    properties:

      # The locale service DOES NOT KNOW which locales are
      # available for the product currently selected for installation.
      # When the user chooses a product, SupportedLocales should be set.
      # It affects the output of LabelsForLocales
      # and the valid inputs for Locales.
      readwrite as      SupportedLocales = ["es_ES.UTF-8", "en_US.UTF-8"];

      # NOTE: "as" has different meaning to systemd,
      # we have a list of LANG settings, 1st gets passed to systemd,
      # others affect package selection
      readwrite as   Locales = ['cs_CZ.UTF-8', 'de_DE.UTF-8'];

      readwrite s    VConsoleKeyboard = 'cz-qwerty';
  };
};

Systemd

For reference, the systemd API for Locale(Language) and Keyboard is this:
$ gdbus introspect -y -d org.freedesktop.locale1 -o /org/freedesktop/locale1
node /org/freedesktop/locale1 {
  interface org.freedesktop.locale1 {
    methods:
      SetLocale(in  as locale,
                in  b interactive);
      SetVConsoleKeyboard(in  s keymap,
                          in  s keymap_toggle,
                          in  b convert,
                          in  b interactive);
      SetX11Keyboard(in  s layout,
                     in  s model,
                     in  s variant,
                     in  s options,
                     in  b convert,
                     in  b interactive);
…
$ busctl --system introspect org.freedesktop.locale1 /org/freedesktop/locale1
(all properties are read-only and emit PropertiesChanged)
.Locale                 property  as    1 "LANG=en_US.UTF-8"
.VConsoleKeymap         property  s     "cz-lat2-us"
.VConsoleKeymapToggle   property  s     ""
.X11Layout              property  s     "cz,us"
.X11Model               property  s     "pc105"
.X11Options             property  s     "terminate:ctrl_alt_bksp,grp:shift_togg…
.X11Variant             property  s     "qwerty,basic"

Timezone

node /org/opensuse/Agama/TimeDate1 {
  interface org.opensuse.Agama.TimeDate1 {
    methods:
      ListTimezones(
        in  s     display_locale # "de_DE.UTF-8"
        out a(ss) id_label_pairs # [('Europe/Prague', 'Europa/Prag')]
      )
      # success? do we need a specific return value other than some Error?
      Commit();
    properties:
      readwrite s Timezone = 'Europe/Prague';
      readwrite b LocalRTC = false;
  };
};

Systemd

For reference, the systemd API for Time and Timezone is this:

(I find gdbus verbose output better for methods and busctl terse output better for properties)

$ gdbus introspect -y -d org.freedesktop.timedate1 -o /org/freedesktop/timedate1                        
node /org/freedesktop/timedate1 { …
  interface org.freedesktop.timedate1 { …
    methods:
      SetTime(in  x usec_utc,
              in  b relative,
              in  b interactive);
      SetTimezone(in  s timezone,
                  in  b interactive);
      SetLocalRTC(in  b local_rtc,
                  in  b fix_system,
                  in  b interactive);
      SetNTP(in  b use_ntp,
             in  b interactive);
      ListTimezones(out as timezones);
…
$ busctl --system introspect org.freedesktop.timedate1 /org/freedesktop/timedate1
NAME                      TYPE      SIG  RESULT/VALUE     FLAGS
(properties are read only)
.CanNTP                   property  b    true             -
.LocalRTC                 property  b    false            emits-change
.NTP                      property  b    false            emits-change
.NTPSynchronized          property  b    false            -
.RTCTimeUSec              property  t    1681214874000000 -
.TimeUSec                 property  t    1681214874046139 -
.Timezone                 property  s    "Europe/Prague"  emits-change

"LocalRTC" means "is the local time zone used for the real time clock", so it's !hwclock_in_UTC