lib/yard/i18n/pot_generator.rb in yard-0.9.18 vs lib/yard/i18n/pot_generator.rb in yard-0.9.19

- old
+ new

@@ -1,290 +1,290 @@ -# frozen_string_literal: true -require "stringio" - -module YARD - module I18n - # The +PotGenerator+ generates POT format string from - # {CodeObjects::Base} and {CodeObjects::ExtraFileObject}. - # - # == POT and PO - # - # POT is an acronym for "Portable Object Template". POT is a - # template file to create PO file. The extension for POT is - # ".pot". PO file is an acronym for "Portable Object". PO file has - # many parts of message ID (msgid) that is translation target - # message and message string (msgstr) that is translated message - # of message ID. If you want to translate "Hello" in English into - # "Bonjour" in French, "Hello" is the msgid ID and "Bonjour" is - # msgstr. The extension for PO is ".po". - # - # == How to extract msgids - # - # The +PotGenerator+ has two parse methods: - # - # * {#parse_objects} for {CodeObjects::Base} - # * {#parse_files} for {CodeObjects::ExtraFileObject} - # - # {#parse_objects} extracts msgids from docstring and tags of - # {CodeObjects::Base} objects. The docstring of - # {CodeObjects::Base} object is parsed and a paragraph is - # extracted as a msgid. Tag name and tag text are extracted as - # msgids from a tag. - # - # {#parse_files} extracts msgids from - # {CodeObjects::ExtraFileObject} objects. The file content of - # {CodeObjects::ExtraFileObject} object is parsed and a paragraph - # is extracted as a msgid. - # - # == Usage - # - # To create a .pot file by +PotGenerator+, instantiate a - # +PotGenerator+ with a relative working directory path from a - # directory path that has created .pot file, parse - # {CodeObjects::Base} objects and {CodeObjects::ExtraFileObject} - # objects, generate a POT and write the generated POT to a .pot - # file. The relative working directory path is ".." when the - # working directory path is "." and the POT is wrote into - # "po/yard.pot". - # - # @example Generate a .pot file - # po_file_path = "po/yard.pot" - # po_file_directory_pathname = Pathname.new(po_file_path).directory) - # working_directory_pathname = Pathname.new(".") - # relative_base_path = working_directory_pathname.relative_path_from(po_file_directory_pathname).to_s - # # relative_base_path -> ".." - # generator = YARD::I18n::PotGenerator.new(relative_base_path) - # generator.parse_objects(objects) - # generator.parse_files(files) - # pot = generator.generate - # po_file_directory_pathname.mkpath - # File.open(po_file_path, "w") do |pot_file| - # pot_file.print(pot) - # end - # @see http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html - # GNU gettext manual about details of PO file - class PotGenerator - # Extracted messages. - # - # @return [Messages] - # @since 0.8.1 - attr_reader :messages - - # Creates a POT generator that uses +relative_base_path+ to - # generate locations for a msgid. +relative_base_path+ is - # prepended to all locations. - # - # @param [String] relative_base_path a relative working - # directory path from a directory path that has created .pot - # file. - def initialize(relative_base_path) - @relative_base_path = relative_base_path - @extracted_objects = {} - @messages = Messages.new - end - - # Parses {CodeObjects::Base} objects and stores extracted msgids - # into {#messages} - # - # @param [Array<CodeObjects::Base>] objects a list of - # {CodeObjects::Base} to be parsed. - # @return [void] - def parse_objects(objects) - objects.each do |object| - extract_documents(object) - end - end - - # Parses {CodeObjects::ExtraFileObject} objects and stores - # extracted msgids into {#messages}. - # - # @param [Array<CodeObjects::ExtraFileObject>] files a list - # of {CodeObjects::ExtraFileObject} objects to be parsed. - # @return [void] - def parse_files(files) - files.each do |file| - extract_paragraphs(file) - end - end - - # Generates POT from +@messages+. - # - # One PO file entry is generated from a +Message+ in - # +@messages+. - # - # Locations of the +Message+ are used to generate the reference - # line that is started with "#: ". +relative_base_path+ passed - # when the generater is created is prepended to each path in location. - # - # Comments of the +Message+ are used to generate the - # translator-comment line that is started with "# ". - # - # @return [String] POT format string - def generate - pot = String.new(header) - sorted_messages = @messages.sort_by do |message| - sorted_locations = message.locations.sort - sorted_locations.first || [] - end - sorted_messages.each do |message| - generate_message(pot, message) - end - pot - end - - private - - def header - <<-EOH -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\\n" -"Report-Msgid-Bugs-To: \\n" -"POT-Creation-Date: #{generate_pot_creation_date_value}\\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n" -"Language-Team: LANGUAGE <LL@li.org>\\n" -"Language: \\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" - -EOH - end - - def current_time - @current_time ||= Time.now - end - - def generate_pot_creation_date_value - current_time.strftime("%Y-%m-%d %H:%M%z") - end - - def generate_message(pot, message) - message.comments.sort.each do |comment| - pot << "# #{comment}\n" unless comment.empty? - end - message.locations.sort.each do |path, line| - pot << "#: #{@relative_base_path}/#{path}:#{line}\n" - end - escaped_message_id = escape_message_id(message.id) - escaped_message_id = escaped_message_id.gsub(/\n/, "\\\\n\"\n\"") - pot << "msgid \"#{escaped_message_id}\"\n" - pot << "msgstr \"\"\n" - pot << "\n" - pot - end - - def escape_message_id(message_id) - message_id.gsub(/(\\|")/) do - special_character = $1 - "\\#{special_character}" - end - end - - def register_message(id) - @messages.register(id) - end - - def extract_documents(object) - return if @extracted_objects.key?(object) - - @extracted_objects[object] = true - case object - when CodeObjects::NamespaceObject - object.children.each do |child| - extract_documents(child) - end - end - - if object.group - message = register_message(object.group) - object.files.each do |path, line| - message.add_location(path, line) - end - message.add_comment(object.path) unless object.path.empty? - end - - docstring = object.docstring - unless docstring.empty? - text = Text.new(StringIO.new(docstring)) - text.extract_messages do |type, *args| - case type - when :paragraph - paragraph, line_no = *args - message = register_message(paragraph.rstrip) - object.files.each do |path, line| - message.add_location(path, (docstring.line || line) + line_no) - end - message.add_comment(object.path) unless object.path.empty? - else - raise "should not reach here: unexpected type: #{type}" - end - end - end - docstring.tags.each do |tag| - extract_tag_documents(tag) - end - end - - def extract_tag_documents(tag) - extract_tag_name(tag) - extract_tag_text(tag) - - extract_documents(tag) if Tags::OverloadTag === tag - end - - def extract_tag_name(tag) - return if tag.name.nil? - return if tag.name.is_a?(String) && tag.name.empty? - key = "tag|#{tag.tag_name}|#{tag.name}" - message = register_message(key) - tag.object.files.each do |path, line| - message.add_location(path, line) - end - tag_label = String.new("@#{tag.tag_name}") - tag_label << " [#{tag.types.join(', ')}]" if tag.types - message.add_comment(tag_label) - end - - def extract_tag_text(tag) - return if tag.text.nil? - return if tag.text.empty? - message = register_message(tag.text) - tag.object.files.each do |path, line| - message.add_location(path, line) - end - tag_label = String.new("@#{tag.tag_name}") - tag_label << " [#{tag.types.join(', ')}]" if tag.types - tag_label << " #{tag.name}" if tag.name - message.add_comment(tag_label) - end - - def extract_paragraphs(file) - File.open(file.filename) do |input| - text = Text.new(input, :have_header => true) - text.extract_messages do |type, *args| - case type - when :attribute - name, value, line_no = *args - message = register_message(value) - message.add_location(file.filename, line_no) - message.add_comment(name) - when :paragraph - paragraph, line_no = *args - message = register_message(paragraph.rstrip) - message.add_location(file.filename, line_no) - else - raise "should not reach here: unexpected type: #{type}" - end - end - end - end - end - end -end +# frozen_string_literal: true +require "stringio" + +module YARD + module I18n + # The +PotGenerator+ generates POT format string from + # {CodeObjects::Base} and {CodeObjects::ExtraFileObject}. + # + # == POT and PO + # + # POT is an acronym for "Portable Object Template". POT is a + # template file to create PO file. The extension for POT is + # ".pot". PO file is an acronym for "Portable Object". PO file has + # many parts of message ID (msgid) that is translation target + # message and message string (msgstr) that is translated message + # of message ID. If you want to translate "Hello" in English into + # "Bonjour" in French, "Hello" is the msgid ID and "Bonjour" is + # msgstr. The extension for PO is ".po". + # + # == How to extract msgids + # + # The +PotGenerator+ has two parse methods: + # + # * {#parse_objects} for {CodeObjects::Base} + # * {#parse_files} for {CodeObjects::ExtraFileObject} + # + # {#parse_objects} extracts msgids from docstring and tags of + # {CodeObjects::Base} objects. The docstring of + # {CodeObjects::Base} object is parsed and a paragraph is + # extracted as a msgid. Tag name and tag text are extracted as + # msgids from a tag. + # + # {#parse_files} extracts msgids from + # {CodeObjects::ExtraFileObject} objects. The file content of + # {CodeObjects::ExtraFileObject} object is parsed and a paragraph + # is extracted as a msgid. + # + # == Usage + # + # To create a .pot file by +PotGenerator+, instantiate a + # +PotGenerator+ with a relative working directory path from a + # directory path that has created .pot file, parse + # {CodeObjects::Base} objects and {CodeObjects::ExtraFileObject} + # objects, generate a POT and write the generated POT to a .pot + # file. The relative working directory path is ".." when the + # working directory path is "." and the POT is wrote into + # "po/yard.pot". + # + # @example Generate a .pot file + # po_file_path = "po/yard.pot" + # po_file_directory_pathname = Pathname.new(po_file_path).directory) + # working_directory_pathname = Pathname.new(".") + # relative_base_path = working_directory_pathname.relative_path_from(po_file_directory_pathname).to_s + # # relative_base_path -> ".." + # generator = YARD::I18n::PotGenerator.new(relative_base_path) + # generator.parse_objects(objects) + # generator.parse_files(files) + # pot = generator.generate + # po_file_directory_pathname.mkpath + # File.open(po_file_path, "w") do |pot_file| + # pot_file.print(pot) + # end + # @see http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html + # GNU gettext manual about details of PO file + class PotGenerator + # Extracted messages. + # + # @return [Messages] + # @since 0.8.1 + attr_reader :messages + + # Creates a POT generator that uses +relative_base_path+ to + # generate locations for a msgid. +relative_base_path+ is + # prepended to all locations. + # + # @param [String] relative_base_path a relative working + # directory path from a directory path that has created .pot + # file. + def initialize(relative_base_path) + @relative_base_path = relative_base_path + @extracted_objects = {} + @messages = Messages.new + end + + # Parses {CodeObjects::Base} objects and stores extracted msgids + # into {#messages} + # + # @param [Array<CodeObjects::Base>] objects a list of + # {CodeObjects::Base} to be parsed. + # @return [void] + def parse_objects(objects) + objects.each do |object| + extract_documents(object) + end + end + + # Parses {CodeObjects::ExtraFileObject} objects and stores + # extracted msgids into {#messages}. + # + # @param [Array<CodeObjects::ExtraFileObject>] files a list + # of {CodeObjects::ExtraFileObject} objects to be parsed. + # @return [void] + def parse_files(files) + files.each do |file| + extract_paragraphs(file) + end + end + + # Generates POT from +@messages+. + # + # One PO file entry is generated from a +Message+ in + # +@messages+. + # + # Locations of the +Message+ are used to generate the reference + # line that is started with "#: ". +relative_base_path+ passed + # when the generater is created is prepended to each path in location. + # + # Comments of the +Message+ are used to generate the + # translator-comment line that is started with "# ". + # + # @return [String] POT format string + def generate + pot = String.new(header) + sorted_messages = @messages.sort_by do |message| + sorted_locations = message.locations.sort + sorted_locations.first || [] + end + sorted_messages.each do |message| + generate_message(pot, message) + end + pot + end + + private + + def header + <<-EOH +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\\n" +"Report-Msgid-Bugs-To: \\n" +"POT-Creation-Date: #{generate_pot_creation_date_value}\\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n" +"Language-Team: LANGUAGE <LL@li.org>\\n" +"Language: \\n" +"MIME-Version: 1.0\\n" +"Content-Type: text/plain; charset=UTF-8\\n" +"Content-Transfer-Encoding: 8bit\\n" + +EOH + end + + def current_time + @current_time ||= Time.now + end + + def generate_pot_creation_date_value + current_time.strftime("%Y-%m-%d %H:%M%z") + end + + def generate_message(pot, message) + message.comments.sort.each do |comment| + pot << "# #{comment}\n" unless comment.empty? + end + message.locations.sort.each do |path, line| + pot << "#: #{@relative_base_path}/#{path}:#{line}\n" + end + escaped_message_id = escape_message_id(message.id) + escaped_message_id = escaped_message_id.gsub(/\n/, "\\\\n\"\n\"") + pot << "msgid \"#{escaped_message_id}\"\n" + pot << "msgstr \"\"\n" + pot << "\n" + pot + end + + def escape_message_id(message_id) + message_id.gsub(/(\\|")/) do + special_character = $1 + "\\#{special_character}" + end + end + + def register_message(id) + @messages.register(id) + end + + def extract_documents(object) + return if @extracted_objects.key?(object) + + @extracted_objects[object] = true + case object + when CodeObjects::NamespaceObject + object.children.each do |child| + extract_documents(child) + end + end + + if object.group + message = register_message(object.group) + object.files.each do |path, line| + message.add_location(path, line) + end + message.add_comment(object.path) unless object.path.empty? + end + + docstring = object.docstring + unless docstring.empty? + text = Text.new(StringIO.new(docstring)) + text.extract_messages do |type, *args| + case type + when :paragraph + paragraph, line_no = *args + message = register_message(paragraph.rstrip) + object.files.each do |path, line| + message.add_location(path, (docstring.line || line) + line_no) + end + message.add_comment(object.path) unless object.path.empty? + else + raise "should not reach here: unexpected type: #{type}" + end + end + end + docstring.tags.each do |tag| + extract_tag_documents(tag) + end + end + + def extract_tag_documents(tag) + extract_tag_name(tag) + extract_tag_text(tag) + + extract_documents(tag) if Tags::OverloadTag === tag + end + + def extract_tag_name(tag) + return if tag.name.nil? + return if tag.name.is_a?(String) && tag.name.empty? + key = "tag|#{tag.tag_name}|#{tag.name}" + message = register_message(key) + tag.object.files.each do |path, line| + message.add_location(path, line) + end + tag_label = String.new("@#{tag.tag_name}") + tag_label << " [#{tag.types.join(', ')}]" if tag.types + message.add_comment(tag_label) + end + + def extract_tag_text(tag) + return if tag.text.nil? + return if tag.text.empty? + message = register_message(tag.text) + tag.object.files.each do |path, line| + message.add_location(path, line) + end + tag_label = String.new("@#{tag.tag_name}") + tag_label << " [#{tag.types.join(', ')}]" if tag.types + tag_label << " #{tag.name}" if tag.name + message.add_comment(tag_label) + end + + def extract_paragraphs(file) + File.open(file.filename) do |input| + text = Text.new(input, :have_header => true) + text.extract_messages do |type, *args| + case type + when :attribute + name, value, line_no = *args + message = register_message(value) + message.add_location(file.filename, line_no) + message.add_comment(name) + when :paragraph + paragraph, line_no = *args + message = register_message(paragraph.rstrip) + message.add_location(file.filename, line_no) + else + raise "should not reach here: unexpected type: #{type}" + end + end + end + end + end + end +end