1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Basic support of internationalization through the generation of id-to-string tables
18 intrude import literal
19 private import annotation
20 private import parser_util
23 redef class ToolContext
24 # Main phase of `language`
25 var localize_phase
: Phase = new I18NPhase(self, [literal_phase
])
28 private class I18NPhase
31 redef fun process_annotated_node
(nmodule
, nat
) do
32 if not nat
.name
== "i18n" then return
34 if not nmodule
isa AModuledecl then
35 toolcontext
.error
(nmodule
.location
, "Error: The localized language can only be used on module declarations.")
39 var domain
= nmodule
.n_name
.n_id
.text
41 var lang
: nullable String = null
42 if nat
.n_args
.length
> 0 then
43 lang
= nat
.arg_as_string
(toolcontext
.modelbuilder
)
44 if lang
== null then return
47 var module_dir
= nmodule
.location
.file
.filename
.dirname
.realpath
48 var locale_dir
= module_dir
/ "languages"
50 if not locale_dir
.file_exists
then locale_dir
.mkdir
52 var amodule
= nmodule
.parent
.as(AModule)
54 var vi
= new StringFinder(domain
, locale_dir
, toolcontext
, amodule
)
55 vi
.enter_visit
(amodule
)
57 var module_name
= nmodule
.location
.file
.filename
.basename
(".nit")
59 var pot_path
= locale_dir
/ module_name
60 var arr
= vi
.strings
.values
.to_a
61 var po
= new POFile(arr
)
62 po
.write_template
(pot_path
)
65 for i
in po
.strings
do
69 var lang_dir
= locale_dir
/ lang
70 if not lang_dir
.file_exists
then lang_dir
.mkdir
72 var messages_dir
= lang_dir
/ "LC_MESSAGES"
73 if not messages_dir
.file_exists
then messages_dir
.mkdir
75 po
.write_to_file
(messages_dir
/ module_name
)
78 var lit
= new LiteralVisitor(toolcontext
)
79 lit
.enter_visit
(amodule
)
83 private class StringFinder
86 # Strings in the file, used to generate .pot and .po files
87 var strings
= new HashMap[String, PObject]
89 # Domain of the strings to internationalize
92 # Location of the languages file
93 var languages_location
: String
95 # Context for the visitor, used only for the parse_expr
96 var toolcontext
: ToolContext
98 # The module we are working on
103 n
.accept_string_finder
(self)
107 redef fun enter_visit
(e
) do
108 if e
isa AAnnotation then return
112 # Adds a String to the list of strings of the module
114 # The string needs to be pre-formatted to C standards (escape_to_c)
115 fun add_string
(s
: String, loc
: Location) do
116 var locstr
= "{amodule.mmodule.mgroup.name}::{amodule.mmodule.name} {loc.line_start}--{loc.column_start}:{loc.column_end}"
117 if not strings
.has_key
(s
) then
118 var po
= new PObject([locstr
], s
, "")
121 strings
[s
].locations
.push locstr
127 private fun accept_string_finder
(v
: StringFinder) do end
130 redef class AStringExpr
132 redef fun accept_string_finder
(v
) do
133 var str
= value
.escape_to_gettext
134 var code
= "\"{str}\
".get_translation(\"{v.domain}\
", \"{v.languages_location}\
")"
135 var parse
= v
.toolcontext
.parse_expr
(code
)
137 v
.add_string
(str
, location
)
141 redef class ASuperstringExpr
143 redef fun accept_string_finder
(v
) do
145 var exprs
= new Array[AExpr]
147 if i
isa AStartStringExpr or i
isa AEndStringExpr or i
isa AMidStringExpr then
148 assert i
isa AStringFormExpr
150 fmt
+= str
.replace
("%", "%%")
154 fmt
+= (exprs
.length-1
).to_s
157 fmt
= fmt
.escape_to_gettext
158 v
.add_string
(fmt
, location
)
159 var code
= "\"{fmt}\
".get_translation(\"{v.domain}\
", \"{v.languages_location}\
").format()"
160 var parse
= v
.toolcontext
.parse_expr
(code
)
161 if not parse
isa ACallExpr then
162 v
.toolcontext
.error
(location
, "Fatal error in i18n annotation, the parsed superstring could not be generated properly")
165 var parse_exprs
= parse
.n_args
.n_exprs
166 parse_exprs
.add_all exprs
173 # Locations are optional, they just serve for translation purposes
174 # to help the translator with the context of the message if necessary
176 # msgid and msgstr are the map of translate to translated strings in the po file.
178 # Array since the same string can be encountered at several places
179 var locations
: Array[String]
180 # Identifier of the string to translate (i.e. the string itself)
181 var msgid
: String is writable
182 # Translation of the string
183 var msgstr
: String is writable
186 # A GNU gettext .po/.pot file
190 # Map of the strings's `msgid` and `msgstr`
192 # Read from a PO file
193 var strings
: Array[PObject]
195 redef fun write_to_file
(path
) do
196 if not path
.has_suffix
(".po") then path
+= ".po"
200 redef fun write_to
(ofs
) do
202 ofs
.write
("#: {i.locations.join(", ")}\n")
203 ofs
.write
("msgid \"{i.msgid}\
"\n")
204 ofs
.write
("msgstr \"{i.msgstr}\
"\n\n")
206 ofs
.write
("# Generated file, do not modify\n")
209 # Writes the information of the POFile to a .pot template file
210 fun write_template
(path
: String) do
211 if not path
.has_suffix
(".pot") then path
+= ".pot"
212 var f
= new FileWriter.open
(path
)
219 private fun escape_to_gettext
: String
221 return escape_to_c
.replace
("\{", "\\\{").replace("\}", "\\\}")