compiler: Updated toolchain for proper byte literal support
[nit.git] / src / frontend / i18n_phase.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 # Basic support of internationalization through the generation of id-to-string tables
16 module i18n_phase
17
18 intrude import literal
19 private import annotation
20 private import parser_util
21 import astbuilder
22
23 redef class ToolContext
24 # Main phase of `language`
25 var localize_phase: Phase = new I18NPhase(self, [literal_phase])
26 end
27
28 private class I18NPhase
29 super Phase
30
31 redef fun process_annotated_node(nmodule, nat) do
32 if not nat.name == "i18n" then return
33
34 if not nmodule isa AModuledecl then
35 toolcontext.error(nmodule.location, "Error: The localized language can only be used on module declarations.")
36 return
37 end
38
39 var domain = nmodule.n_name.n_id.text
40
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
45 end
46
47 var module_dir = nmodule.location.file.filename.dirname.realpath
48 var locale_dir = module_dir / "languages"
49
50 if not locale_dir.file_exists then locale_dir.mkdir
51
52 var amodule = nmodule.parent.as(AModule)
53
54 var vi = new StringFinder(domain, locale_dir, toolcontext, amodule)
55 vi.enter_visit(amodule)
56
57 var module_name = nmodule.location.file.filename.basename(".nit")
58
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)
63
64 if lang != null then
65 for i in po.strings do
66 i.msgstr = i.msgid
67 end
68
69 var lang_dir = locale_dir / lang
70 if not lang_dir.file_exists then lang_dir.mkdir
71
72 var messages_dir = lang_dir / "LC_MESSAGES"
73 if not messages_dir.file_exists then messages_dir.mkdir
74
75 po.write_to_file(messages_dir / module_name)
76 end
77
78 var lit = new LiteralVisitor(toolcontext)
79 lit.enter_visit(amodule)
80 end
81 end
82
83 private class StringFinder
84 super Visitor
85
86 # Strings in the file, used to generate .pot and .po files
87 var strings = new HashMap[String, PObject]
88
89 # Domain of the strings to internationalize
90 var domain: String
91
92 # Location of the languages file
93 var languages_location: String
94
95 # Context for the visitor, used only for the parse_expr
96 var toolcontext: ToolContext
97
98 # The module we are working on
99 var amodule: AModule
100
101 redef fun visit(n)
102 do
103 n.accept_string_finder(self)
104 n.visit_all(self)
105 end
106
107 redef fun enter_visit(e) do
108 if e isa AAnnotation then return
109 super
110 end
111
112 # Adds a String to the list of strings of the module
113 #
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, "")
119 strings[s] = po
120 else
121 strings[s].locations.push locstr
122 end
123 end
124 end
125
126 redef class ANode
127 private fun accept_string_finder(v: StringFinder) do end
128 end
129
130 redef class AStringExpr
131
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")
135 replace_with(parse)
136 v.add_string(str, location)
137 end
138 end
139
140 redef class ASuperstringExpr
141
142 redef fun accept_string_finder(v) do
143 var fmt = ""
144 var exprs = new Array[AExpr]
145 for i in n_exprs do
146 if i isa AStringFormExpr then
147 fmt += i.value.as(not null)
148 else
149 fmt += "%"
150 exprs.push i
151 fmt += exprs.length.to_s
152 end
153 end
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")
159 return
160 end
161 var parse_exprs = parse.n_args.n_exprs
162 parse_exprs.add_all exprs
163 replace_with parse
164 end
165 end
166
167 # .po file entry
168 #
169 # Locations are optional, they just serve for translation purposes
170 # to help the translator with the context of the message if necessary
171 #
172 # msgid and msgstr are the map of translate to translated strings in the po file.
173 class PObject
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
180 end
181
182 # A GNU gettext .po/.pot file
183 class POFile
184 super Writable
185
186 # Map of the strings's `msgid` and `msgstr`
187 #
188 # Read from a PO file
189 var strings: Array[PObject]
190
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)
194 strings.add_all sm
195 end
196
197 redef fun write_to_file(path) do
198 if not path.has_suffix(".po") then path += ".po"
199 super path
200 end
201
202 redef fun write_to(ofs) do
203 for i in strings do
204 ofs.write("#: {i.locations.join(", ")}\n")
205 ofs.write("msgid \"{i.msgid}\"\n")
206 ofs.write("msgstr \"{i.msgstr}\"\n\n")
207 end
208 ofs.write("# Generated file, do not modify\n")
209 end
210
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)
215 write_to(f)
216 f.close
217 end
218 end