#!/usr/bin/env bash # System locale, via localectl. # # panama-locale list -> [{value, label, detail}] # panama-locale get -> the current LANG, e.g. en_US.UTF-8 # panama-locale set # # Locale codes are not names. "pt_BR.UTF-8" tells you what it means only if you # already know, which defeats the point of a picker, so codes are resolved # against the iso-codes database into "Portuguese (Brazil)" the way GNOME does. # The code stays visible as the row's detail, because it is what actually gets # written and someone choosing between two Spanish variants needs to see it. # # The join happens in a single jq pass. Doing it per locale meant 327 jq # invocations, which took long enough to be visible when opening the page. # # Setting the locale is a privileged operation: localectl goes through polkit, # which prompts. It also only takes effect for programs started afterwards, so # the caller is responsible for saying a sign-out is needed -- this script does # not pretend the running session changed. set -uo pipefail readonly ISO_LANG=/usr/share/iso-codes/json/iso_639-2.json readonly ISO_COUNTRY=/usr/share/iso-codes/json/iso_3166-1.json cmd_get() { localectl status 2>/dev/null \ | awk -F'LANG=' '/System Locale:/ { print $2; exit }' \ | tr -d '[:space:]' } cmd_list() { local locales locales="$(localectl list-locales 2>/dev/null)" || locales="" if [[ -z "$locales" ]]; then printf '[]\n' return 0 fi # Without iso-codes installed the codes are still perfectly usable; they # just do not get friendly names. That is a degraded list, not a failure. if [[ ! -r "$ISO_LANG" || ! -r "$ISO_COUNTRY" ]]; then jq -Rn --rawfile raw /dev/stdin \ '[$raw | split("\n")[] | select(length > 0) | {value: ., label: ., detail: ""}]' \ <<<"$locales" return 0 fi jq -Rn \ --slurpfile languages "$ISO_LANG" \ --slurpfile countries "$ISO_COUNTRY" \ --rawfile raw /dev/stdin ' # alpha_2 -> name, for both databases. Languages without a two-letter # code cannot appear in a locale name, so they are simply absent. ($languages[0]["639-2"] | map(select(.alpha_2)) | INDEX(.alpha_2) | map_values(.name)) as $lang | ($countries[0]["3166-1"] | INDEX(.alpha_2) | map_values(.name)) as $country | [ $raw | split("\n")[] | select(length > 0) | . as $value # en_US.UTF-8 -> ["en", "US"]; the codeset and any @modifier are # not part of the human name. | ($value | split(".")[0] | split("@")[0] | split("_")) as $parts | ($lang[$parts[0]] // $parts[0]) as $language | (if ($parts | length) > 1 then $country[$parts[1]] else null end) as $region | { value: $value, label: (if $region then "\($language) (\($region))" else $language end), detail: $value } ] | sort_by(.label) ' <<<"$locales" } cmd_set() { local locale="${1:-}" # Constrained rather than passed through: this reaches a privileged # command, and the set of legal locale names is narrow and well known. [[ "$locale" =~ ^[a-zA-Z0-9_@.-]+$ ]] || { printf 'panama-locale: refusing a locale name with unexpected characters\n' >&2 return 2 } localectl list-locales 2>/dev/null | grep -qxF "$locale" || { printf 'panama-locale: %s is not an installed locale\n' "$locale" >&2 return 2 } localectl set-locale "LANG=$locale" } case "${1:-list}" in list) cmd_list ;; get) cmd_get ;; set) shift; cmd_set "${1:-}" ;; *) printf 'usage: panama-locale [list|get|set ]\n' >&2; exit 2 ;; esac