Skip to content

Commit cfd316b

Browse files
committed
Refactor i18n API
- Add Locale::Domain struct to avoid ambiguous overloads - Accept std::string_view to simplify passing std::strings - Add Locale::translateAll functions for translating string vectors - Add s_tr macro to return std::string directly - Translate tab names from .ui.json files - Allow to store .pot and .po files in separate locations - Fix typo in compile_translations.py
1 parent 98fee0d commit cfd316b

15 files changed

+202
-115
lines changed

cmake/Modules/I18nHelpers.cmake

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,15 @@ function(mr_add_translations TARGET_NAME)
1919

2020
set(POT_FILES "")
2121
set(MO_OUTPUT_FILES "")
22-
foreach(LOCALE_DIR ${LOCALE_PATHS})
23-
file(GLOB LOCALE_NAMES LIST_DIRECTORIES true RELATIVE "${LOCALE_DIR}" "${LOCALE_DIR}/*")
24-
25-
foreach(DOMAIN_NAME ${LOCALE_DOMAINS})
22+
foreach(DOMAIN_NAME ${LOCALE_DOMAINS})
23+
set(FOUND_LOCALES "")
24+
foreach(LOCALE_DIR ${LOCALE_PATHS})
2625
set(POT_FILE "${LOCALE_DIR}/${DOMAIN_NAME}.pot")
27-
if(NOT EXISTS ${POT_FILE})
28-
continue()
26+
if(EXISTS ${POT_FILE})
27+
list(APPEND POT_FILES ${POT_FILE})
2928
endif()
30-
list(APPEND POT_FILES ${POT_FILE})
3129

32-
set(FOUND_LOCALES "")
30+
file(GLOB LOCALE_NAMES LIST_DIRECTORIES true RELATIVE "${LOCALE_DIR}" "${LOCALE_DIR}/*")
3331
foreach(LOCALE_NAME ${LOCALE_NAMES})
3432
set(PO_FILE "${LOCALE_DIR}/${LOCALE_NAME}/${DOMAIN_NAME}.po")
3533
if(NOT EXISTS ${PO_FILE})
@@ -48,15 +46,11 @@ function(mr_add_translations TARGET_NAME)
4846
COMMAND ${MSGFMT_EXECUTABLE} ${PO_FILE} --output-file=${MO_OUTPUT_FILE} --check
4947
)
5048
endforeach(LOCALE_NAME)
49+
endforeach(LOCALE_DIR)
5150

52-
list(LENGTH FOUND_LOCALES FOUND_LOCALES_COUNT)
53-
if(${FOUND_LOCALES_COUNT} EQUAL 1)
54-
message(STATUS "Found ${FOUND_LOCALES_COUNT} translation for ${DOMAIN_NAME}.")
55-
else()
56-
message(STATUS "Found ${FOUND_LOCALES_COUNT} translations for ${DOMAIN_NAME}.")
57-
endif()
58-
endforeach(DOMAIN_NAME)
59-
endforeach(LOCALE_DIR)
51+
list(LENGTH FOUND_LOCALES FOUND_LOCALES_COUNT)
52+
message(STATUS "Found ${FOUND_LOCALES_COUNT} translation(s) for ${DOMAIN_NAME}.")
53+
endforeach(DOMAIN_NAME)
6054

6155
if(POT_FILES)
6256
install(

locale/MRRibbonCommonMenuStructure.pot

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,23 @@ msgstr ""
299299
msgid "Show information about currently selected object."
300300
msgstr ""
301301

302+
msgctxt "Tab name"
303+
msgid "Home"
304+
msgstr ""
305+
306+
msgctxt "Tab name"
307+
msgid "View"
308+
msgstr ""
309+
310+
msgctxt "Tab name"
311+
msgid "Select"
312+
msgstr ""
313+
314+
msgctxt "Tab name"
315+
msgid "CT"
316+
msgstr ""
317+
318+
msgctxt "Tab name"
319+
msgid "Test"
320+
msgstr ""
321+

scripts/gettext/compile_translations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def find_gettext_command(cmd):
4646
if not mo_output_dir.exists():
4747
mo_output_dir.mkdir(parents=True)
4848

49-
mo_output_file = mo_output_dir / f"{domain_name}.po"
49+
mo_output_file = mo_output_dir / f"{domain_name}.mo"
5050
subprocess.run([
5151
msgfmt,
5252
po_file,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import shutil
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
8+
9+
def find_gettext_command(cmd):
10+
if path := shutil.which(cmd):
11+
return path
12+
if gettext_root := os.getenv('GETTEXT_ROOT'):
13+
lookup_paths = [
14+
Path(gettext_root),
15+
Path(gettext_root) / "bin",
16+
]
17+
lookup_path = os.pathsep.join(str(p) for p in lookup_paths)
18+
if path := shutil.which(cmd, path=lookup_path):
19+
return path
20+
return None
21+
22+
23+
if __name__ == "__main__":
24+
if len(sys.argv) < 3:
25+
print("Usage: merge_po_from_pot.py PO_DIR POT_FILE [POT_FILE ...]")
26+
sys.exit(0)
27+
28+
_, po_dir, *pot_files = sys.argv
29+
po_dir = Path(po_dir)
30+
31+
msgmerge = find_gettext_command('msgmerge')
32+
if not msgmerge:
33+
print(
34+
"Cannot find msgmerge. Set GETTEXT_ROOT environment variable.",
35+
file=sys.stderr,
36+
)
37+
sys.exit(1)
38+
39+
for pot_file in pot_files:
40+
pot_file = Path(pot_file)
41+
domain_name = pot_file.stem
42+
43+
for po_file in po_dir.glob(f"*/{domain_name}.po"):
44+
locale_name = po_file.parent.name
45+
print(f"Updating {locale_name} locale for {domain_name} ...")
46+
47+
subprocess.run([
48+
msgmerge,
49+
"--update",
50+
po_file,
51+
pot_file,
52+
], check=True)
Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
#!/usr/bin/env python3
22
import json
3-
import os
4-
import shutil
5-
import subprocess
63
import sys
74
from pathlib import Path
85

@@ -17,40 +14,23 @@
1714
'''
1815

1916

20-
def find_gettext_command(cmd):
21-
if path := shutil.which(cmd):
22-
return path
23-
if gettext_root := os.getenv('GETTEXT_ROOT'):
24-
lookup_paths = [
25-
Path(gettext_root),
26-
Path(gettext_root) / "bin",
27-
]
28-
lookup_path = os.pathsep.join(str(p) for p in lookup_paths)
29-
if path := shutil.which(cmd, path=lookup_path):
30-
return path
31-
return None
32-
33-
3417
if __name__ == "__main__":
35-
if len(sys.argv) != 3:
36-
print("Usage: update_json_translations.py POT_FILE INPUT_JSON")
18+
if len(sys.argv) not in (3, 4):
19+
print("Usage: update_json_translations.py POT_FILE ITEMS_JSON [UI_JSON]")
3720
sys.exit(0)
3821

39-
_, pot_file, input_json = sys.argv
40-
pot_file = Path(pot_file)
41-
input_json = Path(input_json)
22+
pot_file = Path(sys.argv[1])
23+
input_json = Path(sys.argv[2])
24+
# Auto-detect paired .ui.json if not provided explicitly
25+
if len(sys.argv) == 4:
26+
ui_json = Path(sys.argv[3])
27+
else:
28+
ui_json_path = input_json.with_suffix('').with_suffix('.ui.json')
29+
ui_json = ui_json_path if ui_json_path.exists() else None
4230

43-
msgmerge = find_gettext_command('msgmerge')
44-
if not msgmerge:
45-
print(
46-
"Cannot find msgmerge. Set GETTEXT_ROOT environment variable.",
47-
file=sys.stderr,
48-
)
49-
sys.exit(1)
50-
51-
output_dir = pot_file.parent
5231
domain_name = pot_file.stem
5332

33+
# Contextless records from .items.json (captions, tooltips)
5434
records = []
5535
with open(input_json, 'r') as f:
5636
doc = json.load(f)
@@ -65,6 +45,13 @@ def find_gettext_command(cmd):
6545
if 'Tooltip' in item:
6646
records.append(item['Tooltip'])
6747

48+
# Tab name records from .ui.json (with "Tab name" context)
49+
tab_name_records = []
50+
if ui_json is not None:
51+
with open(ui_json, 'r') as f:
52+
ui_doc = json.load(f)
53+
tab_name_records = [name for tab in ui_doc.get('Tabs', []) if (name := tab.get('Name'))]
54+
6855
with open(pot_file, 'w') as f:
6956
f.write(POT_HEADER)
7057
for rec in records:
@@ -73,14 +60,9 @@ def find_gettext_command(cmd):
7360
f.write(f'msgid "{rec}"\n')
7461
f.write('msgstr ""\n')
7562
f.write('\n')
63+
for name in tab_name_records:
64+
f.write('msgctxt "Tab name"\n')
65+
f.write(f'msgid "{name}"\n')
66+
f.write('msgstr ""\n')
67+
f.write('\n')
7668

77-
for po_file in output_dir.glob(f"*/{domain_name}.po"):
78-
locale_name = po_file.parent.name
79-
print(f"Updating {locale_name} locale ...")
80-
81-
subprocess.run([
82-
msgmerge,
83-
"--update",
84-
po_file,
85-
pot_file,
86-
], check=True)

scripts/gettext/update_translations.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,6 @@ def find_gettext_command(cmd):
4040
)
4141
sys.exit(1)
4242

43-
msgmerge = find_gettext_command('msgmerge')
44-
if not msgmerge:
45-
print(
46-
"Cannot find msgmerge. Set GETTEXT_ROOT environment variable.",
47-
file=sys.stderr,
48-
)
49-
sys.exit(1)
50-
51-
output_dir = pot_file.parent
5243
domain_name = pot_file.stem
5344

5445
input_files = list(map(str, itertools.chain(
@@ -85,14 +76,3 @@ def find_gettext_command(cmd):
8576
print(line, end='')
8677

8778
# TODO: parse .items.json files
88-
89-
for po_file in output_dir.glob(f"*/{domain_name}.po"):
90-
locale_name = po_file.parent.name
91-
print(f"Updating {locale_name} locale ...")
92-
93-
subprocess.run([
94-
msgmerge,
95-
"--update",
96-
po_file,
97-
pot_file,
98-
], check=True)

scripts/gettext/xgettext_options.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
-k_tr:1c,2,2t
1515
-k_tr:1,2,3t
1616
-k_tr:1c,2,3,4t
17+
-ks_tr:1,1t
18+
-ks_tr:1c,2,2t
19+
-ks_tr:1,2,3t
20+
-ks_tr:1c,2,3,4t
1721
-kf_tr:1,1t
1822
-kf_tr:1c,2,2t
1923
-kf_tr:1,2,3t

source/MRViewer/MRI18n.cpp

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,49 @@
77
#include <boost/locale/message.hpp>
88
#pragma warning( pop )
99

10+
#include <cassert>
11+
#include <cstring>
12+
1013
namespace MR::Locale
1114
{
1215

13-
std::string translate( const char* msg, int domainId )
16+
namespace
17+
{
18+
19+
inline const char* asCStr( std::string_view sv )
20+
{
21+
assert( std::strlen( sv.data() ) == sv.size() );
22+
return sv.data();
23+
}
24+
25+
} // namespace
26+
27+
std::string translate( std::string_view msg, Domain domain )
1428
{
15-
if ( domainId < 0 )
16-
return translate_noop( msg );
17-
return boost::locale::translate( msg ).str( get(), domainId );
29+
if ( domain.id < 0 )
30+
return translate_noop( asCStr( msg ) );
31+
return boost::locale::translate( asCStr( msg ) ).str( get(), domain.id );
1832
}
1933

20-
std::string translate( const char* context, const char* msg, int domainId )
34+
std::string translate( std::string_view context, std::string_view msg, Domain domain )
2135
{
22-
if ( domainId < 0 )
23-
return translate_noop( context, msg );
24-
return boost::locale::translate( context, msg ).str( get(), domainId );
36+
if ( domain.id < 0 )
37+
return translate_noop( asCStr( context ), asCStr( msg ) );
38+
return boost::locale::translate( asCStr( context ), asCStr( msg ) ).str( get(), domain.id );
2539
}
2640

27-
std::string translate( const char* single, const char* plural, Int64 n, int domainId )
41+
std::string translate( std::string_view single, std::string_view plural, Int64 n, Domain domain )
2842
{
29-
if ( domainId < 0 )
30-
return translate_noop( single, plural, n );
31-
return boost::locale::translate( single, plural, n ).str( get(), domainId );
43+
if ( domain.id < 0 )
44+
return translate_noop( asCStr( single ), asCStr( plural ), n );
45+
return boost::locale::translate( asCStr( single ), asCStr( plural ), n ).str( get(), domain.id );
3246
}
3347

34-
std::string translate( const char* context, const char* single, const char* plural, Int64 n, int domainId )
48+
std::string translate( std::string_view context, std::string_view single, std::string_view plural, Int64 n, Domain domain )
3549
{
36-
if ( domainId < 0 )
37-
return translate_noop( context, single, plural, n );
38-
return boost::locale::translate( context, single, plural, n ).str( get(), domainId );
50+
if ( domain.id < 0 )
51+
return translate_noop( asCStr( context ), asCStr( single ), asCStr( plural ), n );
52+
return boost::locale::translate( asCStr( context ), asCStr( single ), asCStr( plural ), n ).str( get(), domain.id );
3953
}
4054

4155
} // namespace MR::Locale

0 commit comments

Comments
 (0)