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.with_strings
(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
.as(not null).escape_to_c
134 var parse
= v
.toolcontext
.parse_expr
("\"{str}\
".get_translation(\"{v.domain}\
", \"{v.languages_location}\
").unescape_nit")
136 v
.add_string
(str
, location
)
140 redef class ASuperstringExpr
142 redef fun accept_string_finder
(v
) do
144 var exprs
= new Array[AExpr]
146 if i
isa AStringFormExpr then
147 fmt
+= i
.value
.as(not null)
151 fmt
+= exprs
.length
.to_s
154 fmt
= fmt
.escape_to_c
155 v
.add_string
(fmt
, location
)
156 var parse
= v
.toolcontext
.parse_expr
("\"{fmt}\
".get_translation(\"{v.domain}\
", \"{v.languages_location}\
").unescape_nit.format()")
157 if not parse
isa ACallExpr then
158 v
.toolcontext
.error
(location
, "Fatal error in i18n annotation, the parsed superstring could not be generated properly")
161 var parse_exprs
= parse
.n_args
.n_exprs
162 parse_exprs
.add_all exprs
169 # Locations are optional, they just serve for translation purposes
170 # to help the translator with the context of the message if necessary
172 # msgid and msgstr are the map of translate to translated strings in the po file.
174 # Array since the same string can be encountered at several places
175 var locations
: Array[String]
176 # Identifier of the string to translate (i.e. the string itself)
177 var msgid
: String is writable
178 # Translation of the string
179 var msgstr
: String is writable
182 # A GNU gettext .po/.pot file
186 # Map of the strings's `msgid` and `msgstr`
188 # Read from a PO file
189 var strings
: Array[PObject]
191 # Creates a PO file with strings built-in
192 init with_strings
(sm
: Array[PObject])do
193 strings
= new Array[PObject].with_capacity
(sm
.length
)
197 redef fun write_to_file
(path
) do
198 if not path
.has_suffix
(".po") then path
+= ".po"
202 redef fun write_to
(ofs
) do
204 ofs
.write
("#: {i.locations.join(", ")}\n")
205 ofs
.write
("msgid \"{i.msgid}\
"\n")
206 ofs
.write
("msgstr \"{i.msgstr}\
"\n\n")
208 ofs
.write
("# Generated file, do not modify\n")
211 # Writes the information of the POFile to a .pot template file
212 fun write_template
(path
: String) do
213 if not path
.has_suffix
(".pot") then path
+= ".pot"
214 var f
= new FileWriter.open
(path
)