compiler: Added i18n phase and annotation
authorLucas Bajolet <r4pass@hotmail.com>
Tue, 21 Apr 2015 20:24:39 +0000 (16:24 -0400)
committerLucas Bajolet <r4pass@hotmail.com>
Mon, 27 Apr 2015 14:51:07 +0000 (10:51 -0400)
Signed-off-by: Lucas Bajolet <r4pass@hotmail.com>

src/frontend/frontend.nit
src/frontend/i18n_phase.nit [new file with mode: 0644]

index 28e36bd..68ea75b 100644 (file)
@@ -26,6 +26,7 @@ import deriving
 import check_annotation
 import glsl_validation
 import parallelization_phase
+import i18n_phase
 
 redef class ToolContext
        # FIXME: there is conflict in linex in nitc, so use this trick to force invocation
diff --git a/src/frontend/i18n_phase.nit b/src/frontend/i18n_phase.nit
new file mode 100644 (file)
index 0000000..f068dc1
--- /dev/null
@@ -0,0 +1,185 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Basic support of internationalization through the generation of id-to-string tables
+module i18n_phase
+
+intrude import literal
+private import annotation
+private import parser_util
+import astbuilder
+
+redef class ToolContext
+       # Main phase of `language`
+       var localize_phase: Phase = new I18NPhase(self, [literal_phase])
+end
+
+private class I18NPhase
+       super Phase
+
+       redef fun process_annotated_node(nmodule, nat) do
+               if not nat.name == "i18n" then return
+
+               if not nmodule isa AModuledecl then
+                       toolcontext.error(nmodule.location, "Error: The localized language can only be used on module declarations.")
+                       return
+               end
+
+               var domain = nmodule.n_name.n_id.text
+
+               var lang: nullable String = null
+               if nat.n_args.length > 0 then
+                       lang = nat.arg_as_string(toolcontext.modelbuilder)
+                       if lang == null then return
+               end
+
+               var module_dir = nmodule.location.file.filename.dirname.realpath
+               var locale_dir = module_dir / "languages"
+
+               if not locale_dir.file_exists then locale_dir.mkdir
+
+               var amodule = nmodule.parent.as(AModule)
+
+               var vi = new StringFinder(domain, locale_dir, toolcontext, amodule)
+               vi.enter_visit(amodule)
+
+               var module_name = nmodule.location.file.filename.basename(".nit")
+
+               var pot_path = locale_dir / module_name
+               var arr = vi.strings.values.to_a
+               var po = new POFile.with_strings(arr)
+               po.write_template(pot_path)
+
+               if lang != null then
+                       for i in po.strings do
+                               i.msgstr = i.msgid
+                       end
+
+                       var lang_dir = locale_dir / lang
+                       if not lang_dir.file_exists then lang_dir.mkdir
+
+                       var messages_dir = lang_dir / "LC_MESSAGES"
+                       if not messages_dir.file_exists then messages_dir.mkdir
+
+                       po.write_to_file(messages_dir / module_name)
+               end
+
+               var lit = new LiteralVisitor(toolcontext)
+               lit.enter_visit(amodule)
+       end
+end
+
+private class StringFinder
+       super Visitor
+
+       # Strings in the file, used to generate .pot and .po files
+       var strings = new HashMap[String, PObject]
+
+       # Domain of the strings to internationalize
+       var domain: String
+
+       # Location of the languages file
+       var languages_location: String
+
+       # Context for the visitor, used only for the parse_expr
+       var toolcontext: ToolContext
+
+       # The module we are working on
+       var amodule: AModule
+
+       redef fun visit(n)
+       do
+               n.accept_string_finder(self)
+               n.visit_all(self)
+       end
+
+       redef fun enter_visit(e) do
+               if e isa AAnnotation then return
+               super
+       end
+end
+
+redef class ANode
+       private fun accept_string_finder(v: StringFinder) do end
+end
+
+redef class AStringExpr
+
+       redef fun accept_string_finder(v) do
+               var str = value.as(not null).escape_to_c
+               var parse = v.toolcontext.parse_expr("\"{str}\".get_translation(\"{v.domain}\", \"{v.languages_location}\").unescape_nit")
+               var loc = location
+               var locstr = "{v.amodule.mmodule.mgroup.name}::{v.amodule.mmodule.name} {loc.line_start}--{loc.column_start}:{loc.column_end}"
+               if not v.strings.has_key(str) then
+                       var po = new PObject([locstr], str, "")
+                       v.strings[str] = po
+               else
+                       v.strings[str].locations.push locstr
+               end
+               replace_with(parse)
+       end
+end
+
+# .po file entry
+#
+# Locations are optional, they just serve for translation purposes
+# to help the translator with the context of the message if necessary
+#
+# msgid and msgstr are the map of translate to translated strings in the po file.
+class PObject
+       # Array since the same string can be encountered at several places
+       var locations: Array[String]
+       # Identifier of the string to translate (i.e. the string itself)
+       var msgid: String is writable
+       # Translation of the string
+       var msgstr: String is writable
+end
+
+# A GNU gettext .po/.pot file
+class POFile
+       super Writable
+
+       # Map of the strings's `msgid` and `msgstr`
+       #
+       # Read from a PO file
+       var strings: Array[PObject]
+
+       # Creates a PO file with strings built-in
+       init with_strings(sm: Array[PObject])do
+               strings = new Array[PObject].with_capacity(sm.length)
+               strings.add_all sm
+       end
+
+       redef fun write_to_file(path) do
+               if not path.has_suffix(".po") then path += ".po"
+               super path
+       end
+
+       redef fun write_to(ofs) do
+               for i in strings do
+                       ofs.write("#: {i.locations.join(", ")}\n")
+                       ofs.write("msgid \"{i.msgid}\"\n")
+                       ofs.write("msgstr \"{i.msgstr}\"\n\n")
+               end
+               ofs.write("# Generated file, do not modify\n")
+       end
+
+       # Writes the information of the POFile to a .pot template file
+       fun write_template(path: String) do
+               if not path.has_suffix(".pot") then path += ".pot"
+               var f = new FileWriter.open(path)
+               write_to(f)
+               f.close
+       end
+end