Merge: i18n annotation
[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 end
112
113 redef class ANode
114 private fun accept_string_finder(v: StringFinder) do end
115 end
116
117 redef class AStringExpr
118
119 redef fun accept_string_finder(v) do
120 var str = value.as(not null).escape_to_c
121 var parse = v.toolcontext.parse_expr("\"{str}\".get_translation(\"{v.domain}\", \"{v.languages_location}\").unescape_nit")
122 var loc = location
123 var locstr = "{v.amodule.mmodule.mgroup.name}::{v.amodule.mmodule.name} {loc.line_start}--{loc.column_start}:{loc.column_end}"
124 if not v.strings.has_key(str) then
125 var po = new PObject([locstr], str, "")
126 v.strings[str] = po
127 else
128 v.strings[str].locations.push locstr
129 end
130 replace_with(parse)
131 end
132 end
133
134 # .po file entry
135 #
136 # Locations are optional, they just serve for translation purposes
137 # to help the translator with the context of the message if necessary
138 #
139 # msgid and msgstr are the map of translate to translated strings in the po file.
140 class PObject
141 # Array since the same string can be encountered at several places
142 var locations: Array[String]
143 # Identifier of the string to translate (i.e. the string itself)
144 var msgid: String is writable
145 # Translation of the string
146 var msgstr: String is writable
147 end
148
149 # A GNU gettext .po/.pot file
150 class POFile
151 super Writable
152
153 # Map of the strings's `msgid` and `msgstr`
154 #
155 # Read from a PO file
156 var strings: Array[PObject]
157
158 # Creates a PO file with strings built-in
159 init with_strings(sm: Array[PObject])do
160 strings = new Array[PObject].with_capacity(sm.length)
161 strings.add_all sm
162 end
163
164 redef fun write_to_file(path) do
165 if not path.has_suffix(".po") then path += ".po"
166 super path
167 end
168
169 redef fun write_to(ofs) do
170 for i in strings do
171 ofs.write("#: {i.locations.join(", ")}\n")
172 ofs.write("msgid \"{i.msgid}\"\n")
173 ofs.write("msgstr \"{i.msgstr}\"\n\n")
174 end
175 ofs.write("# Generated file, do not modify\n")
176 end
177
178 # Writes the information of the POFile to a .pot template file
179 fun write_template(path: String) do
180 if not path.has_suffix(".pot") then path += ".pot"
181 var f = new FileWriter.open(path)
182 write_to(f)
183 f.close
184 end
185 end