Skip to content

Commit b46e60a

Browse files
authored
Merge pull request metabrainz#1618 from phw/PICARD-1929-nsis-translation-transifex
PICARD-1929: Translate NSIS with Transifex
2 parents 5b37bfc + a1d95dd commit b46e60a

20 files changed

+400
-77
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ build/
2323
coverage.xml
2424
dist/
2525
installer/picard-setup.nsi
26+
installer/i18n/out/*.nsh
2627
locale/
2728
org.musicbrainz.Picard.appdata.xml
2829
picard.egg-info

.tx/config

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ source_file = po/appstream/picard-appstream.pot
1515
source_lang = en
1616
type = PO
1717

18+
[musicbrainz.picard_installer]
19+
file_filter = installer/i18n/sources/<lang>.json
20+
source_file = installer/i18n/sources/en.json
21+
source_lang = en
22+
type = KEYVALUEJSON
23+
1824
[musicbrainz.countries]
1925
file_filter = po/countries/<lang>.po
2026
source_file = po/countries/countries.pot

RELEASING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ git ls-tree --full-tree -r HEAD --name-only |while read f; do sed -i '1s/^\xEF\x
5252
## Get latest translations from Transifex
5353

5454
```bash
55-
python setup.py get_po_files && git diff --quiet || git commit -m 'Update .po files' -- po/
55+
python setup.py pull_translations && git diff --quiet || git commit -m 'Update .po files' -- po/
5656
```
5757

5858
## Synchronize generated consts

installer/i18n/json2nsh.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Picard, the next-generation MusicBrainz tagger
5+
#
6+
# Copyright (C) 2020 Philipp Wolfer
7+
#
8+
# This program is free software; you can redistribute it and/or
9+
# modify it under the terms of the GNU General Public License
10+
# as published by the Free Software Foundation; either version 2
11+
# of the License, or (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program; if not, write to the Free Software
20+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21+
22+
import glob
23+
import json
24+
import os.path
25+
26+
import nshutil as nsh
27+
28+
29+
def language_from_filename(path):
30+
lang = os.path.splitext(os.path.basename(path))[0]
31+
return (nsh.code_to_language(lang), lang)
32+
33+
34+
def write_langstring(f, language, identifier, text):
35+
langstring = nsh.make_langstring(language, identifier, text)
36+
f.write(langstring)
37+
38+
39+
def merge_translations(*translations):
40+
merged = {}
41+
for trans in translations:
42+
for k, v in trans.items():
43+
if v:
44+
merged[k] = v
45+
return merged
46+
47+
48+
def main():
49+
scriptdir = os.path.dirname(os.path.abspath(__file__))
50+
sourcesdir = os.path.join(scriptdir, 'sources')
51+
outdir = os.path.join(scriptdir, 'out')
52+
os.makedirs(outdir, exist_ok=True)
53+
54+
# Read the english sources for defaults
55+
with open(os.path.join(sourcesdir, 'en.json'), 'r', encoding='utf-8') as infile:
56+
data_en = json.loads(infile.read())
57+
58+
for path in glob.glob(os.path.join(sourcesdir, '*.json')):
59+
language, language_code = language_from_filename(path)
60+
if not language:
61+
print(f'Unknown language code "{language_code}", skipping')
62+
continue
63+
target_file = os.path.join(outdir, f'{language}.nsh')
64+
print(f'{path} => {target_file}')
65+
with open(path, 'r', encoding='utf-8') as infile:
66+
data = json.loads(infile.read())
67+
data = merge_translations(data_en, data)
68+
with open(target_file, 'w+', encoding='utf-8') as outfile:
69+
for identifier, text in data.items():
70+
write_langstring(outfile, language, identifier, text)
71+
72+
73+
if __name__ == "__main__":
74+
main()

installer/i18n/nsh2json.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Picard, the next-generation MusicBrainz tagger
5+
#
6+
# Copyright (C) 2020 Philipp Wolfer
7+
#
8+
# This program is free software; you can redistribute it and/or
9+
# modify it under the terms of the GNU General Public License
10+
# as published by the Free Software Foundation; either version 2
11+
# of the License, or (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program; if not, write to the Free Software
20+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21+
22+
import glob
23+
import json
24+
import os.path
25+
26+
import nshutil as nsh
27+
28+
29+
def language_from_filename(path):
30+
lang = os.path.splitext(os.path.basename(path))[0]
31+
return (lang, nsh.language_to_code(lang))
32+
33+
34+
def extract_strings(f):
35+
for line in f:
36+
parsed = nsh.parse_langstring(line)
37+
if parsed:
38+
yield parsed
39+
40+
41+
def main():
42+
scriptdir = os.path.dirname(os.path.abspath(__file__))
43+
sourcesdir = os.path.join(scriptdir, 'sources')
44+
outdir = os.path.join(scriptdir, 'out')
45+
46+
for path in glob.glob(os.path.join(outdir, '*.nsh')):
47+
language, language_code = language_from_filename(path)
48+
if not language_code:
49+
print(f'Unknown language "{language}", skipping')
50+
continue
51+
target_file = os.path.join(sourcesdir, f'{language_code}.json')
52+
print(f'{path} => {target_file}')
53+
with open(path, 'r', encoding='utf-8') as infile:
54+
output = {}
55+
for identifier, text in extract_strings(infile):
56+
output[identifier] = text
57+
58+
with open(target_file, 'w+', encoding='utf-8') as outfile:
59+
outfile.write(json.dumps(output, ensure_ascii=False, indent=4))
60+
61+
62+
if __name__ == "__main__":
63+
main()

installer/i18n/nshutil.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Picard, the next-generation MusicBrainz tagger
5+
#
6+
# Copyright (C) 2020 Philipp Wolfer
7+
#
8+
# This program is free software; you can redistribute it and/or
9+
# modify it under the terms of the GNU General Public License
10+
# as published by the Free Software Foundation; either version 2
11+
# of the License, or (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program; if not, write to the Free Software
20+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21+
22+
import re
23+
24+
25+
# See list of available NSIS languages at
26+
# https://sourceforge.net/p/nsis/code/HEAD/tree/NSIS/trunk/Contrib/Language%20files/
27+
LANGUAGES = {
28+
'Afrikaans': 'af',
29+
'Albanian': 'sq',
30+
'Arabic': 'ar',
31+
'Asturian': 'ast',
32+
'Basque': 'eu',
33+
'Belarusian': 'be',
34+
'Bosnian': 'bs',
35+
'Breton': 'br',
36+
'Bulgarian': 'bg',
37+
'Catalan': 'ca',
38+
'Cibemba': 'bem',
39+
'Corsican': 'co',
40+
'Croation': 'hr',
41+
'Czech': 'cs',
42+
'Danish': 'da',
43+
'Dutch': 'nl',
44+
'English': 'en',
45+
'Esperanto': 'eo',
46+
'Estonian': 'et',
47+
'Farsi': 'fa',
48+
'Finnish': 'fi',
49+
'French': 'fr',
50+
'Galician': 'gl',
51+
'Georgian': 'ka',
52+
'German': 'de',
53+
'Greek': 'el',
54+
'Hebrew': 'he',
55+
'Hindi': 'hi',
56+
'Hungarian': 'hu',
57+
'Icelandic': 'is',
58+
'Igbo': 'ig',
59+
'Indonesian': 'id',
60+
'Irish': 'ga',
61+
'Italian': 'it',
62+
'Japanese': 'ja',
63+
'Khmer': 'km',
64+
'Korean': 'ko',
65+
'Kurdish': 'ku',
66+
'Latvian': 'lv',
67+
'Lithuanian': 'lt',
68+
'Luxembourgish': 'lb',
69+
'Macedonian': 'mk',
70+
'Malagasy': 'mg',
71+
'Malay': 'ms_MY',
72+
'Mongolian': 'mn',
73+
'Norwegian': 'nb',
74+
'NorwegianNynorsk': 'nn',
75+
'Polish': 'pl',
76+
'Portuguese': 'pt',
77+
'PortugueseBR': 'pt_BR',
78+
'Romanian': 'ro',
79+
'Russian': 'ru',
80+
'ScotsGaelic': 'sco',
81+
'Serbian': 'sr',
82+
'SimpChinese': 'zh-Hans',
83+
'Slovak': 'sk',
84+
'Slovenian': 'sl',
85+
'Spanish': 'es',
86+
'Swahili': 'sw',
87+
'Swedish': 'sv',
88+
'Tatar': 'tt',
89+
'Thai': 'th',
90+
'TradChinese': 'zh-Hant',
91+
'Turkish': 'tr',
92+
'Ukrainian': 'uk',
93+
'Uzbek': 'uz',
94+
'Vietnamese': 'vi',
95+
'Welsh': 'cy',
96+
'Yoruba': 'yo',
97+
}
98+
99+
_R_LANGUAGES = dict([(code, name) for name, code in LANGUAGES.items()])
100+
101+
# See https://nsis.sourceforge.io/Docs/Chapter4.html#varstrings
102+
ESCAPE_CHARS = {
103+
r'$\r': '\r',
104+
r'$\n': '\n',
105+
r'$\t': '\t',
106+
r'$\"': '"',
107+
r'$\'': "'",
108+
r'$\`': '`',
109+
}
110+
111+
RE_LANGSTRING_LINE = re.compile(r'LangString\s+(?P<identifier>[A-Za-z0-9_]+)\s+\${LANG_[A-Z]+}\s+["\'`](?P<text>.*)["\'`]$')
112+
113+
114+
def language_to_code(language):
115+
return LANGUAGES.get(language)
116+
117+
118+
def code_to_language(language_code):
119+
return _R_LANGUAGES.get(language_code)
120+
121+
122+
def escape_string(text):
123+
for escape, char in ESCAPE_CHARS.items():
124+
if char in ("'", "`"): # No need to escape quotes other than ""
125+
continue
126+
text = text.replace(char, escape)
127+
return text
128+
129+
130+
def unescape_string(text):
131+
for escape, char in ESCAPE_CHARS.items():
132+
text = text.replace(escape, char)
133+
return text
134+
135+
136+
def parse_langstring(line):
137+
match = RE_LANGSTRING_LINE.match(line)
138+
if match:
139+
return (
140+
match.group('identifier'),
141+
unescape_string(match.group('text'))
142+
)
143+
else:
144+
return None
145+
146+
147+
def make_langstring(language, identifier, text):
148+
language = language.upper()
149+
text = escape_string(text)
150+
return f'LangString {identifier} ${{LANG_{language}}} "{text}"\n'

installer/i18n/sources/de.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"MsgAlreadyInstalled": "${PRODUCT_NAME} ist bereits installiert. \n\nKlicken Sie „OK“ um die vorherige Version zu deinstallieren oder „Abbrechen“ um das Update abzubrechen.",
3+
"MsgApplicationRunning": "Die Anwendung ${PRODUCT_NAME} wird ausgeführt. Bitte schließen und erneut versuchen.",
4+
"MsgRequires64Bit": "Diese Version von ${PRODUCT_NAME} erfordert ein 64-bit Windows-System.",
5+
"MuiDescriptionRequired": "Installiert ${PRODUCT_NAME} mit den für die Ausführung erforderlichen Dateien.",
6+
"MuiDescriptionLang": "Installiert Übersetzungen von ${PRODUCT_NAME} in verschiedenen Sprachen.",
7+
"MuiDescriptionShortcuts": "Installiert Verknüpfungen, um ${PRODUCT_NAME} zu starten.",
8+
"MuiDescriptionDesktop": "Installiert eine Verknüpfung auf dem Desktop.",
9+
"MuiDescriptionStarteMenu": "Installiert eine Verknüpfung im Startmenü.",
10+
"OptionRemoveSettings": "Einstellungen und persönliche Daten entfernen",
11+
"SectionDesktop": "Desktop",
12+
"SectionLanguages": "Sprachen",
13+
"SectionRequired": "Programmdateien (erforderlich)",
14+
"SectionShortcuts": "Verknüpfungen",
15+
"SectionStartMenu": "Startmenü"
16+
}

installer/i18n/sources/en.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"MsgAlreadyInstalled": "${PRODUCT_NAME} is already installed. \n\nClick \"OK\" to uninstall the previous version or \"Cancel\" to cancel this upgrade.",
3+
"MsgApplicationRunning": "The application ${PRODUCT_NAME} is running. Please close it and try again.",
4+
"MsgRequires64Bit": "This version of ${PRODUCT_NAME} requires a 64-bit Windows system.",
5+
"MuiDescriptionRequired": "Installs ${PRODUCT_NAME} along with the necessary files for it run.",
6+
"MuiDescriptionLang": "Installs translations of ${PRODUCT_NAME} in different languages.",
7+
"MuiDescriptionShortcuts": "Installs shortcuts to launch ${PRODUCT_NAME}.",
8+
"MuiDescriptionDesktop": "Installs a shortcut on the desktop.",
9+
"MuiDescriptionStarteMenu": "Installs a shortcut in the Start Menu.",
10+
"OptionRemoveSettings": "Remove settings and personal data",
11+
"SectionDesktop": "Desktop",
12+
"SectionLanguages": "Languages",
13+
"SectionRequired": "Program files (required)",
14+
"SectionShortcuts": "Shortcuts",
15+
"SectionStartMenu": "Start menu"
16+
}

installer/i18n/sources/es.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"MsgAlreadyInstalled": "${PRODUCT_NAME} ya está instalado . \n\nClick \"OK\" para desinstalar la versión anterior o \"Cancel\" para cancelar esta actualización. ",
3+
"MsgApplicationRunning": "La aplicación ${PRODUCT_NAME} está en marcha. Por favor cierrelo e inténtelo de nuevo.",
4+
"MsgRequires64Bit": "La versión de ${PRODUCT_NAME} requiere un Windows de 64-bits.",
5+
"MuiDescriptionRequired": "Instalar ${PRODUCT_NAME} junto con los archivos necesarios para que funcione.",
6+
"MuiDescriptionLang": "Instalar traducciones de ${PRODUCT_NAME} en diferentes lenguajes.",
7+
"MuiDescriptionShortcuts": "Instalar atajos para iniciar ${PRODUCT_NAME}.",
8+
"MuiDescriptionDesktop": "Instalar un atajo en el escritorio",
9+
"MuiDescriptionStarteMenu": "Instalar un atajo en el Menú de Inicio",
10+
"OptionRemoveSettings": "Remover ajustes e información personal",
11+
"SectionDesktop": "Escritorio",
12+
"SectionLanguages": "Idiomas",
13+
"SectionRequired": "Archivos de programa (requerido)",
14+
"SectionShortcuts": "Atajos",
15+
"SectionStartMenu": "Menú de inicio"
16+
}

installer/i18n/sources/fr.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"MsgAlreadyInstalled": "${PRODUCT_NAME} est déjà installé. \n\nCliquez sur « OK » pour désinstaller la version précédente ou « Annuler » pour annuler cette mise à jour.",
3+
"MsgApplicationRunning": "L’application ${PRODUCT_NAME} est en cours d’exécution. Veuillez la stopper et essayer à nouveau.",
4+
"MsgRequires64Bit": "Cette version de ${PRODUCT_NAME} requiert un système Windows 64 bits.",
5+
"MuiDescriptionRequired": "Installe ${PRODUCT_NAME} ainsi que les fichiers nécessaires à son exécution.",
6+
"MuiDescriptionLang": "Installe les traductions de ${PRODUCT_NAME} dans différentes langues.",
7+
"MuiDescriptionShortcuts": "Installe les raccourcis pour lancer ${PRODUCT_NAME}.",
8+
"MuiDescriptionDesktop": "Installe un raccourci sur le bureau.",
9+
"MuiDescriptionStarteMenu": "Installe un raccourci dans le menu Démarrer.",
10+
"OptionRemoveSettings": "Supprimer les préférences et les données personnelles",
11+
"SectionDesktop": "Bureau",
12+
"SectionLanguages": "Langues",
13+
"SectionRequired": "Fichiers du programme (requis)",
14+
"SectionShortcuts": "Raccourcis",
15+
"SectionStartMenu": "Menu Démarrer"
16+
}

0 commit comments

Comments
 (0)