Compare commits

..

1 Commits

Author SHA1 Message Date
kdmukai
c8e383c0f6 basic zh experiment 2024-11-18 10:45:53 -06:00
46 changed files with 5355 additions and 24024 deletions

View File

@ -1,138 +0,0 @@
"""
Utility to compare screenshots before and after a change and generate a report of the
differences.
Expected usage in a GitHub Actions workflow; compare `dev` with the `$INCOMING_CHANGES_REF` in the
associated PR or merge that triggered the CI run:
python src/seedsigner/resources/seedsigner-translations/.github/diff_report/diff_screenshots.py ./artifacts/dev ./artifacts/incoming ./artifacts/diff $INCOMING_CHANGES_REF
"""
import argparse
import glob
import hashlib
import os
import pathlib
import shutil
parser = argparse.ArgumentParser(prog=__name__)
parser.add_argument("before_dir", type=str, help="Directory containing screenshots before the incoming changes")
parser.add_argument("after_dir", type=str, help="Directory containing screenshots after the incoming changes")
parser.add_argument("output_dir", type=str, help="Directory to save the screenshots diff report")
parser.add_argument("incoming_changes_ref", type=str, help="Branch name or commit hash that contains the incoming changes")
args = parser.parse_args()
# `before_dir` includes the branch name we'll be merging into
baseline_branch = args.before_dir.split(os.path.sep)[-1]
incoming_changes_ref = args.incoming_changes_ref
def list_files_recursively(path: str) -> list[str]:
""" Return a list of paths to all png files in the directory tree """
return glob.glob(path + "/**/*.png", recursive=True)
def compute_file_hash(file_path: str) -> str:
""" Return the file hash using sha256 """
hash_func = hashlib.new('sha256')
with open(file_path, 'rb') as file:
while chunk := file.read(8192): # Read the file in chunks of 8192 bytes
hash_func.update(chunk)
return hash_func.hexdigest()
def get_pathname_fragment(path:str) -> str:
""" Extract the last 3 parts of the path:
en/tools_views/ToolsCalcFinalWordDoneView.png
These paths will be the same in the "before" and "after" directories.
"""
parts = path.split(os.path.sep)
if len(parts) < 3:
raise ValueError(f"Path should have at least 3 parts: {path}")
return os.path.sep.join(parts[-3:])
def get_locale_and_screenshot_name(path: str) -> tuple[str, str]:
""" Parse the path to extract the locale and the screenshot name.
Assumes we're working with a path like:
en/tools_views/ToolsCalcFinalWordDoneView.png
"""
parts = path.split(os.path.sep)
if len(parts) != 3:
raise ValueError(f"Path should have 3 parts: {path}")
return parts[0], parts[-1].split(".")[0]
# Recursively list and hash all png files in the "before" directory
before_screenshots = {}
paths_before = []
for file in list_files_recursively(args.before_dir):
screenshot_path = get_pathname_fragment(file)
before_screenshots[screenshot_path] = compute_file_hash(file)
paths_before.append(screenshot_path)
# Do the same for the "after" directory, but do the diff while we're here
only_in_after = []
diffs: list[str] = []
paths_after = []
for file in list_files_recursively(args.after_dir):
screenshot_path = get_pathname_fragment(file)
if screenshot_path not in before_screenshots:
only_in_after.append(screenshot_path)
elif before_screenshots[screenshot_path] != compute_file_hash(file):
diffs.append(screenshot_path)
paths_after.append(screenshot_path)
only_in_before = set(paths_before) - set(paths_after)
html_content = "<h1>Screenshots diff report</h1>"
html_content += f"""<p>Comparing {baseline_branch} to {incoming_changes_ref}</p>"""
output_dir_before = os.path.join(args.output_dir, "before")
output_dir_after = os.path.join(args.output_dir, "after")
os.makedirs(output_dir_before, exist_ok=True)
os.makedirs(output_dir_after, exist_ok=True)
for screenshot_path in only_in_before:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot only in before: {locale}: {screenshot_name}")
os.makedirs(os.path.join(output_dir_before, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.before_dir, screenshot_path), os.path.join(output_dir_before, screenshot_path))
html_content += f"<p>{locale}: REMOVED {screenshot_name}</br><img src='{os.path.join('before', screenshot_path)}'></p></br></br>"
for screenshot_path in only_in_after:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot only in after: {locale}: {screenshot_name}")
os.makedirs(os.path.join(output_dir_after, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.after_dir, screenshot_path), os.path.join(output_dir_after, screenshot_path))
html_content += f"<p>{locale}: ADDED {screenshot_name}</br><img src='{os.path.join('after', screenshot_path)}'></p></br></br>"
for screenshot_path in diffs:
locale, screenshot_name = get_locale_and_screenshot_name(screenshot_path)
print(f"Screenshot different: {locale}: {screenshot_name}")
# Copy both screenshots to the output dir
os.makedirs(os.path.join(output_dir_before, os.path.dirname(screenshot_path)), exist_ok=True)
os.makedirs(os.path.join(output_dir_after, os.path.dirname(screenshot_path)), exist_ok=True)
shutil.copy(os.path.join(args.before_dir, screenshot_path), os.path.join(output_dir_before, screenshot_path))
shutil.copy(os.path.join(args.after_dir, screenshot_path), os.path.join(output_dir_after, screenshot_path))
html_content += f"<p>{locale}: {screenshot_name}</br><img src='{os.path.join('before', screenshot_path)}'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img src='{os.path.join('after', screenshot_path)}'></p></br></br>"
if not only_in_after and not only_in_before and not diffs:
print("No differences found")
html_content += "<h1>No differences found</h1>"
script_dir = pathlib.Path(__file__).parent.resolve()
html_output = ""
with open(os.path.join(script_dir, "index.html"), "r") as f:
html_output = f.read().replace("{{ content }}", html_content)
with open(os.path.join(args.output_dir, "index.html"), "w") as f:
f.write(html_output)
# Also copy the css file; source: https://github.com/picocss/pico
shutil.copy(os.path.join(script_dir, "pico.min.css"), os.path.join(args.output_dir, "pico.min.css"))

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>SeedSigner screenshot diffs</title>
<link rel="stylesheet" href="pico.min.css" />
</head>
<body>
<main class="container">
{{ content }}
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,91 +0,0 @@
name: CI
on:
push:
branches:
- dev
- main
pull_request:
concurrency:
# Concurrency group that uses the workflow name and PR number if available
# or commit SHA as a fallback. If a new build is triggered under that
# concurrency group while a previous build is running it will be canceled.
# Repeated pushes to a PR will cancel all previous builds, while multiple
# merges to main will not cancel.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
env:
# head_ref: source branch name of a PR; null when action isn't a PR.
# sha: hash of a commit / merge.
INCOMING_CHANGES_REF: ${{ github.head_ref || github.sha }}
steps:
- name: Checkout main repo 'dev'
uses: actions/checkout@v4
with:
repository: 'SeedSigner/seedsigner'
ref: dev
submodules: true
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
sudo apt-get install libzbar0
python -m pip install --upgrade pip
pip install -r requirements.txt -r tests/requirements.txt -r l10n/requirements-l10n.txt
pip install -e .
- name: Generate current 'dev' screenshots
run: |
mkdir -p artifacts/dev
python -m pytest tests/screenshot_generator/generator.py
sleep 10
mv ./seedsigner-screenshots/* ./artifacts/dev/
- name: Checkout updated translations (PR)
uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request' }}
with:
path: src/seedsigner/resources/seedsigner-translations
ref: ${{ github.event.pull_request.head.sha }}
- name: Checkout updated translations (Push)
uses: actions/checkout@v4
if: ${{ github.event_name == 'push' }}
with:
path: src/seedsigner/resources/seedsigner-translations
ref: ${{ github.sha }}
- name: Compile updated translations catalogs
run: |
python setup.py compile_catalog
cd src/seedsigner/resources/seedsigner-translations
git status
- name: Generate latest screenshots
run: |
rm -rf seedsigner-screenshots
mkdir -p artifacts/incoming
python -m pytest tests/screenshot_generator/generator.py
sleep 10
mv ./seedsigner-screenshots/* ./artifacts/incoming/
- name: Diff screenshots
run: |
mkdir -p artifacts/diff
python src/seedsigner/resources/seedsigner-translations/.github/diff_report/diff_screenshots.py ./artifacts/dev ./artifacts/incoming ./artifacts/diff $INCOMING_CHANGES_REF
- name: Clean up artifacts
run: |
rm -rf ./artifacts/incoming
rm -rf ./artifacts/dev
mv ./artifacts/diff/* ./artifacts
rmdir ./artifacts/diff
- name: Archive CI Artifacts
uses: actions/upload-artifact@v4
with:
name: ci-artifacts
path: artifacts/**
retention-days: 60
# Upload also when tests fail. The workflow result (red/green) will
# be not effected by this.
if: always()

4
.gitignore vendored
View File

@ -1,5 +1 @@
.DS_Store
# Only check in *.po files since *.mo
# files can be generated from from them
*.mo

View File

@ -1,12 +0,0 @@
[main]
host = https://app.transifex.com
[o:seedsigner:p:seedsigner:r:messagespot]
file_filter = l10n/<lang>/LC_MESSAGES/messages.po
source_file = ../../../../l10n/messages.pot
type = PO
minimum_perc = 15
resource_name = messages.pot
replace_edited_strings = false
keep_translations = false
lang_map = zh-Hans: zh_Hans_CN

View File

@ -1,47 +1 @@
# SeedSigner Translations
## Transifex CLI
You can pull the latest translations directly from Transifex for all translated languages.
You'll need to create a Transifex user if you don't already have one. Your user will also
need some minimal role permissions within the SeedSigner Transifex project in order for
your user's API key to have access to the translations data.
* Install the [Transifex CLI](https://developers.transifex.com/docs/cli).
* [Create an API key](https://help.transifex.com/en/articles/6248858-generating-an-api-token)
for your Transifex user.
* From the `seedsigner/src/seedsigner/resources/seedsigner-translations` dir run:
```bash
# --force, -f Force the download of the translations files regardless of whether timestamps on the local computer are newer than those on the server (default: false)
# --all, -a Whether to download all files (default: false)
tx pull -f --all
```
* We use `-f` because as we manipulate the .po files through our PR process, they may
end up with newer timestamps even though the translation content has not changed.
That timestamp would then mislead the `tx pull` to disregard newer translations.
* Then from the SeedSigner project root, compile the catalogs to process the *.po files into
*.mo:
```bash
python setup.py compile_catalog
```
### Pulling translations for a specific language
Use the `--language, -l` flag with the `--force, -f` flag:
```bash
# Example: Spanish ("es")
tx pull -f -l es
# Or comma-separated list
tx pull -f -l pl,no
```
### Misc notes
The `.tx/config` is set to `minimum-perc = 15` (this means that the CLI will skip any
language whose translation completion falls below this minimum percentage).
You can manually override this by adding, for example, `--minimum-perc 25` to require at
least 25% translation completion.
# SeedSigner Translations

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +0,0 @@
"""
Extracts all unique characters that appear in the translated strings for the specified
locale.
This is a utility for build / dev purposes only.
"""
if __name__ == "__main__":
import argparse
import os
from babel.messages import mofile
# Define required input args and help text
parser = argparse.ArgumentParser(
description="Extracts all unique characters that appear in the translated strings for the specified locale."
)
parser.usage = "python3 extract_characters_from_babel_mo.py <locale>"
parser.add_argument("locale", help="Target locale (e.g. es, pt_BR, zh_Hans_CN)")
parser.add_argument("--debug", action="store_true", help="Enable debug output")
args = parser.parse_args()
debug = args.debug
basic_chars = set((chr(c) for c in range(0x20, 0x7E + 1))) # all basic ascii chars from SPACE to "~"
mo_fullfilename = os.path.join(os.pardir, "l10n", args.locale, "LC_MESSAGES", "messages.mo")
try:
with open(mo_fullfilename, "rb") as f:
catalog = mofile.read_mo(f)
except FileNotFoundError:
print(f"Could not find translations for locale \"{args.locale}\" ({mo_fullfilename})")
exit(1)
id_chars = set()
translations_chars = set()
for msg in catalog:
if msg.id:
if isinstance(msg.id, list):
# plural message
# get chars from all plural forms
for msgid in msg.id:
id_chars.update(msgid)
else:
# singular message
id_chars.update(msg.id)
if msg.string:
if isinstance(msg.string, list):
# plural message
# get chars from all plural forms
for msgstring in msg.string:
translations_chars.update(msgstring)
else:
# singular message
translations_chars.update(msg.string)
if debug:
# Print the difference between the chars in the ids vs the basic_chars
print("Chars in ids but not in basic_chars:", sorted(set("".join(id_chars)) - set(basic_chars)))
# And show the opposite
print("Chars in basic_chars but not in ids:", sorted(basic_chars - set("".join(id_chars))))
print("Chars in translations:", "".join(sorted(translations_chars)))
# get a unique list of chars from all translation messages and included_chars string
chars = sorted(translations_chars.union(basic_chars))
# convert set to string
chars_string = "".join(chars)
# remove newlines
chars_string = chars_string.replace("\n", "").replace("\r", "")
print(chars_string)

View File

@ -1 +0,0 @@
Babel