l10n: implement status language builtin

Based on the discussion in
https://github.com/fish-shell/fish-shell/pull/11967

Introduce a `status language` builtin, which has subcommands for
controlling and inspecting fish's message localization status.

The motivation for this is that using only the established environment
variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, and `LANG` can cause
problems when fish interprets them differently from GNU gettext.
In addition, these are not well-suited for users who want to override
their normal localization settings only for fish, since fish would
propagate the values of these variables to its child processes.

Configuration via these variables still works as before, but now there
is the `status language set` command, which allows overriding the
localization configuration.
If `status language set` is used, the language precedence list will be
taken from its remaining arguments.
Warnings will be shown for invalid arguments.
Once this command was used, the localization related environment
variables are ignored.
To go back to taking the configuration from the environment variables
after `status language set` was executed, users can run `status language
unset`.

Running `status language` without arguments shows information about the
current message localization status, allowing users to better understand
how their settings are interpreted by fish.

The `status language list-available` command shows which languages are
available to choose from, which is used for completions.

This commit eliminates dependencies from the `gettext_impl` module to
code in fish's main crate, allowing for extraction of this module into
its own crate in a future commit.

Closes #12106
This commit is contained in:
Daniel Rainer
2025-11-24 00:22:13 +01:00
committed by Johannes Altmanninger
parent c0b95a0ee1
commit aa8f5fc77e
18 changed files with 776 additions and 112 deletions

View File

@@ -20,7 +20,6 @@ Color and key binding variables are no longer set in universal scope
2. universal variables as a source of truth are easy to misunderstand,
compared to configuration files like ``config.fish``.
Deprecations and removed features
---------------------------------
- Erasing a color variable (e.g. by running ``set -e fish_color_command``)
@@ -30,6 +29,10 @@ Deprecations and removed features
- ``fish_config theme choose`` now clears only color variables that were set by earlier invocations of a ``fish_config theme choose`` command
(which includes fish's default theme).
Scripting improvements
----------------------
- New :ref:`status language <status-language>` commands allow showing and modifying language settings for fish messages without having to modify environment variables.
Interactive improvements
------------------------
- When typing immediately after starting fish, the first prompt is now rendered correctly.

View File

@@ -23,6 +23,8 @@ If other programs launched via fish should respect these locale variables they h
For :envvar:`LANGUAGE` you can use a list, or use colons to separate multiple languages.
If the :ref:`status language set <status-language>` command was used, its arguments specify the language precedence, and the environment variables are ignored.
Options
-------

View File

@@ -33,6 +33,7 @@ Synopsis
status list-files [PATH ...]
status terminal
status test-terminal-feature FEATURE
status language [list-available|set [LANGUAGE ...]|unset]
Description
-----------
@@ -146,6 +147,32 @@ The following operations (subcommands) are available:
Currently the only available *FEATURE* is :ref:`scroll-content-up <term-compat-indn>`.
An error will be printed when passed an unrecognized feature.
.. _status-language:
**language**
Show or modify message localization settings.
When invoked without arguments, the current language settings are shown.
Available subcommands:
**list-available**
prints the language names for which fish has translations.
These names can be used with the **set** subcommand.
**set**
sets the language precedence for fish's messages.
Overrides language settings configured via :ref:`environment variables <variables-locale>`, but only applies to fish itself, not to any child processes.
Takes a list of language names from the set shown by the **list-available** subcommand.
For some languages, fish's translation catalogs are incomplete, meaning not all messages can be shown in these languages.
Therefore, we allow specifying a list here, with translations taken from the first specified language which has a translation available for a message.
For example, after running ``status language set pt_BR fr``, all messages which have a translation into Brazilian Portuguese will be shown in that language.
The remaining messages will be shown in French, if a French translation is available.
If none of the specified languages have a translation available for a message, the message will be shown in English.
**unset**
undoes the effects of the **set** subcommand.
Language settings will be taken from environment variables again.
Notes
-----

View File

@@ -1549,7 +1549,8 @@ You can change the settings of fish by changing the values of certain variables.
.. describe:: Locale Variables
Locale variables such as :envvar:`LANG`, :envvar:`LC_ALL`, :envvar:`LC_MESSAGES`, :envvar:`LC_NUMERIC` and :envvar:`LC_TIME` set the language option for the shell and subprograms. See the section :ref:`Locale variables <variables-locale>` for more information.
Locale variables such as :envvar:`LANG`, :envvar:`LC_ALL`, :envvar:`LC_MESSAGES`, :envvar:`LC_NUMERIC` and :envvar:`LC_TIME` set the language option for the shell and subprograms.
See the section :ref:`Locale variables <variables-locale>` and :ref:`status language <status-language>` for more information.
.. describe:: Color variables

View File

@@ -83,6 +83,10 @@ msgstr "$status ist kein gültiger Befehl. Siehe `help %s`"
msgid "%s"
msgstr ""
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr "%s %s: %s: ungültiger Unterbefehl\n"
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -881,6 +885,10 @@ msgstr "Abbruch"
msgid "Abort (Alias for SIGABRT)"
msgstr ""
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "Adressbereichsfehler"
@@ -1229,6 +1237,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "Funktionen auflisten oder entfernen"
@@ -1279,6 +1290,9 @@ msgstr "Nie"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "Kein TTY für interaktive Shell (tcgetpgrp schlug fehl)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1775,6 +1789,9 @@ msgstr "Befehlsersetzung sind in Befehlsposition nicht erlaubt. Probier stattdes
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr "Vervollständigung hat die maximale Rekursionstiefe erreicht, möglicher Kreis?"
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr "Verweis auf ein Verzeichnis"
@@ -1800,10 +1817,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "aus der Quelldatei %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "in der Befehlsersetzung\n"
@@ -3386,6 +3410,9 @@ msgstr ""
msgid "Show modification time"
msgstr "Änderungsdatum anzeigen"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -81,6 +81,10 @@ msgstr ""
msgid "%s"
msgstr ""
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr ""
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -879,6 +883,10 @@ msgstr "Abort"
msgid "Abort (Alias for SIGABRT)"
msgstr "Abort (Alias for SIGABRT)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "Address boundary error"
@@ -1227,6 +1235,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "List or remove functions"
@@ -1277,6 +1288,9 @@ msgstr "Never"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "No TTY for interactive shell (tcgetpgrp failed)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1773,6 +1787,9 @@ msgstr ""
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr ""
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr ""
@@ -1798,10 +1815,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "from sourcing file %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "in command substitution\n"
@@ -3384,6 +3408,9 @@ msgstr ""
msgid "Show modification time"
msgstr "Show modification time"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -212,6 +212,10 @@ msgstr "$status nest pas une commande valide. Voir « help %s »"
msgid "%s"
msgstr "%s"
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr "%s %s : %s : sous-commande invalide\n"
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -1010,6 +1014,10 @@ msgstr "Abandon"
msgid "Abort (Alias for SIGABRT)"
msgstr "Abandon (Alias pour SIGABRT)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "Erreur de frontière dadresse"
@@ -1358,6 +1366,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "Lister ou supprimer des fonctions"
@@ -1408,6 +1419,9 @@ msgstr "Jamais"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "Pas de TTY pour shell interactif (tcgetpgrp a échoué)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1904,6 +1918,9 @@ msgstr ""
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr ""
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr ""
@@ -1929,10 +1946,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "du fichier source %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "dans la substitution de commande\n"
@@ -3515,6 +3539,9 @@ msgstr ""
msgid "Show modification time"
msgstr "Afficher lhorodatage de modification"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -77,6 +77,10 @@ msgstr ""
msgid "%s"
msgstr ""
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr ""
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -875,6 +879,10 @@ msgstr "Przerwij"
msgid "Abort (Alias for SIGABRT)"
msgstr ""
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr ""
@@ -1223,6 +1231,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "Wypisz lub usuń funkcje"
@@ -1273,6 +1284,9 @@ msgstr "Nigdy"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr ""
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1769,6 +1783,9 @@ msgstr ""
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr ""
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr ""
@@ -1794,10 +1811,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr ""
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "w zastępstwie polecenia \n"
@@ -3380,6 +3404,9 @@ msgstr ""
msgid "Show modification time"
msgstr ""
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -82,6 +82,10 @@ msgstr ""
msgid "%s"
msgstr ""
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr ""
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -880,6 +884,10 @@ msgstr "Abortado"
msgid "Abort (Alias for SIGABRT)"
msgstr "Abortado (Outro nome para SIGABRT)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "Erro de fronteira de endereço (Falha de segmentação)"
@@ -1228,6 +1236,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "Lista ou remove funções"
@@ -1278,6 +1289,9 @@ msgstr "Nunca"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "Não há TTY para shell interativo (falha em tcgetpgrp)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1774,6 +1788,9 @@ msgstr ""
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr ""
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr ""
@@ -1799,10 +1816,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "do arquivo %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "na substituição de comando\n"
@@ -3385,6 +3409,9 @@ msgstr ""
msgid "Show modification time"
msgstr "Mostra horário de modificação"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -78,6 +78,10 @@ msgstr ""
msgid "%s"
msgstr ""
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr ""
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr ""
@@ -876,6 +880,10 @@ msgstr "Avbrott"
msgid "Abort (Alias for SIGABRT)"
msgstr "Avbrott (Alias för SIGABRT)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "Minnesadress korsar segmentgräns"
@@ -1224,6 +1232,9 @@ msgstr ""
msgid "Jobs getting started or continued"
msgstr ""
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "Visa eller ta bort funktioner"
@@ -1274,6 +1285,9 @@ msgstr "Aldrig"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr ""
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr ""
@@ -1770,6 +1784,9 @@ msgstr ""
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr ""
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr ""
@@ -1795,10 +1812,17 @@ msgid ""
"`help %s` will show an online version\n"
msgstr ""
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr ""
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "i kommandosubstitution\n"
@@ -3381,6 +3405,9 @@ msgstr ""
msgid "Show modification time"
msgstr "Visa ändringstid"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr ""

View File

@@ -104,6 +104,10 @@ msgstr "$status 不是有效的命令。参见 `help %s`"
msgid "%s"
msgstr "%s"
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr "%s %s: %s: 无效的子命令\n"
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr "%s %s: 名为 %s 且适用于命令 %s 的缩写已存在,无法重命名 %s\n"
@@ -902,6 +906,10 @@ msgstr "中止"
msgid "Abort (Alias for SIGABRT)"
msgstr "中止 (SIGABRT 的别名)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "地址边界错误"
@@ -1253,6 +1261,9 @@ msgstr "正在改变状况的作业"
msgid "Jobs getting started or continued"
msgstr "正在启动或继续的作业"
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "列出或移除函数"
@@ -1303,6 +1314,9 @@ msgstr "从不"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "交互 shell 没有 TTY (tcgetpgrp 失败)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr "未找到通配符 '%s' 的匹配项。参见 `help %s`。"
@@ -1799,6 +1813,9 @@ msgstr "在命令位置不允许替换命令。试使用 var=(你的命令) $var
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr "补全达到了最大的递归深度,可能存在循环?"
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr "目录符号链接"
@@ -1827,10 +1844,17 @@ msgstr ""
"文档可能未安装。\n"
"`help %s` 将显示在线版本\n"
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "从源文件 %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "在命令替换中\n"
@@ -3413,6 +3437,9 @@ msgstr "显示与光标下记号相关的 man 页面条目或函数描述"
msgid "Show modification time"
msgstr "显示修改时间"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr "显示自修改时间以来的秒数"

View File

@@ -77,6 +77,10 @@ msgstr "$status 不是有效的命令。參見「help %s」"
msgid "%s"
msgstr "%s"
#, c-format
msgid "%s %s: %s: invalid subcommand\n"
msgstr "%s %s%s無效的子命令\n"
#, c-format
msgid "%s %s: Abbreviation %s already exists for commands %s, cannot rename %s\n"
msgstr "%s %s名為 %s 且適用於命令 %s 的縮寫已存在,無法重新命名 %s\n"
@@ -876,6 +880,10 @@ msgstr "中止"
msgid "Abort (Alias for SIGABRT)"
msgstr "中止SIGABRT 的別名)"
#, c-format
msgid "Active languages (%s):"
msgstr ""
msgid "Address boundary error"
msgstr "位址邊界錯誤"
@@ -1227,6 +1235,9 @@ msgstr "作業變更狀態"
msgid "Jobs getting started or continued"
msgstr "作業開始或繼續"
msgid "Language specifiers appear repeatedly:"
msgstr ""
msgid "List or remove functions"
msgstr "列出或移除函式"
@@ -1277,6 +1288,9 @@ msgstr "永不"
msgid "No TTY for interactive shell (tcgetpgrp failed)"
msgstr "沒有互動式 shell 所需的 TTYtcgetpgrp 失敗了)"
msgid "No catalogs available for language specifiers:"
msgstr ""
#, c-format
msgid "No matches for wildcard '%s'. See `help %s`."
msgstr "wildcard「%s」無匹配項目。參見「help %s」。"
@@ -1774,6 +1788,9 @@ msgstr "不允許在命令處進行命令替換。試試 var=(命令) $var ..."
msgid "completion reached maximum recursion depth, possible cycle?"
msgstr "補全達到最大遞迴深度,可能是循環?"
msgid "default"
msgstr ""
msgid "dir symlink"
msgstr "目錄象徵式連結"
@@ -1802,10 +1819,17 @@ msgstr ""
"可能未安裝文件。\n"
"「help %s」將顯示線上版本\n"
msgid "from command `status language set`"
msgstr ""
#, c-format
msgid "from sourcing file %s\n"
msgstr "在載入的檔案 %s\n"
#, c-format
msgid "from variable %s"
msgstr ""
msgid "in command substitution\n"
msgstr "在命令替換\n"
@@ -3388,6 +3412,9 @@ msgstr "顯示和游標處的詞元有關的手冊頁或函式說明"
msgid "Show modification time"
msgstr "顯示修改時間"
msgid "Show or change fish's language settings"
msgstr ""
msgid "Show seconds since the modification time"
msgstr "顯示自修改時間以來的秒數"

View File

@@ -23,6 +23,7 @@ set -l __fish_status_all_commands \
is-login \
is-no-job-control \
job-control \
language \
line-number \
list-files \
print-stack-trace \
@@ -80,3 +81,19 @@ complete -f -c status -n "__fish_seen_subcommand_from job-control" -a none -d "S
complete -f -c status -n "__fish_seen_subcommand_from get-file" -a '(status list-files 2>/dev/null)'
complete -f -c status -n "__fish_seen_subcommand_from list-files" -a '(status list-files 2>/dev/null)'
# Tests equality between the command line with the first item removed
# and the function's arguments.
function __fish_status_is_exact_subcommand
set -l line (commandline -pxc)[2..]
test "$line" = "$argv"
end
# Tests if the command line with the first item removed starts with the provided arguments.
function __fish_status_is_subcommand_prefix
set -l prefix (string escape --style=regex -- (string join -- ' ' $argv))
set -l line (string join -- ' ' (commandline -pxc)[2..])
string match -rq -- "^$prefix" $line
end
complete -f -c status -n "not __fish_seen_subcommand_from $__fish_status_all_commands" -a language -d "Show or change fish's language settings"
complete -f -c status -n "__fish_status_is_exact_subcommand language" -a "(echo list-available\tShow languages usable with \'status language set\'\nset\tSet the language\(s\) used for fish\'s messages\nunset\tUndo effects of \'status language set\'\n)"
complete -f -c status -n "__fish_status_is_subcommand_prefix language set" -a "(status language list-available)"

View File

@@ -67,6 +67,9 @@
pub BUILTIN_ERR_INVALID_SUBCMD
"%s: %s: invalid subcommand\n"
pub BUILTIN_ERR_INVALID_SUBSUBCMD
"%s %s: %s: invalid subcommand\n"
/// Error messages for unexpected args.
pub BUILTIN_ERR_ARG_COUNT0
"%s: missing argument\n"

View File

@@ -75,6 +75,7 @@ fn to_wstr(self) -> &'static wstr {
(STATUS_IS_NO_JOB_CTRL, "is-no-job-control"),
(STATUS_LINE_NUMBER, "line-number", "current-line-number"),
(STATUS_LIST_FILES, "list-files"),
(STATUS_LANGUAGE, "language"),
(STATUS_SET_JOB_CONTROL, "job-control"),
(STATUS_STACK_TRACE, "stack-trace", "print-stack-trace"),
(STATUS_TERMINAL, "terminal"),
@@ -303,7 +304,7 @@ fn parse_cmd_opts(
*optind = w.wopt_index;
return Ok(SUCCESS);
Ok(SUCCESS)
}
struct EmptyEmbed;
@@ -489,6 +490,47 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
streams.out.append(src);
return Ok(SUCCESS);
}
STATUS_LANGUAGE => {
cfg_if! {
if #[cfg(not(feature = "localize-messages"))] {
streams.err.append(L!("fish was built with the `localize-messages` feature disabled. The `status language` command is unavailable.\n"));
return Err(STATUS_CMD_ERROR);
} else {
if args.is_empty() {
streams.out.append(crate::wutil::gettext::status_language());
return Ok(SUCCESS);
}
match args[0].to_string().as_str() {
"list-available" => {
streams.out.append(crate::wutil::gettext::list_available_languages());
return Ok(SUCCESS);
},
"set" => {
let langs = args[1..]
.iter()
.map(|lang| lang.to_string())
.collect::<Vec<_>>();
let lints = crate::wutil::gettext::update_from_status_language_builtin(&langs);
let formatted_lints = lints.display_all();
if !formatted_lints.is_empty() {
streams.err.append(&formatted_lints);
}
return Ok(SUCCESS);
}
"unset" => {
crate::wutil::gettext::unset_from_status_language_builtin(parser.vars());
return Ok(SUCCESS);
}
invalid => {
streams
.err
.append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBSUBCMD, cmd, subcmd.to_wstr(), invalid));
return Err(STATUS_INVALID_ARGS);
}
}
}
}
}
STATUS_LIST_FILES => {
use crate::util::wcsfilecmp_glob;
let mut paths = vec![];
@@ -733,6 +775,7 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
| STATUS_TEST_FEATURE
| STATUS_GET_FILE
| STATUS_LIST_FILES
| STATUS_LANGUAGE
| STATUS_TEST_TERMINAL_FEATURE => {
unreachable!("")
}

View File

@@ -540,7 +540,7 @@ fn init_locale(vars: &EnvStack) {
invalidate_numeric_locale();
#[cfg(feature = "localize-messages")]
crate::wutil::gettext::update_locale_from_env(vars);
crate::wutil::gettext::update_from_env(vars);
}
pub fn use_posix_spawn() -> bool {

View File

@@ -1,19 +1,195 @@
use std::sync::Mutex;
#[cfg(feature = "localize-messages")]
use crate::env::EnvStack;
use crate::env::{EnvStack, Environment};
use crate::wchar::prelude::*;
use once_cell::sync::Lazy;
#[cfg(feature = "localize-messages")]
mod gettext_impl {
use crate::env::{EnvStack, Environment};
use fish_gettext_maps::CATALOGS;
use once_cell::sync::Lazy;
use std::{collections::HashSet, sync::Mutex};
type Catalog = &'static phf::Map<&'static str, &'static str>;
pub struct SetLanguageLints<'a> {
pub duplicates: Vec<&'a str>,
pub non_existing: Vec<&'a str>,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LanguagePrecedenceOrigin {
Default,
LocaleVariable(LocaleVariable),
LanguageEnvVar,
StatusLanguage,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LocaleVariable {
#[allow(clippy::upper_case_acronyms)]
LANG,
LC_MESSAGES,
LC_ALL,
}
impl LocaleVariable {
fn as_language_precedence_origin(&self) -> LanguagePrecedenceOrigin {
LanguagePrecedenceOrigin::LocaleVariable(*self)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::LANG => "LANG",
Self::LC_MESSAGES => "LC_MESSAGES",
Self::LC_ALL => "LC_ALL",
}
}
}
impl std::fmt::Display for LocaleVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
struct InternalLocalizationState {
precedence_origin: LanguagePrecedenceOrigin,
language_precedence: Vec<(String, Catalog)>,
}
pub struct PublicLocalizationState {
pub precedence_origin: LanguagePrecedenceOrigin,
pub language_precedence: Vec<String>,
}
/// Stores the current localization status.
/// `is_active` indicates whether localization is currently active, and the reason if it is
/// not.
/// The `origin` indicates where the values in `language_precedence` were taken from.
/// `language_precedence` stores the catalogs in the order they should be used.
///
/// This struct should be updated when the relevant variables change or `status language` is used
/// to modify the localization state.
static LOCALIZATION_STATE: Lazy<Mutex<InternalLocalizationState>> =
Lazy::new(|| Mutex::new(InternalLocalizationState::new()));
impl InternalLocalizationState {
fn new() -> Self {
Self {
precedence_origin: LanguagePrecedenceOrigin::Default,
language_precedence: vec![],
}
}
fn to_public(&self) -> PublicLocalizationState {
PublicLocalizationState {
precedence_origin: self.precedence_origin,
language_precedence: self
.language_precedence
.iter()
.map(|(lang, _)| lang.to_owned())
.collect(),
}
}
fn update_from_env(
&mut self,
message_locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
// Do not override values set via `status language`.
if self.precedence_origin == LanguagePrecedenceOrigin::StatusLanguage {
return;
}
if let Some((precedence_origin, locale)) = &message_locale {
// Regular locale names start with lowercase letters (`ll_CC`, followed by some suffix).
// The C or POSIX locale is special, and often used to disable localization.
// Their names are upper-case, but variants with suffixes (`C.UTF-8`) exist.
// To ensure that such variants are accounted for, we match on prefixes of the
// locale name.
// https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html#tag_07_02
fn is_c_locale(locale: &str) -> bool {
locale.starts_with('C') || locale.starts_with("POSIX")
}
if is_c_locale(locale) {
self.precedence_origin =
LanguagePrecedenceOrigin::LocaleVariable(*precedence_origin);
self.language_precedence.clear();
return;
}
}
let (precedence_origin, language_list) = if let Some(list) = language_var {
(LanguagePrecedenceOrigin::LanguageEnvVar, list)
} else if let Some((precedence_origin, locale)) = message_locale {
let mut normalized_name = String::new();
// Strip off encoding and modifier. (We always expect UTF-8 and don't support modifiers.)
for c in locale.chars() {
if c.is_alphabetic() || c == '_' {
normalized_name.push(c);
} else {
break;
}
}
// At this point, the normalized_name should have the shape `ll` or `ll_CC`.
(
precedence_origin.as_language_precedence_origin(),
vec![normalized_name],
)
} else {
(LanguagePrecedenceOrigin::Default, vec![])
};
let mut seen_languages = HashSet::new();
self.language_precedence = language_list
.into_iter()
.flat_map(|lang| find_existing_catalogs(&lang))
.filter(|(lang, _)| seen_languages.insert(lang.to_owned()))
.collect();
self.precedence_origin = precedence_origin;
}
fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
&mut self,
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut seen = HashSet::new();
let mut duplicates = vec![];
for lang in langs {
let lang = lang.as_ref();
if !seen.insert(lang) {
duplicates.push(lang)
}
}
let mut existing_langs = vec![];
let mut non_existing = vec![];
for lang in langs {
let lang = lang.as_ref();
if let Some(catalog) = CATALOGS.get(lang) {
existing_langs.push((lang.to_owned(), *catalog));
} else {
non_existing.push(lang);
}
}
let mut seen = HashSet::new();
let unique_langs = existing_langs
.into_iter()
.filter(|(lang, _)| seen.insert(lang.to_owned()))
.collect();
self.language_precedence = unique_langs;
self.precedence_origin = LanguagePrecedenceOrigin::StatusLanguage;
SetLanguageLints {
duplicates,
non_existing,
}
}
}
/// Tries to find catalogs for `language`.
/// `language` must be an ISO 639 language code, optionally followed by an underscore and an ISO
/// 3166 country/territory code.
@@ -52,125 +228,220 @@ fn find_existing_catalogs(language: &str) -> Vec<(String, Catalog)> {
}
}
/// The precedence list of user-preferred languages, obtained from the relevant environment
/// variables.
/// This should be updated when the relevant variables change.
static LANGUAGE_PRECEDENCE: Lazy<Mutex<Vec<Catalog>>> = Lazy::new(|| Mutex::new(Vec::new()));
/// Four environment variables can be used to select languages.
/// A detailed description is available at
/// <https://www.gnu.org/software/gettext/manual/html_node/Setting-the-POSIX-Locale.html>
/// Our does not replicate the behavior exactly.
/// See the following description.
///
/// There are three variables which can be used for setting the locale for messages:
/// 1. `LC_ALL`
/// 2. `LC_MESSAGES`
/// 3. `LANG`
///
/// The value of the first one set to a non-zero value will be considered.
/// If it is set to the `C` locale (we consider any value starting with `C` as the `C` locale),
/// localization will be disabled.
/// Otherwise, the variable `LANGUAGE` is checked. If it is non-empty, it is considered a
/// colon-separated list of languages. Languages are listed with descending priority, meaning
/// we will localize each message into the first language with a localization available.
/// Each language is specified by a 2 or 3 letter ISO 639 language code, optionally followed by
/// an underscore and an ISO 3166 country/territory code. If the second part is omitted, some
/// variant of the language will be used if localizations exist for one. We make no guarantees
/// about which variant that will be.
/// In addition to the colon-separated format, using a list with one language per element is
/// also supported.
///
/// Returns the (possibly empty) preference list of languages.
fn get_language_preferences_from_env(vars: &EnvStack) -> Vec<String> {
use crate::wchar::L;
fn normalize_locale_name(locale: &str) -> String {
// Strips off the encoding and modifier parts.
let mut normalized_name = String::new();
// Strip off encoding and modifier. (We always expect UTF-8 and don't support modifiers.)
for c in locale.chars() {
if c.is_alphabetic() || c == '_' {
normalized_name.push(c);
} else {
break;
}
}
// At this point, the normalized_name should have the shape `ll` or `ll_CC`.
normalized_name
}
fn check_language_var(vars: &EnvStack) -> Option<Vec<String>> {
let langs = vars.get(L!("LANGUAGE"))?;
let langs = langs.as_list();
let filtered_langs: Vec<String> = langs
.iter()
.filter(|lang| !lang.is_empty())
.map(|lang| normalize_locale_name(&lang.to_string()))
.collect();
if filtered_langs.is_empty() {
return None;
}
Some(filtered_langs)
}
// Locale value is determined by the first of these three variables set to a non-zero
// value.
if let Some(locale) = vars
.get(L!("LC_ALL"))
.or_else(|| vars.get(L!("LC_MESSAGES")).or_else(|| vars.get(L!("LANG"))))
{
let locale = locale.as_string().to_string();
if locale.starts_with('C') {
// Do not localize in C locale.
return vec![];
}
// `LANGUAGE` has higher precedence than the locale value.
if let Some(precedence_list) = check_language_var(vars) {
return precedence_list;
}
// Use the locale value if `LANGUAGE` is not set.
vec![normalize_locale_name(&locale)]
} else if let Some(precedence_list) = check_language_var(vars) {
// Use the `LANGUAGE` value if locale is not set.
return precedence_list;
} else {
// None of the relevant variables are set, so we will not localize.
vec![]
}
pub(super) fn update_from_env(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_env(locale, language_var);
}
/// Implementation of the function with the same name in super.
pub(super) fn update_locale_from_env(vars: &EnvStack) {
let mut seen_languages = HashSet::new();
let mut language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
*language_precedence = get_language_preferences_from_env(vars)
.into_iter()
.flat_map(|lang| find_existing_catalogs(&lang))
.filter(|(lang, _)| seen_languages.insert(lang.to_owned()))
.map(|(_, catalog)| catalog)
.collect();
pub(super) fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_status_language_builtin(langs)
}
pub(super) fn unset_from_status_language_builtin(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.precedence_origin = LanguagePrecedenceOrigin::Default;
localization_state.update_from_env(locale, language_var);
}
pub(super) fn status_language() -> PublicLocalizationState {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.to_public()
}
pub(super) fn gettext(message_str: &'static str) -> Option<&'static str> {
let language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
let localization_state = LOCALIZATION_STATE.lock().unwrap();
// Use the localization from the highest-precedence language that has one available.
for catalog in language_precedence.iter() {
for (_, catalog) in localization_state.language_precedence.iter() {
if let Some(localized_str) = catalog.get(message_str) {
return Some(localized_str);
}
}
None
}
pub(super) fn list_available_languages() -> Vec<&'static str> {
let mut langs: Vec<_> = CATALOGS.entries().map(|(&lang, _)| lang).collect();
langs.sort();
langs
}
}
#[cfg(feature = "localize-messages")]
fn get_message_locale(vars: &EnvStack) -> Option<(gettext_impl::LocaleVariable, String)> {
use gettext_impl::LocaleVariable;
let get = |var_str: &wstr, var: LocaleVariable| {
vars.get_unless_empty(var_str)
.map(|val| (var, val.as_string().to_string()))
};
get(L!("LC_ALL"), LocaleVariable::LC_ALL)
.or_else(|| get(L!("LC_MESSAGES"), LocaleVariable::LC_MESSAGES))
.or_else(|| get(L!("LANG"), LocaleVariable::LANG))
}
#[cfg(feature = "localize-messages")]
fn get_language_var(vars: &EnvStack) -> Option<Vec<String>> {
let langs = vars.get_unless_empty(L!("LANGUAGE"))?;
let langs = langs.as_list();
let filtered_langs: Vec<String> = langs
.iter()
.filter(|lang| !lang.is_empty())
.map(|lang| lang.to_string())
.collect();
if filtered_langs.is_empty() {
return None;
}
Some(filtered_langs)
}
/// Call this when one of `LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG` changes.
/// Updates internal state such that the correct localizations will be used in subsequent
/// localization requests.
///
/// For deciding how to localize, the following is done:
///
/// 1. If the language precedence was set via `status language`, env vars are ignored.
/// 2. Check the first non-empty value of the env vars `LC_ALL`, `LC_MESSAGES`, `LANG`. If it
/// starts with `C` we consider this a C locale and disable localization.
/// 3. Otherwise, the value of the `LANGUAGE` env var is used, if non-empty. This allows specifying
/// multiple languages, with languages specified first taking precedence, e.g.
/// `LANGUAGE=zh_TW:zh_CN:pt_BR`
/// 4. Otherwise, the first non-empty value of the env vars `LC_ALL`, `LC_MESSAGES`, `LANG` is
/// used. This can only specify a single language, e.g. `LANG=de_AT.UTF-8`.
/// There, we normalize locale names by stripping off the suffix, leaving only the `ll_CC` part.
/// 5. Otherwise, localization will not happen.
///
/// If users specify `ll_CC` as a language and we don't have a catalog for this language, but we
/// have one for `ll`, that will be used instead. If users specify `ll` (without specifying a
/// language variant), which we discourage, and we don't have a catalog for `ll`, but we do have
/// one for `ll_CC`, that will be used as a fallback. If we have multiple `ll_*` catalogs, all of
/// them will be used, in arbitrary order.
#[cfg(feature = "localize-messages")]
pub fn update_locale_from_env(vars: &EnvStack) {
gettext_impl::update_locale_from_env(vars);
pub fn update_from_env(vars: &EnvStack) {
gettext_impl::update_from_env(get_message_locale(vars), get_language_var(vars));
}
#[cfg(feature = "localize-messages")]
fn append_space_separated_list<S: AsRef<str>>(
string: &mut WString,
list: impl IntoIterator<Item = S>,
) {
for lang in list.into_iter() {
string.push(' ');
string.push_utfstr(&crate::common::escape(
WString::from_str(lang.as_ref()).as_utfstr(),
));
}
}
#[cfg(feature = "localize-messages")]
pub struct SetLanguageLints<'a> {
duplicates: Vec<&'a str>,
non_existing: Vec<&'a str>,
}
#[cfg(feature = "localize-messages")]
impl<'a> From<gettext_impl::SetLanguageLints<'a>> for SetLanguageLints<'a> {
fn from(lints: gettext_impl::SetLanguageLints<'a>) -> Self {
Self {
duplicates: lints.duplicates,
non_existing: lints.non_existing,
}
}
}
#[cfg(feature = "localize-messages")]
impl<'a> SetLanguageLints<'a> {
pub fn display_duplicates(&self) -> WString {
let mut result = WString::new();
if self.duplicates.is_empty() {
return result;
}
result.push_utfstr(wgettext!("Language specifiers appear repeatedly:"));
append_space_separated_list(&mut result, &self.duplicates);
result.push('\n');
result
}
pub fn display_non_existing(&self) -> WString {
let mut result = WString::new();
if self.non_existing.is_empty() {
return result;
}
result.push_utfstr(wgettext!("No catalogs available for language specifiers:"));
append_space_separated_list(&mut result, &self.non_existing);
result.push('\n');
result
}
pub fn display_all(&self) -> WString {
let mut result = WString::new();
result.push_utfstr(&self.display_duplicates());
result.push_utfstr(&self.display_non_existing());
result
}
}
/// Call this when the `status language` builtin should update the language precedence.
/// `langs` should be the list of languages the precedence should be set to.
#[cfg(feature = "localize-messages")]
pub fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
gettext_impl::update_from_status_language_builtin(langs).into()
}
#[cfg(feature = "localize-messages")]
pub fn unset_from_status_language_builtin(vars: &EnvStack) {
gettext_impl::unset_from_status_language_builtin(
get_message_locale(vars),
get_language_var(vars),
);
}
#[cfg(feature = "localize-messages")]
pub fn status_language() -> WString {
use gettext_impl::LanguagePrecedenceOrigin;
let localization_state = gettext_impl::status_language();
let mut result = WString::new();
localizable_consts!(
LANGUAGE_LIST_VARIABLE_ORIGIN "from variable %s"
);
let origin_string = match localization_state.precedence_origin {
LanguagePrecedenceOrigin::Default => wgettext!("default").to_owned(),
LanguagePrecedenceOrigin::LocaleVariable(var) => {
wgettext_fmt!(LANGUAGE_LIST_VARIABLE_ORIGIN, var.as_str())
}
LanguagePrecedenceOrigin::LanguageEnvVar => {
wgettext_fmt!(LANGUAGE_LIST_VARIABLE_ORIGIN, "LANGUAGE")
}
LanguagePrecedenceOrigin::StatusLanguage => {
wgettext!("from command `status language set`").to_owned()
}
};
result.push_utfstr(&wgettext_fmt!("Active languages (%s):", origin_string));
append_space_separated_list(&mut result, &localization_state.language_precedence);
result.push('\n');
result
}
#[cfg(feature = "localize-messages")]
pub fn list_available_languages() -> WString {
let mut languages = WString::new();
for lang in gettext_impl::list_available_languages() {
languages.push_str(lang);
languages.push('\n');
}
languages
}
#[cfg(not(feature = "localize-messages"))]
@@ -180,13 +451,13 @@ pub fn initialize_gettext() {}
/// available. Without this, early error messages cannot be localized.
#[cfg(feature = "localize-messages")]
pub fn initialize_gettext() {
let locale_vars = EnvStack::new();
env_stack_set_from_env!(locale_vars, "LANGUAGE");
env_stack_set_from_env!(locale_vars, "LC_ALL");
env_stack_set_from_env!(locale_vars, "LC_MESSAGES");
env_stack_set_from_env!(locale_vars, "LANG");
let vars = EnvStack::new();
env_stack_set_from_env!(vars, "LANGUAGE");
env_stack_set_from_env!(vars, "LC_ALL");
env_stack_set_from_env!(vars, "LC_MESSAGES");
env_stack_set_from_env!(vars, "LANG");
gettext_impl::update_locale_from_env(&locale_vars);
gettext_impl::update_from_env(get_message_locale(&vars), get_language_var(&vars));
}
/// Use this function to localize a message.

View File

@@ -55,6 +55,12 @@ begin
set -l LANG C
echo (_ file)
# CHECK: file
set -l LANG POSIX
echo (_ file)
# CHECK: file
set -l LANG C.UTF-8
echo (_ file)
# CHECK: file
end
echo (_ file)
# CHECK: arquivo
@@ -73,6 +79,13 @@ end
echo (_ file)
# CHECK: arquivo
# Check that empty vars are ignored
begin
set -l LC_ALL
echo (_ file)
# CHECK: arquivo
end
# Check that all relevant locale variables are respected.
set --erase LANG
set --erase LC_MESSAGES
@@ -104,3 +117,71 @@ echo (_ file)
set -l LC_ALL de_DE.utf8
echo (_ file)
# CHECK: Datei
# Check `status language` builtin
set --erase LANG
set --erase LC_MESSAGES
set --erase LC_ALL
set --erase LANGUAGE
status language
# CHECK: Active languages (default):
echo (_ file)
# CHECK: file
set -l LANGUAGE pt_BR de_DE
status language
# CHECK: Active languages (from variable LANGUAGE): pt_BR de
echo (_ file)
# CHECK: arquivo
# We have fr but not fr_FR. For the builtin command, only exact matches are allowed.
status language set fr_FR de pt_BR
# CHECKERR: No catalogs available for language specifiers: fr_FR
status language
# CHECK: Active languages (from command `status language set`): de pt_BR
echo (_ file)
# CHECK: Datei
set -l LANGUAGE zh_TW
status language
# CHECK: Active languages (from command `status language set`): de pt_BR
echo (_ file)
# CHECK: Datei
set -l LC_MESSAGES C
status language
# CHECK: Active languages (from command `status language set`): de pt_BR
echo (_ file)
# CHECK: Datei
status language unset
status language
# CHECK: Active languages (from variable LC_MESSAGES):
echo (_ file)
# CHECK: file
set --erase LC_MESSAGES
status language
# CHECK: Active languages (from variable LANGUAGE): zh_TW
echo (_ file)
# CHECK: 檔案
set --erase LANGUAGE
status language
# CHECK: Active languages (default):
echo (_ file)
# CHECK: file
# Check `status language set` warnings
status language set asdf
# CHECKERR: No catalogs available for language specifiers: asdf
# This will have to be changed if we add catalogs for languages used here.
status language set zh_HK it_IT
# CHECKERR: No catalogs available for language specifiers: zh_HK it_IT
status language set de de
# CHECKERR: Language specifiers appear repeatedly: de
status language set \xff quote\"
# CHECKERR: No catalogs available for language specifiers: \Xff 'quote"'