mirror of https://github.com/django/django.git
Patch by Claude for #16084.
This commit is contained in:
parent
b9c8bbf372
commit
2babab0bb3
|
@ -19,25 +19,28 @@ STATUS_OK = 0
|
|||
|
||||
@total_ordering
|
||||
class TranslatableFile(object):
|
||||
def __init__(self, dirpath, file_name):
|
||||
def __init__(self, dirpath, file_name, locale_dir):
|
||||
self.file = file_name
|
||||
self.dirpath = dirpath
|
||||
self.locale_dir = locale_dir
|
||||
|
||||
def __repr__(self):
|
||||
return "<TranslatableFile: %s>" % os.sep.join([self.dirpath, self.file])
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.dirpath == other.dirpath and self.file == other.file
|
||||
return self.path == other.path
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.dirpath == other.dirpath:
|
||||
return self.file < other.file
|
||||
return self.dirpath < other.dirpath
|
||||
return self.path < other.path
|
||||
|
||||
def process(self, command, potfile, domain, keep_pot=False):
|
||||
@property
|
||||
def path(self):
|
||||
return os.path.join(self.dirpath, self.file)
|
||||
|
||||
def process(self, command, domain):
|
||||
"""
|
||||
Extract translatable literals from self.file for :param domain:
|
||||
creating or updating the :param potfile: POT file.
|
||||
Extract translatable literals from self.file for :param domain:,
|
||||
creating or updating the POT file.
|
||||
|
||||
Uses the xgettext GNU gettext utility.
|
||||
"""
|
||||
|
@ -91,8 +94,6 @@ class TranslatableFile(object):
|
|||
if status != STATUS_OK:
|
||||
if is_templatized:
|
||||
os.unlink(work_file)
|
||||
if not keep_pot and os.path.exists(potfile):
|
||||
os.unlink(potfile)
|
||||
raise CommandError(
|
||||
"errors happened while running xgettext on %s\n%s" %
|
||||
(self.file, errors))
|
||||
|
@ -100,11 +101,14 @@ class TranslatableFile(object):
|
|||
# Print warnings
|
||||
command.stdout.write(errors)
|
||||
if msgs:
|
||||
# Write/append messages to pot file
|
||||
potfile = os.path.join(self.locale_dir, '%s.pot' % str(domain))
|
||||
if is_templatized:
|
||||
old = '#: ' + work_file[2:]
|
||||
new = '#: ' + orig_file[2:]
|
||||
msgs = msgs.replace(old, new)
|
||||
write_pot_file(potfile, msgs)
|
||||
|
||||
if is_templatized:
|
||||
os.unlink(work_file)
|
||||
|
||||
|
@ -232,21 +236,21 @@ class Command(NoArgsCommand):
|
|||
settings.configure(USE_I18N = True)
|
||||
|
||||
self.invoked_for_django = False
|
||||
self.locale_paths = []
|
||||
self.default_locale_path = None
|
||||
if os.path.isdir(os.path.join('conf', 'locale')):
|
||||
localedir = os.path.abspath(os.path.join('conf', 'locale'))
|
||||
self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
self.invoked_for_django = True
|
||||
# Ignoring all contrib apps
|
||||
self.ignore_patterns += ['contrib/*']
|
||||
elif os.path.isdir('locale'):
|
||||
localedir = os.path.abspath('locale')
|
||||
else:
|
||||
raise CommandError("This script should be run from the Django Git "
|
||||
"tree or your project or app tree. If you did indeed run it "
|
||||
"from the Git checkout or your project or application, "
|
||||
"maybe you are just missing the conf/locale (in the django "
|
||||
"tree) or locale (for project and application) directory? It "
|
||||
"is not created automatically, you have to create it by hand "
|
||||
"if you want to enable i18n for your project or application.")
|
||||
self.locale_paths.extend(list(settings.LOCALE_PATHS))
|
||||
# Allow to run makemessages inside an app dir
|
||||
if os.path.isdir('locale'):
|
||||
self.locale_paths.append(os.path.abspath('locale'))
|
||||
if self.locale_paths:
|
||||
self.default_locale_path = self.locale_paths[0]
|
||||
|
||||
# We require gettext version 0.15 or newer.
|
||||
output, errors, status = _popen('xgettext --version')
|
||||
|
@ -261,24 +265,25 @@ class Command(NoArgsCommand):
|
|||
"gettext 0.15 or newer. You are using version %s, please "
|
||||
"upgrade your gettext toolset." % match.group())
|
||||
|
||||
potfile = self.build_pot_file(localedir)
|
||||
|
||||
# Build po files for each selected locale
|
||||
locales = []
|
||||
if locale is not None:
|
||||
locales += locale.split(',') if not isinstance(locale, list) else locale
|
||||
elif process_all:
|
||||
locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir))
|
||||
locales = [os.path.basename(l) for l in locale_dirs]
|
||||
|
||||
try:
|
||||
potfiles = self.build_potfiles()
|
||||
|
||||
# Build po files for each selected locale
|
||||
locales = []
|
||||
if locale is not None:
|
||||
locales = locale.split(',') if not isinstance(locale, list) else locale
|
||||
elif process_all:
|
||||
locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))
|
||||
locales = [os.path.basename(l) for l in locale_dirs]
|
||||
|
||||
for locale in locales:
|
||||
if self.verbosity > 0:
|
||||
self.stdout.write("processing locale %s\n" % locale)
|
||||
self.write_po_file(potfile, locale)
|
||||
for potfile in potfiles:
|
||||
self.write_po_file(potfile, locale)
|
||||
finally:
|
||||
if not self.keep_pot and os.path.exists(potfile):
|
||||
os.unlink(potfile)
|
||||
if not self.keep_pot:
|
||||
self.remove_potfiles()
|
||||
|
||||
def build_pot_file(self, localedir):
|
||||
file_list = self.find_files(".")
|
||||
|
@ -292,9 +297,41 @@ class Command(NoArgsCommand):
|
|||
f.process(self, potfile, self.domain, self.keep_pot)
|
||||
return potfile
|
||||
|
||||
def build_potfiles(self):
|
||||
"""Build pot files and apply msguniq to them"""
|
||||
file_list = self.find_files(".")
|
||||
self.remove_potfiles()
|
||||
for f in file_list:
|
||||
f.process(self, self.domain)
|
||||
|
||||
potfiles = []
|
||||
for path in self.locale_paths:
|
||||
potfile = os.path.join(path, '%s.pot' % str(self.domain))
|
||||
if not os.path.exists(potfile):
|
||||
continue
|
||||
msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' %
|
||||
(self.wrap, self.location, potfile))
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
raise CommandError(
|
||||
"errors happened while running msguniq\n%s" % errors)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
with open(potfile, 'w') as fp:
|
||||
fp.write(msgs)
|
||||
potfiles.append(potfile)
|
||||
return potfiles
|
||||
|
||||
def remove_potfiles(self):
|
||||
for path in self.locale_paths:
|
||||
pot_path = os.path.join(path, '%s.pot' % str(self.domain))
|
||||
if os.path.exists(pot_path):
|
||||
os.unlink(pot_path)
|
||||
|
||||
def find_files(self, root):
|
||||
"""
|
||||
Helper method to get all files in the given root.
|
||||
Helper function to get all files in the given root. Also check that there
|
||||
is a matching locale dir for each file.
|
||||
"""
|
||||
|
||||
def is_ignored(path, ignore_patterns):
|
||||
|
@ -315,12 +352,26 @@ class Command(NoArgsCommand):
|
|||
dirnames.remove(dirname)
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write('ignoring directory %s\n' % dirname)
|
||||
elif dirname == 'locale':
|
||||
dirnames.remove(dirname)
|
||||
self.locale_paths.insert(0, os.path.join(os.path.abspath(dirpath), dirname))
|
||||
for filename in filenames:
|
||||
if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), self.ignore_patterns):
|
||||
file_path = os.path.normpath(os.path.join(dirpath, filename))
|
||||
if is_ignored(file_path, self.ignore_patterns):
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write('ignoring file %s in %s\n' % (filename, dirpath))
|
||||
else:
|
||||
all_files.append(TranslatableFile(dirpath, filename))
|
||||
locale_dir = None
|
||||
for path in self.locale_paths:
|
||||
if os.path.abspath(dirpath).startswith(os.path.dirname(path)):
|
||||
locale_dir = path
|
||||
break
|
||||
if not locale_dir:
|
||||
locale_dir = self.default_locale_path
|
||||
if not locale_dir:
|
||||
raise CommandError(
|
||||
"Unable to find a locale path to store translations for file %s" % file_path)
|
||||
all_files.append(TranslatableFile(dirpath, filename, locale_dir))
|
||||
return sorted(all_files)
|
||||
|
||||
def write_po_file(self, potfile, locale):
|
||||
|
@ -328,16 +379,8 @@ class Command(NoArgsCommand):
|
|||
Creates or updates the PO file for self.domain and :param locale:.
|
||||
Uses contents of the existing :param potfile:.
|
||||
|
||||
Uses mguniq, msgmerge, and msgattrib GNU gettext utilities.
|
||||
Uses msgmerge, and msgattrib GNU gettext utilities.
|
||||
"""
|
||||
msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' %
|
||||
(self.wrap, self.location, potfile))
|
||||
if errors:
|
||||
if status != STATUS_OK:
|
||||
raise CommandError(
|
||||
"errors happened while running msguniq\n%s" % errors)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
|
||||
basedir = os.path.join(os.path.dirname(potfile), locale, 'LC_MESSAGES')
|
||||
if not os.path.isdir(basedir):
|
||||
|
@ -345,8 +388,6 @@ class Command(NoArgsCommand):
|
|||
pofile = os.path.join(basedir, '%s.po' % str(self.domain))
|
||||
|
||||
if os.path.exists(pofile):
|
||||
with open(potfile, 'w') as fp:
|
||||
fp.write(msgs)
|
||||
msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' %
|
||||
(self.wrap, self.location, pofile, potfile))
|
||||
if errors:
|
||||
|
@ -355,8 +396,10 @@ class Command(NoArgsCommand):
|
|||
"errors happened while running msgmerge\n%s" % errors)
|
||||
elif self.verbosity > 0:
|
||||
self.stdout.write(errors)
|
||||
elif not self.invoked_for_django:
|
||||
msgs = self.copy_plural_forms(msgs, locale)
|
||||
else:
|
||||
msgs = open(potfile, 'r').read()
|
||||
if not self.invoked_for_django:
|
||||
msgs = self.copy_plural_forms(msgs, locale)
|
||||
msgs = msgs.replace(
|
||||
"#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % self.domain, "")
|
||||
with open(pofile, 'w') as fp:
|
||||
|
|
|
@ -193,7 +193,8 @@ Ignore files or directories matching this glob-style pattern. Use multiple
|
|||
times to ignore more (makemessages command).
|
||||
.TP
|
||||
.I \-\-no\-default\-ignore
|
||||
Don't ignore the common private glob-style patterns 'CVS', '.*' and '*~' (makemessages command).
|
||||
Don't ignore the common private glob-style patterns 'CVS', '.*', '*~' and '*.pyc'
|
||||
(makemessages command).
|
||||
.TP
|
||||
.I \-\-no\-wrap
|
||||
Don't break long message lines into several lines (makemessages command).
|
||||
|
|
|
@ -472,7 +472,7 @@ Example usage::
|
|||
Use the ``--ignore`` or ``-i`` option to ignore files or directories matching
|
||||
the given :mod:`glob`-style pattern. Use multiple times to ignore more.
|
||||
|
||||
These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``
|
||||
These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``, ``'*.pyc'``
|
||||
|
||||
Example usage::
|
||||
|
||||
|
@ -499,7 +499,7 @@ for technically skilled translators to understand each message's context.
|
|||
.. versionadded:: 1.6
|
||||
|
||||
Use the ``--keep-pot`` option to prevent django from deleting the temporary
|
||||
.pot file it generates before creating the .po file. This is useful for
|
||||
.pot files it generates before creating the .po file. This is useful for
|
||||
debugging errors which may prevent the final language files from being created.
|
||||
|
||||
runfcgi [options]
|
||||
|
|
|
@ -1543,24 +1543,9 @@ All message file repositories are structured the same way. They are:
|
|||
* ``$PYTHONPATH/django/conf/locale/<language>/LC_MESSAGES/django.(po|mo)``
|
||||
|
||||
To create message files, you use the :djadmin:`django-admin.py makemessages <makemessages>`
|
||||
tool. You only need to be in the same directory where the ``locale/`` directory
|
||||
is located. And you use :djadmin:`django-admin.py compilemessages <compilemessages>`
|
||||
tool. And you use :djadmin:`django-admin.py compilemessages <compilemessages>`
|
||||
to produce the binary ``.mo`` files that are used by ``gettext``.
|
||||
|
||||
You can also run :djadmin:`django-admin.py compilemessages
|
||||
--settings=path.to.settings <compilemessages>` to make the compiler process all
|
||||
the directories in your :setting:`LOCALE_PATHS` setting.
|
||||
|
||||
Finally, you should give some thought to the structure of your translation
|
||||
files. If your applications need to be delivered to other users and will be used
|
||||
in other projects, you might want to use app-specific translations. But using
|
||||
app-specific translations and project-specific translations could produce weird
|
||||
problems with :djadmin:`makemessages`: it will traverse all directories below
|
||||
the current path and so might put message IDs into a unified, common message
|
||||
file for the current project that are already in application message files.
|
||||
|
||||
The easiest way out is to store applications that are not part of the project
|
||||
(and so carry their own translations) outside the project tree. That way,
|
||||
:djadmin:`django-admin.py makemessages <makemessages>`, when ran on a project
|
||||
level will only extract strings that are connected to your explicit project and
|
||||
not strings that are distributed independently.
|
||||
|
|
|
@ -5,10 +5,13 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import management
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils._os import upath
|
||||
from django.utils import six
|
||||
from django.utils.six import StringIO
|
||||
|
||||
|
||||
|
@ -352,3 +355,44 @@ class MultipleLocaleExtractionTests(ExtractorTests):
|
|||
management.call_command('makemessages', locale='pt,de,ch', verbosity=0)
|
||||
self.assertTrue(os.path.exists(self.PO_FILE_PT))
|
||||
self.assertTrue(os.path.exists(self.PO_FILE_DE))
|
||||
|
||||
|
||||
class CustomLayoutExtractionTests(ExtractorTests):
|
||||
def setUp(self):
|
||||
self._cwd = os.getcwd()
|
||||
self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir')
|
||||
|
||||
def test_no_locale_raises(self):
|
||||
os.chdir(self.test_dir)
|
||||
with six.assertRaisesRegex(self, management.CommandError,
|
||||
"Unable to find a locale path to store translations for file"):
|
||||
management.call_command('makemessages', locale=LOCALE, verbosity=0)
|
||||
|
||||
@override_settings(
|
||||
LOCALE_PATHS=(os.path.join(os.path.dirname(upath(__file__)), 'project_dir/project_locale'),)
|
||||
)
|
||||
def test_project_locale_paths(self):
|
||||
"""
|
||||
Test that:
|
||||
* translations for app containing locale folder are stored in that folder
|
||||
* translations outside of that app are in LOCALE_PATHS[0]
|
||||
"""
|
||||
os.chdir(self.test_dir)
|
||||
self.addCleanup(shutil.rmtree, os.path.join(settings.LOCALE_PATHS[0], LOCALE))
|
||||
self.addCleanup(shutil.rmtree, os.path.join(self.test_dir, 'app_with_locale/locale', LOCALE))
|
||||
|
||||
management.call_command('makemessages', locale=LOCALE, verbosity=0)
|
||||
project_de_locale = os.path.join(
|
||||
self.test_dir, 'project_locale/de/LC_MESSAGES/django.po',)
|
||||
app_de_locale = os.path.join(
|
||||
self.test_dir, 'app_with_locale/locale/de/LC_MESSAGES/django.po',)
|
||||
self.assertTrue(os.path.exists(project_de_locale))
|
||||
self.assertTrue(os.path.exists(app_de_locale))
|
||||
|
||||
with open(project_de_locale, 'r') as fp:
|
||||
po_contents = force_text(fp.read())
|
||||
self.assertMsgId('This app has no locale directory', po_contents)
|
||||
self.assertMsgId('This is a project-level string', po_contents)
|
||||
with open(app_de_locale, 'r') as fp:
|
||||
po_contents = force_text(fp.read())
|
||||
self.assertMsgId('This app has a locale directory', po_contents)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
|
||||
string = _("This is a project-level string")
|
|
@ -0,0 +1,4 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
|
||||
string = _("This app has no locale directory")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
|
||||
string = _("This app has a locale directory")
|
||||
|
|
@ -33,7 +33,7 @@ if can_run_extraction_tests:
|
|||
JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests,
|
||||
CopyPluralFormsExtractorTests, NoWrapExtractorTests,
|
||||
NoLocationExtractorTests, KeepPotFileExtractorTests,
|
||||
MultipleLocaleExtractionTests)
|
||||
MultipleLocaleExtractionTests, CustomLayoutExtractionTests)
|
||||
if can_run_compilation_tests:
|
||||
from .commands.compilation import (PoFileTests, PoFileContentsTests,
|
||||
PercentRenderingTests, MultipleLocaleCompilationTests)
|
||||
|
|
Loading…
Reference in New Issue