Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4ccf40a
Add json_builder.py with --refresh and --split flags
cheweaaron609-png Jun 14, 2026
c10d538
Remove json_splitter.py reference from pyproject.toml
cheweaaron609-png Jun 14, 2026
649381f
Fix path injection warning by resolving output path
cheweaaron609-png Jun 14, 2026
6a62c23
Rename holidays_intermediate.json to holidays_l10n.json and track in …
cheweaaron609-png Jun 20, 2026
5b3a0b1
Refactor to instance-based class, fix flat-language split bug, add ba…
cheweaaron609-png Jun 26, 2026
343f5a8
Add Aaron Chewe to CONTRIBUTORS
cheweaaron609-png Jun 26, 2026
11a7a68
Refactor script
KJhellico Jun 26, 2026
e181dd1
Merge branch 'dev' into gsoc/my-l10n-per-locale
KJhellico Jun 26, 2026
5a7da51
Latest translations
KJhellico Jun 26, 2026
64007a5
Add new comment's field
KJhellico Jun 26, 2026
3c3be1d
Merge remote-tracking branch 'upstream/dev' into generate-l10n-per-lo…
cheweaaron609-png Jul 11, 2026
3dbf3d8
Latest translations
KJhellico Jul 11, 2026
19975f6
Keep new comment value
KJhellico Jul 11, 2026
5a00682
Add Cap 149 pre-2012 citation for HK Lunar New Year's Eve and new_com…
cheweaaron609-png Jul 14, 2026
7d36890
HK Lunar New Year's Eve update
KJhellico Jul 14, 2026
3966675
XHKG fix
KJhellico Jul 14, 2026
a88d76f
Add generate_locale_po_files.py for per-locale .pot and .po generation
cheweaaron609-png Jul 19, 2026
eef00f1
Update script
KJhellico Jul 19, 2026
c215d95
Merge branch 'dev' into gsoc/l10n-per-locale
KJhellico Jul 19, 2026
8c737aa
Update JSON with latest translations
KJhellico Jul 19, 2026
8dda65e
Merge branch 'dev' into gsoc/l10n-per-locale
KJhellico Jul 20, 2026
26e4a37
Update JSON with latest translations
KJhellico Jul 20, 2026
4a98ea9
Split Independence Day
KJhellico Jul 20, 2026
4253841
Replace tr() strings with l10n msgid
cheweaaron609-png Jul 25, 2026
84b63a1
Fix replace_tr_strings.py
cheweaaron609-png Jul 25, 2026
838f614
Merge branch 'dev' into gsoc/l10n-per-locale
KJhellico Jul 26, 2026
696f4c1
Refactor replace_tr_strings.py
cheweaaron609-png Jul 26, 2026
55e1eea
Refactor replace_tr_strings.py further
cheweaaron609-png Jul 26, 2026
4b6d917
Update replace_tr_strings.py
cheweaaron609-png Jul 26, 2026
caf9f76
Split nested entries in json
cheweaaron609-png Jul 26, 2026
dc673e6
Add warning and handle multi-line comments in replace_tr_strings.py
cheweaaron609-png Jul 28, 2026
da84edc
Revert "Split nested entries in json"
cheweaaron609-png Jul 28, 2026
9b13a10
Fix multi-line comment removal
cheweaaron609-png Jul 28, 2026
716f4e5
Merge branch 'dev' into gsoc/l10n-per-locale
KJhellico Jul 29, 2026
d7d8719
Update JSON with latest translations
KJhellico Jul 29, 2026
ac116cc
Merge branch 'dev' into gsoc/l10n-per-locale
KJhellico Aug 22, 2026
01a8664
Update JSON with latest translations
KJhellico Aug 22, 2026
e0cc6c0
Merge branch 'refs/heads/dev' into gsoc/l10n-per-locale
KJhellico Aug 23, 2026
d20b2c4
Update JSON with latest translations
KJhellico Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ __pycache__
*.DS_Store
*.egg-info
*.mo
scripts/l10n/holidays_l10n.json.bak
*.pyc
build/*
coverage.lcov
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Aadesh Shrivastava
Aaqil Yousuf
Aaron Chewe
Aaron Picht
Aart Goossens
Abdelkhalek Boukli Hacene
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ lint.per-file-ignores."scripts/archive_links.py" = [ "T201" ]
lint.per-file-ignores."scripts/generate_release_notes.py" = [ "T201" ]
lint.per-file-ignores."scripts/generate_site_assets.py" = [ "T201" ]
lint.per-file-ignores."scripts/generate_snapshots.py" = [ "T201" ]
lint.per-file-ignores."scripts/l10n/generate_locale_po_files.py" = [ "T201" ]
lint.per-file-ignores."scripts/l10n/generate_mo_files.py" = [ "T201" ]
lint.per-file-ignores."scripts/l10n/generate_po_files.py" = [ "T201" ]
lint.per-file-ignores."scripts/l10n/json_builder.py" = [ "FBT", "T201" ]
lint.per-file-ignores."scripts/l10n/l10n_helper.py" = [ "T201" ]
lint.per-file-ignores."scripts/l10n/replace_tr_strings.py" = [ "T201" ]
lint.per-file-ignores."scripts/normalize_text.py" = [ "T201" ]
lint.per-file-ignores."tests/common.py" = [ "N802" ]
lint.per-file-ignores."tests/test_holiday_base.py" = [ "S301" ]
Expand Down
144 changes: 144 additions & 0 deletions scripts/l10n/generate_locale_po_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
Comment thread
KJhellico marked this conversation as resolved.


# holidays
# --------
# A fast, efficient Python library for generating country, province and state
# specific sets of holidays on the fly. It aims to make determining whether a
# specific date is a holiday as fast and flexible as possible.
#
# Authors: Vacanza Team and individual contributors (see CONTRIBUTORS file)
# dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023
# ryanss <ryanssdev@icloud.com> (c) 2014-2017
# Website: https://github.com/vacanza/holidays
# License: MIT (see LICENSE file)

"""Generate per-locale .pot and .po files from the JSON file.

Run with:
python scripts/l10n/generate_locale_po_files.py

This generates:
* holidays/locale/holidays.pot - master template with all msgids
* holidays/locale/{lang}.po - one per language
"""

import json
import re
import sys
from datetime import datetime
from pathlib import Path

from polib import POEntry, POFile

sys.path.insert(0, str(Path.cwd()))
from holidays import __version__ as package_version

JSON_PATH = Path("scripts/l10n/holidays_l10n.json")
LOCALE_PATH = Path("holidays/locale")
HEADER_PATH = Path("docs/file_header.txt")
POT_FILENAME = "holidays.pot"
WRAP_WIDTH = 99
MSGID_BUGS_ADDRESS = "l10n@vacanza.dev"
TRANSLATOR_PATTERN = re.compile(r"[^\s<]+(?:\s+[^\s<]+)*\s+<[^@\s<]+@[^@\s<.]+(?:\.[^@\s<.]+)+>")


class LocalePOGenerator:
"""Generates per-locale .pot and .po files from the JSON."""

def __init__(self) -> None:
if not JSON_PATH.exists():
raise FileNotFoundError(f"{JSON_PATH} not found!")
with JSON_PATH.open(encoding="utf-8") as f:
self.data: list[dict] = json.load(f)

self.timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M%z")
self.license_header = self._get_license_header()

@staticmethod
def _get_license_header() -> str:
"""Read and format the license header from docs/file_header.txt."""
content = HEADER_PATH.read_text(encoding="utf-8").lstrip("\n")
Comment thread
KJhellico marked this conversation as resolved.
return (
"\n".join(
f"# {stripped}" if (stripped := line.rstrip()) else "#"
for line in content.splitlines()
)
+ "\n#"
)

def _make_metadata(self, lang: str | None = None) -> dict:
"""Build standard Gettext metadata for a .po or .pot file."""
return {
"Project-Id-Version": f"Holidays {package_version}",
"Report-Msgid-Bugs-To": MSGID_BUGS_ADDRESS,
"POT-Creation-Date": self.timestamp,
"PO-Revision-Date": self.timestamp,
"Last-Translator": f"Holidays Localization Team <{MSGID_BUGS_ADDRESS}>",
"Language-Team": "Holidays Localization Team",
"Language": lang or "en",
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=UTF-8",
"Content-Transfer-Encoding": "8bit",
"X-Source-Language": "en",
}

def _write_po_header(self, po_path: Path, lang: str | None) -> None:
"""Prepend the license header to a written .po file."""
output = [
self.license_header,
f"# Holidays {lang} localization." if lang else "# Holidays localization.",
"#",
po_path.read_text(encoding="utf-8"),
]
po_path.write_text("\n".join(output), encoding="utf-8", newline="\n")

Check failure on line 94 in scripts/l10n/generate_locale_po_files.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=vacanza_python-holidays&issues=AZ963qcJcUMiqJpTOZ4X&open=AZ963qcJcUMiqJpTOZ4X&pullRequest=3585

def _build_po(self, lang: str | None = None) -> None:
"""Build a per-locale .po file for a given language."""
po = POFile(wrapwidth=WRAP_WIDTH)
po.metadata = self._make_metadata(lang)

for entry in self.data:
po.append(
POEntry(
msgid=entry["msgid"],
msgstr=entry["messages"].get(lang) or "",
Comment thread
KJhellico marked this conversation as resolved.
comment=entry.get("new_comment"),
Comment thread
KJhellico marked this conversation as resolved.
flags=["c-format"] if "%" in entry["msgid"] else [],
)
)

po_path = LOCALE_PATH / (f"{lang}.po" if lang else POT_FILENAME)
po.save(str(po_path), newline="\n")
self._write_po_header(po_path, lang)
print(f"Saved {po_path} ({len(po)} entries)")

def _collect_languages(self) -> set[str]:
"""Collect all language codes present across all entries."""
langs: set[str] = set()
for entry in self.data:
for lang, val in entry["messages"].items():
if isinstance(val, str):
langs.add(lang)
else:
Comment thread
KJhellico marked this conversation as resolved.
raise ValueError(
f"Msgid `{entry['msgid']}` contains multiple translation for lang `{lang}`"
)

return langs

def run(self) -> None:
"""Generate the .pot and all per-locale .po files."""
langs = self._collect_languages()
# build .pot file.
self._build_po()

print(f"Generating .po files for {len(langs)} languages...")
for lang in sorted(langs):
self._build_po(lang)

print("Done.")


if __name__ == "__main__":
LocalePOGenerator().run()
Loading