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