You're right that assigning the LC_*
shell variables does cause bash
to call POSIX setlocale()
for the corresponding category with the value of the variable whether they're exported or not. For LANG
, it calls setlocale(LC_ALL, thevalue)
followed setlocale(LC_*)
again for all the LC_*
variable. For LANGUAGE
, it doesn't do anything.
Now, bash
is the shell of the GNU project. For localization of text, it uses GNU gettext
, also known as libintl
. It even comes with its own version bundled with the source which you can compile in bash
if you call the configure
script with --with-included-gettext
.
gettext
looks up message translations in a per-language database. Which language it is is determined by the value of LC_MESSAGES
category though can be overridden by the $LANGUAGE
environment variable.
According to the gettext documentation, the previous call to setlocale()
should be the one that determines the value for the category, but there are some complications:
For multithreaded applications, there is currently no standard API that gettext can use to retrieve that value. bash
is not a multithreaded application, but even what setlocale(category, NULL)
returns is implementation defined and in practice not always usable.
So in practice, gettext only uses setlocale()
to retrieve the language name when built as part of the GNU libc or on a system where the libc is the GNU libc (like the one built with bash
with --with-included-gettext
on a GNU system) because it knows it can rely on it.
On other systems, it uses getenv()
to determine the locale, irrespective of how setlocale()
was invoked earlier, which is why you're seeing that behaviour.
Exporting those variables is an easy work around. One could argue that if they're not exported, they're not part of the environment anyway. POSIX is not very clear on that. Another way to look at it is that the translation is not done by bash
, but by a third party mechanism, so just like when executing other commands, we need to use environment variables to pass the locale information between the two software (here bash
and gettext
).
Now, on GNU systems, it actually gets worse.
As seen above, gettext is included in the GNU libc. $LANGUAGE
takes precedence over $LC_MESSAGE
but $LANGUAGE
is not part of the POSIX locale API, that's an extension on top of it.
So while on a GNU system, gettext will use setlocale(LC_MESSAGES, NULL)
to get the name for the LC_MESSAGES category, for LANGUAGE
, it always uses getenv()
, LANGUAGE
is not a locale category.
The problem is that bash
manages the environment by itself as part of its variable handling, disconnected from the libc's environ[]
array. It does have its own getenv()
which does query its own version of the environment, but when gettext
is built as part of the libc, and bash
is dynamically linked dgettext()
calls the getenv()
from the libc as that's an internal call within the libc, not bash
's one, so will only get the $LANGUAGE
value from the time bash
was started.
So on GNU systems, unless bash
was linked statically or built with --with-included-gettext
, any change to $LANGUAGE
will be ignored for the messages generated by bash
, whether the variable is exported or not. On other systems, that's fine (as long as $LANGUAGE
is exported) as gettext is not part of the libc, so it does call bash
's getenv()
.
On Debian:
$ LANGUAGE=fr bash -c 'LANGUAGE=es; eval fi'
bash: eval: ligne 0: erreur de syntaxe près du symbole inattendu « fi »
bash: eval: ligne 0: `fi'
(message in French, the value of $LANGUAGE
at the time bash
was invoked, not Spanish).
Actually it's not much better with other shells.
zsh
is not translated to other languages but does use strerror()
which does use gettext
internally on GNU systems:
$ LANGUAGE=fr zsh -c 'LANGUAGE=es; true</x; LANGUAGE=en; true</a; true < /etc/shadow'
zsh:1: no existe el archivo o el directorio: /x
zsh:1: no existe el archivo o el directorio: /a
zsh:1: permission denied: /etc/shadow
The LANGUAGE=es
was honoured but see how the second message for ENOENT has not been displayed in English (presumably cached by gettext somehow; that cache should have been invalidated when $LANGUAGE
changed but that was not the case).
LC_MESSAGES
does have some effect. How does that post explain either of these observations? – Nov 16 '18 at 07:00LANG
andLC_*
variables are a special case; they get validated when assigned in a way that no (few?) other variables do. But the point is that variables don’t become visible (especially to external programs) and effective until they are exported, and transformed from shell variables into environment variables. … (Cont’d) – Scott - Слава Україні Nov 16 '18 at 07:29getenv
, and there’s no mechanism for short-circuiting them to look for a *shell variable* calledLC_MESSAGES
when no environment variable by that name exists. – Scott - Слава Україні Nov 16 '18 at 07:29setlocale()
, notgetenv()
to query the current locale.bash
as shown by the OP does callsetlocale()
when theLC_*
variables are modified to set the current locale, so it seems there's something wrong with that library or possibly the waybash
uses it. – Stéphane Chazelas Nov 16 '18 at 07:40setlocale
is being called, and AFAIK that function doesn't have an option to just validate but not set. – Nov 16 '18 at 07:40getenv()
ifsetlocale()
is not available. It is available on macOS though... but maybe the Homebrew build scripts are't picking it up, or are ignoring it for whatever reason. – Kusalananda Nov 16 '18 at 08:11setlocale("fr_FR.UTF-8", LC_ALL)
followed bydgettext()
. The messages are not translated into the language passed tosetlocale()
(thoughtruss
shows the corresponding.mo
file is being open if there's noLC_*
variable in the environment), but instead into the language referenced by the LC_* variables. One would need to look at the source to see why, but that very much looks like a bug. – Stéphane Chazelas Nov 16 '18 at 08:21export
ing of variables. – Nov 19 '18 at 14:22bash: syntax error near unexpected token
fi'even after exporting and no Japanese text.
locale` however reflects the change. Anyone knows anything? – Pacerier May 29 '19 at 10:31