1# Copyright 2003, 2004, 2005, 2006 Vladimir Prus
2# Distributed under the Boost Software License, Version 1.0.
3# (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)
4
5#  This module support GNU gettext internationalization utilities.
6#
7#  It provides two main target rules: 'gettext.catalog', used for
8#  creating machine-readable catalogs from translations files, and
9#  'gettext.update', used for update translation files from modified
10#  sources.
11#
12#  To add i18n support to your application you should follow these
13#  steps.
14#
15#  - Decide on a file name which will contain translations and
16#  what main target name will be used to update it. For example::
17#
18#    gettext.update update-russian : russian.po a.cpp my_app ;
19#
20#  - Create the initial translation file by running::
21#
22#    bjam update-russian
23#
24#  - Edit russian.po. For example, you might change fields like LastTranslator.
25#
26#  - Create a main target for final message catalog::
27#
28#    gettext.catalog russian : russian.po ;
29#
30#  The machine-readable catalog will be updated whenever you update
31#  "russian.po". The "russian.po" file will be updated only on explicit
32#  request. When you're ready to update translations, you should
33#
34#  - Run::
35#
36#    bjam update-russian
37#
38#  - Edit "russian.po" in appropriate editor.
39#
40#  The next bjam run will convert "russian.po" into machine-readable form.
41#
42#  By default, translations are marked by 'i18n' call. The 'gettext.keyword'
43#  feature can be used to alter this.
44
45
46import targets ;
47import property-set ;
48import virtual-target ;
49import "class" : new ;
50import project ;
51import type ;
52import generators ;
53import errors ;
54import feature : feature ;
55import toolset : flags ;
56import regex ;
57
58.path = "" ;
59
60# Initializes the gettext module.
61rule init ( path ? # Path where all tools are located. If not specified,
62                   # they should be in PATH.
63          )
64{
65    if $(.initialized) && $(.path) != $(path)
66    {
67        errors.error "Attempt to reconfigure with different path" ;
68    }
69    .initialized = true ;
70    if $(path)
71    {
72        .path = $(path)/ ;
73    }
74}
75
76# Creates a main target 'name', which, when updated, will cause
77# file 'existing-translation' to be updated with translations
78# extracted from 'sources'. It's possible to specify main target
79# in sources --- it which case all target from dependency graph
80# of those main targets will be scanned, provided they are of
81# appropricate type. The 'gettext.types' feature can be used to
82# control the types.
83#
84# The target will be updated only if explicitly requested on the
85# command line.
86rule update ( name : existing-translation sources + : requirements * )
87{
88    local project = [ project.current ] ;
89
90    targets.main-target-alternative
91      [ new typed-target $(name) : $(project) : gettext.UPDATE :
92        $(existing-translation) $(sources)
93        : [ targets.main-target-requirements $(requirements) : $(project) ]
94      ] ;
95    $(project).mark-target-as-explicit $(name) ;
96}
97
98
99# The human editable source, containing translation.
100type.register gettext.PO : po ;
101# The machine readable message catalog.
102type.register gettext.catalog : mo ;
103# Intermediate type produce by extracting translations from
104# sources.
105type.register gettext.POT : pot ;
106# Pseudo type used to invoke update-translations generator
107type.register gettext.UPDATE ;
108
109# Identifies the keyword that should be used when scanning sources.
110# Default: i18n
111feature gettext.keyword : : free ;
112# Contains space-separated list of sources types which should be scanned.
113# Default: "C CPP"
114feature gettext.types : : free ;
115
116generators.register-standard gettext.compile : gettext.PO : gettext.catalog ;
117
118class update-translations-generator : generator
119{
120    import regex : split ;
121    import property-set ;
122
123    rule __init__ ( * : * )
124    {
125        generator.__init__ $(1) : $(2) : $(3) : $(4) : $(5) : $(6) : $(7) : $(8) : $(9) ;
126    }
127
128    # The rule should be called with at least two sources. The first source
129    # is the translation (.po) file to update. The remaining sources are targets
130    # which should be scanned for new messages. All sources files for those targets
131    # will be found and passed to the 'xgettext' utility, which extracts the
132    # messages for localization. Those messages will be merged to the .po file.
133    rule run ( project name ? : property-set : sources * : multiple ? )
134    {
135        local types = [ $(property-set).get <gettext.types> ] ;
136        types ?= "C CPP" ;
137        types = [ regex.split $(types) " " ] ;
138
139        local keywords = [ $(property-set).get <gettext.keyword> ] ;
140        property-set = [ property-set.create $(keywords:G=<gettext.keyword>) ] ;
141
142        # First deterime the list of sources that must be scanned for
143        # messages.
144        local all-sources ;
145        # CONSIDER: I'm not sure if the logic should be the same as for 'stage':
146        # i.e. following dependency properties as well.
147        for local s in $(sources[2-])
148        {
149            all-sources += [ virtual-target.traverse $(s) : : include-sources ] ;
150        }
151        local right-sources ;
152        for local s in $(all-sources)
153        {
154            if [ $(s).type ] in $(types)
155            {
156                right-sources += $(s) ;
157            }
158        }
159
160        local .constructed ;
161        if $(right-sources)
162        {
163            # Create the POT file, which will contain list of messages extracted
164            # from the sources.
165            local extract =
166              [ new action $(right-sources) : gettext.extract : $(property-set) ] ;
167            local new-messages = [ new file-target $(name) : gettext.POT
168              : $(project) : $(extract) ] ;
169
170            # Create a notfile target which will update the existing translation file
171            # with new messages.
172            local a = [ new action $(sources[1]) $(new-messages)
173              : gettext.update-po-dispatch ] ;
174            local r = [ new notfile-target $(name) : $(project) : $(a) ] ;
175            .constructed = [ virtual-target.register $(r) ] ;
176        }
177        else
178        {
179            errors.error "No source could be scanned by gettext tools" ;
180        }
181        return $(.constructed) ;
182    }
183}
184generators.register [ new update-translations-generator gettext.update : : gettext.UPDATE ] ;
185
186flags gettext.extract KEYWORD <gettext.keyword> ;
187actions extract
188{
189    $(.path)xgettext -k$(KEYWORD:E=i18n) -o $(<) $(>)
190}
191
192# Does really updating of po file. The tricky part is that
193# we're actually updating one of the sources:
194# $(<) is the NOTFILE target we're updating
195# $(>[1]) is the PO file to be really updated.
196# $(>[2]) is the PO file created from sources.
197#
198# When file to be updated does not exist (during the
199# first run), we need to copy the file created from sources.
200# In all other cases, we need to update the file.
201rule update-po-dispatch
202{
203    NOCARE $(>[1]) ;
204    gettext.create-po $(<) : $(>) ;
205    gettext.update-po $(<) : $(>) ;
206    _ on $(<) = " " ;
207    ok on $(<) = "" ;
208    EXISTING_PO on $(<) = $(>[1]) ;
209}
210
211# Due to fancy interaction of existing and updated, this rule can be called with
212# one source, in which case we copy the lonely source into EXISTING_PO, or with
213# two sources, in which case the action body expands to nothing. I'd really like
214# to have "missing" action modifier.
215actions quietly existing updated create-po bind EXISTING_PO
216{
217    cp$(_)"$(>[1])"$(_)"$(EXISTING_PO)"$($(>[2]:E=ok))
218}
219
220actions updated update-po bind EXISTING_PO
221{
222    $(.path)msgmerge$(_)-U$(_)"$(EXISTING_PO)"$(_)"$(>[1])"
223}
224
225actions gettext.compile
226{
227    $(.path)msgfmt -o $(<) $(>)
228}
229
230IMPORT $(__name__) : update : : gettext.update ;
231