compiler: Added prefixed and suffixed `String` 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(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.escape_to_gettext
134 var code = "\"{str}\".get_translation(\"{v.domain}\", \"{v.languages_location}\")"
135 var parse = v.toolcontext.parse_expr(code)
136 replace_with(parse)
137 v.add_string(str, location)
138 end
139 end
140
141 redef class ASuperstringExpr
142
143 redef fun accept_string_finder(v) do
144 var fmt = ""
145 var exprs = new Array[AExpr]
146 for i in n_exprs do
147 if i isa AStartStringExpr or i isa AEndStringExpr or i isa AMidStringExpr then
148 assert i isa AStringFormExpr
149 var str = i.value
150 fmt += str.replace("%", "%%")
151 else
152 fmt += "%"
153 exprs.push i
154 fmt += (exprs.length-1).to_s
155 end
156 end
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")
163 return
164 end
165 var parse_exprs = parse.n_args.n_exprs
166 parse_exprs.add_all exprs
167 replace_with parse
168 end
169 end
170
171 # .po file entry
172 #
173 # Locations are optional, they just serve for translation purposes
174 # to help the translator with the context of the message if necessary
175 #
176 # msgid and msgstr are the map of translate to translated strings in the po file.
177 class PObject
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
184 end
185
186 # A GNU gettext .po/.pot file
187 class POFile
188 super Writable
189
190 # Map of the strings's `msgid` and `msgstr`
191 #
192 # Read from a PO file
193 var strings: Array[PObject]
194
195 redef fun write_to_file(path) do
196 if not path.has_suffix(".po") then path += ".po"
197 super path
198 end
199
200 redef fun write_to(ofs) do
201 for i in strings do
202 ofs.write("#: {i.locations.join(", ")}\n")
203 ofs.write("msgid \"{i.msgid}\"\n")
204 ofs.write("msgstr \"{i.msgstr}\"\n\n")
205 end
206 ofs.write("# Generated file, do not modify\n")
207 end
208
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)
213 write_to(f)
214 f.close
215 end
216 end
217
218 redef class Text
219 private fun escape_to_gettext: String
220 do
221 return escape_to_c.replace("\{", "\\\{").replace("\}", "\\\}")
222 end
223 end