From e1d447f1cf65444faf48a3147d3149b74708f101 Mon Sep 17 00:00:00 2001 From: Oluwatobi Shokunbi Date: Thu, 23 Jul 2026 15:21:10 +0100 Subject: [PATCH 1/2] fix(internal): append CHAR_LIMIT to translation descriptions in export script Fixes #1613 by updating the export_translations script to automatically calculate and append [CHAR_LIMIT=xxx] to each translation_description that doesn't already have one. The limit defaults to ~1.5x the English string length, rounded up to the nearest 5. Also fixes Python 2 print statement for Python 3 compatibility. --- scripts/translations/export_translations.py | 36 ++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/scripts/translations/export_translations.py b/scripts/translations/export_translations.py index 5c10b58e20..2fbd86e41a 100644 --- a/scripts/translations/export_translations.py +++ b/scripts/translations/export_translations.py @@ -1,5 +1,6 @@ # coding=UTF-8 +import math import os import re import sys @@ -9,6 +10,24 @@ PREFIXED_NAME_START = 'name="fui_' UNPREFIXED_NAME_START = 'name="' +CHAR_LIMIT_PATTERN = re.compile(r'\[CHAR_LIMIT=\d+\]') +TRANSLATION_DESC_PATTERN = re.compile(r'(translation_description="[^"]*?)(")') +XML_TAG_PATTERN = re.compile(r'<[^>]+>') +STRING_VALUE_PATTERN = re.compile(r'>([^<]*(?:]*>[^<]*[^<]*)*)') + + +def _extract_visible_text(value): + """Strip XML tags to get the visible text length.""" + return XML_TAG_PATTERN.sub('', value).strip() + + +def _calculate_char_limit(english_text): + """Return ~1.5x the English text length, rounded up to the nearest 5.""" + length = len(english_text) + limit = math.ceil(length * 1.5) + return int(math.ceil(limit / 5.0) * 5) + + class ExportTranslationsScript(BaseStringScript): def ProcessTag(self, line, type): @@ -16,13 +35,20 @@ def ProcessTag(self, line, type): if PREFIXED_NAME_START in joined: joined = joined.replace(PREFIXED_NAME_START, UNPREFIXED_NAME_START) - return joined.split('\n') - else: - return line + + if 'translation_description=' in joined and not CHAR_LIMIT_PATTERN.search(joined): + match = STRING_VALUE_PATTERN.search(joined) + if match: + visible = _extract_visible_text(match.group(1)) + if visible: + limit = _calculate_char_limit(visible) + joined = TRANSLATION_DESC_PATTERN.sub( + r'\1 [CHAR_LIMIT=%d]\2' % limit, joined, count=1) + + return joined.split('\n') def WriteFile(self, file_name, file_contents): - # Override to just print the contents - print file_contents + print(file_contents) if __name__ == '__main__': ets = ExportTranslationsScript() From e510af5b02c74a526366952549d2d2d000c0f184 Mon Sep 17 00:00:00 2001 From: Oluwatobi Shokunbi Date: Thu, 23 Jul 2026 16:01:41 +0100 Subject: [PATCH 2/2] fix(internal): append CHAR_LIMIT to translation descriptions in export script Fixes #1613 by updating the export_translations script to automatically calculate and append [CHAR_LIMIT=xxx] to each translation_description that doesn't already have one. The limit defaults to ~1.5x the English string length, rounded up to the nearest 5. Handles plurals items individually, decodes XML entities and Android escape sequences for accurate length calculation, and preserves existing manual limits. Also fixes Python 2 print statement for Python 3 compatibility. --- scripts/translations/export_translations.py | 51 +++++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/scripts/translations/export_translations.py b/scripts/translations/export_translations.py index 2fbd86e41a..ffb96bcdc0 100644 --- a/scripts/translations/export_translations.py +++ b/scripts/translations/export_translations.py @@ -14,11 +14,24 @@ TRANSLATION_DESC_PATTERN = re.compile(r'(translation_description="[^"]*?)(")') XML_TAG_PATTERN = re.compile(r'<[^>]+>') STRING_VALUE_PATTERN = re.compile(r'>([^<]*(?:]*>[^<]*[^<]*)*)') +ITEM_VALUE_PATTERN = re.compile(r'>([^<]*(?:]*>[^<]*[^<]*)*)') + +XML_ENTITIES = {'&': '&', '<': '<', '>': '>', '"': '"', ''': "'"} +ESCAPE_PATTERN = re.compile(r'\\(u[0-9A-Fa-f]{4}|n|t|\'|")') + + +def _decode_entities(text): + """Decode XML entities and Android escape sequences to get true visible length.""" + for entity, char in XML_ENTITIES.items(): + text = text.replace(entity, char) + text = ESCAPE_PATTERN.sub('X', text) + return text def _extract_visible_text(value): - """Strip XML tags to get the visible text length.""" - return XML_TAG_PATTERN.sub('', value).strip() + """Strip XML tags and decode entities to get the visible text length.""" + text = XML_TAG_PATTERN.sub('', value).strip() + return _decode_entities(text) def _calculate_char_limit(english_text): @@ -28,6 +41,23 @@ def _calculate_char_limit(english_text): return int(math.ceil(limit / 5.0) * 5) +def _add_char_limit(text, value_pattern): + """Add [CHAR_LIMIT=xxx] to translation_description if missing.""" + if 'translation_description=' not in text: + return text + if CHAR_LIMIT_PATTERN.search(text): + return text + match = value_pattern.search(text) + if not match: + return text + visible = _extract_visible_text(match.group(1)) + if not visible: + return text + limit = _calculate_char_limit(visible) + return TRANSLATION_DESC_PATTERN.sub( + r'\1 [CHAR_LIMIT=%d]\2' % limit, text, count=1) + + class ExportTranslationsScript(BaseStringScript): def ProcessTag(self, line, type): @@ -36,14 +66,15 @@ def ProcessTag(self, line, type): if PREFIXED_NAME_START in joined: joined = joined.replace(PREFIXED_NAME_START, UNPREFIXED_NAME_START) - if 'translation_description=' in joined and not CHAR_LIMIT_PATTERN.search(joined): - match = STRING_VALUE_PATTERN.search(joined) - if match: - visible = _extract_visible_text(match.group(1)) - if visible: - limit = _calculate_char_limit(visible) - joined = TRANSLATION_DESC_PATTERN.sub( - r'\1 [CHAR_LIMIT=%d]\2' % limit, joined, count=1) + if type == self.TYPE_STR: + joined = _add_char_limit(joined, STRING_VALUE_PATTERN) + elif type == self.TYPE_PLUR: + result_lines = [] + for l in joined.split('\n'): + if '' in l: + l = _add_char_limit(l, ITEM_VALUE_PATTERN) + result_lines.append(l) + joined = '\n'.join(result_lines) return joined.split('\n')