Compare commits

..
11 Commits
Author SHA1 Message Date
yyc12345 65d751c64c fix: make clarification for Info.plist CFBundleLocalizations 2026-09-24 15:44:00 +08:00
yyc12345 45bf57d936 fix: enlarge the boundary of liquid range literal to system value 2026-09-22 20:26:28 +08:00
yyc12345 ea4b629d2c fix: create the parent directory of generated pot file 2026-09-22 15:20:14 +08:00
yyc12345 387808f482 fix: fix RC example issue 2026-09-22 13:45:07 +08:00
yyc12345 3069ad4e26 fix: fix liquid range literal issue by hacking it
use hacking way to resolve the issue that liquid range literal has hard +/- 1024 boundary which blocks the ability of rendering RC template
2026-09-22 11:43:29 +08:00
yyc12345 5491092037 doc: update README 2026-09-21 21:29:01 +08:00
yyc12345 cc9d1b534e chore: add keywords and classifiers for project 2026-09-21 21:19:27 +08:00
yyc12345 ef8103f1fd doc: change doc hierarchy 2026-09-21 21:15:59 +08:00
yyc12345 046c06679c doc: add content for examples 2026-09-21 21:04:24 +08:00
yyc12345 429422715c fix: liquid render function now create parent dir if possible
liquid render function now create the parent directory of its target if possible to fix the issue that user need manually create directory hierarchy for multiple output render command.
2026-09-21 20:49:41 +08:00
yyc12345 835e3437c7 fix: remove redundant whitespace in template 2026-09-21 20:39:41 +08:00
22 changed files with 192 additions and 36 deletions
+2 -1
View File
@@ -44,6 +44,7 @@ Additionally, we check [the document about localization file path](https://learn
macOS `.plist` and its localization mechanism involve multiple file formats. All formats of them can be browsed from their official documents:
- `Info.plist` file: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/PropertyLists/UnderstandXMLPlist/UnderstandXMLPlist.html
- Available keys and values in `Info.plist` file: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html
- `InfoPlist.strings` and other string resource files: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html
- UTI string: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
@@ -71,4 +72,4 @@ TODO...
### Bump Manifest Version Up
- Update version value in document `doc/manifest.md`.
- Update version value in example `doc/example.*.toml`.
- Update version value in example `example/*.toml`.
+24 -1
View File
@@ -29,13 +29,36 @@ MetaGlot supports compiling metadata from the following formats:
| Platform | Metadata format |
|---|---|
| Windows | Windows reosurce file `.rc` |
| Windows | Windows resource file `.rc` |
| Windows | MSIX manifest (`Package.appxmanifest` or `AppxManifest.xml`) and localization files (`.resw` or `.resjson`) |
| Windows | InnoSetup script (`.iss`) and its localization files (`.isl`) |
| Linux | `.desktop` file |
| Linux | AppStream XML file (`.metainfo.xml`) |
| macOS | `Info.plist` and its localization files (`InfoPlist.strings`) |
## Getting Started
A typical MetaGlot workflow consists of four steps:
1. Write the TOML string manifest of your metadata — extracted by a tool or written by hand.
2. Generate a POT template from the manifest with `metaglot pot`.
3. Bring the translation PO files up to date with the POT using `metaglot update`.
4. Produce the platform-native files with `metaglot render` for every metadata type you ship.
When you need more information along the way, refer to the following resources:
- Run a command with the `--help` option to inspect its command line arguments.
- Read the documents in the `doc` directory for the TOML manifest format, the render contexts and the available Liquid filters.
- Browse the examples in the `example` directory for ready-to-study samples.
## Environment Variables
MetaGlot uses following environment variables at runtime:
| Variable | Description |
|---|---|
| `METAGLOT_MSGMERGE` | Path to the `msgmerge` executable used by `metaglot update`, overriding the default lookup on `PATH`. |
## License
MetaGlot is released under the MIT License. See `LICENSE` for details.
+22
View File
@@ -0,0 +1,22 @@
# Render Context
A render context is the set of top-level variables that MetaGlot passes into a user-provided Liquid template when `metaglot render` executes. The context carries the resolved translation data — one entry per language read from the PO files, each holding the text of every manifest entry — together with the extra variables a metadata type needs, such as the manifest's source language or per-language identifiers.
Every rendering shares the following semantics:
- Templates are read as UTF-8, and the rendered output is written as UTF-8 (without BOM) using the system's native line endings. The parent folder of an output file is created when it does not exist.
- The renderer runs in strict mode: referencing a variable or a property that does not exist fails the render.
- The text of an entry falls back to the manifest source string when the entry is untranslated, missing from the PO file, or marked fuzzy.
- The language of a PO file is resolved from its `Language:` header, falling back to the file name without extension; two PO files describing the same language and PO entries unknown to the manifest are rejected.
- Some metadata types consist of multiple files; for them the `--*-path` command line arguments are themselves in-memory Liquid templates, rendered with the same mechanism into per-file output paths.
Filters are documented separately in [filters.md](filters.md).
The render context of every supported metadata type is described in its own document:
- [Windows RC resource files](render-context/rc.md) — language identifiers, string table statements and version information values.
- [Windows MSIX packages](render-context/msix.md) — the package manifest and the per-language resources files.
- [Windows InnoSetup scripts](render-context/inno.md) — the installation script and the per-language messages files.
- [Linux desktop entries](render-context/desktop.md) — localized keys with locale postfixes.
- [Linux AppStream metadata](render-context/appstream.md) — translations marked with `xml:lang`.
- [macOS bundles](render-context/plist.md) — the bundle `Info.plist` and the per-language `InfoPlist.strings` files.
@@ -45,4 +45,4 @@ AppStream uses the element without `xml:lang` as the untranslated default, mirro
## Filters
All filters described in [filters.md](filters.md) are available when rendering AppStream XML files: the general filters available in every rendering, and the AppStream filters such as `xml_escape`. Note that translated descriptions may legitimately carry AppStream markup (e.g. `<em>`); such values must not be passed through `xml_escape`, and deciding what to escape is the template author's responsibility.
All filters described in [filters.md](../filters.md) are available when rendering AppStream XML files: the general filters available in every rendering, and the AppStream filters such as `xml_escape`. Note that translated descriptions may legitimately carry AppStream markup (e.g. `<em>`); such values must not be passed through `xml_escape`, and deciding what to escape is the template author's responsibility.
@@ -41,4 +41,4 @@ Name={{ default.strings.name | desktop_escape }}
## Filters
All filters described in [filters.md](filters.md) are available when rendering desktop entry files: the general filters available in every rendering, and the desktop filters such as `desktop_escape`.
All filters described in [filters.md](../filters.md) are available when rendering desktop entry files: the general filters available in every rendering, and the desktop filters such as `desktop_escape`.
@@ -38,7 +38,7 @@ Keep the `case` branches aligned with the languages you ship PO files for: an un
## Path Template Contexts
The `--iss-path` and `--isl-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](filters.md) are available to them.
The `--iss-path` and `--isl-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](../filters.md) are available to them.
### `--iss-path`
@@ -10,7 +10,7 @@ Every component below renders with its **own environment**, so the filters avail
## Path Template Contexts
The `--manifest-path` and `--resources-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](filters.md) are available to them.
The `--manifest-path` and `--resources-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](../filters.md) are available to them.
### `--manifest-path`
@@ -10,7 +10,7 @@ Every component below renders with its **own environment**, so the filters avail
## Path Template Contexts
The `--plist-path` and `--strings-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](filters.md) are available to them.
The `--plist-path` and `--strings-path` arguments are in-memory Liquid templates rendered into output paths. Only the general filters described in [filters.md](../filters.md) are available to them.
### `--plist-path`
@@ -61,6 +61,8 @@ The `--plist-template` and `--strings-template` arguments point to template file
</array>
```
Note that `CFBundleLocalizations` declares the localizations handled by the application itself rather than those discovered through the bundle localization mechanism — think of a Qt application localizing through its embedded translations. MetaGlot only knows the metadata languages (the languages receiving an `InfoPlist.strings` file), so rendering `langs` into this key assumes the application's self-handled UI languages and its metadata languages are the same set — the common case when the project translates both in lockstep. When the two differ, maintain the list in the template yourself instead of iterating `langs`. An application localizing entirely through the bundle mechanism usually does not need this key at all.
The source language always leads `langs` and always gets an InfoPlist.strings file: it comes from a PO file when one provides it, and is otherwise synthesized from the manifest's source strings. Every language listed in `langs` is guaranteed a complete `.lproj` folder.
### `--strings-template`
@@ -37,4 +37,4 @@ The resolved text of an entry falls back to the manifest source string when the
## Filters
All filters described in [filters.md](filters.md) are available when rendering RC files: the general filters available in every rendering, and the RC filters such as `rc_escape`.
All filters described in [filters.md](../filters.md) are available when rendering RC files: the general filters available in every rendering, and the RC filters such as `rc_escape`.
+7
View File
@@ -26,6 +26,13 @@
<true/>
<key>NSHumanReadableCopyright</key>
<string>{{ default.strings.copyright | xml_escape }}</string>
{% comment %}
CFBundleLocalizations declares the localizations the application handles by itself,
not those discovered through the bundle localization mechanism.
This example assumes the application's UI languages (e.g. Qt-embedded translations) match its metadata languages,
so it renders the metadata language list directly;
maintain your own list here if the two ever differ.
{% endcomment %}
<key>CFBundleLocalizations</key>
<array>
{% for lang in langs %}
+39
View File
@@ -0,0 +1,39 @@
# Examples
This folder holds one example per supported metadata type. Two kinds of files appear here:
- `.toml` files are string manifests: they declare the translatable strings of one metadata type.
- `.liquid` files are render templates: they decide how the translated strings are written back into the platform-native file.
## Windows RC Resource File
- `example.rc.toml` — The string manifest of the RC example.
- `example.rc.liquid` — Renders the `.rc` file, including string tables and version information blocks.
## Windows MSIX Package
- `AppxManifest.xml.toml` — The string manifest of the MSIX example.
- `AppxManifest.xml.liquid` — Renders the `AppxManifest.xml` package manifest, including the resource language list.
- `Resources.resw.liquid`, `Resources.resjson.liquid` — Render the per-language resources file. They hold the same content in the two accepted resource formats; pick whichever matches the packaging pipeline.
## Windows InnoSetup Script
- `example.iss.toml` — The string manifest of the InnoSetup example.
- `example.iss.liquid` — Renders the `.iss` installation script, including its `[Languages]` section.
- `example.isl.liquid` — Renders the per-language `.isl` messages files.
## Linux Desktop Entry
- `example.desktop.toml` — The string manifest of the desktop entry example.
- `example.desktop.liquid` — Renders the `.desktop` file, including localized keys such as `Name[zh_CN]`.
## Linux AppStream Metadata
- `example.metainfo.xml.toml` — The string manifest of the AppStream example.
- `example.metainfo.xml.liquid` — Renders the `.metainfo.xml` file, including translations marked with `xml:lang`.
## macOS Bundle
- `Info.plist.toml` — The string manifest of the macOS bundle example.
- `Info.plist.liquid` — Renders the bundle `Info.plist`.
- `InfoPlist.strings.liquid` — Renders the per-language `InfoPlist.strings` localization files.
+10 -10
View File
@@ -9,17 +9,17 @@ Compression=lzma
SolidCompression=yes
[Languages]
{% for lang in langs %}
{% case lang %}
{% when 'zh_CN' %}
{% assign base = 'compiler:Languages\ChineseSimplified.isl' %}
{% when 'ja' %}
{% assign base = 'compiler:Languages\Japanese.isl' %}
{% else %}
{% assign base = 'compiler:Default.isl' %}
{% endcase %}
{% for lang in langs -%}
{% case lang -%}
{% when 'zh_CN' -%}
{% assign base = 'compiler:Languages\ChineseSimplified.isl' -%}
{% when 'ja' -%}
{% assign base = 'compiler:Languages\Japanese.isl' -%}
{% else -%}
{% assign base = 'compiler:Default.isl' -%}
{% endcase -%}
Name: "{{ lang }}"; MessagesFile: "{{ base }},Strings\{{ lang }}.isl"
{% endfor %}
{% endfor -%}
[Tasks]
Name: "desktopicon"; Description: "{cm:task_desktop_icon}"
+16 -9
View File
@@ -4,15 +4,15 @@
// String tables
// ---------------------------
{% for lang in languages %}
{% for lang in languages -%}
// {{ lang.name }}
LANGUAGE {{ lang.primary }}, {{ lang.sublanguage }}
STRINGTABLE
BEGIN
{% for i in (1000..1002) %}
{%- for i in (1000..1002) -%}
{% assign key = i | to_string %}
{{ i }} "{{ lang.strings[key] | rc_escape }}"
{% endfor %}
{%- endfor %}
END
{% endfor %}
@@ -20,17 +20,23 @@ END
// Version information
// ---------------------------
{% for lang in languages -%}
LANGUAGE {{ lang.primary }}, {{ lang.sublanguage }}
1 VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEFLAGMASK 0x3fL
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
#endif
FILEOS 0x40004L // VOS_NT_WINDOWS32
FILETYPE 0x1L // VFT_APP
FILESUBTYPE 0x0L // VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
{% for lang in languages %}
BLOCK "{{ lang.block_key }}"
BEGIN
VALUE "Comments", "https://github.com/SarasasChipWorkshop/metaglot"
@@ -43,10 +49,11 @@ BEGIN
VALUE "ProductName", "{{ lang.strings.product_name | rc_escape }}"
VALUE "ProductVersion", "1.0.0.0"
END
{% endfor %}
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", {% for lang in languages %}{{ lang.langid }}, {{ lang.code_page }}{% unless forloop.last %}, {% endunless %}{% endfor %}
VALUE "Translation", {{ lang.langid }}, {{ lang.code_page }}
END
END
{% endfor -%}
+30 -1
View File
@@ -8,12 +8,41 @@ authors = [
]
requires-python = ">=3.13"
license = { text = "MIT" }
keywords = [
"localization",
"l10n",
"i18n",
"translation",
"gettext",
"pot",
"po",
"metadata",
"rc",
"msix",
"appstream",
"desktop-entry",
"plist",
"innosetup",
]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Internationalization",
"Topic :: Software Development :: Localization",
"Topic :: Software Development :: Build Tools",
]
dependencies = [
"langcodes>=3.5.1",
"polib>=1.2.0",
"pycountry>=26.2.16",
"pydantic>=2.11.7",
"python-liquid>=2.2.0",
"python-liquid==2.3.1",
]
[project.urls]
+3
View File
@@ -1,10 +1,13 @@
import logging
import sys
from argparse import ArgumentParser
from . import hack
from .cmds import run, register, parse
def main() -> None:
hack.relax_liquid_range_literal_boundary()
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
parser = ArgumentParser(
-2
View File
@@ -276,7 +276,6 @@ def run(opts: InnoRenderOpts) -> None:
iss_out = opts.out_dir / render_path(
iss_path_env, opts.in_iss_path, _InnoIssPathContext()
)
iss_out.parent.mkdir(parents=True, exist_ok=True)
render(
iss_template_env,
opts.in_iss_template,
@@ -288,7 +287,6 @@ def run(opts: InnoRenderOpts) -> None:
isl_out = opts.out_dir / render_path(
isl_path_env, opts.in_isl_path, _InnoIslPathContext(lang)
)
isl_out.parent.mkdir(parents=True, exist_ok=True)
render(
isl_template_env,
opts.in_isl_template,
-2
View File
@@ -278,7 +278,6 @@ def run(opts: MsixRenderOpts) -> None:
manifest_out = opts.out_dir / render_path(
manifest_path_env, opts.in_manifest_path, _MsixManifestPathContext()
)
manifest_out.parent.mkdir(parents=True, exist_ok=True)
render(
manifest_template_env,
opts.in_manifest_template,
@@ -292,7 +291,6 @@ def run(opts: MsixRenderOpts) -> None:
opts.in_resources_path,
_MsixResourcesPathContext(lang),
)
resources_out.parent.mkdir(parents=True, exist_ok=True)
render(
resources_template_env,
opts.in_resources_template,
-2
View File
@@ -302,7 +302,6 @@ def run(opts: PlistRenderOpts) -> None:
plist_out = opts.out_dir / render_path(
plist_path_env, opts.in_plist_path, _PlistInfoPathContext()
)
plist_out.parent.mkdir(parents=True, exist_ok=True)
render(
plist_template_env,
opts.in_plist_template,
@@ -316,7 +315,6 @@ def run(opts: PlistRenderOpts) -> None:
opts.in_strings_path,
_PlistStringsPathContext(lang),
)
strings_out.parent.mkdir(parents=True, exist_ok=True)
render(
strings_template_env,
opts.in_strings_template,
+26
View File
@@ -0,0 +1,26 @@
"""Runtime hacks over third-party dependencies.
Each hack should be applied at the very start of ``main`` and
documents why it exists and when it can be dropped.
"""
import sys
import liquid.builtin.expressions.primitive as _primitive
# TODO:
# When python-liquid relax the boundary of range literal,
# remove this hack.
def relax_liquid_range_literal_boundary() -> None:
"""Relax python-liquid's range literal bounds.
python-liquid (pinned to 2.3.1) clamps the endpoints of range literals
such as ``(1000..1028)`` to plus/minus 1024, silently truncating the
loop to 1000..1023. Windows RC string identifiers legitimately go up
to 65535, so templates iterating numeric ID ranges lose entries
without any error. Relax the bounds the library itself defines
to a more large boundary (as its commented)
"""
_primitive.MAX_RANGE = sys.maxsize
_primitive.MIN_RANGE = -sys.maxsize
+1
View File
@@ -216,6 +216,7 @@ def generate_pot(manifest: Manifest, output_path: Path) -> None:
comment=entry.comment or None,
)
)
output_path.parent.mkdir(parents=True, exist_ok=True)
po.save(str(output_path))
logging.info("Wrote %d entries to POT file %s.", len(po), output_path)
+3 -1
View File
@@ -46,7 +46,8 @@ def render(
"""Render a Liquid template and write the result to a file.
The template is read as UTF-8. The rendered output is written as UTF-8
using the system's native line endings.
using the system's native line endings. The parent folder of the output
file is created when it does not exist.
:param env: The environment to render with.
:param template_path: The path of the template file.
@@ -72,6 +73,7 @@ def render(
f"failed to render template '{template_path}': {exc}"
) from exc
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(content, encoding="utf-8")
logging.info("Wrote rendered output to %s", output_path)
Generated
+1 -1
View File
@@ -108,7 +108,7 @@ requires-dist = [
{ name = "polib", specifier = ">=1.2.0" },
{ name = "pycountry", specifier = ">=26.2.16" },
{ name = "pydantic", specifier = ">=2.11.7" },
{ name = "python-liquid", specifier = ">=2.2.0" },
{ name = "python-liquid", specifier = "==2.3.1" },
]
[[package]]