# ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENSE BLOCK ***** # # Some misc utilities... import os import sys import collections from bl_i18n_utils import settings COMMENT_PREFIX = settings.COMMENT_PREFIX WARN_NC = settings.WARN_MSGID_NOT_CAPITALIZED NC_ALLOWED = settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED def stripeol(s): return s.rstrip("\n\r") # XXX For now, we assume that all messages > 30 chars are tooltips! def is_tooltip(msgid): return len(msgid) > 30 def parse_messages(fname): """ Returns a tupple (messages, states, stats). messages is an odereddict of dicts {(ctxt, msgid): {msgid_lines:, msgstr_lines:, comment_lines:, msgctxt_lines:}}. states is a dict of three sets of (msgid, ctxt), and a boolean flag indicating the .po is somewhat broken {trans_msg:, fuzzy_msg:, comm_msg:, is_broken:}. stats is a dict of values {tot_msg:, trans_msg:, tot_ttips:, trans_ttips:, comm_msg:, nbr_signs:, nbr_trans_signs:, contexts: set()}. Note: This function will silently "arrange" mis-formated entries, thus using afterward write_messages() should always produce a po-valid file, though not correct! """ tot_messages = 0 tot_tooltips = 0 trans_messages = 0 trans_tooltips = 0 comm_messages = 0 nbr_signs = 0 nbr_trans_signs = 0 contexts = set() reading_msgid = False reading_msgstr = False reading_msgctxt = False reading_comment = False is_translated = False is_fuzzy = False is_commented = False is_broken = False msgid_lines = [] msgstr_lines = [] msgctxt_lines = [] comment_lines = [] messages = getattr(collections, 'OrderedDict', dict)() translated_messages = set() fuzzy_messages = set() commented_messages = set() def clean_vars(): nonlocal reading_msgid, reading_msgstr, reading_msgctxt, \ reading_comment, is_fuzzy, is_translated, is_commented, \ msgid_lines, msgstr_lines, msgctxt_lines, comment_lines reading_msgid = reading_msgstr = reading_msgctxt = \ reading_comment = False is_tooltip = is_fuzzy = is_translated = is_commented = False msgid_lines = [] msgstr_lines = [] msgctxt_lines = [] comment_lines = [] def finalize_message(): nonlocal reading_msgid, reading_msgstr, reading_msgctxt, \ reading_comment, is_fuzzy, is_translated, is_commented, \ msgid_lines, msgstr_lines, msgctxt_lines, comment_lines, \ messages, translated_messages, fuzzy_messages, \ commented_messages, \ tot_messages, tot_tooltips, trans_messages, trans_tooltips, \ comm_messages, nbr_signs, nbr_trans_signs, contexts msgid = "".join(msgid_lines) msgctxt = "".join(msgctxt_lines) msgkey = (msgctxt, msgid) is_ttip = is_tooltip(msgid) # Never allow overriding existing msgid/msgctxt pairs! if msgkey in messages: clean_vars() return nbr_signs += len(msgid) if is_commented: commented_messages.add(msgkey) elif is_fuzzy: fuzzy_messages.add(msgkey) elif is_translated: translated_messages.add(msgkey) nbr_trans_signs += len("".join(msgstr_lines)) messages[msgkey] = {"msgid_lines" : msgid_lines, "msgstr_lines" : msgstr_lines, "comment_lines": comment_lines, "msgctxt_lines": msgctxt_lines} if is_commented: comm_messages += 1 else: tot_messages += 1 if is_ttip: tot_tooltips += 1 if not is_fuzzy and is_translated: trans_messages += 1 if is_ttip: trans_tooltips += 1 if msgctxt not in contexts: contexts.add(msgctxt) clean_vars() with open(fname, 'r', encoding="utf-8") as f: for line_nr, line in enumerate(f): line = stripeol(line) if line == "": finalize_message() elif line.startswith("msgctxt") or \ line.startswith("".join((COMMENT_PREFIX, "msgctxt"))): reading_comment = False reading_ctxt = True if line.startswith(COMMENT_PREFIX): is_commented = True line = line[9 + len(COMMENT_PREFIX):-1] else: line = line[9:-1] msgctxt_lines.append(line) elif line.startswith("msgid") or \ line.startswith("".join((COMMENT_PREFIX, "msgid"))): reading_comment = False reading_msgid = True if line.startswith(COMMENT_PREFIX): is_commented = True line = line[7 + len(COMMENT_PREFIX):-1] else: line = line[7:-1] msgid_lines.append(line) elif line.startswith("msgstr") or \ line.startswith("".join((COMMENT_PREFIX, "msgstr"))): if not reading_msgid: is_broken = True else: reading_msgid = False reading_msgstr = True if line.startswith(COMMENT_PREFIX): line = line[8 + len(COMMENT_PREFIX):-1] if not is_commented: is_broken = True else: line = line[8:-1] if is_commented: is_broken = True msgstr_lines.append(line) if line: is_translated = True elif line.startswith("#"): if reading_msgid: if is_commented: msgid_lines.append(line[1 + len(COMMENT_PREFIX):-1]) else: msgid_lines.append(line) is_broken = True elif reading_msgstr: if is_commented: msgstr_lines.append(line[1 + len(COMMENT_PREFIX):-1]) else: msgstr_lines.append(line) is_broken = True else: if line.startswith("#, fuzzy"): is_fuzzy = True else: comment_lines.append(line) reading_comment = True else: if reading_msgid: msgid_lines.append(line[1:-1]) elif reading_msgstr: line = line[1:-1] msgstr_lines.append(line) if not is_translated and line: is_translated = True else: is_broken = True # If no final empty line, last message is not finalized! if reading_msgstr: finalize_message() return (messages, {"trans_msg": translated_messages, "fuzzy_msg": fuzzy_messages, "comm_msg" : commented_messages, "is_broken": is_broken}, {"tot_msg" : tot_messages, "trans_msg" : trans_messages, "tot_ttips" : tot_tooltips, "trans_ttips" : trans_tooltips, "comm_msg" : comm_messages, "nbr_signs" : nbr_signs, "nbr_trans_signs": nbr_trans_signs, "contexts" : contexts}) def write_messages(fname, messages, commented, fuzzy): "Write in fname file the content of messages (similar to parse_messages " \ "returned values). commented and fuzzy are two sets containing msgid. " \ "Returns the number of written messages." num = 0 with open(fname, 'w', encoding="utf-8") as f: for msgkey, val in messages.items(): msgctxt, msgid = msgkey f.write("\n".join(val["comment_lines"])) # Only mark as fuzzy if msgstr is not empty! if msgkey in fuzzy and "".join(val["msgstr_lines"]): f.write("\n#, fuzzy") if msgkey in commented: if msgctxt: f.write("\n{}msgctxt \"".format(COMMENT_PREFIX)) f.write("\"\n{}\"".format(COMMENT_PREFIX).join( val["msgctxt_lines"])) f.write("\"") f.write("\n{}msgid \"".format(COMMENT_PREFIX)) f.write("\"\n{}\"".format(COMMENT_PREFIX).join( val["msgid_lines"])) f.write("\"\n{}msgstr \"".format(COMMENT_PREFIX)) f.write("\"\n{}\"".format(COMMENT_PREFIX).join( val["msgstr_lines"])) f.write("\"\n\n") else: if msgctxt: f.write("\nmsgctxt \"") f.write("\"\n\"".join(val["msgctxt_lines"])) f.write("\"") f.write("\nmsgid \"") f.write("\"\n\"".join(val["msgid_lines"])) f.write("\"\nmsgstr \"") f.write("\"\n\"".join(val["msgstr_lines"])) f.write("\"\n\n") num += 1 return num def gen_empty_messages(blender_rev, time_str, year_str): """Generate an empty messages & state data (only header if present!).""" header_key = ("", "") messages = getattr(collections, 'OrderedDict', dict)() messages[header_key] = { "msgid_lines": [""], "msgctxt_lines": [], "msgstr_lines": [ "Project-Id-Version: Blender r{}\\n" "".format(blender_rev), "Report-Msgid-Bugs-To: \\n", "POT-Creation-Date: {}\\n" "".format(time_str), "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n", "Last-Translator: FULL NAME \\n", "Language-Team: LANGUAGE \\n", "Language: \\n", "MIME-Version: 1.0\\n", "Content-Type: text/plain; charset=UTF-8\\n", "Content-Transfer-Encoding: 8bit\\n" ], "comment_lines": [ "# Blender's translation file (po format).", "# Copyright (C) {} The Blender Foundation." "".format(year_str), "# This file is distributed under the same " "# license as the Blender package.", "# FIRST AUTHOR , YEAR.", "#", ], } states = {"trans_msg": set(), "fuzzy_msg": {header_key}, "comm_msg": set(), "is_broken": False} return messages, states def print_stats(stats, glob_stats=None, prefix=""): """ Print out some stats about a po file. glob_stats is for making global stats over several po's. """ tot_msgs = stats["tot_msg"] trans_msgs = stats["trans_msg"] tot_ttips = stats["tot_ttips"] trans_ttips = stats["trans_ttips"] comm_msgs = stats["comm_msg"] nbr_signs = stats["nbr_signs"] nbr_trans_signs = stats["nbr_trans_signs"] contexts = stats["contexts"] lvl = lvl_ttips = lvl_trans_ttips = lvl_ttips_in_trans = lvl_comm = 0.0 if tot_msgs > 0: lvl = float(trans_msgs) / float(tot_msgs) lvl_ttips = float(tot_ttips) / float(tot_msgs) lvl_comm = float(comm_msgs) / float(tot_msgs+comm_msgs) if tot_ttips > 0: lvl_trans_ttips = float(trans_ttips) / float(tot_ttips) if trans_msgs > 0: lvl_ttips_in_trans = float(trans_ttips) / float(trans_msgs) if glob_stats: glob_stats["nbr"] += 1.0 glob_stats["lvl"] += lvl glob_stats["lvl_ttips"] += lvl_ttips glob_stats["lvl_trans_ttips"] += lvl_trans_ttips glob_stats["lvl_ttips_in_trans"] += lvl_ttips_in_trans glob_stats["lvl_comm"] += lvl_comm glob_stats["nbr_trans_signs"] += nbr_trans_signs if glob_stats["nbr_signs"] == 0: glob_stats["nbr_signs"] = nbr_signs glob_stats["contexts"] |= contexts lines = ("", "{:>6.1%} done! ({} translated messages over {}).\n" "".format(lvl, trans_msgs, tot_msgs), "{:>6.1%} of messages are tooltips ({} over {}).\n" "".format(lvl_ttips, tot_ttips, tot_msgs), "{:>6.1%} of tooltips are translated ({} over {}).\n" "".format(lvl_trans_ttips, trans_ttips, tot_ttips), "{:>6.1%} of translated messages are tooltips ({} over {}).\n" "".format(lvl_ttips_in_trans, trans_ttips, trans_msgs), "{:>6.1%} of messages are commented ({} over {}).\n" "".format(lvl_comm, comm_msgs, comm_msgs + tot_msgs), "This translation is currently made of {} signs.\n" "".format(nbr_trans_signs)) print(prefix.join(lines)) return 0