From: Adrian Vovk Date: Sun, 4 Feb 2024 17:27:01 +0000 (-0500) Subject: user-record: Add languages field X-Git-Tag: v256-rc1~873^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F31206%2Fhead;p=thirdparty%2Fsystemd.git user-record: Add languages field This field is like preferredLanguage, but takes a priority list of languages instead. If an app isn't translated into a user's primary language, it can fall back to one of the other languages in the list thus making the app more accessible to the user. For instance: in my experience, many Ukrainians are fluent in Russian, often significantly better than English (especially if they are of a generation that grew up during the USSR). Such a person might set this new variable to ["uk_UA.UTF-8", "ru_UA.UTF-8"] so that software that lacks Ukrainian translations will first try Russian translations before defaulting to English. Fixes #31290 --- diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md index 60f75bf39d1..aba45c39f42 100644 --- a/docs/USER_RECORD.md +++ b/docs/USER_RECORD.md @@ -310,11 +310,22 @@ string. The string should be a `tzdata` compatible location string, for example: `Europe/Berlin`. `preferredLanguage` → A string indicating the preferred language/locale for the -user. When logging in +user. It is combined with the `additionalLanguages` field to initialize the `$LANG` +and `$LANGUAGE` environment variables on login; see below for more details. This string +should be in a format compatible with the `$LANG` environment variable, for example: +`de_DE.UTF-8`. + +`additionalLanguages` → An array of strings indicating the preferred languages/locales +that should be used in the event that translations for the `preferredLanguage` are +missing, listed in order of descending priority. This allows multi-lingual users to +specify all the languages that they know, so software lacking translations in the user's +primary language can try another language that the user knows rather than falling back to +the default English. All entries in this field must be valid locale names, compatible with +the `$LANG` variable, for example: `de_DE.UTF-8`. When logging in [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) -will automatically initialize the `$LANG` environment variable from this -string. The string hence should be in a format compatible with this environment -variable, for example: `de_DE.UTF8`. +will prepend `preferredLanguage` (if set) to this list (if set), remove duplicates, +and then automatically initialize the `$LANGUAGE` variable with the resulting list. +It will also initialize `$LANG` variable with the first entry in the resulting list. `niceLevel` → An integer value in the range -20…19. When logging in [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) @@ -744,7 +755,7 @@ that may be used in this section are identical to the equally named ones in the `regular` section (i.e. at the top-level object). Specifically, these are: `iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, -`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`, +`preferredLanguage`, `additionalLanguages`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`, `notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`, `accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`, `mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`, diff --git a/man/homectl.xml b/man/homectl.xml index 0143b2ac4eb..0e79f82e0f2 100644 --- a/man/homectl.xml +++ b/man/homectl.xml @@ -366,10 +366,11 @@ LANG - Takes a specifier indicating the preferred language of the user. The - $LANG environment variable is initialized from this value on login, and thus a - value suitable for this environment variable is accepted here, for example - . + Takes a comma- or colon-separated list of languages preferred by the user, ordered + by descending priority. The $LANG and $LANGUAGE environment + variables are initialized from this value on login, and thus values suitible for these environment + variables are accepted here, for example . This option may + be used more than once, in which case the language lists are concatenated. diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl index 0a7bd0d13c8..a9a77d474bb 100644 --- a/shell-completion/bash/homectl +++ b/shell-completion/bash/homectl @@ -146,6 +146,9 @@ _homectl() { --cifs-user-name) comps=$(compgen -A user -- "$cur" ) ;; + --language) + comps=$(localectl list-locales 2>/dev/null) + ;; esac COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) return 0 diff --git a/src/home/homectl.c b/src/home/homectl.c index 222bf36e581..72a24e71562 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2435,7 +2435,7 @@ static int help(int argc, char *argv[], void *userdata) { " --shell=PATH Shell for account\n" " --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n" " --timezone=TIMEZONE Set a time-zone\n" - " --language=LOCALE Set preferred language\n" + " --language=LOCALE Set preferred languages\n" " --ssh-authorized-keys=KEYS\n" " Specify SSH public keys\n" " --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n" @@ -2547,6 +2547,7 @@ static int help(int argc, char *argv[], void *userdata) { } static int parse_argv(int argc, char *argv[]) { + _cleanup_strv_free_ char **arg_languages = NULL; enum { ARG_VERSION = 0x100, @@ -3121,26 +3122,46 @@ static int parse_argv(int argc, char *argv[]) { break; - case ARG_LANGUAGE: - if (isempty(optarg)) { - r = drop_from_identity("language"); + case ARG_LANGUAGE: { + const char *p = optarg; + + if (isempty(p)) { + r = drop_from_identity("preferredLanguage"); if (r < 0) return r; + r = drop_from_identity("additionalLanguages"); + if (r < 0) + return r; + + arg_languages = strv_free(arg_languages); break; } - if (!locale_is_valid(optarg)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg); + for (;;) { + _cleanup_free_ char *word = NULL; - if (locale_is_installed(optarg) <= 0) - log_warning("Locale '%s' is not installed, accepting anyway.", optarg); + r = extract_first_word(&p, &word, ",:", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse locale list: %m"); + if (r == 0) + break; - r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg); - if (r < 0) - return log_error_errno(r, "Failed to set preferredLanguage field: %m"); + if (!locale_is_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word); + + if (locale_is_installed(word) <= 0) + log_warning("Locale '%s' is not installed, accepting anyway.", word); + + r = strv_consume(&arg_languages, TAKE_PTR(word)); + if (r < 0) + return log_oom(); + + strv_uniq(arg_languages); + } break; + } case ARG_NOSUID: case ARG_NODEV: @@ -4021,6 +4042,25 @@ static int parse_argv(int argc, char *argv[]) { if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX) arg_and_resize = true; + if (!strv_isempty(arg_languages)) { + char **additional; + + r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]); + if (r < 0) + return log_error_errno(r, "Failed to update preferred language: %m"); + + additional = strv_skip(arg_languages, 1); + if (!strv_isempty(additional)) { + r = json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional); + if (r < 0) + return log_error_errno(r, "Failed to update additional language list: %m"); + } else { + r = drop_from_identity("additionalLanguages"); + if (r < 0) + return r; + } + } + return 1; } diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 99993517567..197729dd345 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -606,6 +606,7 @@ static int apply_user_record_settings( bool debug, uint64_t default_capability_bounding_set, uint64_t default_capability_ambient_set) { + _cleanup_strv_free_ char **langs = NULL; int r; assert(handle); @@ -651,18 +652,38 @@ static int apply_user_record_settings( } } - if (ur->preferred_language) { - if (locale_is_installed(ur->preferred_language) <= 0) - pam_debug_syslog(handle, debug, - "Preferred language specified in user record is not valid or not installed, not setting $LANG."); - else { - _cleanup_free_ char *joined = NULL; + r = user_record_languages(ur, &langs); + if (r < 0) + pam_syslog_errno(handle, LOG_ERR, r, + "Failed to acquire user's language preferences, ignoring: %m"); + else if (strv_isempty(langs)) + ; /* User has no preference set so we do nothing */ + else if (locale_is_installed(langs[0]) <= 0) + pam_debug_syslog(handle, debug, + "Preferred languages specified in user record are not installed locally, not setting $LANG or $LANGUAGE."); + else { + _cleanup_free_ char *lang = NULL; + + lang = strjoin("LANG=", langs[0]); + if (!lang) + return pam_log_oom(handle); - joined = strjoin("LANG=", ur->preferred_language); + r = pam_putenv_and_log(handle, lang, debug); + if (r != PAM_SUCCESS) + return r; + + if (strv_length(langs) > 1) { + _cleanup_free_ char *joined = NULL, *language = NULL; + + joined = strv_join(langs, ":"); if (!joined) return pam_log_oom(handle); - r = pam_putenv_and_log(handle, joined, debug); + language = strjoin("LANGUAGE=", joined); + if (!language) + return pam_log_oom(handle); + + r = pam_putenv_and_log(handle, language, debug); if (r != PAM_SUCCESS) return r; } diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c index 28fa7a86324..ac0c7a776ab 100644 --- a/src/shared/user-record-show.c +++ b/src/shared/user-record-show.c @@ -23,6 +23,7 @@ const char *user_record_state_color(const char *state) { } void user_record_show(UserRecord *hr, bool show_full_group_info) { + _cleanup_strv_free_ char **langs = NULL; const char *hd, *ip, *shell; UserStorage storage; usec_t t; @@ -237,15 +238,15 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { if (hr->time_zone) printf(" Time Zone: %s\n", hr->time_zone); - if (hr->preferred_language) - printf(" Language: %s\n", hr->preferred_language); - - if (!strv_isempty(hr->environment)) - STRV_FOREACH(i, hr->environment) { - printf(i == hr->environment ? - " Environment: %s\n" : - " %s\n", *i); - } + r = user_record_languages(hr, &langs); + if (r < 0) { + errno = -r; + printf(" Languages: (can't acquire: %m)\n"); + } else if (!strv_isempty(langs)) { + STRV_FOREACH(i, langs) + printf(i == langs ? " Languages: %s" : ", %s", *i); + printf("\n"); + } if (hr->locked >= 0) printf(" Locked: %s\n", yes_no(hr->locked)); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 47ef30418eb..966abc5c421 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -10,6 +10,7 @@ #include "glyph-util.h" #include "hexdecoct.h" #include "hostname-util.h" +#include "locale-util.h" #include "memory-util.h" #include "path-util.h" #include "pkcs11-util.h" @@ -146,6 +147,7 @@ static UserRecord* user_record_free(UserRecord *h) { strv_free(h->environment); free(h->time_zone); free(h->preferred_language); + strv_free(h->additional_languages); rlimit_free_all(h->rlimits); free(h->skeleton_directory); @@ -535,6 +537,62 @@ static int json_dispatch_environment(const char *name, JsonVariant *variant, Jso return strv_free_and_replace(*l, n); } +static int json_dispatch_locale(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + + if (!locale_is_valid(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid locale.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_locales(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **n = NULL; + char ***l = userdata; + const char *locale; + JsonVariant *e; + int r; + + if (json_variant_is_null(variant)) { + *l = strv_free(*l); + return 0; + } + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + if (!json_variant_is_string(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + locale = json_variant_string(e); + if (!locale_is_valid(locale)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of valid locales.", strna(name)); + + r = strv_extend(&n, locale); + if (r < 0) + return json_log_oom(variant, flags); + } + + return strv_free_and_replace(*l, n); +} + JSON_DISPATCH_ENUM_DEFINE(json_dispatch_user_disposition, UserDisposition, user_disposition_from_string); static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_user_storage, UserStorage, user_storage_from_string); @@ -1171,7 +1229,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, - { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 }, + { "additionalLanguages", JSON_VARIANT_ARRAY, json_dispatch_locales, offsetof(UserRecord, additional_languages), 0 }, { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, @@ -1506,7 +1565,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, - { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_locale, offsetof(UserRecord, preferred_language), 0 }, + { "additionalLanguages", JSON_VARIANT_ARRAY, json_dispatch_locales, offsetof(UserRecord, additional_languages), 0 }, { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, @@ -2034,6 +2094,27 @@ uint64_t user_record_capability_ambient_set(UserRecord *h) { return parse_caps_strv(h->capability_ambient_set) & user_record_capability_bounding_set(h); } +int user_record_languages(UserRecord *h, char ***ret) { + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(h); + assert(ret); + + if (h->preferred_language) { + l = strv_new(h->preferred_language); + if (!l) + return -ENOMEM; + } + + r = strv_extend_strv(&l, h->additional_languages, /* filter_duplicates= */ true); + if (r < 0) + return r; + + *ret = TAKE_PTR(l); + return 0; +} + uint64_t user_record_ratelimit_next_try(UserRecord *h) { assert(h); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index c8e402fd071..0afbc796d28 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -252,6 +252,7 @@ typedef struct UserRecord { char **environment; char *time_zone; char *preferred_language; + char **additional_languages; int nice_level; struct rlimit *rlimits[_RLIMIT_MAX]; @@ -415,6 +416,7 @@ AutoResizeMode user_record_auto_resize_mode(UserRecord *h); uint64_t user_record_rebalance_weight(UserRecord *h); uint64_t user_record_capability_bounding_set(UserRecord *h); uint64_t user_record_capability_ambient_set(UserRecord *h); +int user_record_languages(UserRecord *h, char ***ret); int user_record_build_image_path(UserStorage storage, const char *user_name_and_realm, char **ret);