In my local Django sandbox, I've turned django.utils.translation into
a package that contains __init__.py, trans_real.py and trans_null.py.
The __init__.py contains this:
from django.conf import settings
if settings.USE_I18N:
from trans_real import *
else:
from trans_null import *
trans_real.py is the former django/utils/translation.py, and
trans_null.py contains identity versions of the gettext(), etc.,
functions that don't actually do anything. This is a nice, clean way
of avoiding the overhead of all that gettext() stuff if USE_I18N is
set to False.
The problem with this arrangement is that it causes any import of
django.utils.translation to load django.conf.settings -- and the
settings file in itself imports the translation hooks, because of the
LANGUAGES setting in global_settings.py:
from django.utils.translation import gettext_lazy as _
LANGUAGES = (
('bn', _('Bengali')),
('cs', _('Czech')),
('cy', _('Welsh')),
('da', _('Danish')),
('de', _('German')),
# ...
)
So, in order to make this work, I'm proposing that we *disallow*
translation strings in settings files -- so that the
django.utils.translation file can import from settings and we avoid
circular import references.
This leaves the problem: Where do we put the translation strings for
the available languages? For that, we could put the translation
strings in in django/utils/translation/trans_null.py and change any
instance of LANGUAGES to apply the gettext call at runtime rather than
compile time. Or is there a better way to solve the problem?
Adrian
--
Adrian Holovaty
holovaty.com | djangoproject.com
On Sat, 2006-07-01 at 12:52 -0500, Adrian Holovaty wrote:
[...]
> The problem with this arrangement is that it causes any import of
> django.utils.translation to load django.conf.settings -- and the
> settings file in itself imports the translation hooks, because of the
> LANGUAGES setting in global_settings.py:
>
> from django.utils.translation import gettext_lazy as _
> LANGUAGES = (
> ('bn', _('Bengali')),
> ('cs', _('Czech')),
> ('cy', _('Welsh')),
> ('da', _('Danish')),
> ('de', _('German')),
> # ...
> )
>
> So, in order to make this work, I'm proposing that we *disallow*
> translation strings in settings files -- so that the
> django.utils.translation file can import from settings and we avoid
> circular import references.
>
> This leaves the problem: Where do we put the translation strings for
> the available languages? For that, we could put the translation
> strings in in django/utils/translation/trans_null.py and change any
> instance of LANGUAGES to apply the gettext call at runtime rather than
> compile time. Or is there a better way to solve the problem?
The way the translation extraction code (xgettext) works is to search
for particular keywords that are the names (or aliases) of functions
marking translatable strings. One of these keywords is gettext_noop,
which is the "mark for translation, but don't translate yet" function.
So you could leave the strings in settings.py, providing you define a
function called gettext_noop. All this function does is return the
string passed to it (that is all it does for real, anyway). Then the
strings are marked with as gettext_noop('Bengali'), etc, and it should
all Just Work(tm).
There are no namespacing issues here because xgettext just looks for
strings matching the keyword. So having a little
def gettext_noop(s): return s
in global_settings.py shouldn't hurt anything. You still need to then
wrap all uses of settings.LANGUAGES in a call to gettext() or _().
So does this help or hurt? For translators, it doesn't really matter
where these strings are, just as long as they are picked up by the
xgettext runs to generate the .po files. For i18n maintainers (Hugo),
they need to be able to find this list reasonably quickly for adding to
it. Splitting up the list so that two places have to be updated (the
list of codes in global_settings -- which I don't think you can remove
without making things awkward -- plus the mapping of code to translated
string) is not ideal (leads to mistakes).
Translatable strings in the project's settings.py are a small problem.
They would have to define their own gettext_noop() as above. Not a real
burden in those cases, I guess.
If the extra gettext_noop function in global_Settings.py doesn't offend
your sense of style too much, I would prefer that approach, just to keep
everything in one place.
Best wishes,
Malcolm
I've made the changes you've suggested (in a local sandbox), but
there's one problem left: The bottom of django/conf/__init__.py does
the following:
from django.utils import translation
translation.install()
This registers the _() function in the __builtins__ namespace.
That code can no longer live there, because the new
django.utils.translation depends on the USE_I18N setting. But if it
doesn't live in django/conf/__init__.py, where should it live? What's
a place where we can put it where we can be sure it'll always be run?
I'm a bit stumped...
I'm not sure I like what I'm about to suggest, but let's throw it out
and see how it sounds...
How would you feel about making a requirement that settings must be
configured prior to attempting to use translations. By this, I mean,
Django must know that it is either using DJANGO_SETTINGS_MODULE or
manual configuration prior to trying to use _(). In that case, you could
put the import into __builtins__ into LazySettings._import_settings()
and LazySettings.configure(), because you will know the value of
USE_I18N at the end of both of those methods.
The reason I don't really like this (and I think it's a loser if I go
with "trust my gut", but I'm not 100% sure why) is the requirement to
"clarify" which settings are being used first. It might be a bit too
easy to forget to do that and it will be an absolute bear to debug.
The other thing I can think of is to install a function initially that
installs the "real" function over the top of itself the first time it is
called (see attached lazy-install.py for a proof of concept). The first
time the lazy _() is called, it can check settings.USE_I18N, which will
always be accessible at that point (the first access to "settings"
configures it if it has not already been done).
This is all starting to get a bit twisted, but might be unavoidable
unless you go back to the original plan of shipping the strings out of
global_settings and into another file.
I'd probably go for option 2 and a bunch of comments for future
explorers.
Regards,
Malcolm
That would indeed solve this, but I agree with you that it's not the
best solution. Namely, we shouldn't have to require that a person use
settings before using _().
> The other thing I can think of is to install a function initially that
> installs the "real" function over the top of itself the first time it is
> called (see attached lazy-install.py for a proof of concept). The first
> time the lazy _() is called, it can check settings.USE_I18N, which will
> always be accessible at that point (the first access to "settings"
> configures it if it has not already been done).
This, I like. The function can originally be installed in
django/conf/__init__.py (where the translation.install() call
currently lives), and it can load from the django.utils.translation
backend appropriately. It's actually not *too* hackish, either...Well,
not as much as the other solution, I guess.
Any other thoughts on this from anybody? I'll go ahead and commit this
some time Monday, although I'm traveling at the moment and may not get
to it until Tuesday.
I've committed this in http://code.djangoproject.com/changeset/3271 .