diff --git a/.github/workflows/localize.yml b/.github/workflows/localize.yml new file mode 100644 index 00000000..eafc7430 --- /dev/null +++ b/.github/workflows/localize.yml @@ -0,0 +1,45 @@ +name: localize + +on: + push: + branches: [nightly] + paths: # prevents workflow from running unless files in these directories change + - 'sunshine/**' # only localizing files inside sunshine directory + workflow_dispatch: + +jobs: + localize: + name: Update Localization + if: ${{ github.event.pull_request.merged }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Python 3.9 + uses: actions/setup-python@v3 # https://github.com/actions/setup-python + with: + python-version: '3.9' + + - name: Set up Python 3.9 Dependencies + run: | + cd ./scripts + python -m pip install --upgrade pip setuptools + python -m pip install -r requirements.txt + + - name: Set up xgettext + run: | + sudo apt-get update -y && \ + sudo apt-get --reinstall install -y \ + gettext + + - name: Update Strings + run: | + python ./scripts/_locale.py --extract + + - name: GitHub Commit & Push # push changes back into nightly + uses: actions-js/push@v1.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: nightly + message: localization updated by localize workflow diff --git a/.gitignore b/.gitignore index 6fb681e7..39afd65a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ cmake-build* /assets/web/fonts/fontawesome-free-web/scss/ /assets/web/fonts/fontawesome-free-web/sprites/ /assets/web/fonts/fontawesome-free-web/svgs/ + +# Translations +*.mo +*.pot diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..d014a00c --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,17 @@ +"base_path": "." +"base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) +"preserve_hierarchy": false # flatten tree on crowdin + +"files" : [ + { + "source" : "/locale/*.po", + "translation" : "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", + "languages_mapping": { + "two_letters_code": { + # map non-two letter codes here, left side is crowdin designation, right side is babel designation + "en-GB": "en_GB", + "en-US": "en_US" + } + } + } +] diff --git a/scripts/_locale.py b/scripts/_locale.py new file mode 100644 index 00000000..82e172cc --- /dev/null +++ b/scripts/_locale.py @@ -0,0 +1,156 @@ +"""_locale.py + +Functions related to building, initializing, updating, and compiling localization translations. + +Borrowed from RetroArcher. +""" +# standard imports +import argparse +import datetime +import os +import subprocess + +project_name = 'Sunshine' + +script_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.dirname(script_dir) +locale_dir = os.path.join(root_dir, 'locale') +project_dir = os.path.join(root_dir, project_name.lower()) + +year = datetime.datetime.now().year + +# retroarcher target locales +target_locales = [ + 'de', # Deutsch + 'en', # English + 'en_GB', # English (United Kingdom) + 'en_US', # English (United States) + 'es', # español + 'fr', # français + 'it', # italiano + 'ru', # русский +] + + +def x_extract(): + """Executes `xgettext extraction` in subprocess.""" + + commands = [ + 'xgettext', + f'--default-domain={project_name.lower()}', + f'--output={os.path.join(locale_dir, project_name.lower() + ".po")}', + '--language=C++', + '--boost', + '--from-code=utf-8', + '-F', + f'--msgid-bugs-address=github.com/{project_name.lower()}', + f'--copyright-holder={project_name}', + f'--package-name={project_name}', + '--package-version=v0' + ] + + pot_filepath = os.path.join(locale_dir, f'{project_name.lower()}.po') + + extensions = ['cpp', 'h', 'm', 'mm'] + + # find input files + for root, dirs, files in os.walk(project_dir, topdown=True): + for name in files: + filename = os.path.join(root, name) + extension = filename.rsplit('.', 1)[-1] + if extension in extensions: # append input files + commands.append(filename) + + print(commands) + proc = subprocess.run(args=commands, cwd=root_dir) + + # fix header + body = "" + with open(file=pot_filepath, mode='r') as file: + for line in file.readlines(): + if line != '"Language: \\n"\n': # do not include this line + if line == '# SOME DESCRIPTIVE TITLE.\n': + body += f'# Translations template for {project_name}.\n' + elif line.startswith('#') and 'YEAR' in line: + body += line.replace('YEAR', str(year)) + elif line.startswith('#') and 'PACKAGE' in line: + body += line.replace('PACKAGE', project_name) + else: + body += line + + # rewrite pot file with updated header + with open(file=pot_filepath, mode='w+') as file: + file.write(body) + + +def babel_init(locale_code: str): + """Executes `pybabel init` in subprocess. + + :param locale_code: str - locale code + """ + commands = [ + 'pybabel', + 'init', + '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), + '-d', locale_dir, + '-D', project_name.lower(), + '-l', locale_code + ] + + print(commands) + proc = subprocess.run(args=commands, cwd=root_dir) + + +def babel_update(): + """Executes `pybabel update` in subprocess.""" + commands = [ + 'pybabel', + 'update', + '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'), + '-d', locale_dir, + '-D', project_name.lower(), + '--update-header-comment' + ] + + print(commands) + proc = subprocess.run(args=commands, cwd=root_dir) + + +def babel_compile(): + """Executes `pybabel compile` in subprocess.""" + commands = [ + 'pybabel', + 'compile', + '-d', locale_dir, + '-D', project_name.lower() + ] + + print(commands) + proc = subprocess.run(args=commands, cwd=root_dir) + + +if __name__ == '__main__': + # Set up and gather command line arguments + parser = argparse.ArgumentParser( + description='Script helps update locale_id translations. Translations must be done manually.') + + parser.add_argument('--extract', action='store_true', help='Extract messages from c++ files.') + parser.add_argument('--init', action='store_true', help='Initialize any new locales specified in target locales.') + parser.add_argument('--update', action='store_true', help='Update existing locales.') + parser.add_argument('--compile', action='store_true', help='Compile translated locales.') + + args = parser.parse_args() + + if args.extract: + x_extract() + + if args.init: + for locale_id in target_locales: + if not os.path.isdir(os.path.join(locale_dir, locale_id)): + babel_init(locale_code=locale_id) + + if args.update: + babel_update() + + if args.compile: + babel_compile() diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000..9d236e72 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +Babel==2.9.1