← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/translation_stats into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/translation_stats into lp:widelands.

Commit message:
Show translation stats next to the language selection menu and invite translators if a translation is incomplete, with the help of the Translate Toolkit.

- Added a new utils script "update_translation_stats.py" that will write translation statistics to data/i18n/translation_stats.conf.
  This script is now called on every translation pull.
- Added translation stats + invitation to translators to Options.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/translation_stats/+merge/332029

Some translations have very little work accomplished, so I thought we'd better tell the user so. This also gives us an opportunity to invite translators.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/translation_stats into lp:widelands.
=== added file 'data/i18n/translation_stats.conf'
--- data/i18n/translation_stats.conf	1970-01-01 00:00:00 +0000
+++ data/i18n/translation_stats.conf	2017-10-09 19:42:37 +0000
@@ -0,0 +1,239 @@
+[ar]
+translated=3400
+total=41289
+
+[ast]
+translated=471
+total=41289
+
+[bg]
+translated=41289
+total=41289
+
+[br]
+translated=422
+total=41289
+
+[ca]
+translated=41289
+total=41289
+
+[cs]
+translated=40858
+total=41289
+
+[da]
+translated=40774
+total=41289
+
+[de]
+translated=41009
+total=41289
+
+[el]
+translated=493
+total=41289
+
+[en_CA]
+translated=558
+total=41289
+
+[en_GB]
+translated=40774
+total=41289
+
+[en_US]
+translated=7
+total=41289
+
+[eo]
+translated=4455
+total=41289
+
+[es]
+translated=39621
+total=41289
+
+[et]
+translated=628
+total=41289
+
+[eu]
+translated=1359
+total=41289
+
+[fa]
+translated=123
+total=41289
+
+[fi]
+translated=41289
+total=41289
+
+[fr]
+translated=40891
+total=41289
+
+[ga]
+translated=0
+total=41289
+
+[gd]
+translated=41289
+total=41289
+
+[gl]
+translated=10511
+total=41289
+
+[he]
+translated=422
+total=41289
+
+[hi]
+translated=26
+total=41289
+
+[hr]
+translated=3239
+total=41289
+
+[hu]
+translated=33263
+total=41289
+
+[ia]
+translated=126
+total=41289
+
+[id]
+translated=34
+total=41289
+
+[it]
+translated=36675
+total=41289
+
+[ja]
+translated=7025
+total=41289
+
+[jv]
+translated=22
+total=41289
+
+[ka]
+translated=4
+total=41289
+
+[ko]
+translated=209
+total=41289
+
+[krl]
+translated=14
+total=41289
+
+[la]
+translated=4012
+total=41289
+
+[lt]
+translated=202
+total=41289
+
+[mr]
+translated=7
+total=41289
+
+[ms]
+translated=4099
+total=41289
+
+[my]
+translated=30
+total=41289
+
+[nb]
+translated=9192
+total=41289
+
+[nds]
+translated=876
+total=41289
+
+[nl]
+translated=34028
+total=41289
+
+[nn]
+translated=2946
+total=41289
+
+[oc]
+translated=195
+total=41289
+
+[pl]
+translated=41009
+total=41289
+
+[pt]
+translated=28352
+total=41289
+
+[pt_BR]
+translated=14495
+total=41289
+
+[ro]
+translated=1125
+total=41289
+
+[ru]
+translated=41289
+total=41289
+
+[rw]
+translated=49
+total=41289
+
+[si]
+translated=283
+total=41289
+
+[sk]
+translated=39563
+total=41289
+
+[sl]
+translated=1046
+total=41289
+
+[sr]
+translated=177
+total=41289
+
+[sv]
+translated=38623
+total=41289
+
+[tr]
+translated=1942
+total=41289
+
+[uk]
+translated=3621
+total=41289
+
+[vi]
+translated=1199
+total=41289
+
+[zh_CN]
+translated=2245
+total=41289
+
+[zh_TW]
+translated=655
+total=41289

=== modified file 'src/ui_fsmenu/options.cc'
--- src/ui_fsmenu/options.cc	2017-09-11 16:59:41 +0000
+++ src/ui_fsmenu/options.cc	2017-10-09 19:42:37 +0000
@@ -36,6 +36,7 @@
 #include "graphic/text/bidi.h"
 #include "graphic/text/font_set.h"
 #include "graphic/text_constants.h"
+#include "graphic/text_layout.h"
 #include "helper.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/constants.h"
@@ -47,23 +48,6 @@
 
 namespace {
 
-// Data model for the entries in the language selection list.
-struct LanguageEntry {
-	LanguageEntry(const std::string& init_localename,
-	              const std::string& init_descname,
-	              const std::string& init_sortname)
-	   : localename(init_localename), descname(init_descname), sortname(init_sortname) {
-	}
-
-	bool operator<(const LanguageEntry& other) const {
-		return sortname < other.sortname;
-	}
-
-	std::string localename;  // ISO code for the locale
-	std::string descname;    // Native language name
-	std::string sortname;    // ASCII Language name used for sorting
-};
-
 // Locale identifiers can look like this: ca_ES@xxxxxxxxxxxx-8
 // The contents of 'selected_locale' will be changed to match the 'current_locale'
 void find_selected_locale(std::string* selected_locale, const std::string& current_locale) {
@@ -120,21 +104,23 @@
      // Tabs
      tabs_(this, g_gr->images().get("images/ui_basic/but1.png"), UI::TabPanel::Type::kBorder),
 
-     box_interface_(&tabs_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
+     box_interface_(&tabs_, 0, 0, UI::Box::Horizontal, 0, 0, padding_),
+     box_interface_left_(&box_interface_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
      box_windows_(&tabs_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
      box_sound_(&tabs_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
      box_saving_(&tabs_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
      box_game_(&tabs_, 0, 0, UI::Box::Vertical, 0, 0, padding_),
 
      // Interface options
-     language_dropdown_(&box_interface_,
+     // NOCOM this will only work when bug-536489-pictorial-dropdown has been merged.
+     language_dropdown_(&box_interface_left_,
                         0,
                         0,
                         100,  // 100 is arbitrary, will be resized in layout().
                         100,  // 100 is arbitrary, will be resized in layout().
                         24,
                         _("Language")),
-     resolution_dropdown_(&box_interface_,
+     resolution_dropdown_(&box_interface_left_,
                           0,
                           0,
                           100,  // 100 is arbitrary, will be resized in layout().
@@ -142,10 +128,10 @@
                           24,
                           _("In-game resolution")),
 
-     fullscreen_(&box_interface_, Vector2i::zero(), _("Fullscreen"), "", 0),
-     inputgrab_(&box_interface_, Vector2i::zero(), _("Grab Input"), "", 0),
-
-     sb_maxfps_(&box_interface_, 0, 0, 0, 0, opt.maxfps, 0, 99, _("Maximum FPS:")),
+     fullscreen_(&box_interface_left_, Vector2i::zero(), _("Fullscreen"), "", 0),
+     inputgrab_(&box_interface_left_, Vector2i::zero(), _("Grab Input"), "", 0),
+     sb_maxfps_(&box_interface_left_, 0, 0, 0, 0, opt.maxfps, 0, 99, _("Maximum FPS:")),
+     translation_info_(&box_interface_, 0, 0, 100, 100),
 
      // Windows options
      snap_win_overlap_only_(
@@ -232,6 +218,7 @@
      os_(opt) {
 	// Set up UI Elements
 	title_.set_fontsize(UI_FONT_SIZE_BIG);
+	translation_info_.force_new_renderer();
 
 	// Buttons
 	button_box_.add(UI::g_fh1->fontset()->is_rtl() ? &ok_ : &cancel_);
@@ -252,18 +239,14 @@
 		tabs_.activate(os_.active_tab);
 	}
 
-	box_interface_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
-	box_windows_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
-	box_sound_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
-	box_saving_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
-	box_game_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
-
 	// Interface
-	box_interface_.add(&language_dropdown_);
-	box_interface_.add(&resolution_dropdown_);
-	box_interface_.add(&fullscreen_);
-	box_interface_.add(&inputgrab_);
-	box_interface_.add(&sb_maxfps_);
+	box_interface_.add(&box_interface_left_);
+	box_interface_.add(&translation_info_, UI::Box::Resizing::kExpandBoth);
+	box_interface_left_.add(&language_dropdown_);
+	box_interface_left_.add(&resolution_dropdown_);
+	box_interface_left_.add(&fullscreen_);
+	box_interface_left_.add(&inputgrab_);
+	box_interface_left_.add(&sb_maxfps_);
 
 	// Windows
 	box_windows_.add(&snap_win_overlap_only_);
@@ -290,6 +273,8 @@
 	box_game_.add(&single_watchwin_);
 
 	// Bind actions
+	language_dropdown_.selected.connect(
+	   boost::bind(&FullscreenMenuOptions::update_language_stats, this, false));
 	cancel_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_back, this));
 	apply_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_apply, this));
 	ok_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_ok, this));
@@ -360,6 +345,7 @@
 
 	// Language options
 	add_languages_to_list(opt.language);
+	update_language_stats(true);
 	layout();
 }
 
@@ -369,8 +355,7 @@
 	butw_ = get_w() / 5;
 	buth_ = get_h() * 9 / 200;
 	hmargin_ = get_w() * 19 / 200;
-	tab_panel_width_ = get_inner_w() - 2 * hmargin_;
-	column_width_ = tab_panel_width_ - padding_;
+	int tab_panel_width = get_inner_w() - 2 * hmargin_;
 	tab_panel_y_ = get_h() * 14 / 100;
 
 	// Title
@@ -382,48 +367,53 @@
 	apply_.set_desired_size(butw_, buth_);
 	ok_.set_desired_size(butw_, buth_);
 	button_box_.set_pos(Vector2i(hmargin_ + butw_ / 3, get_inner_h() - hmargin_));
-	button_box_.set_size(tab_panel_width_ - 2 * butw_ / 3, buth_);
+	button_box_.set_size(tab_panel_width - 2 * butw_ / 3, buth_);
 
 	// Tabs
 	tabs_.set_pos(Vector2i(hmargin_, tab_panel_y_));
-	tabs_.set_size(tab_panel_width_, get_inner_h() - tab_panel_y_ - buth_ - hmargin_);
+	tabs_.set_size(tab_panel_width, get_inner_h() - tab_panel_y_ - buth_ - hmargin_);
+
+	tab_panel_width -= padding_;
+	const int column_width = tab_panel_width / 2;
 
 	// Interface
-	language_dropdown_.set_desired_size(column_width_ / 2, language_dropdown_.get_h());
+	box_interface_left_.set_desired_size(column_width + padding_, tabs_.get_inner_h());
+	box_interface_.set_size(tabs_.get_inner_w(), tabs_.get_inner_h());
+	language_dropdown_.set_desired_size(column_width, language_dropdown_.get_h());
 	language_dropdown_.set_height(tabs_.get_h() - language_dropdown_.get_y() - buth_ - 3 * padding_);
-	resolution_dropdown_.set_desired_size(column_width_ / 2, resolution_dropdown_.get_h());
+	resolution_dropdown_.set_desired_size(column_width, resolution_dropdown_.get_h());
 	resolution_dropdown_.set_height(tabs_.get_h() - resolution_dropdown_.get_y() - buth_ -
 	                                3 * padding_);
 
-	fullscreen_.set_desired_size(column_width_, fullscreen_.get_h());
-	inputgrab_.set_desired_size(column_width_, inputgrab_.get_h());
-	sb_maxfps_.set_unit_width(column_width_ / 4);
-	sb_maxfps_.set_desired_size(column_width_ / 2, sb_maxfps_.get_h());
+	fullscreen_.set_desired_size(column_width, fullscreen_.get_h());
+	inputgrab_.set_desired_size(column_width, inputgrab_.get_h());
+	sb_maxfps_.set_unit_width(column_width / 2);
+	sb_maxfps_.set_desired_size(column_width, sb_maxfps_.get_h());
 
 	// Windows options
-	snap_win_overlap_only_.set_desired_size(column_width_, snap_win_overlap_only_.get_h());
-	dock_windows_to_edges_.set_desired_size(column_width_, dock_windows_to_edges_.get_h());
-	animate_map_panning_.set_desired_size(column_width_, animate_map_panning_.get_h());
+	snap_win_overlap_only_.set_desired_size(tab_panel_width, snap_win_overlap_only_.get_h());
+	dock_windows_to_edges_.set_desired_size(tab_panel_width, dock_windows_to_edges_.get_h());
+	animate_map_panning_.set_desired_size(tab_panel_width, animate_map_panning_.get_h());
 	sb_dis_panel_.set_unit_width(200);
-	sb_dis_panel_.set_desired_size(column_width_, sb_dis_panel_.get_h());
+	sb_dis_panel_.set_desired_size(tab_panel_width, sb_dis_panel_.get_h());
 	sb_dis_border_.set_unit_width(200);
-	sb_dis_border_.set_desired_size(column_width_, sb_dis_border_.get_h());
+	sb_dis_border_.set_desired_size(tab_panel_width, sb_dis_border_.get_h());
 
 	// Sound options
-	music_.set_desired_size(column_width_, music_.get_h());
-	fx_.set_desired_size(column_width_, fx_.get_h());
-	message_sound_.set_desired_size(column_width_, message_sound_.get_h());
+	music_.set_desired_size(tab_panel_width, music_.get_h());
+	fx_.set_desired_size(tab_panel_width, fx_.get_h());
+	message_sound_.set_desired_size(tab_panel_width, message_sound_.get_h());
 
 	// Saving options
 	sb_autosave_.set_unit_width(250);
-	sb_autosave_.set_desired_size(column_width_, sb_autosave_.get_h());
+	sb_autosave_.set_desired_size(tab_panel_width, sb_autosave_.get_h());
 	sb_rolling_autosave_.set_unit_width(250);
-	sb_rolling_autosave_.set_desired_size(column_width_, sb_rolling_autosave_.get_h());
-	zip_.set_desired_size(column_width_, zip_.get_h());
-	write_syncstreams_.set_desired_size(column_width_, write_syncstreams_.get_h());
+	sb_rolling_autosave_.set_desired_size(tab_panel_width, sb_rolling_autosave_.get_h());
+	zip_.set_desired_size(tab_panel_width, zip_.get_h());
+	write_syncstreams_.set_desired_size(tab_panel_width, write_syncstreams_.get_h());
 
 	// Game options
-	transparent_chat_.set_desired_size(column_width_, transparent_chat_.get_h());
+	transparent_chat_.set_desired_size(tab_panel_width, transparent_chat_.get_h());
 }
 
 void FullscreenMenuOptions::add_languages_to_list(const std::string& current_locale) {
@@ -432,7 +422,7 @@
 	language_dropdown_.add(_("Try system language"), "", nullptr, current_locale == "");
 	language_dropdown_.add("English", "en", nullptr, current_locale == "en");
 
-	// Add translation directories to the list
+	// Add translation directories to the list. We use a vector so we can call std::sort on it.
 	std::vector<LanguageEntry> entries;
 	std::string selected_locale;
 
@@ -460,7 +450,9 @@
 
 				std::string name = i18n::make_ligatures(table->get_string("name").c_str());
 				const std::string sortname = table->get_string("sort_name");
-				entries.push_back(LanguageEntry(localename, name, sortname));
+				LanguageEntry* entry = new LanguageEntry(localename, name, sortname);
+				entries.push_back(*entry);
+				language_entries_.insert(std::make_pair(localename, *entry));
 
 				if (localename == current_locale) {
 					selected_locale = current_locale;
@@ -484,6 +476,67 @@
 	}
 }
 
+/**
+ * Updates the language statistics message according to the currently selected locale.
+ * @param include_system_lang We only want to include the system lang if it matches the Widelands
+ * locale.
+ */
+void FullscreenMenuOptions::update_language_stats(bool include_system_lang) {
+	int percent = 100;
+	std::string message = "";
+	if (language_dropdown_.has_selection()) {
+		std::string locale = language_dropdown_.get_selected();
+		// Empty locale means try system locale
+		if (locale.empty() && include_system_lang) {
+			std::vector<std::string> parts;
+			boost::split(parts, i18n::get_locale(), boost::is_any_of("."));
+			if (language_entries_.count(parts[0]) == 1) {
+				locale = parts[0];
+			} else {
+				boost::split(parts, parts[0], boost::is_any_of("@"));
+				if (language_entries_.count(parts[0]) == 1) {
+					locale = parts[0];
+				} else {
+					boost::split(parts, parts[0], boost::is_any_of("_"));
+					if (language_entries_.count(parts[0]) == 1) {
+						locale = parts[0];
+					}
+				}
+			}
+		}
+
+		// If we have the locale, grab the stats and set the message
+		if (language_entries_.count(locale) == 1) {
+			try {
+				const LanguageEntry& entry = language_entries_[locale];
+				Profile prof("i18n/translation_stats.conf");
+				Section& s = prof.get_safe_section(locale);
+				percent = floor(100.f * s.get_int("translated") / s.get_int("total"));
+				if (percent == 100) {
+					message = (boost::format(_("The translation into %s is complete.")) %
+								  entry.descname)
+									 .str();
+				} else {
+					message = (boost::format(_("The translation into %s is %d%% complete.")) %
+								  entry.descname % percent)
+									 .str();
+				}
+			} catch (...) {
+			}
+		}
+	}
+
+	// We will want some help with incomplete translations
+	if (percent <= 90) {
+		message = message + " " +
+		          (boost::format(_("If you wish to help us translate, please visit %s")) %
+		           "<font underline=1>widelands.org/wiki/TranslatingWidelands</font>")
+		             .str();
+	}
+	// Make font a bit smaller so the link will fit at 800x600 resolution.
+	translation_info_.set_text(as_uifont(message, 12));
+}
+
 void FullscreenMenuOptions::clicked_apply() {
 	end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kApplyOptions);
 }

=== modified file 'src/ui_fsmenu/options.h'
--- src/ui_fsmenu/options.h	2017-09-11 08:09:07 +0000
+++ src/ui_fsmenu/options.h	2017-10-09 19:42:37 +0000
@@ -104,6 +104,7 @@
 
 	// Fills the language selection list
 	void add_languages_to_list(const std::string& current_locale);
+	void update_language_stats(bool include_system_lang);
 
 	// Saves the options and reloads the active tab
 	void clicked_apply();
@@ -112,8 +113,6 @@
 	uint32_t butw_;
 	uint32_t buth_;
 	uint32_t hmargin_;
-	uint32_t tab_panel_width_;
-	uint32_t column_width_;
 	uint32_t tab_panel_y_;
 
 	UI::Textarea title_;
@@ -123,6 +122,7 @@
 	// UI elements
 	UI::TabPanel tabs_;
 	UI::Box box_interface_;
+	UI::Box box_interface_left_;
 	UI::Box box_windows_;
 	UI::Box box_sound_;
 	UI::Box box_saving_;
@@ -134,6 +134,7 @@
 	UI::Checkbox fullscreen_;
 	UI::Checkbox inputgrab_;
 	UI::SpinBox sb_maxfps_;
+	UI::MultilineTextarea translation_info_;
 
 	// Windows options
 	UI::Checkbox snap_win_overlap_only_;
@@ -170,6 +171,26 @@
 
 	/// All supported screen resolutions.
 	std::vector<ScreenResolution> resolutions_;
+
+	// Data model for the entries in the language selection list.
+	struct LanguageEntry {
+		LanguageEntry(const std::string& init_localename,
+		              const std::string& init_descname,
+		              const std::string& init_sortname)
+		   : localename(init_localename), descname(init_descname), sortname(init_sortname) {
+		}
+		LanguageEntry() : LanguageEntry("", "", "") {
+		}
+
+		bool operator<(const LanguageEntry& other) const {
+			return sortname < other.sortname;
+		}
+
+		std::string localename;  // ISO code for the locale
+		std::string descname;    // Native language name
+		std::string sortname;    // ASCII Language name used for sorting
+	};
+	std::map<std::string, LanguageEntry> language_entries_;
 };
 
 #endif  // end of include guard: WL_UI_FSMENU_OPTIONS_H

=== modified file 'utils/merge_and_push_translations.sh'
--- utils/merge_and_push_translations.sh	2017-05-19 07:41:26 +0000
+++ utils/merge_and_push_translations.sh	2017-10-09 19:42:37 +0000
@@ -67,6 +67,11 @@
 
 # Update catalogues.
 utils/buildcat.py
+
+# Update statistics.
+utils/update_translation_stats.py
+
+# Commit and push.
 bzr commit -m "Fetched translations and updated catalogues."
 bzr push lp:widelands
 

=== added file 'utils/update_translation_stats.py'
--- utils/update_translation_stats.py	1970-01-01 00:00:00 +0000
+++ utils/update_translation_stats.py	2017-10-09 19:42:37 +0000
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+
+"""Uses pocount from the Translate Toolkit to write translation statistics to
+data/i18n/translation_stats.conf.
+
+You will need to have the Translate Toolkit installed:
+http://toolkit.translatehouse.org/
+
+For Debian-based Linux: sudo apt-get install translate-toolkit
+
+"""
+
+from collections import defaultdict
+from subprocess import call, check_output, CalledProcessError
+import os.path
+import re
+import subprocess
+import sys
+import traceback
+
+#############################################################################
+# Data Containers                                                           #
+#############################################################################
+
+
+class TranslationStats:
+    """Total source words and translated source words."""
+
+    def __init__(self):
+        self.total = 0
+        self.translated = 0
+
+
+#############################################################################
+# Main Loop                                                                 #
+#############################################################################
+
+def generate_translation_stats(po_dir, output_file):
+    locale_stats = defaultdict(TranslationStats)
+
+    sys.stdout.write('Fetching translation stats ')
+
+    # We get errors for non-po files in the base po dir, so we have to walk
+    # the subdirs.
+    for subdir in sorted(os.listdir(po_dir), key=str.lower):
+        subdir = os.path.join(po_dir, subdir)
+        if not os.path.isdir(subdir):
+            continue
+
+        sys.stdout.write('.')
+        sys.stdout.flush()
+
+        try:
+            # We need shell=True, otherwise we get "No such file or directory".
+            stats_output = check_output(
+                ['pocount ' + subdir + ' --short-words'], stderr=subprocess.STDOUT, shell=True)
+            if 'ERROR' in stats_output:
+                print('\nError running pocount:\n' + stats_output.split('\n', 1)
+                      [0]) + '\nAborted creating translation statistics.'
+                return False
+
+        except CalledProcessError:
+            print('Failed to run pocount:\n  FILE: ' + po_dir +
+                  '\n  ' + stats_output.split('\n', 1)[1])
+            return False
+
+        result = stats_output.split('\n')
+
+        # Format provided by pocount:
+        # /home/<snip>/po/<textdomain>/<locale>.po  source words: total: 1701	| 500t	0f	1201u	| 29%t	0%f	70%u
+        regex_translated = re.compile(
+            '/\S+/(\w+)\.po\s+source words: total: (\d+)\t\| (\d+)t\t\d+f\t\d+u\t\| (\d+)%t\t\d+%f\t\d+%u')
+
+        for line in result:
+            match = regex_translated.match(line)
+            if match:
+                entry = TranslationStats()
+                locale = match.group(1)
+
+                if locale in locale_stats:
+                    entry = locale_stats[locale]
+
+                entry.total = entry.total + int(match.group(2))
+                entry.translated = entry.translated + int(match.group(3))
+                locale_stats[locale] = entry
+
+    print('\n\nLocale\tTotal\tTranslated')
+    print('------\t-----\t----------')
+    result = ''
+    for locale in sorted(locale_stats.keys(), key=str.lower):
+        entry = locale_stats[locale]
+        print(locale + '\t' + str(entry.total) + '\t' + str(entry.translated))
+        result = result + '[' + locale + ']\n'
+        result = result + 'translated=' + str(entry.translated) + '\n'
+        result = result + 'total=' + str(entry.total) + '\n\n'
+
+    with open(output_file, 'w+') as destination:
+        destination.write(result[:-1])  # Strip the final \n
+    print('\nResult written to ' + output_file)
+    return True
+
+
+def main():
+    try:
+        po_dir = os.path.abspath(os.path.join(
+            os.path.dirname(__file__), '../po'))
+        output_file = os.path.abspath(os.path.join(
+            os.path.dirname(__file__), '../data/i18n/translation_stats.conf'))
+        result = generate_translation_stats(po_dir, output_file)
+        return result
+
+    except Exception:
+        print('Something went wrong:')
+        traceback.print_exc()
+        return 1
+
+if __name__ == '__main__':
+    sys.exit(main())


Follow ups