Merge lp:~songofacandy/bzr/i18n-command into lp:bzr

Proposed by methane
Status: Merged
Merged at revision: 5994
Proposed branch: lp:~songofacandy/bzr/i18n-command
Merge into: lp:bzr
Diff against target: 571 lines (+272/-33)
8 files modified
bzrlib/builtins.py (+1/-0)
bzrlib/commands.py (+48/-25)
bzrlib/export_pot.py (+14/-5)
bzrlib/help.py (+2/-2)
bzrlib/help_topics/__init__.py (+1/-0)
bzrlib/i18n.py (+1/-1)
bzrlib/tests/test_export_pot.py (+46/-0)
bzrlib/tests/test_help.py (+159/-0)
To merge this branch: bzr merge lp:~songofacandy/bzr/i18n-command
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
Review via email: mp+62752@code.launchpad.net

Description of the change

Translate command help

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.6 KiB)

8 +

Spurious ?

20 + i18n.uninstall()
21 +

Hmm, that's weird, who did install what ? Why do we need to start
uninstalling ?

Said otherwise, this reveals some weird initialization and I
think we want a better way to achieve it.

To avoid a controversial discussion about general deployment I
think you should focus on initializing the i18n/i10n only for the
help command. We can revisit that later once we are happy with
the results.

46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section will
47 + # be shown after "Description" section.
48 + doc = cmd_gettext(doc)

/me blinks

I'm sure this will be eaiser to understand if you added a
corresponding test.

58 - result += ':Purpose: %s\n' % purpose
59 + result += ':%s: %s\n' % (gettext('Purpose'), purpose)

Hmm, this pattern will probably occurs quite frequently and I'm
pretty sure the translators will sometimes need to tweak the
format Is there some existing facility we can reuse so this can
be spelt something like:

  result += gettext(':Purpose: %s\n' % (gettext(purpose),)

or even better:

  result += gettext_xxx(':Purpose: %(purpose)s\n', dict(purpose=purpose))

?

74 + options = option.get_optparser(self.options(), True)
75 + options = options.format_option_help()

How about:

 parser = option.get_optparser(self.options(), True)
 options = parser.format_option_help()

88 + if options.startswith('Options:') or options.startswith('options:'):
89 + # Python 2.4 version of optparse uses 'options'.
90 + result += ':%s:%s' % (gettext('Options'), options[len('options:'):])

Forget about python-2.4, we don't support it anymore.

135 - sys.stdout.write(self.get_help_text())
136 + self._setup_outf()
137 + self.outf.write(self.get_help_text())
138 return 0
139 if 'usage' in opts: # e.g. bzr add --usage
140 sys.stdout.write(self.get_help_text(verbose=False))

/me blinks

Nice to address this bug, but why on earth is it needed ? This
should really be addressed far earlier no ? Can you find where or
do you need help here ?

Also, it looks like the exact same bug affect 'usage'. I realize
this may be out of scope for this proposal, so may be we want a
different proposal that, just let me know if you want to do it or
leave it to me.

145 + @staticmethod
146 + def get_gettext():

I don't understand why you need a static method there ? I realize
*you* don't need 'self' but may be some plugins will, no ?

149 + NOTE: Commands provided by plugins should override this to use own
150 + i18n system.

typo/style nits: We don't use 'NOTE:' all caps, may be just 'Note:' or may be just nothing ;) s/to use own/to use their own/

165 + opt_no_i18n = opt_no_aliases = False

Excuse my ignorance, but shouldn't that be opt_no_i10n here ?

182 + if not opt_no_i18n:
183 + # selftest uninstalls i18n later.
184 + i18n_install()

Ha ! Here is the install !

So, yeah, it will be clearer to start with if install/uninstall
is done by the help command only and the appropriate tests,
keeping it under control will help us understand how/when/where
it's needed.

We had trouble in the past about such global options, I'd rather
avoid the fallbacks right from the start.

This is by no means a lack of trust, I'm just being caut...

Read more...

review: Needs Information
Revision history for this message
Vincent Ladeuil (vila) wrote :

Also ! This proposal is a significant step in the i10n process as it will lead to the first visible results for our users so it definitely deserves news entries (and credits !).

Don't worry about writing imperfect English, we'll find native speakers to catch *my* spelling mistakes :)

Revision history for this message
methane (songofacandy) wrote :

> 8 +
>
> Spurious ?

I've removed it.

>
> 20 + i18n.uninstall()
> 21 +
>
> Hmm, that's weird, who did install what ? Why do we need to start
> uninstalling ?

I think of many other commands like log or status use i18n in the future.
I18n only breaks selftest.

>
> Said otherwise, this reveals some weird initialization and I
> think we want a better way to achieve it.
>
> To avoid a controversial discussion about general deployment I
> think you should focus on initializing the i18n/i10n only for the
> help command. We can revisit that later once we are happy with
> the results.

I am not against on enabling i18n only for help command until other
commands needs i18n.

But, how? When selftest calls help, enabling i18n breaks many tests.
I don't think replacing `i18n.uninstall()` with global variable like
`opt_no_i18n=True` is beautiful answer.

One idea come to my mind is adding `use_i18n = False` class attribute
to the Command class. The cmd_help overrides it with `use_i18n = True`.
Then, selftest overrides `use_i18n = False` again.

Do you think OK with this idea?

Revision history for this message
Vincent Ladeuil (vila) wrote :

> I think of many other commands like log or status use i18n in the future.

I expect so !

> I18n only breaks selftest.

Let's focus on that then.

> I am not against on enabling i18n only for help command until other
> commands needs i18n.

Cool.

>
> But, how? When selftest calls help, enabling i18n breaks many tests.

I think we don't want to blindly activate i18n during tests. Quite the contrary, we probably want to *never* activate it by default (which should translate to either totally disabling any i18n code or may be just using a NullTranslation ?)

So, this kind of isolation is generally done in bzrlib.tests.TestCase, if you push a revision with some tests failing, I should be able to fix that.

Revision history for this message
methane (songofacandy) wrote :
Download full text (5.0 KiB)

> 46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section will
> 47 + # be shown after "Description" section.
> 48 + doc = cmd_gettext(doc)
>
> /me blinks
>
> I'm sure this will be eaiser to understand if you added a
> corresponding test.

Is the test for garbage input and garbage output really needed?

I've modified export-pot commands to don't exports paragraph starting
with ":Usage:". I think this is not beautiful but enough.

>
> 58 - result += ':Purpose: %s\n' % purpose
> 59 + result += ':%s: %s\n' % (gettext('Purpose'), purpose)
>
> Hmm, this pattern will probably occurs quite frequently and I'm
> pretty sure the translators will sometimes need to tweak the
> format Is there some existing facility we can reuse so this can
> be spelt something like:
>
> result += gettext(':Purpose: %s\n' % (gettext(purpose),)
>
> or even better:
>
> result += gettext_xxx(':Purpose: %(purpose)s\n', dict(purpose=purpose))
>
> ?

OK, gettext(':Purpose: %s\n') % (gettext(purpose),) may be good.

>
> 74 + options = option.get_optparser(self.options(), True)
> 75 + options = options.format_option_help()
>
> How about:
>
> parser = option.get_optparser(self.options(), True)
> options = parser.format_option_help()
>
> 88 + if options.startswith('Options:') or options.startswith('options:'):
> 89 + # Python 2.4 version of optparse uses 'options'.
> 90 + result += ':%s:%s' % (gettext('Options'), options[len('options:'):])
>
> Forget about python-2.4, we don't support it anymore.
>

I'll fix.

> 135 - sys.stdout.write(self.get_help_text())
> 136 + self._setup_outf()
> 137 + self.outf.write(self.get_help_text())
> 138 return 0
> 139 if 'usage' in opts: # e.g. bzr add --usage
> 140 sys.stdout.write(self.get_help_text(verbose=False))
>
> /me blinks
>
> Nice to address this bug, but why on earth is it needed ? This
> should really be addressed far earlier no ? Can you find where or
> do you need help here ?

It was not problem before because get_help_text() returns bytestring
containing only ascii characters.
Since i18n is enabled, get_help_text() returns unicode though `outf`
is needed.

>
> Also, it looks like the exact same bug affect 'usage'. I realize
> this may be out of scope for this proposal, so may be we want a
> different proposal that, just let me know if you want to do it or
> leave it to me.

Ah, I've forgot to address --usage. I'll fix it.

>
>
> 145 + @staticmethod
> 146 + def get_gettext():
>
> I don't understand why you need a static method there ? I realize
> *you* don't need 'self' but may be some plugins will, no ?
>
> 149 + NOTE: Commands provided by plugins should override this to use own
> 150 + i18n system.
>
> typo/style nits: We don't use 'NOTE:' all caps, may be just 'Note:' or may be
> just nothing ;) s/to use own/to use their own/
>

I'll fix these.

> 165 + opt_no_i18n = opt_no_aliases = False
>
> Excuse my ignorance, but shouldn't that be opt_no_i10n here ?

Do you mean "l10n"?
I agree on "opt_no_l10n" or "opt_no_L10N" is better than "i10n".

>
> 196 + if p.splitlines()[0] == ':Usage:':
> 197 +...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (9.9 KiB)

>>>>> INADA Naoki <email address hidden> writes:

    >> 46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section will
    >> 47 + # be shown after "Description" section.
    >> 48 + doc = cmd_gettext(doc)
    >>
    >> /me blinks
    >>
    >> I'm sure this will be eaiser to understand if you added a
    >> corresponding test.

    > Is the test for garbage input and garbage output really needed?

later you have:

221 + def filter(p):
222 + # ':Usage:' has special meaning in help topics.
223 + # This is usage example of command and should not be translated.
224 + if p.splitlines()[0] == ':Usage:':
225 + return True

Reading that, I think I understand. The later is needed to prepare the
file for translators while the former is making sure we don't try to
translate when we display the command help. Is that right ?

    > I've modified export-pot commands to don't exports paragraph
    > starting with ":Usage:". I think this is not beautiful but enough.

Indeed, now it makes sense, sorry for being a bit obtuse here :)

So I think what I wanted is:

    >> 46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section will
    >> 47 + # be shown after "Description" section.

# be shown after "Description" section and we don't want to translate the usage string.

But we still want to translate 'Usage' itself right ? Like we want to
translate 'Description', 'Tips', 'See Also' and so on. Do we reuse these
specifics strings from somewhere or are they part of the strings our
translators will process ?

(Also see below for other related points)

<snip/>

    > OK, gettext(':Purpose: %s\n') % (gettext(purpose),) may be good.

Right, that looks like an easier idiom.

    >> 135 - sys.stdout.write(self.get_help_text())
    >> 136 + self._setup_outf()
    >> 137 + self.outf.write(self.get_help_text())
    >> 138 return 0
    >> 139 if 'usage' in opts: # e.g. bzr add --usage
    >> 140 sys.stdout.write(self.get_help_text(verbose=False))
    >>
    >> /me blinks
    >>
    >> Nice to address this bug, but why on earth is it needed ? This
    >> should really be addressed far earlier no ? Can you find where or
    >> do you need help here ?

    > It was not problem before because get_help_text() returns bytestring
    > containing only ascii characters.
    > Since i18n is enabled, get_help_text() returns unicode though `outf`
    > is needed.

Sorry, badly phrased. I understand why we need to use outf, I don't
understand why we need to call set_outf (or rather why it has not been
already called).

    >>
    >> Also, it looks like the exact same bug affect 'usage'. I realize
    >> this may be out of scope for this proposal, so may be we want a
    >> different proposal that, just let me know if you want to do it or
    >> leave it to me.

    > Ah, I've forgot to address --usage. I'll fix it.

Right, so same fix twice, hence my worries about why we need to fix that
as it seems to reveal a deeper issue...

    >>
    >>
    >> 145 + @staticmethod
    >> 146 + def get_gettext():
    >>
    >> I don't understand why you need a static method there ? I realize
    >> *you* don't need 's...

Revision history for this message
Martin Pool (mbp) wrote :

I'd like a second opinion but this looks good to me.

It may make future reviews faster if you can summarize the change in the mp
description.
On Jun 1, 2011 6:42 PM, "INADA Naoki" <email address hidden> wrote:
> INADA Naoki has proposed merging lp:~songofacandy/bzr/i18n-command into
lp:bzr.
>
> Requested reviews:
> Vincent Ladeuil (vila)
>
> For more details, see:
> https://code.launchpad.net/~songofacandy/bzr/i18n-command/+merge/62752
>
> Translate command help
> --
> https://code.launchpad.net/~songofacandy/bzr/i18n-command/+merge/62752
> Your team bzr-core is subscribed to branch lp:bzr.

Revision history for this message
methane (songofacandy) wrote :
Download full text (3.3 KiB)

> >>>>> INADA Naoki <email address hidden> writes:
>
> >> 46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section
> will
> >> 47 + # be shown after "Description" section.
> >> 48 + doc = cmd_gettext(doc)
> >>
> >> /me blinks
> >>
> >> I'm sure this will be eaiser to understand if you added a
> >> corresponding test.
>
> > Is the test for garbage input and garbage output really needed?
>
> later you have:
>
> 221 + def filter(p):
> 222 + # ':Usage:' has special meaning in help topics.
> 223 + # This is usage example of command and should not be translated.
> 224 + if p.splitlines()[0] == ':Usage:':
> 225 + return True
>
> Reading that, I think I understand. The later is needed to prepare the
> file for translators while the former is making sure we don't try to
> translate when we display the command help. Is that right ?

Yes, I want to avoid translating ':Usage:' because it is used as a keyword
in Command.get_help_text().

>
> > I've modified export-pot commands to don't exports paragraph
> > starting with ":Usage:". I think this is not beautiful but enough.
>
> Indeed, now it makes sense, sorry for being a bit obtuse here :)
>
> So I think what I wanted is:
>
> >> 46 + # NOTE: If cmd_gettext translates ':Usage:\n', the section
> will
> >> 47 + # be shown after "Description" section.
>
> # be shown after "Description" section and we don't want to translate the
> usage string.

OK, I'll fix the comment.

>
> But we still want to translate 'Usage' itself right ? Like we want to
> translate 'Description', 'Tips', 'See Also' and so on. Do we reuse these
> specifics strings from somewhere or are they part of the strings our
> translators will process ?
>
> (Also see below for other related points)

Yes, get_help_text() removes ":Usage:" in the docstring and write separately.

> result += gettext(':Usage:\n%s') % (usage,) + '\n'

> >> 135 - sys.stdout.write(self.get_help_text())
> >> 136 + self._setup_outf()
> >> 137 + self.outf.write(self.get_help_text())
> >> 138 return 0
> >> 139 if 'usage' in opts: # e.g. bzr add --usage
> >> 140 sys.stdout.write(self.get_help_text(verbose=False))
> >>
> >> /me blinks
> >>
> >> Nice to address this bug, but why on earth is it needed ? This
> >> should really be addressed far earlier no ? Can you find where or
> >> do you need help here ?
>
> > It was not problem before because get_help_text() returns bytestring
> > containing only ascii characters.
> > Since i18n is enabled, get_help_text() returns unicode though `outf`
> > is needed.
>
> Sorry, badly phrased. I understand why we need to use outf, I don't
> understand why we need to call set_outf (or rather why it has not been
> already called).
>
> >>
> >> Also, it looks like the exact same bug affect 'usage'. I realize
> >> this may be out of scope for this proposal, so may be we want a
> >> different proposal that, just let me know if you want to do it or
> >> leave it to me.
>
> > Ah, I've forgot to address --usag...

Read more...

Revision history for this message
methane (songofacandy) wrote :

Then, I'll remove translating option help in this mp.

I'll add a new class that inherits OptionParser and overrides
format_option_help() in future mp.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> Then, I'll remove translating option help in this mp.
>
> I'll add a new class that inherits OptionParser and overrides
> format_option_help() in future mp.

That sounds like the best way to move forward.

Mark this proposal NeedsReview when you're ready.

Revision history for this message
methane (songofacandy) wrote :

I think I've done what I said "I'll do it."
Anything else?

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> INADA Naoki writes:

    > I think I've done what I said "I'll do it."
    > Anything else?

Sorry for the delay I had some lag in my mail.

I'll look into it asap.

Revision history for this message
Vincent Ladeuil (vila) wrote :

So, the bad news first: 6 test failures:

bzrlib.tests.test_help.TestCommandHelpI18n.test_command_help_includes_see_also
bzrlib.tests.test_help.TestCommandHelpI18n.test_command_only_additional_see_also
bzrlib.tests.test_help.TestCommandHelpI18n.test_command_with_additional_see_also
bzrlib.tests.test_help.TestCommandHelpI18n.test_get_help_text
bzrlib.tests.test_help.TestCommandHelpI18n.test_help_custom_section_ordering
bzrlib.tests.test_help.TestCommandHelpI18n.test_help_text_custom_usage

I made some modifications to make these failures easier to diagnose (in lp:~vila/bzr/i18n-command) but you didn't merge them.

The good news now: I've merged these modifications again and what
happens is that your last commit broke them by installing the
"right" _translations (NullTranslation) when translating the
command help which defeats the installation done by the tests.

Also, these modifications included removing your modification
pattern:

60 - result += ':Purpose: %s\n' % purpose
61 + result += gettext(':Purpose: %s') % (purpose,) + '\n'

Specifically, I'm talking about removing the '\n' from the string
to be translated. If you have a strong reason to do so, let's put
it back and let's to do it everywhere it matters.

For i18n._translations, what we want is a lazy installation so
that it's done only once and that tests can force a specific
translation. This way, we can (as we do now) activate a
translation only when we need it: to generate the command help.

I went ahead and:

- initialize i18n._translation to None instead of NullTranslations,

- spell '_translations' with a final 's' (for consistency with
  gettext itself and ZzzTranslationsForDoc)

I'll land with these modifications and a news entry (please
review it and let me know what you think).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2011-06-16 16:45:18 +0000
+++ bzrlib/builtins.py 2011-06-23 16:06:27 +0000
@@ -3728,6 +3728,7 @@
3728 randomize=None, exclude=None, strict=False,3728 randomize=None, exclude=None, strict=False,
3729 load_list=None, debugflag=None, starting_with=None, subunit=False,3729 load_list=None, debugflag=None, starting_with=None, subunit=False,
3730 parallel=None, lsprof_tests=False):3730 parallel=None, lsprof_tests=False):
3731 from bzrlib import i18n
3731 from bzrlib import tests3732 from bzrlib import tests
37323733
3733 if testspecs_list is not None:3734 if testspecs_list is not None:
37343735
=== modified file 'bzrlib/commands.py'
--- bzrlib/commands.py 2011-05-19 09:32:38 +0000
+++ bzrlib/commands.py 2011-06-23 16:06:27 +0000
@@ -44,6 +44,11 @@
44""")44""")
4545
46from bzrlib.hooks import Hooks46from bzrlib.hooks import Hooks
47from bzrlib.i18n import (
48 gettext,
49 gettext_per_paragraph,
50 install as i18n_install,
51 )
47# Compatibility - Option used to be in commands.52# Compatibility - Option used to be in commands.
48from bzrlib.option import Option53from bzrlib.option import Option
49from bzrlib.plugin import disable_plugins, load_plugins54from bzrlib.plugin import disable_plugins, load_plugins
@@ -408,6 +413,7 @@
408 takes_options = []413 takes_options = []
409 encoding_type = 'strict'414 encoding_type = 'strict'
410 invoked_as = None415 invoked_as = None
416 l10n = True
411417
412 hidden = False418 hidden = False
413419
@@ -486,8 +492,17 @@
486 message explaining how to obtain full help.492 message explaining how to obtain full help.
487 """493 """
488 doc = self.help()494 doc = self.help()
489 if not doc:495 if doc:
490 doc = "No help for this command."496 # Note: If self.gettext() translates ':Usage:\n', the section will
497 # be shown after "Description" section and we don't want to
498 # translate the usage string.
499 # Though, bzr export-pot don't exports :Usage: section and it must
500 # not be translated.
501 if self.l10n:
502 i18n_install() # Install i18n only for get_help_text for now.
503 doc = self.gettext(doc)
504 else:
505 doc = gettext("No help for this command.")
491506
492 # Extract the summary (purpose) and sections out from the text507 # Extract the summary (purpose) and sections out from the text
493 purpose,sections,order = self._get_help_parts(doc)508 purpose,sections,order = self._get_help_parts(doc)
@@ -500,11 +515,11 @@
500515
501 # The header is the purpose and usage516 # The header is the purpose and usage
502 result = ""517 result = ""
503 result += ':Purpose: %s\n' % purpose518 result += gettext(':Purpose: %s') % (purpose,) + '\n'
504 if usage.find('\n') >= 0:519 if usage.find('\n') >= 0:
505 result += ':Usage:\n%s\n' % usage520 result += gettext(':Usage:\n%s') % (usage,) + '\n'
506 else:521 else:
507 result += ':Usage: %s\n' % usage522 result += gettext(':Usage: %s') % (usage,) + '\n'
508 result += '\n'523 result += '\n'
509524
510 # Add the options525 # Add the options
@@ -512,7 +527,8 @@
512 # XXX: optparse implicitly rewraps the help, and not always perfectly,527 # XXX: optparse implicitly rewraps the help, and not always perfectly,
513 # so we get <https://bugs.launchpad.net/bzr/+bug/249908>. -- mbp528 # so we get <https://bugs.launchpad.net/bzr/+bug/249908>. -- mbp
514 # 20090319529 # 20090319
515 options = option.get_optparser(self.options()).format_option_help()530 parser = option.get_optparser(self.options())
531 options = parser.format_option_help()
516 # FIXME: According to the spec, ReST option lists actually don't532 # FIXME: According to the spec, ReST option lists actually don't
517 # support options like --1.14 so that causes syntax errors (in Sphinx533 # support options like --1.14 so that causes syntax errors (in Sphinx
518 # at least). As that pattern always appears in the commands that534 # at least). As that pattern always appears in the commands that
@@ -522,10 +538,7 @@
522 if not plain and options.find(' --1.14 ') != -1:538 if not plain and options.find(' --1.14 ') != -1:
523 options = options.replace(' format:\n', ' format::\n\n', 1)539 options = options.replace(' format:\n', ' format::\n\n', 1)
524 if options.startswith('Options:'):540 if options.startswith('Options:'):
525 result += ':' + options541 result += gettext(':Options:%s') % (options[len('options:'):],)
526 elif options.startswith('options:'):
527 # Python 2.4 version of optparse
528 result += ':Options:' + options[len('options:'):]
529 else:542 else:
530 result += options543 result += options
531 result += '\n'544 result += '\n'
@@ -536,26 +549,26 @@
536 if sections.has_key(None):549 if sections.has_key(None):
537 text = sections.pop(None)550 text = sections.pop(None)
538 text = '\n '.join(text.splitlines())551 text = '\n '.join(text.splitlines())
539 result += ':%s:\n %s\n\n' % ('Description',text)552 result += gettext(':Description:\n %s') % (text,) + '\n\n'
540553
541 # Add the custom sections (e.g. Examples). Note that there's no need554 # Add the custom sections (e.g. Examples). Note that there's no need
542 # to indent these as they must be indented already in the source.555 # to indent these as they must be indented already in the source.
543 if sections:556 if sections:
544 for label in order:557 for label in order:
545 if sections.has_key(label):558 if label in sections:
546 result += ':%s:\n%s\n' % (label,sections[label])559 result += ':%s:\n%s\n' % (label, sections[label])
547 result += '\n'560 result += '\n'
548 else:561 else:
549 result += ("See bzr help %s for more details and examples.\n\n"562 result += (gettext("See bzr help %s for more details and examples.\n\n")
550 % self.name())563 % self.name())
551564
552 # Add the aliases, source (plug-in) and see also links, if any565 # Add the aliases, source (plug-in) and see also links, if any
553 if self.aliases:566 if self.aliases:
554 result += ':Aliases: '567 result += gettext(':Aliases: ')
555 result += ', '.join(self.aliases) + '\n'568 result += ', '.join(self.aliases) + '\n'
556 plugin_name = self.plugin_name()569 plugin_name = self.plugin_name()
557 if plugin_name is not None:570 if plugin_name is not None:
558 result += ':From: plugin "%s"\n' % plugin_name571 result += gettext(':From: plugin "%s"\n') % plugin_name
559 see_also = self.get_see_also(additional_see_also)572 see_also = self.get_see_also(additional_see_also)
560 if see_also:573 if see_also:
561 if not plain and see_also_as_links:574 if not plain and see_also_as_links:
@@ -567,11 +580,10 @@
567 see_also_links.append(item)580 see_also_links.append(item)
568 else:581 else:
569 # Use a Sphinx link for this entry582 # Use a Sphinx link for this entry
570 link_text = ":doc:`%s <%s-help>`" % (item, item)583 link_text = gettext(":doc:`%s <%s-help>`") % (item, item)
571 see_also_links.append(link_text)584 see_also_links.append(link_text)
572 see_also = see_also_links585 see_also = see_also_links
573 result += ':See also: '586 result += gettext(':See also: %s') % ', '.join(see_also) + '\n'
574 result += ', '.join(see_also) + '\n'
575587
576 # If this will be rendered as plain text, convert it588 # If this will be rendered as plain text, convert it
577 if plain:589 if plain:
@@ -658,13 +670,14 @@
658 def run_argv_aliases(self, argv, alias_argv=None):670 def run_argv_aliases(self, argv, alias_argv=None):
659 """Parse the command line and run with extra aliases in alias_argv."""671 """Parse the command line and run with extra aliases in alias_argv."""
660 args, opts = parse_args(self, argv, alias_argv)672 args, opts = parse_args(self, argv, alias_argv)
673 self._setup_outf()
661674
662 # Process the standard options675 # Process the standard options
663 if 'help' in opts: # e.g. bzr add --help676 if 'help' in opts: # e.g. bzr add --help
664 sys.stdout.write(self.get_help_text())677 self.outf.write(self.get_help_text())
665 return 0678 return 0
666 if 'usage' in opts: # e.g. bzr add --usage679 if 'usage' in opts: # e.g. bzr add --usage
667 sys.stdout.write(self.get_help_text(verbose=False))680 self.outf.write(self.get_help_text(verbose=False))
668 return 0681 return 0
669 trace.set_verbosity_level(option._verbosity_level)682 trace.set_verbosity_level(option._verbosity_level)
670 if 'verbose' in self.supported_std_options:683 if 'verbose' in self.supported_std_options:
@@ -685,8 +698,6 @@
685 all_cmd_args = cmdargs.copy()698 all_cmd_args = cmdargs.copy()
686 all_cmd_args.update(cmdopts)699 all_cmd_args.update(cmdopts)
687700
688 self._setup_outf()
689
690 try:701 try:
691 return self.run(**all_cmd_args)702 return self.run(**all_cmd_args)
692 finally:703 finally:
@@ -751,6 +762,14 @@
751 return None762 return None
752 return getdoc(self)763 return getdoc(self)
753764
765 def gettext(self, message):
766 """Returns the gettext function used to translate this command's help.
767
768 Commands provided by plugins should override this to use their
769 own i18n system.
770 """
771 return gettext_per_paragraph(message)
772
754 def name(self):773 def name(self):
755 """Return the canonical name for this command.774 """Return the canonical name for this command.
756775
@@ -1041,8 +1060,8 @@
1041 argv = _specified_or_unicode_argv(argv)1060 argv = _specified_or_unicode_argv(argv)
1042 trace.mutter("bzr arguments: %r", argv)1061 trace.mutter("bzr arguments: %r", argv)
10431062
1044 opt_lsprof = opt_profile = opt_no_plugins = opt_builtin = \1063 opt_lsprof = opt_profile = opt_no_plugins = opt_builtin = \
1045 opt_no_aliases = False1064 opt_no_l10n = opt_no_aliases = False
1046 opt_lsprof_file = opt_coverage_dir = None1065 opt_lsprof_file = opt_coverage_dir = None
10471066
1048 # --no-plugins is handled specially at a very early stage. We need1067 # --no-plugins is handled specially at a very early stage. We need
@@ -1065,6 +1084,8 @@
1065 opt_no_plugins = True1084 opt_no_plugins = True
1066 elif a == '--no-aliases':1085 elif a == '--no-aliases':
1067 opt_no_aliases = True1086 opt_no_aliases = True
1087 elif a == '--no-l10n':
1088 opt_no_l10n = True
1068 elif a == '--builtin':1089 elif a == '--builtin':
1069 opt_builtin = True1090 opt_builtin = True
1070 elif a == '--concurrency':1091 elif a == '--concurrency':
@@ -1106,6 +1127,8 @@
11061127
1107 cmd = argv.pop(0)1128 cmd = argv.pop(0)
1108 cmd_obj = get_cmd_object(cmd, plugins_override=not opt_builtin)1129 cmd_obj = get_cmd_object(cmd, plugins_override=not opt_builtin)
1130 if opt_no_l10n:
1131 cmd.l10n = False
1109 run = cmd_obj.run_argv_aliases1132 run = cmd_obj.run_argv_aliases
1110 run_argv = [argv, alias_argv]1133 run_argv = [argv, alias_argv]
11111134
11121135
=== modified file 'bzrlib/export_pot.py'
--- bzrlib/export_pot.py 2011-05-12 12:19:35 +0000
+++ bzrlib/export_pot.py 2011-06-23 16:06:27 +0000
@@ -76,10 +76,12 @@
76 'msgid %s\n' % _normalize(s) +76 'msgid %s\n' % _normalize(s) +
77 'msgstr ""\n')77 'msgstr ""\n')
7878
79def _poentry_per_paragraph(outf, path, lineno, msgid):79def _poentry_per_paragraph(outf, path, lineno, msgid, filter=lambda x: False):
80 # TODO: How to split long help?80 # TODO: How to split long help?
81 paragraphs = msgid.split('\n\n')81 paragraphs = msgid.split('\n\n')
82 for p in paragraphs:82 for p in paragraphs:
83 if filter(p):
84 continue
83 _poentry(outf, path, lineno, p)85 _poentry(outf, path, lineno, p)
84 lineno += p.count('\n') + 286 lineno += p.count('\n') + 2
8587
@@ -145,7 +147,7 @@
145 'help of %r option of %r command' % (name, cmd.name()))147 'help of %r option of %r command' % (name, cmd.name()))
146148
147149
148def _write_command_help(outf, cmd_name, cmd):150def _write_command_help(outf, cmd):
149 path = inspect.getfile(cmd.__class__)151 path = inspect.getfile(cmd.__class__)
150 if path.endswith('.pyc'):152 if path.endswith('.pyc'):
151 path = path[:-1]153 path = path[:-1]
@@ -155,9 +157,16 @@
155 lineno = offsets[cmd.__doc__]157 lineno = offsets[cmd.__doc__]
156 doc = inspect.getdoc(cmd)158 doc = inspect.getdoc(cmd)
157159
158 _poentry_per_paragraph(outf, path, lineno, doc)160 def filter(p):
161 # ':Usage:' has special meaning in help topics.
162 # This is usage example of command and should not be translated.
163 if p.splitlines()[0] == ':Usage:':
164 return True
165
166 _poentry_per_paragraph(outf, path, lineno, doc, filter)
159 _command_options(outf, path, cmd)167 _command_options(outf, path, cmd)
160168
169
161def _command_helps(outf):170def _command_helps(outf):
162 """Extract docstrings from path.171 """Extract docstrings from path.
163172
@@ -172,7 +181,7 @@
172 if command.hidden:181 if command.hidden:
173 continue182 continue
174 note("Exporting messages from builtin command: %s", cmd_name)183 note("Exporting messages from builtin command: %s", cmd_name)
175 _write_command_help(outf, cmd_name, command)184 _write_command_help(outf, command)
176185
177 plugin_path = plugin.get_core_plugin_path()186 plugin_path = plugin.get_core_plugin_path()
178 core_plugins = glob(plugin_path + '/*/__init__.py')187 core_plugins = glob(plugin_path + '/*/__init__.py')
@@ -189,7 +198,7 @@
189 continue198 continue
190 note("Exporting messages from plugin command: %s in %s",199 note("Exporting messages from plugin command: %s in %s",
191 cmd_name, command.plugin_name())200 cmd_name, command.plugin_name())
192 _write_command_help(outf, cmd_name, command)201 _write_command_help(outf, command)
193202
194203
195def _error_messages(outf):204def _error_messages(outf):
196205
=== modified file 'bzrlib/help.py'
--- bzrlib/help.py 2011-05-11 16:52:02 +0000
+++ bzrlib/help.py 2011-06-23 16:06:27 +0000
@@ -22,7 +22,6 @@
22# TODO: `help commands --all` should show hidden commands22# TODO: `help commands --all` should show hidden commands
2323
24import sys24import sys
25import textwrap
2625
27from bzrlib import (26from bzrlib import (
28 commands as _mod_commands,27 commands as _mod_commands,
@@ -30,6 +29,7 @@
30 help_topics,29 help_topics,
31 osutils,30 osutils,
32 plugin,31 plugin,
32 utextwrap,
33 )33 )
3434
3535
@@ -96,7 +96,7 @@
96 else:96 else:
97 firstline = ''97 firstline = ''
98 helpstring = '%-*s %s%s' % (max_name, cmd_name, firstline, plugin_name)98 helpstring = '%-*s %s%s' % (max_name, cmd_name, firstline, plugin_name)
99 lines = textwrap.wrap(99 lines = utextwrap.wrap(
100 helpstring, subsequent_indent=indent,100 helpstring, subsequent_indent=indent,
101 width=width,101 width=width,
102 break_long_words=False)102 break_long_words=False)
103103
=== modified file 'bzrlib/help_topics/__init__.py'
--- bzrlib/help_topics/__init__.py 2010-11-06 10:55:58 +0000
+++ bzrlib/help_topics/__init__.py 2011-06-23 16:06:27 +0000
@@ -316,6 +316,7 @@
316--builtin Use the built-in version of a command, not the plugin version.316--builtin Use the built-in version of a command, not the plugin version.
317 This does not suppress other plugin effects.317 This does not suppress other plugin effects.
318--no-plugins Do not process any plugins.318--no-plugins Do not process any plugins.
319--no-l10n Do not translate messages.
319--concurrency Number of processes that can be run concurrently (selftest).320--concurrency Number of processes that can be run concurrently (selftest).
320321
321--profile Profile execution using the hotshot profiler.322--profile Profile execution using the hotshot profiler.
322323
=== modified file 'bzrlib/i18n.py'
--- bzrlib/i18n.py 2011-05-27 21:38:33 +0000
+++ bzrlib/i18n.py 2011-06-23 16:06:27 +0000
@@ -75,7 +75,7 @@
7575
76def uninstall():76def uninstall():
77 global _translation77 global _translation
78 _translation = _null_translation78 _translation = _gettext.NullTranslations()
7979
8080
81def _get_locale_dir():81def _get_locale_dir():
8282
=== modified file 'bzrlib/tests/test_export_pot.py'
--- bzrlib/tests/test_export_pot.py 2011-05-11 17:44:45 +0000
+++ bzrlib/tests/test_export_pot.py 2011-06-23 16:06:27 +0000
@@ -18,10 +18,14 @@
18import textwrap18import textwrap
1919
20from bzrlib import (20from bzrlib import (
21 commands,
21 export_pot,22 export_pot,
22 tests,23 tests,
23 )24 )
2425
26import re
27
28
25class TestEscape(tests.TestCase):29class TestEscape(tests.TestCase):
2630
27 def test_simple_escape(self):31 def test_simple_escape(self):
@@ -145,3 +149,45 @@
145 "EGG\\n"149 "EGG\\n"
146 msgstr ""\n150 msgstr ""\n
147 ''')151 ''')
152
153
154class TestExportCommandHelp(PoEntryTestCase):
155
156 def test_command_help(self):
157
158 class cmd_Demo(commands.Command):
159 __doc__ = """A sample command.
160
161 :Usage:
162 bzr demo
163
164 :Examples:
165 Example 1::
166
167 cmd arg1
168
169 Blah Blah Blah
170 """
171
172 export_pot._write_command_help(self._outf, cmd_Demo())
173 result = self._outf.getvalue()
174 # We don't concern abount filename and lineno here.
175 result = re.sub(r'^#: [^\n]+\n', '', result, flags=re.MULTILINE)
176
177 self.assertEqualDiff(
178 'msgid "A sample command."\n'
179 'msgstr ""\n'
180 '\n' # :Usage: should not be translated.
181 'msgid ""\n'
182 '":Examples:\\n"\n'
183 '" Example 1::"\n'
184 'msgstr ""\n'
185 '\n'
186 'msgid " cmd arg1"\n'
187 'msgstr ""\n'
188 '\n'
189 'msgid "Blah Blah Blah"\n'
190 'msgstr ""\n'
191 '\n',
192 result
193 )
148194
=== modified file 'bzrlib/tests/test_help.py'
--- bzrlib/tests/test_help.py 2011-01-12 01:01:53 +0000
+++ bzrlib/tests/test_help.py 2011-06-23 16:06:27 +0000
@@ -22,10 +22,14 @@
22 errors,22 errors,
23 help,23 help,
24 help_topics,24 help_topics,
25 i18n,
25 plugin,26 plugin,
26 tests,27 tests,
27 )28 )
2829
30from bzrlib.tests.test_i18n import ZzzTranslations
31import re
32
2933
30class TestHelp(tests.TestCase):34class TestHelp(tests.TestCase):
3135
@@ -295,6 +299,161 @@
295 ' Blah blah blah.\n\n')299 ' Blah blah blah.\n\n')
296300
297301
302class ZzzTranslationsForDoc(ZzzTranslations):
303
304 _section_pat = re.compile(':\w+:\\n\\s+')
305 _indent_pat = re.compile('\\s+')
306
307 def zzz(self, s):
308 m = self._section_pat.match(s)
309 if m is None:
310 m = self._indent_pat.match(s)
311 if m:
312 return u'%szz{{%s}}' % (m.group(0), s[m.end():])
313 return u'zz{{%s}}' % s
314
315
316class TestCommandHelpI18n(tests.TestCase):
317 """Tests for help on translated commands."""
318
319 def setUp(self):
320 super(TestCommandHelpI18n, self).setUp()
321 self.overrideAttr(i18n, '_translation', ZzzTranslationsForDoc())
322
323 def test_command_help_includes_see_also(self):
324 class cmd_WithSeeAlso(commands.Command):
325 __doc__ = """A sample command."""
326 _see_also = ['foo', 'bar']
327 cmd = cmd_WithSeeAlso()
328 helptext = cmd.get_help_text()
329 self.assertEndsWith(
330 helptext,
331 ' -v, --verbose Display more information.\n'
332 ' -q, --quiet Only display errors and warnings.\n'
333 ' -h, --help Show help message.\n'
334 '}}\n'
335 'zz{{:See also: bar, foo}}\n')
336
337 def test_get_help_text(self):
338 """Commands have a get_help_text method which returns their help."""
339 class cmd_Demo(commands.Command):
340 __doc__ = """A sample command."""
341 cmd = cmd_Demo()
342 helptext = cmd.get_help_text()
343 self.assertStartsWith(helptext,
344 'zz{{:Purpose: zz{{A sample command.}}}}\n'
345 'zz{{:Usage: bzr Demo}}\n')
346 self.assertEndsWith(helptext,
347 ' -h, --help Show help message.\n}}\n')
348
349 def test_command_with_additional_see_also(self):
350 class cmd_WithSeeAlso(commands.Command):
351 __doc__ = """A sample command."""
352 _see_also = ['foo', 'bar']
353 cmd = cmd_WithSeeAlso()
354 helptext = cmd.get_help_text(['gam'])
355 self.assertEndsWith(
356 helptext,
357 ' -v, --verbose Display more information.\n'
358 ' -q, --quiet Only display errors and warnings.\n'
359 ' -h, --help Show help message.\n'
360 '}}\n'
361 'zz{{:See also: bar, foo, gam}}\n')
362
363 def test_command_only_additional_see_also(self):
364 class cmd_WithSeeAlso(commands.Command):
365 __doc__ = """A sample command."""
366 cmd = cmd_WithSeeAlso()
367 helptext = cmd.get_help_text(['gam'])
368 self.assertEndsWith(
369 helptext,
370 'zz{{:Options:\n'
371 ' --usage Show usage message and options.\n'
372 ' -v, --verbose Display more information.\n'
373 ' -q, --quiet Only display errors and warnings.\n'
374 ' -h, --help Show help message.\n'
375 '}}\n'
376 'zz{{:See also: gam}}\n')
377
378
379 def test_help_custom_section_ordering(self):
380 """Custom descriptive sections should remain in the order given."""
381 class cmd_Demo(commands.Command):
382 __doc__ = """A sample command.
383
384 Blah blah blah.
385
386 :Formats:
387 Interesting stuff about formats.
388
389 :Examples:
390 Example 1::
391
392 cmd arg1
393
394 :Tips:
395 Clever things to keep in mind.
396 """
397 cmd = cmd_Demo()
398 helptext = cmd.get_help_text()
399 self.assertEqualDiff(
400 helptext,
401 'zz{{:Purpose: zz{{A sample command.}}}}\n'
402 'zz{{:Usage: bzr Demo}}\n'
403 '\n'
404 'zz{{:Options:\n'
405 ' --usage Show usage message and options.\n'
406 ' -v, --verbose Display more information.\n'
407 ' -q, --quiet Only display errors and warnings.\n'
408 ' -h, --help Show help message.\n'
409 '}}\n'
410 'Description:\n'
411 ' zz{{zz{{Blah blah blah.}}}}\n'
412 '\n'
413 'Formats:\n'
414 ' zz{{Interesting stuff about formats.}}\n'
415 '\n'
416 'Examples:\n'
417 ' zz{{Example 1::}}\n'
418 '\n'
419 ' zz{{cmd arg1}}\n'
420 '\n'
421 'Tips:\n'
422 ' zz{{Clever things to keep in mind.}}\n'
423 '\n')
424
425 def test_help_text_custom_usage(self):
426 """Help text may contain a custom usage section."""
427 class cmd_Demo(commands.Command):
428 __doc__ = """A sample command.
429
430 :Usage:
431 cmd Demo [opts] args
432
433 cmd Demo -h
434
435 Blah blah blah.
436 """
437 cmd = cmd_Demo()
438 helptext = cmd.get_help_text()
439 self.assertEquals(helptext,
440 'zz{{:Purpose: zz{{A sample command.}}}}\n'
441 'zz{{:Usage:\n'
442 ' zz{{cmd Demo [opts] args}}\n'
443 '\n'
444 ' zz{{cmd Demo -h}}\n'
445 '}}\n'
446 '\n'
447 'zz{{:Options:\n'
448 ' --usage Show usage message and options.\n'
449 ' -v, --verbose Display more information.\n'
450 ' -q, --quiet Only display errors and warnings.\n'
451 ' -h, --help Show help message.\n'
452 '}}\n'
453 'Description:\n'
454 ' zz{{zz{{Blah blah blah.}}}}\n\n')
455
456
298class TestRegisteredTopic(TestHelp):457class TestRegisteredTopic(TestHelp):
299 """Tests for the RegisteredTopic class."""458 """Tests for the RegisteredTopic class."""
300459