← Back to team overview

widelands-dev team mailing list archive

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

 

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

Commit message:
Added 2 codecheck rules for translation markup.

- Ensure that translators' comments are immediately above their translations.
- Detect inconsistent tag format for translators' comments
- Check that all translatable strings that have multiple printf placeholders
  have defined those as reversible.
- Check that unordered, ordered and boost placeholders aren't mixed up in the
  same string.
- Check that ngettext singular and plural strings have the same placeholders.
- Check that placeholders are numbered in ascending order.

Requested reviews:
  Widelands Developers (widelands-dev)

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

These checks will fix the following i18n issues for the C++ side of things:

1. clang-format can break up code so that TRANSLATORS comments are no longer immediately above their gettext calls. This means that xgettext will no longer pick them up to show them to our translators.

2. I tried to reverse the order of 2 placeholders during translation today and forgot to add numbers, resulting in an invalid translation. So, it's best if the numbers are already in the source string - this will also make quality checks on the translations easier.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/codecheck_translations into lp:widelands.
=== modified file 'cmake/codecheck/CodeCheck.py'
--- cmake/codecheck/CodeCheck.py	2015-04-04 15:36:44 +0000
+++ cmake/codecheck/CodeCheck.py	2017-11-11 14:46:17 +0000
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
-# encoding: utf-8
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
 #
 
 """

=== added file 'cmake/codecheck/rules/translations_printf'
--- cmake/codecheck/rules/translations_printf	1970-01-01 00:00:00 +0000
+++ cmake/codecheck/rules/translations_printf	2017-11-11 14:46:17 +0000
@@ -0,0 +1,162 @@
+#!/usr/bin/python -tt
+
+"""Checks that all translatable strings that have multiple printf placeholders
+have defined those as reversible.
+
+Checks that unordered, ordered and boost placeholders aren't mixed up in the same string.
+
+Checks that ngettext singular and plural strings have the same placeholders.
+
+Checks that placeholders are numbered in ascending order.
+
+"""
+
+import re
+
+# Regex to find placeholders
+find_unordered = re.compile(r"[^0-9]\%([0-9#*]*\.*[0-9#]*[a-zA-Z]{1,2})")
+find_ordered = re.compile(r"\%[|]{0,1}(\d\$[0-9#*]*\.*[0-9#]*[a-zA-Z]{1,2})")
+find_boost = re.compile(r"\%(\d)\%")
+
+
+def check_placeholders(entry):
+    """Make sure that a string satisfies our placeholder policy."""
+    sanitized_entry = entry.replace('%%', '')
+    unordered = find_unordered.findall(sanitized_entry)
+    if len(unordered) > 1:
+        return 'Translatable string has multiple sprintf placeholders that are not ordered:'
+    else:
+        ordered = find_ordered.findall(sanitized_entry)
+        boost = find_boost.findall(sanitized_entry)
+        if len(unordered) > 0 and (len(ordered) > 0 or len(boost) > 0):
+            return 'Translatable string is mixing unordered sprintf placeholders with ordered or boost placeholders:'
+        if len(ordered) > 0 and (len(unordered) > 0 or len(boost) > 0):
+            return 'Translatable string is mixing ordered sprintf placeholders with unordered or boost placeholders:'
+        if len(ordered) > 0:
+            for entryno, placeholder in enumerate(ordered, 1):
+                if str(entryno) != placeholder[:placeholder.find('$')]:
+                    return 'Translatable string has an ordered sprintf placeholder "' + placeholder + '" in position ' + str(entryno) + " - the numbers don't match:"
+        if len(boost) > 0:
+            for entryno, placeholder in enumerate(boost, 1):
+                if str(entryno) != placeholder:
+                    return 'Translatable string has an ordered boost placeholder "' + placeholder + '" in position ' + str(entryno) + " - the numbers don't match:"
+    return ''
+
+
+def compare_placeholders(entry1, entry2):
+    """An Ngettext string must have the same splaceholders in its singular and
+    plural strings."""
+    sanitized_entry1 = entry1.replace('%%', '')
+    sanitized_entry2 = entry2.replace('%%', '')
+    placeholders1 = find_unordered.findall(sanitized_entry1)
+    if len(placeholders1) > 0:
+        placeholders2 = find_unordered.findall(sanitized_entry2)
+    else:
+        placeholders1 = find_ordered.findall(sanitized_entry1)
+        if len(placeholders1) > 0:
+            placeholders2 = find_ordered.findall(sanitized_entry2)
+        else:
+            placeholders1 = find_boost.findall(sanitized_entry1)
+            placeholders2 = find_boost.findall(sanitized_entry2)
+    for entryno, placeholder in enumerate(placeholders1, 0):
+        if placeholder != placeholders2[entryno]:
+            return 'Ngettext string has mismatching placeholders "' + placeholder + '" and "' + placeholders2[entryno] + '" in position ' + str(entryno + 1) + ':'
+
+    return ''
+
+
+def find_line(entry, lines):
+    """Find the line for the error entry.
+
+    Grabs the first match, so this is imprecise for multiple matches
+
+    """
+    checkme = entry.split('\n')
+    for lineno, line in enumerate(lines, 0):
+        if checkme[0] in line:
+            is_match = True
+            for rowno, row in enumerate(checkme, 0):
+                if (lineno + rowno) < len(lines):
+                    if not checkme[rowno] in lines[lineno + rowno]:
+                        is_match = False
+                        break
+            if is_match:
+                return lineno + 1
+    return 0
+
+
+def evaluate_matches(lines, fn):
+    """Main check."""
+    result = []
+    gettext_started = False
+    gettext_ended = False
+    gettext_string = ''
+    gettext_line = 0
+
+    searchme = ''.join(lines).strip()
+
+    single_strings = []
+    string_pairs = []
+
+    # Simple gettext
+    # DOTALL makes . match newlines
+    matches = re.findall(r"_\(\"(.*?)\"\)", searchme, re.DOTALL)
+    for match in matches:
+        if '%' in match:
+            single_strings.append(match)
+
+    matches = re.findall(r"\Wgettext\(\"(.*?)\"\)", searchme, re.DOTALL)
+    for match in matches:
+        if '%' in match:
+            single_strings.append(match)
+
+    # pgettext
+    matches = re.findall(
+        r"\Wpgettext\(\".*?\",.+?\"(.*?)\"\)", searchme, re.DOTALL)
+    for match in matches:
+        if '%' in match:
+            single_strings.append(match)
+
+    # ngettext
+    matches = re.findall(
+        r"ngettext\(\"(.*?)\",.+?\"(.*?)\".+?\w+?\)", searchme, re.DOTALL)
+    for match in matches:
+        if '%' in match[0] or '%' in match[1]:
+            string_pairs.append(match)
+
+    # npgettext
+    matches = re.findall(
+        r"npgettext\(\".*?\",.+?(.*?)\",.+?\"(.*?)\".+?\w+?\)", searchme, re.DOTALL)
+    for match in matches:
+        if '%' in match[0] or '%' in match[1]:
+            string_pairs.append(match)
+
+    for entry in single_strings:
+        check_result = check_placeholders(entry)
+        if len(check_result) > 0:
+            # print(check_result)
+            result.append((fn, find_line(entry, lines),
+                           check_result + '\n' + entry))
+
+    for checkme in string_pairs:
+        for entry in checkme:
+            check_result = check_placeholders(entry)
+            if len(check_result) > 0:
+                # print(check_result)
+                result.append((fn, find_line(
+                    checkme[0], lines), check_result + '\n' + checkme[0] + ' - ' + checkme[1]))
+
+        check_result = compare_placeholders(checkme[0], checkme[1])
+        if len(check_result) > 0:
+            # print(check_result)
+            result.append((fn, find_line(
+                checkme[0], lines), check_result + '\n' + checkme[0] + ' - ' + checkme[1]))
+
+    return result
+
+# File this is called on is always called testdir/test.h
+forbidden = [
+]
+
+allowed = [
+]

=== added file 'cmake/codecheck/rules/translators_comments_above_translations'
--- cmake/codecheck/rules/translators_comments_above_translations	1970-01-01 00:00:00 +0000
+++ cmake/codecheck/rules/translators_comments_above_translations	2017-11-11 14:46:17 +0000
@@ -0,0 +1,62 @@
+#!/usr/bin/python -tt
+
+"""Checks that all TRANSLATORS: comments are immediately above the string to be
+translated.
+
+Also checks for malformed translators' comment tag. Will catch
+permutation typos like "TRASNLATORS".
+
+"""
+
+import re
+
+
+def evaluate_matches(lines, fn):
+    result = []
+    translators_comment_started = False
+    translators_comment_ended = False
+    last_translators_comment = ''
+    last_translators_comment_line = 0
+
+    translator_tag = re.compile('.*[*/]\s+[TRANSLATOR]{10}.*')
+
+    for lineno, line in enumerate(lines, 1):
+        # Find start of translators' comment
+        if translator_tag.match(line):
+            translators_comment_started = True
+            # Create error for malformed tag - we want consistency here
+            if not '/** TRANSLATORS: ' in line:
+                result.append((fn, lineno, 'Translators\' comment starting with "' +
+                               line.strip() + '" does not start with: "/** TRANSLATORS:"'))
+        # Comments can go over multiple lines, so we check for its end
+        # separately
+        if translators_comment_started:
+            last_translators_comment = last_translators_comment + ' ' + line
+            if '*/' in line:
+                translators_comment_ended = True
+                last_translators_comment_line = lineno
+
+        # Check for gettext. This can fail if we have another function name ending with _.
+        # Should hopefully fail rarely though, since that's violating our code
+        # style.
+        if '_("' in line or 'gettext' in line:
+            # We have a complete comment, so there should be a gettext call
+            # immediatley afterwards. Otherwise, xgettext won't find the
+            # translators' comment.
+            if translators_comment_ended:
+                if lineno != last_translators_comment_line + 1:
+                    result.append(
+                        (fn, lineno, 'Translators comment is not directly above its translation:\n' + last_translators_comment.strip()))
+            # Reset if there was no complete comment
+            translators_comment_started = False
+            translators_comment_ended = False
+            last_translators_comment = ''
+            last_translators_comment_line = 0
+    return result
+
+# File this is called on is always called testdir/test.h
+forbidden = [
+]
+
+allowed = [
+]

=== modified file 'src/editor/editorinteractive.cc'
--- src/editor/editorinteractive.cc	2017-09-13 07:27:00 +0000
+++ src/editor/editorinteractive.cc	2017-11-11 14:46:17 +0000
@@ -620,9 +620,10 @@
 				   egbase.world(), 64, 64, 0,
 				   /** TRANSLATORS: Default name for new map */
 				   _("No Name"),
-				   /** TRANSLATORS: Map author name when it hasn't been set yet */
 				   g_options.pull_section("global").get_string(
-				      "realname", pgettext("author_name", "Unknown")));
+				      "realname",
+								/** TRANSLATORS: Map author name when it hasn't been set yet */
+								pgettext("author_name", "Unknown")));
 
 				load_all_tribes(&egbase, &loader_ui);
 

=== modified file 'src/editor/tools/info_tool.cc'
--- src/editor/tools/info_tool.cc	2017-08-31 09:49:08 +0000
+++ src/editor/tools/info_tool.cc	2017-11-11 14:46:17 +0000
@@ -58,36 +58,44 @@
 	std::vector<std::string> caps_strings;
 	Widelands::NodeCaps const caps = f.nodecaps();
 	switch (caps & Widelands::BUILDCAPS_SIZEMASK) {
-	/** TRANSLATORS: Editor terrain property: small building plot */
-	case Widelands::BUILDCAPS_SMALL:
+
+	case Widelands::BUILDCAPS_SMALL: {
+		/** TRANSLATORS: Editor terrain property: small building plot */
 		caps_strings.push_back(_("small"));
-		break;
-	/** TRANSLATORS: Editor terrain property: medium building plot */
-	case Widelands::BUILDCAPS_MEDIUM:
+	} break;
+
+	case Widelands::BUILDCAPS_MEDIUM: {
+		/** TRANSLATORS: Editor terrain property: medium building plot */
 		caps_strings.push_back(_("medium"));
-		break;
-	/** TRANSLATORS: Editor terrain property: big building plot */
-	case Widelands::BUILDCAPS_BIG:
+	} break;
+	case Widelands::BUILDCAPS_BIG: {
+		/** TRANSLATORS: Editor terrain property: big building plot */
 		caps_strings.push_back(_("big"));
-		break;
+	} break;
 	default:
 		break;
 	};
-	/** TRANSLATORS: Editor terrain property: space for a flag */
-	if (caps & Widelands::BUILDCAPS_FLAG)
+
+	if (caps & Widelands::BUILDCAPS_FLAG) {
+		/** TRANSLATORS: Editor terrain property: space for a flag */
 		caps_strings.push_back(_("flag"));
-	/** TRANSLATORS: Editor terrain property: mine building plot */
-	if (caps & Widelands::BUILDCAPS_MINE)
+	}
+	if (caps & Widelands::BUILDCAPS_MINE) {
+		/** TRANSLATORS: Editor terrain property: mine building plot */
 		caps_strings.push_back(_("mine"));
-	/** TRANSLATORS: Editor terrain property: port space */
-	if (caps & Widelands::BUILDCAPS_PORT)
+	}
+	if (caps & Widelands::BUILDCAPS_PORT) {
+		/** TRANSLATORS: Editor terrain property: port space */
 		caps_strings.push_back(_("port"));
-	/** TRANSLATORS: Editor terrain property: units can walk on this terrain */
-	if (caps & Widelands::MOVECAPS_WALK)
+	}
+	if (caps & Widelands::MOVECAPS_WALK) {
+		/** TRANSLATORS: Editor terrain property: units can walk on this terrain */
 		caps_strings.push_back(_("walkable"));
-	/** TRANSLATORS: Editor terrain property: units can swim on this terrain (fish, ships) */
-	if (caps & Widelands::MOVECAPS_SWIM)
+	}
+	if (caps & Widelands::MOVECAPS_SWIM) {
+		/** TRANSLATORS: Editor terrain property: units can swim on this terrain (fish, ships) */
 		caps_strings.push_back(_("swimmable"));
+	}
 
 	buf += std::string("• ") +
 	       (boost::format(_("Caps: %s")) %
@@ -124,10 +132,11 @@
 	for (const Widelands::TerrainDescription::Type& terrain_type : ter.get_types()) {
 		terrain_is_strings.push_back(terrain_type.descname);
 	}
-	/** TRANSLATORS: "Is" is a list of terrain properties, e.g. "arable", "unreachable and
-	 * unwalkable" */
-	/** TRANSLATORS: You can also translate this as "Category: %s" or "Property: %s" */
+
 	buf += "• " +
+			/** TRANSLATORS: "Is" is a list of terrain properties, e.g. "arable", "unreachable and
+			 * unwalkable" */
+			/** TRANSLATORS: You can also translate this as "Category: %s" or "Property: %s" */
 	       (boost::format(_("Is: %s")) %
 	        i18n::localize_list(terrain_is_strings, i18n::ConcatenateWith::AMPERSAND))
 	          .str() +

=== modified file 'src/editor/ui_menus/main_menu_load_or_save_map.cc'
--- src/editor/ui_menus/main_menu_load_or_save_map.cc	2017-05-13 11:25:24 +0000
+++ src/editor/ui_menus/main_menu_load_or_save_map.cc	2017-11-11 14:46:17 +0000
@@ -87,8 +87,8 @@
 	                  g_gr->images().get("images/ui_basic/but1.png"), _("Show Map Names"));
 	vbox->add(show_mapnames_, UI::Box::Resizing::kFullSize);
 
-	/** TRANSLATORS: Checkbox title. If this checkbox is enabled, map names aren't translated. */
 	cb_dont_localize_mapnames_ =
+			/** TRANSLATORS: Checkbox title. If this checkbox is enabled, map names aren't translated. */
 	   new UI::Checkbox(vbox, Vector2i::zero(), _("Show original map names"));
 	cb_dont_localize_mapnames_->set_state(false);
 	vbox->add_space(2 * padding_);

=== modified file 'src/editor/ui_menus/main_menu_save_map.cc'
--- src/editor/ui_menus/main_menu_save_map.cc	2017-11-05 19:59:33 +0000
+++ src/editor/ui_menus/main_menu_save_map.cc	2017-11-11 14:46:17 +0000
@@ -219,8 +219,8 @@
 
 void MainMenuSaveMap::set_current_directory(const std::string& filename) {
 	curdir_ = filename;
-	/** TRANSLATORS: The folder that a file will be saved to. */
 	directory_info_.set_text(
+				/** TRANSLATORS: The folder that a file will be saved to. */
 	   (boost::format(_("Current Directory: %s")) % (_("My Maps") + curdir_.substr(basedir_.size())))
 	      .str());
 }

=== modified file 'src/editor/ui_menus/player_menu.cc'
--- src/editor/ui_menus/player_menu.cc	2017-09-13 07:27:00 +0000
+++ src/editor/ui_menus/player_menu.cc	2017-11-11 14:46:17 +0000
@@ -200,8 +200,8 @@
 	map->set_nrplayers(nr_players);
 	{                             //  register new default name for this players
 		assert(nr_players <= 99);  //  2 decimal digits
-		/** TRANSLATORS: Default player name, e.g. Player 1 */
 		const std::string name =
+				/** TRANSLATORS: Default player name, e.g. Player 1 */
 		   (boost::format(_("Player %u")) % static_cast<unsigned int>(nr_players)).str();
 		map->set_scenario_player_name(nr_players, name);
 	}

=== modified file 'src/logic/map_objects/tribes/militarysite.cc'
--- src/logic/map_objects/tribes/militarysite.cc	2017-08-19 22:22:20 +0000
+++ src/logic/map_objects/tribes/militarysite.cc	2017-11-11 14:46:17 +0000
@@ -379,10 +379,11 @@
 		}
 	} else {
 		if (capacity_ > stationed) {
-			/** TRANSLATORS: %1% is the number of soldiers the plural refers to */
-			/** TRANSLATORS: %2% are currently open soldier slots in the building */
-			/** TRANSLATORS: %3% is the maximum number of soldier slots in the building */
+
 			*s = (boost::format(
+						/** TRANSLATORS: %1% is the number of soldiers the plural refers to */
+						/** TRANSLATORS: %2% are currently open soldier slots in the building */
+						/** TRANSLATORS: %3% is the maximum number of soldier slots in the building */
 			         ngettext("%1%(+%2%) soldier (+%3%)", "%1%(+%2%) soldiers (+%3%)", stationed)) %
 			      present % (stationed - present) % (capacity_ - stationed))
 			        .str();

=== modified file 'src/logic/map_objects/tribes/production_program.cc'
--- src/logic/map_objects/tribes/production_program.cc	2017-08-20 17:45:42 +0000
+++ src/logic/map_objects/tribes/production_program.cc	2017-11-11 14:46:17 +0000
@@ -381,16 +381,16 @@
 	}
 	std::string condition = i18n::localize_list(condition_list, i18n::ConcatenateWith::AND);
 	if (1 < group.second) {
-		/** TRANSLATORS: This is an item in a list of wares, e.g. "3x water": */
-		/** TRANSLATORS:    %1$i = "3" */
-		/** TRANSLATORS:    %2$s = "water" */
 		condition =
+				/** TRANSLATORS: This is an item in a list of wares, e.g. "3x water": */
+				/** TRANSLATORS:    %1$i = "3" */
+				/** TRANSLATORS:    %2$s = "water" */
 		   (boost::format(_("%1$ix %2$s")) % static_cast<unsigned int>(group.second) % condition)
 		      .str();
 	}
 
-	/** TRANSLATORS: %s is a list of wares*/
 	std::string result =
+			/** TRANSLATORS: %s is a list of wares*/
 	   (boost::format(_("the building has the following wares: %s")) % condition).str();
 	return result;
 }
@@ -407,16 +407,16 @@
 	}
 	std::string condition = i18n::localize_list(condition_list, i18n::ConcatenateWith::AND);
 	if (1 < group.second) {
-		/** TRANSLATORS: This is an item in a list of wares, e.g. "3x water": */
-		/** TRANSLATORS:    %1$i = "3" */
-		/** TRANSLATORS:    %2$s = "water" */
 		condition =
+				/** TRANSLATORS: This is an item in a list of wares, e.g. "3x water": */
+				/** TRANSLATORS:    %1$i = "3" */
+				/** TRANSLATORS:    %2$s = "water" */
 		   (boost::format(_("%1$ix %2$s")) % static_cast<unsigned int>(group.second) % condition)
 		      .str();
 	}
 
+	std::string result =
 	/** TRANSLATORS: %s is a list of wares*/
-	std::string result =
 	   (boost::format(_("the building doesn’t have the following wares: %s")) % condition).str();
 	return result;
 }

=== modified file 'src/network/constants.h'
--- src/network/constants.h	2017-01-25 18:55:59 +0000
+++ src/network/constants.h	2017-11-11 14:46:17 +0000
@@ -20,8 +20,10 @@
 #ifndef WL_NETWORK_CONSTANTS_H
 #define WL_NETWORK_CONSTANTS_H
 
-#define WIDELANDS_LAN_DISCOVERY_PORT 7394
-#define WIDELANDS_LAN_PROMOTION_PORT 7395
-#define WIDELANDS_PORT 7396
+#include <stdint.h>
+
+constexpr uint16_t kWidelandsLanDiscoveryPort = 7394;
+constexpr uint16_t kWidelandsLanPromotionPort = 7395;
+constexpr uint16_t kWidelandsLanPort = 7396;
 
 #endif  // end of include guard: WL_NETWORK_CONSTANTS_H

=== modified file 'src/network/gamehost.cc'
--- src/network/gamehost.cc	2017-11-06 20:19:56 +0000
+++ src/network/gamehost.cc	2017-11-11 14:46:17 +0000
@@ -487,7 +487,7 @@
 	d->localplayername = playername;
 
 	// create a listening socket
-	d->net = NetHost::listen(WIDELANDS_PORT);
+	d->net = NetHost::listen(kWidelandsLanPort);
 	if (d->net == nullptr) {
 		// This might happen when the widelands socket is already in use
 		throw WLWarning(_("Failed to start the server!"),

=== modified file 'src/network/internet_gaming.cc'
--- src/network/internet_gaming.cc	2017-08-16 04:31:56 +0000
+++ src/network/internet_gaming.cc	2017-11-11 14:46:17 +0000
@@ -610,10 +610,10 @@
 			assert(waitcmd_ == IGPCMD_GAME_CONNECT);
 			waitcmd_ = "";
 			// Save the received IP(s), so the client can connect to the game
-			NetAddress::parse_ip(&gameips_.first, packet.string(), WIDELANDS_PORT);
+			NetAddress::parse_ip(&gameips_.first, packet.string(), kWidelandsLanPort);
 			// If the next value is true, a secondary IP follows
 			if (packet.string() == bool2str(true)) {
-				NetAddress::parse_ip(&gameips_.second, packet.string(), WIDELANDS_PORT);
+				NetAddress::parse_ip(&gameips_.second, packet.string(), kWidelandsLanPort);
 			}
 			log("InternetGaming: Received ips of the game to join: %s %s.\n",
 			    gameips_.first.ip.to_string().c_str(), gameips_.second.ip.to_string().c_str());

=== modified file 'src/network/network_lan_promotion.cc'
--- src/network/network_lan_promotion.cc	2017-06-10 16:36:29 +0000
+++ src/network/network_lan_promotion.cc	2017-11-11 14:46:17 +0000
@@ -23,6 +23,8 @@
 #include <ifaddrs.h>
 #endif
 
+#include <boost/lexical_cast.hpp>
+
 #include "base/i18n.h"
 #include "base/log.h"
 #include "base/warning.h"
@@ -347,11 +349,17 @@
 
 void LanBase::report_network_error() {
 	// No socket open? Sorry, but we can't continue this way
+	const std::vector<std::string> ports_list(
+	   {boost::lexical_cast<std::string>(kWidelandsLanDiscoveryPort),
+	    boost::lexical_cast<std::string>(kWidelandsLanPromotionPort),
+	    boost::lexical_cast<std::string>(kWidelandsLanPort)});
+
 	throw WLWarning(_("Failed to use the local network!"),
+	                /** TRANSLATORS: %s is a list of alternative ports with "or" */
 	                _("Widelands was unable to use the local network. "
-	                  "Maybe some other process is already running a server on port %d, %d or %d "
+	                  "Maybe some other process is already running a server on port %s"
 	                  "or your network setup is broken."),
-	                WIDELANDS_LAN_DISCOVERY_PORT, WIDELANDS_LAN_PROMOTION_PORT, WIDELANDS_PORT);
+	                i18n::localize_list(ports_list, i18n::ConcatenateWith::OR).c_str());
 }
 
 void LanBase::close_socket(boost::asio::ip::udp::socket* socket) {
@@ -367,7 +375,7 @@
 
 /*** class LanGamePromoter ***/
 
-LanGamePromoter::LanGamePromoter() : LanBase(WIDELANDS_LAN_PROMOTION_PORT) {
+LanGamePromoter::LanGamePromoter() : LanBase(kWidelandsLanPromotionPort) {
 
 	needupdate = true;
 
@@ -386,14 +394,14 @@
 	gameinfo.state = LAN_GAME_CLOSED;
 
 	// Don't care about errors at this point
-	broadcast(&gameinfo, sizeof(gameinfo), WIDELANDS_LAN_DISCOVERY_PORT);
+	broadcast(&gameinfo, sizeof(gameinfo), kWidelandsLanDiscoveryPort);
 }
 
 void LanGamePromoter::run() {
 	if (needupdate) {
 		needupdate = false;
 
-		if (!broadcast(&gameinfo, sizeof(gameinfo), WIDELANDS_LAN_DISCOVERY_PORT)) {
+		if (!broadcast(&gameinfo, sizeof(gameinfo), kWidelandsLanDiscoveryPort)) {
 			report_network_error();
 		}
 	}
@@ -423,7 +431,7 @@
 
 /*** class LanGameFinder ***/
 
-LanGameFinder::LanGameFinder() : LanBase(WIDELANDS_LAN_DISCOVERY_PORT), callback(nullptr) {
+LanGameFinder::LanGameFinder() : LanBase(kWidelandsLanDiscoveryPort), callback(nullptr) {
 
 	reset();
 }
@@ -436,7 +444,7 @@
 	strncpy(magic, "QUERY", 8);
 	magic[6] = LAN_PROMOTION_PROTOCOL_VERSION;
 
-	if (!broadcast(magic, 8, WIDELANDS_LAN_PROMOTION_PORT))
+	if (!broadcast(magic, 8, kWidelandsLanPromotionPort))
 		report_network_error();
 }
 
@@ -473,7 +481,7 @@
 
 		if (!was_in_list) {
 			opengames.push_back(new NetOpenGame);
-			addr.port = WIDELANDS_PORT;
+			addr.port = kWidelandsLanPort;
 			opengames.back()->address = addr;
 			opengames.back()->info = info;
 			callback(GameOpened, opengames.back(), userdata);

=== modified file 'src/ui_fsmenu/netsetup_lan.cc'
--- src/ui_fsmenu/netsetup_lan.cc	2017-06-10 16:36:29 +0000
+++ src/ui_fsmenu/netsetup_lan.cc	2017-11-11 14:46:17 +0000
@@ -149,9 +149,9 @@
 	}
 
 	// The user probably entered a hostname on his own. Try to resolve it
-	if (NetAddress::resolve_to_v6(addr, host, WIDELANDS_PORT))
+	if (NetAddress::resolve_to_v6(addr, host, kWidelandsLanPort))
 		return true;
-	if (NetAddress::resolve_to_v4(addr, host, WIDELANDS_PORT))
+	if (NetAddress::resolve_to_v4(addr, host, kWidelandsLanPort))
 		return true;
 	return false;
 }
@@ -193,8 +193,8 @@
 	case LAN_GAME_CLOSED:
 		er.set_string(2, _("Closed"));
 		break;
-	/** TRANSLATORS: The state of a LAN game can be open, closed or unknown */
 	default:
+		/** TRANSLATORS: The state of a LAN game can be open, closed or unknown */
 		er.set_string(2, pgettext("game_state", "Unknown"));
 		break;
 	};

=== modified file 'src/ui_fsmenu/options.cc'
--- src/ui_fsmenu/options.cc	2017-11-11 11:21:58 +0000
+++ src/ui_fsmenu/options.cc	2017-11-11 14:46:17 +0000
@@ -278,7 +278,7 @@
 	apply_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_apply, this));
 	ok_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_ok, this));
 
-	/** TRANSLATORS Options: Save game automatically every: */
+	/** TRANSLATORS: Options: Save game automatically every: */
 	sb_autosave_.add_replacement(0, _("Off"));
 
 	// Fill in data

=== modified file 'src/wlapplication_messages.cc'
--- src/wlapplication_messages.cc	2017-09-11 08:09:07 +0000
+++ src/wlapplication_messages.cc	2017-11-11 14:46:17 +0000
@@ -125,10 +125,10 @@
 	          << endl
 	          << _(" --xres=[...]         Width of the window in pixel.") << endl
 	          << _(" --yres=[...]         Height of the window in pixel.") << endl
-	          /** TRANSLATORS: You may translate true/false, also as on/off or yes/no, but */
-	          /** TRANSLATORS: it HAS TO BE CONSISTENT with the translation in the widelands
-	             textdomain */
 	          << endl
+					 /** TRANSLATORS: You may translate true/false, also as on/off or yes/no, but */
+		          /** TRANSLATORS: it HAS TO BE CONSISTENT with the translation in the widelands
+		             textdomain */
 	          << _("Options for the internal window manager:") << endl
 	          << _(" --animate_map_panning=[yes|no]\n"
 	               "                      Should automatic map movements be animated.")

=== modified file 'src/wui/attack_box.cc'
--- src/wui/attack_box.cc	2017-08-19 22:22:20 +0000
+++ src/wui/attack_box.cc	2017-11-11 14:46:17 +0000
@@ -114,9 +114,8 @@
 	soldiers_slider_->set_enabled(max_attackers > 0);
 	more_soldiers_->set_enabled(max_attackers > soldiers_slider_->get_value());
 	less_soldiers_->set_enabled(soldiers_slider_->get_value() > 0);
-
-	/** TRANSLATORS: %1% of %2% soldiers. Used in Attack box. */
 	soldiers_text_->set_text(
+				/** TRANSLATORS: %1% of %2% soldiers. Used in Attack box. */
 	   (boost::format(_("%1% / %2%")) % soldiers_slider_->get_value() % max_attackers).str());
 
 	more_soldiers_->set_title(std::to_string(max_attackers));

=== modified file 'src/wui/building_statistics_menu.cc'
--- src/wui/building_statistics_menu.cc	2017-08-20 08:34:02 +0000
+++ src/wui/building_statistics_menu.cc	2017-11-11 14:46:17 +0000
@@ -667,7 +667,7 @@
 				   total_soldier_capacity > total_stationed_soldiers);
 				navigation_buttons_[NavigationButton::NextUnproductive]->set_visible(true);
 				navigation_buttons_[NavigationButton::PrevUnproductive]->set_visible(true);
-				/** TRANSLATORS Label for number of buildings that are waiting for soldiers */
+				/** TRANSLATORS: Label for number of buildings that are waiting for soldiers */
 				unproductive_label_.set_text(_("Lacking Soldiers:"));
 				unproductive_box_.set_visible(true);
 				unproductive_label_.set_visible(true);
@@ -677,7 +677,7 @@
 
 		std::string owned_text;
 		if (player.tribe().has_building(id) && (building.is_buildable() || building.is_enhanced())) {
-			/** TRANSLATORS Buildings: owned / under construction */
+			/** TRANSLATORS: Buildings: owned / under construction */
 			owned_text = (boost::format(_("%1%/%2%")) % nr_owned % nr_build).str();
 		} else {
 			owned_text = (boost::format(_("%1%/%2%")) % nr_owned % "–").str();

=== modified file 'src/wui/game_message_menu.cc'
--- src/wui/game_message_menu.cc	2017-09-15 19:45:11 +0000
+++ src/wui/game_message_menu.cc	2017-11-11 14:46:17 +0000
@@ -620,9 +620,8 @@
 	switch (mode) {
 	case Archive:
 		if (no_selections > 1) {
-			/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message.
-			 */
 			button_tooltip =
+					/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message. */
 			   (boost::format(ngettext("Restore the selected %d message",
 			                           "Restore the selected %d messages", no_selections)) %
 			    no_selections)
@@ -634,9 +633,8 @@
 		break;
 	case Inbox:
 		if (no_selections > 1) {
-			/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message.
-			 */
 			button_tooltip =
+					/** TRANSLATORS: Tooltip in the messages window. There is a separate string for 1 message. */
 			   (boost::format(ngettext("Archive the selected %d message",
 			                           "Archive the selected %d messages", no_selections)) %
 			    no_selections)

=== modified file 'src/wui/load_or_save_game.cc'
--- src/wui/load_or_save_game.cc	2017-11-06 20:19:56 +0000
+++ src/wui/load_or_save_game.cc	2017-11-11 14:46:17 +0000
@@ -411,14 +411,14 @@
 					gamedata.savedatestring =
 					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
 					      month day, year */
-					   (boost::format(_("%2% %1%, %3%")) % savedate->tm_mday %
-					    localize_month(savedate->tm_mon) % (1900 + savedate->tm_year))
+					   (boost::format(_("%1% %2%, %3%"))  %
+					    localize_month(savedate->tm_mon) % savedate->tm_mday % (1900 + savedate->tm_year))
 					      .str();
 					gamedata.savedonstring =
 					   /** TRANSLATORS: Display date for choosing a savegame/replay. Placeholders are:
 					      month day, year. This is part of a list. */
-					   (boost::format(_("saved on %2% %1%, %3%")) % savedate->tm_mday %
-					    localize_month(savedate->tm_mon) % (1900 + savedate->tm_year))
+					   (boost::format(_("saved on %1% %2%, %3%"))  %
+					    localize_month(savedate->tm_mon) % savedate->tm_mday % (1900 + savedate->tm_year))
 					      .str();
 				}
 			}

=== modified file 'src/wui/soldierlist.cc'
--- src/wui/soldierlist.cc	2017-08-16 05:10:15 +0000
+++ src/wui/soldierlist.cc	2017-11-11 14:46:17 +0000
@@ -382,9 +382,9 @@
 	int w =
 	   UI::g_fh1->render(
 	               as_uifont((boost::format("%s ")  // We need some extra space to fix bug 724169
-	                          /** TRANSLATORS: Health, Attack, Defense, Evade */
-	                          % (boost::format(_(
-	                                "HP: %1$u/%2$u  AT: %3$u/%4$u  DE: %5$u/%6$u  EV: %7$u/%8$u")) %
+	                          % (boost::format(
+											  /** TRANSLATORS: Health, Attack, Defense, Evade */
+											  _("HP: %1$u/%2$u  AT: %3$u/%4$u  DE: %5$u/%6$u  EV: %7$u/%8$u")) %
 	                             8 % 8 % 8 % 8 % 8 % 8 % 8 % 8))
 	                            .str()))
 	      ->width();


References