Compare commits
11
Commits
6f9a712813
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65d751c64c | ||
|
|
45bf57d936 | ||
|
|
ea4b629d2c | ||
|
|
387808f482 | ||
|
|
3069ad4e26 | ||
|
|
5491092037 | ||
|
|
cc9d1b534e | ||
|
|
ef8103f1fd | ||
|
|
046c06679c | ||
|
|
429422715c | ||
|
|
835e3437c7 |
+2
-1
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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}"
|
||||
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user