Blob Blame History Raw
# Copyright 2003, 2004, 2005, 2006 Vladimir Prus
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)

#  This module support GNU gettext internationalization utilities.
#
#  It provides two main target rules: 'gettext.catalog', used for
#  creating machine-readable catalogs from translations files, and
#  'gettext.update', used for update translation files from modified
#  sources.
#
#  To add i18n support to your application you should follow these
#  steps.
#
#  - Decide on a file name which will contain translations and
#  what main target name will be used to update it. For example::
#
#    gettext.update update-russian : russian.po a.cpp my_app ;
#
#  - Create the initial translation file by running::
#
#    bjam update-russian
#
#  - Edit russian.po. For example, you might change fields like LastTranslator.
#
#  - Create a main target for final message catalog::
#
#    gettext.catalog russian : russian.po ;
#
#  The machine-readable catalog will be updated whenever you update
#  "russian.po". The "russian.po" file will be updated only on explicit
#  request. When you're ready to update translations, you should
#
#  - Run::
#
#    bjam update-russian
#
#  - Edit "russian.po" in appropriate editor.
#
#  The next bjam run will convert "russian.po" into machine-readable form.
#
#  By default, translations are marked by 'i18n' call. The 'gettext.keyword'
#  feature can be used to alter this.


import targets ;
import property-set ;
import virtual-target ;
import "class" : new ;
import project ;
import type ;
import generators ;
import errors ;
import feature : feature ;
import toolset : flags ;
import regex ;

.path = "" ;

# Initializes the gettext module.
rule init ( path ? # Path where all tools are located. If not specified,
                   # they should be in PATH.
          )
{
    if $(.initialized) && $(.path) != $(path)
    {
        errors.error "Attempt to reconfigure with different path" ;
    }
    .initialized = true ;
    if $(path)
    {
        .path = $(path)/ ;
    }
}

# Creates a main target 'name', which, when updated, will cause
# file 'existing-translation' to be updated with translations
# extracted from 'sources'. It's possible to specify main target
# in sources --- it which case all target from dependency graph
# of those main targets will be scanned, provided they are of
# appropricate type. The 'gettext.types' feature can be used to
# control the types.
#
# The target will be updated only if explicitly requested on the
# command line.
rule update ( name : existing-translation sources + : requirements * )
{
    local project = [ project.current ] ;

    targets.main-target-alternative
      [ new typed-target $(name) : $(project) : gettext.UPDATE :
        $(existing-translation) $(sources)
        : [ targets.main-target-requirements $(requirements) : $(project) ]
      ] ;
    $(project).mark-target-as-explicit $(name) ;
}


# The human editable source, containing translation.
type.register gettext.PO : po ;
# The machine readable message catalog.
type.register gettext.catalog : mo ;
# Intermediate type produce by extracting translations from
# sources.
type.register gettext.POT : pot ;
# Pseudo type used to invoke update-translations generator
type.register gettext.UPDATE ;

# Identifies the keyword that should be used when scanning sources.
# Default: i18n
feature gettext.keyword : : free ;
# Contains space-separated list of sources types which should be scanned.
# Default: "C CPP"
feature gettext.types : : free ;

generators.register-standard gettext.compile : gettext.PO : gettext.catalog ;

class update-translations-generator : generator
{
    import regex : split ;
    import property-set ;

    rule __init__ ( * : * )
    {
        generator.__init__ $(1) : $(2) : $(3) : $(4) : $(5) : $(6) : $(7) : $(8) : $(9) ;
    }

    # The rule should be called with at least two sources. The first source
    # is the translation (.po) file to update. The remaining sources are targets
    # which should be scanned for new messages. All sources files for those targets
    # will be found and passed to the 'xgettext' utility, which extracts the
    # messages for localization. Those messages will be merged to the .po file.
    rule run ( project name ? : property-set : sources * : multiple ? )
    {
        local types = [ $(property-set).get <gettext.types> ] ;
        types ?= "C CPP" ;
        types = [ regex.split $(types) " " ] ;

        local keywords = [ $(property-set).get <gettext.keyword> ] ;
        property-set = [ property-set.create $(keywords:G=<gettext.keyword>) ] ;

        # First deterime the list of sources that must be scanned for
        # messages.
        local all-sources ;
        # CONSIDER: I'm not sure if the logic should be the same as for 'stage':
        # i.e. following dependency properties as well.
        for local s in $(sources[2-])
        {
            all-sources += [ virtual-target.traverse $(s) : : include-sources ] ;
        }
        local right-sources ;
        for local s in $(all-sources)
        {
            if [ $(s).type ] in $(types)
            {
                right-sources += $(s) ;
            }
        }

        local .constructed ;
        if $(right-sources)
        {
            # Create the POT file, which will contain list of messages extracted
            # from the sources.
            local extract =
              [ new action $(right-sources) : gettext.extract : $(property-set) ] ;
            local new-messages = [ new file-target $(name) : gettext.POT
              : $(project) : $(extract) ] ;

            # Create a notfile target which will update the existing translation file
            # with new messages.
            local a = [ new action $(sources[1]) $(new-messages)
              : gettext.update-po-dispatch ] ;
            local r = [ new notfile-target $(name) : $(project) : $(a) ] ;
            .constructed = [ virtual-target.register $(r) ] ;
        }
        else
        {
            errors.error "No source could be scanned by gettext tools" ;
        }
        return $(.constructed) ;
    }
}
generators.register [ new update-translations-generator gettext.update : : gettext.UPDATE ] ;

flags gettext.extract KEYWORD <gettext.keyword> ;
actions extract
{
    $(.path)xgettext -k$(KEYWORD:E=i18n) -o $(<) $(>)
}

# Does realy updating of po file. The tricky part is that
# we're actually updating one of the sources:
# $(<) is the NOTFILE target we're updating
# $(>[1]) is the PO file to be really updated.
# $(>[2]) is the PO file created from sources.
#
# When file to be updated does not exist (during the
# first run), we need to copy the file created from sources.
# In all other cases, we need to update the file.
rule update-po-dispatch
{
    NOCARE $(>[1]) ;
    gettext.create-po $(<) : $(>) ;
    gettext.update-po $(<) : $(>) ;
    _ on $(<) = " " ;
    ok on $(<) = "" ;
    EXISTING_PO on $(<) = $(>[1]) ;
}

# Due to fancy interaction of existing and updated, this rule can be called with
# one source, in which case we copy the lonely source into EXISTING_PO, or with
# two sources, in which case the action body expands to nothing. I'd really like
# to have "missing" action modifier.
actions quietly existing updated create-po bind EXISTING_PO
{
    cp$(_)"$(>[1])"$(_)"$(EXISTING_PO)"$($(>[2]:E=ok))
}

actions updated update-po bind EXISTING_PO
{
    $(.path)msgmerge$(_)-U$(_)"$(EXISTING_PO)"$(_)"$(>[1])"
}

actions gettext.compile
{
    $(.path)msgfmt -o $(<) $(>)
}

IMPORT $(__name__) : update : : gettext.update ;