Compare commits
15
Commits
18a7309ac7
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65d751c64c | ||
|
|
45bf57d936 | ||
|
|
ea4b629d2c | ||
|
|
387808f482 | ||
|
|
3069ad4e26 | ||
|
|
5491092037 | ||
|
|
cc9d1b534e | ||
|
|
ef8103f1fd | ||
|
|
046c06679c | ||
|
|
429422715c | ||
|
|
835e3437c7 | ||
|
|
6f9a712813 | ||
|
|
d2254d5491 | ||
|
|
5e9fde5052 | ||
|
|
29c2b6d1f6 |
+25
-1
@@ -39,6 +39,30 @@ We use these formats to create Liquid filters.
|
||||
|
||||
Additionally, we check [the document about localization file path](https://learn.microsoft.com/en-us/windows/uwp/app-resources/localize-strings-ui-manifest) to know the language tag used by Windows MSIX. Then we use it to create the representation of it, and convertion code which converts PO file language representation to this representation.
|
||||
|
||||
### macOS Bundle Files
|
||||
|
||||
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
|
||||
|
||||
Based on these document, we know`.plist` file is ordinary XML file, so we create corresponding Liquid filter for it. Oppositely, for `.strings` file, according to [Apple document](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html), we also create corresponding filter.
|
||||
|
||||
[Apple document](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledevelopmentregion) doesn't explicitly declare the standard of `<lang>` part in expected `.strings` file path `Contents/Resources/<lang>.lproj/InfoPlist.strings`. We only can tract its history based on [Apple old document](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html). First it is "human-readable names". such as `English`, `German`, and etc. Then it becomes `language_region`, like `en_GB`. And finally, it possible be a BCP 47 string now based on multiple modern Apple documents. So we are forced to follow this vague standard to design the representation of it and may change it in future if we meet with some issues or more clear standard specification.
|
||||
|
||||
### InnoSetup Script Files
|
||||
|
||||
InnoSetup installation involves multiple file formats. All formats of them can be browsed from their official documents:
|
||||
|
||||
- `.iss` script file: https://jrsoftware.org/ishelp/index.php?topic=languages (the `[Languages]` section) and https://jrsoftware.org/ishelp/index.php?topic=custommessages (the `[CustomMessages]` section)
|
||||
- `.isl` messages file: https://jrsoftware.org/ishelp/index.php?topic=langoptions (the `[LangOptions]` section) and https://jrsoftware.org/ishelp/index.php?topic=messages (the `[Messages]` section)
|
||||
|
||||
Based on these document, we create corresponding Liquid filters for both files, since they share the same message value format.
|
||||
|
||||
Unlike the other targets, InnoSetup has no standard representation of language identifiers: both the internal language names declared in the `[Languages]` section and the message file names are chosen freely by the packager, as its official document says. So there is no language representation or convertion code for this target. The PO language tag carries over verbatim, and the pairing between a language and its base `.isl` file is expressed explicitly by the template author.
|
||||
|
||||
## Bump Version Up
|
||||
|
||||
### Bump Application Version Up
|
||||
@@ -48,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`.
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
|
||||
MetaGlot is a command-line tool that unifies localization metadata for applications shipped on multiple platforms. It extracts or defines translatable strings from platform-specific metadata files, normalizes them into Gettext-compatible POT/PO catalogs, and compiles translated PO files back into platform-native files.
|
||||
|
||||
It currently is designed for projects that ship on:
|
||||
|
||||
- **Windows** using RC resource files
|
||||
- **Linux** using `.desktop` and AppStream MetaInfo XML files
|
||||
- **macOS** using `Info.plist` and localized `InfoPlist.strings` files
|
||||
It currently is designed for projects that ship on Windows, Linux and macOS.
|
||||
|
||||
Instead of asking translators to edit platform-specific formats, MetaGlot lets them work with standard Gettext PO files.
|
||||
|
||||
@@ -29,16 +25,39 @@ MetaGlot does not replace Gettext. It makes Gettext the canonical translation wo
|
||||
|
||||
## Supported Metadata Types
|
||||
|
||||
MetaGlot supports extracting and compiling metadata from the following formats:
|
||||
MetaGlot supports compiling metadata from the following formats:
|
||||
|
||||
| Platform | Metadata format | Localization mechanism |
|
||||
|---|---|---|
|
||||
| Windows | `.rc` | String tables, version resources |
|
||||
| Windows | MSIX manifest (`AppxManifest.xml`) | `<DisplayName>`, `<Description>`, etc. |
|
||||
| Windows | NSIS packaging script (`.nsi`) | Version information keys |
|
||||
| Linux | `.desktop` | `Name`, `Comment`, etc. |
|
||||
| Linux | AppStream XML | `<name>`, `<summary>`, `<description>` |
|
||||
| macOS | `Info.plist` | `InfoPlist.strings`, plist keys |
|
||||
| Platform | Metadata format |
|
||||
|---|---|
|
||||
| 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
|
||||
|
||||
|
||||
@@ -74,3 +74,20 @@ Examples:
|
||||
```liquid
|
||||
{{ "Example" | json_escape }} {% comment %}renders "Example", without quotes{% endcomment %}
|
||||
```
|
||||
|
||||
## Plist Filters
|
||||
|
||||
The following filters are available when rendering macOS bundle files. `xml_escape` is available to the Info.plist file rendering; `plist_strings_escape` is available to the InfoPlist.strings file rendering.
|
||||
|
||||
| Filter | Available to | Description |
|
||||
|---|---|---|
|
||||
| `xml_escape` | Info.plist | The same filter described in the AppStream section above; Info.plist is XML too. |
|
||||
| `plist_strings_escape` | InfoPlist.strings | Escapes a string for use inside an InfoPlist.strings value: `\` becomes `\\`, `"` becomes `\"`, newline, carriage return and tab become `\n`, `\r` and `\t`. Non-ASCII characters carry over unchanged; use the `\Uxxxx` form manually for the rare cases a literal escape is required. |
|
||||
|
||||
## Inno Filters
|
||||
|
||||
The following filter is available to both the `.iss` and the `.isl` file renderings of the Inno Setup pipeline, which share the same message value escaping.
|
||||
|
||||
| Filter | Description |
|
||||
|---|---|
|
||||
| `inno_escape` | Escapes a string for use inside an Inno Setup message value: linefeed and carriage return become `%n`. The `%1`..`%9` argument placeholders and a literal `%` are left untouched - the `%%` escape is only defined for messages that take arguments, so managing `%` is the translator's responsibility. |
|
||||
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Inno Render Context
|
||||
|
||||
This document describes the data that `metaglot render inno` provides to user-provided [Liquid](https://shopify.github.io/liquid/) templates when rendering Inno Setup installation files.
|
||||
|
||||
Like the MSIX and plist renderings, an Inno rendering involves multiple output files: one `.iss` script referencing localized custom messages through `{cm:...}` constants, plus one incremental `.isl` messages file per language carrying the `[CustomMessages]` entries. The output location of every file is resolved by a path template command line argument instead of a fixed output file argument.
|
||||
|
||||
MetaGlot reads templates as UTF-8 and writes every rendered file as UTF-8 (without BOM) using the system's native line endings. Inno Setup 6.3 and later recommend exactly this encoding for `.iss` and `.isl` files, so the output is used as-is. The renderer runs in strict mode: referencing a variable or a property that does not exist fails the render.
|
||||
|
||||
Every component below renders with its **own environment**, so the filters available to one component never leak into another. Path templates resolve to paths relative to the output folder (an absolute rendered path is used as-is); their result is stripped of surrounding whitespace and must not be empty.
|
||||
|
||||
## Language Identification
|
||||
|
||||
Inno Setup has **no standard representation of language identifiers**: the `[Languages]` internal `Name:` and the message file names are both chosen freely by the packager. MetaGlot therefore performs **no conversion** for this target - the PO language tag (`zh_CN`, `en`) carries over verbatim, and the same tag serves as the single join key across everything the pipeline generates:
|
||||
|
||||
- the `Name:` value of a `[Languages]` entry,
|
||||
- the incremental `.isl` file name (e.g. `Strings\zh_CN.isl`),
|
||||
- and the matching branch of the base file pairing in the `.iss` template (see below).
|
||||
|
||||
The pairing between a language and its base `.isl` - the official full translation whose `[LangOptions]` provides the language identity (the Select Language dialog name and the auto-detection identifier) - is knowledge of the template author and is expressed explicitly, for example:
|
||||
|
||||
```liquid
|
||||
{% 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 %}
|
||||
```
|
||||
|
||||
The multi-line decision block renders some blank lines into the INI output, which is harmless; Liquid's whitespace control markers (`{%-`/`-%}`) can remove them once used consistently across templates.
|
||||
|
||||
Keep the `case` branches aligned with the languages you ship PO files for: an unmatched language falls to the `{% else %}` base (usually `compiler:Default.isl`) and inherits the English language identity. Review the rendered `[Languages]` section after adding a new language.
|
||||
|
||||
## 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.
|
||||
|
||||
### `--iss-path`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| (none) | | The context is empty: the path is typically the plain literal `example.iss`. |
|
||||
|
||||
The template is rendered once. Referencing any variable fails the render.
|
||||
|
||||
### `--isl-path`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `lang` | string | The PO language tag of the `.isl` messages file being placed, e.g. `en`, `zh_CN`. |
|
||||
|
||||
The template is rendered once per language, for example:
|
||||
|
||||
```liquid
|
||||
Strings/{{ lang }}.isl
|
||||
```
|
||||
|
||||
## File Render Contexts
|
||||
|
||||
The `--iss-template` and `--isl-template` arguments point to template files. Besides the general filters, the Inno filter (`inno_escape`) is available to both.
|
||||
|
||||
### `--iss-template`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `default` | language | The manifest's source language entry, see below. |
|
||||
| `langs` | array of string | The PO language tag of every language, source language first. |
|
||||
|
||||
The `.iss` script references the localized text through `{cm:...}` constants (e.g. `AppName={cm:app_name}`) and enumerates `[Languages]` from `langs` with the base file pairing shown above. `default` is available for values written directly instead of via constants:
|
||||
|
||||
```liquid
|
||||
AppName={{ default.strings.app_name | inno_escape }}
|
||||
```
|
||||
|
||||
The source language always leads `langs` and always gets an `.isl` messages file: it comes from a PO file when one provides it, and is otherwise synthesized from the manifest's source strings.
|
||||
|
||||
### `--isl-template`
|
||||
|
||||
The members of the current language are exposed at the top level. The structure is identical for every language; only the data differs. There is no `default`.
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | The PO language tag of this `.isl` messages file, e.g. `en_US`, `zh_CN`. |
|
||||
| `strings` | mapping of string to string | Maps every manifest entry key to its resolved text, e.g. `strings.app_name`. |
|
||||
|
||||
The resolved 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 template is rendered once per language.
|
||||
|
||||
The recommended shape of a generated `.isl` is a plain `[CustomMessages]` section: language identity (the `[LangOptions]` fields) is inherited from the base `.isl` listed before it in `MessagesFile`, following the merge order of Inno Setup.
|
||||
|
||||
```liquid
|
||||
[CustomMessages]
|
||||
app_name={{ strings.app_name | inno_escape }}
|
||||
```
|
||||
@@ -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`
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Plist Render Context
|
||||
|
||||
This document describes the data that `metaglot render plist` provides to user-provided [Liquid](https://shopify.github.io/liquid/) templates when rendering macOS bundle metadata files.
|
||||
|
||||
Like the MSIX rendering, a plist rendering involves multiple output files: one `Info.plist` carrying the source-language metadata, plus one `InfoPlist.strings` file per language under a language-tagged folder (typically `Contents/Resources/<lang>.lproj/InfoPlist.strings`). Unlike the MSIX manifest, `Info.plist` carries no resource references: it writes the source strings directly, while every localized value lives in the `InfoPlist.strings` files. The output location of every file is resolved by a path template command line argument instead of a fixed output file argument.
|
||||
|
||||
MetaGlot reads templates as UTF-8 and writes every rendered file as UTF-8 (without BOM) using the system's native line endings. This encoding is a built-in constraint of the tool and is not configurable. The renderer runs in strict mode: referencing a variable or a property that does not exist fails the render.
|
||||
|
||||
Every component below renders with its **own environment**, so the filters available to one component never leak into another. Path templates resolve to paths relative to the output folder (an absolute rendered path is used as-is); their result is stripped of surrounding whitespace and must not be empty.
|
||||
|
||||
## 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.
|
||||
|
||||
### `--plist-path`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| (none) | | The context is empty: the path is typically the plain literal `Contents/Info.plist`. |
|
||||
|
||||
The template is rendered once. Referencing any variable fails the render.
|
||||
|
||||
### `--strings-path`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `lang` | string | The BCP 47 language tag of the InfoPlist.strings file being placed, e.g. `en`, `zh-Hans`, `sr-Latn-RS`. |
|
||||
|
||||
The template is rendered once per language, for example:
|
||||
|
||||
```liquid
|
||||
Contents/Resources/{{ lang }}.lproj/InfoPlist.strings
|
||||
```
|
||||
|
||||
## File Render Contexts
|
||||
|
||||
The `--plist-template` and `--strings-template` arguments point to template files. Besides the general filters, the Info.plist rendering provides `xml_escape`, and the InfoPlist.strings rendering provides `plist_strings_escape`.
|
||||
|
||||
### `--plist-template`
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `default` | language | The manifest's source language entry, see below. |
|
||||
| `langs` | array of string | The BCP 47 language tag of every language, source language first. |
|
||||
|
||||
`Info.plist` writes the source strings directly through `default`:
|
||||
|
||||
```liquid
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>{{ default.strings.app_display_name | xml_escape }}</string>
|
||||
```
|
||||
|
||||
`langs` lists every language that gets an InfoPlist.strings file, useful for enumerating `CFBundleLocalizations`:
|
||||
|
||||
```xml
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
{% for lang in langs %}
|
||||
<string>{{ lang }}</string>
|
||||
{% endfor %}
|
||||
</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`
|
||||
|
||||
The members of the current language are exposed at the top level. The structure is identical for every language; only the data differs. There is no `default`.
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | The language tag of the PO file, e.g. `en_US`, `zh_CN`. |
|
||||
| `lang` | string | The BCP 47 language tag of this InfoPlist.strings file, e.g. `en`, `zh-Hans-CN`, `sr-Latn-RS`. |
|
||||
| `strings` | mapping of string to string | Maps every manifest entry key to its resolved text, e.g. `strings.app_display_name`. |
|
||||
| `sources` | mapping of string to string | Maps every manifest entry key to its manifest source text, e.g. `sources.file_type_jpeg`. |
|
||||
|
||||
The resolved 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 template is rendered once per language.
|
||||
|
||||
`InfoPlist.strings` entries use two key forms, and `sources` exists because of the second one:
|
||||
|
||||
- Top-level `Info.plist` values (`CFBundleDisplayName`, `NSHumanReadableCopyright`, the `NS*UsageDescription` keys, ...) are localized with the **Info.plist key name** as the key.
|
||||
- Nested display values inside arrays (`CFBundleTypeName` of a document type, `UTTypeDescription` of an exported UTI, `CFBundleURLName` of a URL scheme) are localized with their **source text** as the key. One entry keyed by the source text localizes every field sharing that text:
|
||||
|
||||
```liquid
|
||||
"CFBundleDisplayName" = "{{ strings.app_display_name | plist_strings_escape }}";
|
||||
"{{ sources.file_type_jpeg | plist_strings_escape }}" = "{{ strings.file_type_jpeg | plist_strings_escape }}";
|
||||
```
|
||||
|
||||
The `lang` conversion follows BCP 47 with the shared composition rules: the language and country codes carry over hyphenated, known script words (`latin`, `cyrillic`, `arabic`) become script subtags placed before the country (`sr_RS@latin` becomes `sr-Latn-RS`), and other variant words become variant subtags after it. Legacy Gettext variants that the IANA registry does not register (e.g. `euro`) are rejected.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>{{ default.strings.app_display_name | xml_escape }}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>example</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.example.Example</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Example</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<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 %}
|
||||
<string>{{ lang }}</string>
|
||||
{% endfor %}
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{ default.strings.file_type_jpeg | xml_escape }}</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.example.Example.jpeg</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{ default.strings.file_type_png | xml_escape }}</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.example.Example.png</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.example.Example.jpeg</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>{{ default.strings.file_type_jpeg | xml_escape }}</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>jpeg</string>
|
||||
<string>jpg</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.example.Example.png</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>{{ default.strings.file_type_png | xml_escape }}</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>png</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,18 @@
|
||||
version = 1
|
||||
source_language = "en"
|
||||
|
||||
[strings.app_display_name]
|
||||
msgid = "Example"
|
||||
comment = "Display name of the application, localized via the CFBundleDisplayName entry of InfoPlist.strings."
|
||||
|
||||
[strings.copyright]
|
||||
msgid = "Copyright © 2026 Example contributors."
|
||||
comment = "Copyright notice, localized via the NSHumanReadableCopyright entry of InfoPlist.strings."
|
||||
|
||||
[strings.file_type_jpeg]
|
||||
msgid = "JPEG Image"
|
||||
comment = "Name of the JPEG file type. Localized via an InfoPlist.strings entry keyed by the source text, covering both CFBundleTypeName and UTTypeDescription."
|
||||
|
||||
[strings.file_type_png]
|
||||
msgid = "PNG Image"
|
||||
comment = "Name of the PNG file type. Localized the same way as the JPEG one."
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Application display name. The key is the Info.plist key name. */
|
||||
"CFBundleDisplayName" = "{{ strings.app_display_name | plist_strings_escape }}";
|
||||
/* Copyright notice. The key is the Info.plist key name. */
|
||||
"NSHumanReadableCopyright" = "{{ strings.copyright | plist_strings_escape }}";
|
||||
/* File type names. The key is the source text, covering both CFBundleTypeName and UTTypeDescription. */
|
||||
"{{ sources.file_type_jpeg | plist_strings_escape }}" = "{{ strings.file_type_jpeg | plist_strings_escape }}";
|
||||
"{{ sources.file_type_png | plist_strings_escape }}" = "{{ strings.file_type_png | plist_strings_escape }}";
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
[CustomMessages]
|
||||
app_name={{ strings.app_name | inno_escape }}
|
||||
app_versioned_name={{ strings.app_versioned_name | inno_escape }}
|
||||
task_desktop_icon={{ strings.task_desktop_icon | inno_escape }}
|
||||
@@ -0,0 +1,30 @@
|
||||
[Setup]
|
||||
AppId=ExampleApp
|
||||
AppName={cm:app_name}
|
||||
AppVersion=1.0.0
|
||||
AppVerName={cm:app_versioned_name,1.0.0}
|
||||
DefaultDirName={autopf}\{cm:app_name}
|
||||
OutputBaseFilename=example-setup
|
||||
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 -%}
|
||||
Name: "{{ lang }}"; MessagesFile: "{{ base }},Strings\{{ lang }}.isl"
|
||||
{% endfor -%}
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:task_desktop_icon}"
|
||||
|
||||
[Files]
|
||||
Source: "example.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{autodesktop}\{cm:app_name}"; Filename: "{app}\example.exe"; Tasks: desktopicon
|
||||
@@ -0,0 +1,14 @@
|
||||
version = 1
|
||||
source_language = "en"
|
||||
|
||||
[strings.app_name]
|
||||
msgid = "Example"
|
||||
comment = "Name of the application, referenced as {cm:app_name} in the .iss script."
|
||||
|
||||
[strings.app_versioned_name]
|
||||
msgid = "Example %1"
|
||||
comment = "Versioned name of the application, referenced as {cm:app_versioned_name,<version>}. The %1 placeholder receives the version."
|
||||
|
||||
[strings.task_desktop_icon]
|
||||
msgid = "Create a desktop shortcut"
|
||||
comment = "Description of the desktop icon task, referenced as {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(
|
||||
|
||||
@@ -2,18 +2,27 @@ import enum
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from . import rc, desktop, appstream, msix
|
||||
from . import rc, desktop, appstream, msix, plist, inno
|
||||
from .rc import RcRenderOpts
|
||||
from .desktop import DesktopRenderOpts
|
||||
from .appstream import AppStreamRenderOpts
|
||||
from .msix import MsixRenderOpts
|
||||
from .plist import PlistRenderOpts
|
||||
from .inno import InnoRenderOpts
|
||||
|
||||
_SUBCMD_DEST = "metadata"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderOpts:
|
||||
opts: RcRenderOpts | DesktopRenderOpts | AppStreamRenderOpts | MsixRenderOpts
|
||||
opts: (
|
||||
RcRenderOpts
|
||||
| DesktopRenderOpts
|
||||
| AppStreamRenderOpts
|
||||
| MsixRenderOpts
|
||||
| PlistRenderOpts
|
||||
| InnoRenderOpts
|
||||
)
|
||||
"""The option of one of subcommand."""
|
||||
|
||||
|
||||
@@ -22,6 +31,8 @@ class _Metadata(enum.StrEnum):
|
||||
Desktop = "desktop"
|
||||
Appstream = "appstream"
|
||||
Msix = "msix"
|
||||
Plist = "plist"
|
||||
Inno = "inno"
|
||||
|
||||
|
||||
def register(parser: ArgumentParser) -> None:
|
||||
@@ -42,6 +53,14 @@ def register(parser: ArgumentParser) -> None:
|
||||
_Metadata.Msix.value, help="Render Windows MSIX files."
|
||||
)
|
||||
msix.register(subcmd)
|
||||
subcmd = cmds.add_parser(
|
||||
_Metadata.Plist.value, help="Render macOS bundle Info.plist files."
|
||||
)
|
||||
plist.register(subcmd)
|
||||
subcmd = cmds.add_parser(
|
||||
_Metadata.Inno.value, help="Render Inno Setup installation files."
|
||||
)
|
||||
inno.register(subcmd)
|
||||
|
||||
|
||||
def parse(args: dict[str, Any]) -> RenderOpts:
|
||||
@@ -54,6 +73,10 @@ def parse(args: dict[str, Any]) -> RenderOpts:
|
||||
opts = RenderOpts(appstream.parse(args))
|
||||
case _Metadata.Msix:
|
||||
opts = RenderOpts(msix.parse(args))
|
||||
case _Metadata.Plist:
|
||||
opts = RenderOpts(plist.parse(args))
|
||||
case _Metadata.Inno:
|
||||
opts = RenderOpts(inno.parse(args))
|
||||
case _:
|
||||
raise ValueError(f"unhandled render command: {args[_SUBCMD_DEST]}")
|
||||
return opts
|
||||
@@ -69,5 +92,9 @@ def run(opts: RenderOpts) -> None:
|
||||
appstream.run(appstream_opts)
|
||||
case MsixRenderOpts() as msix_opts:
|
||||
msix.run(msix_opts)
|
||||
case PlistRenderOpts() as plist_opts:
|
||||
plist.run(plist_opts)
|
||||
case InnoRenderOpts() as inno_opts:
|
||||
inno.run(inno_opts)
|
||||
case _:
|
||||
raise RuntimeError(f"unhandled render options: {opts.opts!r}")
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from ...filters import register_general_filters, register_inno_filters
|
||||
from ...manifest import load_manifest
|
||||
from ...pofile import (
|
||||
LanguagePack,
|
||||
build_default_pack,
|
||||
resolve_glob_files,
|
||||
resolve_translations,
|
||||
)
|
||||
from ...render import create_environment, render, render_path
|
||||
|
||||
_MANIFEST_DEST = "manifest"
|
||||
_PO_DEST = "po"
|
||||
_ISS_TEMPLATE_DEST = "iss_template"
|
||||
_ISL_TEMPLATE_DEST = "isl_template"
|
||||
_ISS_PATH_DEST = "iss_path"
|
||||
_ISL_PATH_DEST = "isl_path"
|
||||
_OUTPUT_DEST = "output"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InnoRenderOpts:
|
||||
in_manifest: Path
|
||||
"""The path to input manifest file including translation strings."""
|
||||
in_po: list[Path]
|
||||
"""The path to input PO files to read, each may be a glob pattern."""
|
||||
in_iss_template: Path
|
||||
"""The path to the input user-provided Liquid for the .iss script file."""
|
||||
in_isl_template: Path
|
||||
"""The path to the input user-provided Liquid for the .isl messages files."""
|
||||
in_iss_path: str
|
||||
"""The Liquid template resolving the output path of the .iss script file."""
|
||||
in_isl_path: str
|
||||
"""The Liquid template resolving the output path of every .isl messages file."""
|
||||
out_dir: Path
|
||||
"""The path of the output folder holding every rendered file."""
|
||||
|
||||
|
||||
def register(parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--manifest",
|
||||
dest=_MANIFEST_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path to input manifest file including translation strings.",
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--po",
|
||||
dest=_PO_DEST,
|
||||
action="extend",
|
||||
nargs="+",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The PO files to read, each may be a glob pattern.",
|
||||
metavar="GLOB",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--iss-template",
|
||||
dest=_ISS_TEMPLATE_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path to the input user-provided Liquid for the .iss script file.",
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--isl-template",
|
||||
dest=_ISL_TEMPLATE_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help=(
|
||||
"The path to the input user-provided Liquid for the .isl "
|
||||
"messages files."
|
||||
),
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--iss-path",
|
||||
dest=_ISS_PATH_DEST,
|
||||
action="store",
|
||||
type=str,
|
||||
required=True,
|
||||
help=(
|
||||
"The Liquid template resolving the output path of the .iss "
|
||||
"script file, relative to the output folder."
|
||||
),
|
||||
metavar="TEMPLATE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--isl-path",
|
||||
dest=_ISL_PATH_DEST,
|
||||
action="store",
|
||||
type=str,
|
||||
required=True,
|
||||
help=(
|
||||
"The Liquid template resolving the output path of every .isl "
|
||||
"messages file, relative to the output folder. It is rendered "
|
||||
"once per language with the language tag exposed as 'lang'."
|
||||
),
|
||||
metavar="TEMPLATE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
dest=_OUTPUT_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path of the output folder holding every rendered file.",
|
||||
metavar="DIR",
|
||||
)
|
||||
|
||||
|
||||
def parse(args: dict[str, Any]) -> InnoRenderOpts:
|
||||
return InnoRenderOpts(
|
||||
args[_MANIFEST_DEST],
|
||||
args[_PO_DEST],
|
||||
args[_ISS_TEMPLATE_DEST],
|
||||
args[_ISL_TEMPLATE_DEST],
|
||||
args[_ISS_PATH_DEST],
|
||||
args[_ISL_PATH_DEST],
|
||||
args[_OUTPUT_DEST],
|
||||
)
|
||||
|
||||
|
||||
class _InnoStringsView:
|
||||
"""Route template string key lookups to their resolved text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].text
|
||||
|
||||
|
||||
class _InnoIssPathContext:
|
||||
"""The Liquid-facing context of the .iss path template.
|
||||
|
||||
The context is empty: the .iss path is typically a plain literal
|
||||
like 'example.iss', and referencing any variable fails the render.
|
||||
"""
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ()
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _InnoIslPathContext:
|
||||
"""The Liquid-facing context of one .isl path template rendering.
|
||||
|
||||
It is rendered once per language, with the PO language tag of the
|
||||
messages file being placed exposed as ``lang``. Inno Setup has no
|
||||
standard representation of language identifiers, so no conversion
|
||||
happens: the tag is the Gettext PO tag verbatim.
|
||||
"""
|
||||
|
||||
__lang: str
|
||||
"""The PO language tag of the .isl messages file being placed."""
|
||||
|
||||
def __init__(self, lang: str):
|
||||
self.__lang = lang
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("lang",)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
if key == "lang":
|
||||
return self.__lang
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _InnoIslTemplateContext:
|
||||
"""The Liquid-facing top-level context of every .isl messages file
|
||||
rendering.
|
||||
|
||||
The members of the current language are exposed at the top level.
|
||||
The structure is identical for every language; only the data differs.
|
||||
The class also serves as the value of ``default`` inside the .iss
|
||||
template context, where it exposes the source language the same way.
|
||||
"""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The wrapped language pack."""
|
||||
__strings: _InnoStringsView
|
||||
"""The view of the pack's translations."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
self.__strings = _InnoStringsView(pack)
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the exposed template variables."""
|
||||
return ("name", "strings")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "name":
|
||||
return self.__pack.lang.value
|
||||
case "strings":
|
||||
return self.__strings
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _InnoIssTemplateContext:
|
||||
"""The Liquid-facing top-level context of the .iss script file
|
||||
rendering."""
|
||||
|
||||
__default: _InnoIslTemplateContext
|
||||
"""The view of the manifest's source language."""
|
||||
__langs: tuple[str, ...]
|
||||
"""The PO language tags of every language, source language first."""
|
||||
|
||||
def __init__(self, source: LanguagePack, langs: tuple[str, ...]):
|
||||
self.__default = _InnoIslTemplateContext(source)
|
||||
self.__langs = langs
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("default", "langs")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "default":
|
||||
return self.__default
|
||||
case "langs":
|
||||
return self.__langs
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def run(opts: InnoRenderOpts) -> None:
|
||||
manifest = load_manifest(opts.in_manifest)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
|
||||
# The source language always leads the language list and always gets
|
||||
# an .isl messages file: its entry is the fallback localization of
|
||||
# the installer.
|
||||
source = packs.pop(manifest.source_language, None)
|
||||
if source is None:
|
||||
source = build_default_pack(manifest, manifest.source_language)
|
||||
ordered = [source, *packs.values()]
|
||||
# Inno Setup has no standard language identifier representation, so
|
||||
# the PO language tags carry over verbatim.
|
||||
langs = tuple(pack.lang.value for pack in ordered)
|
||||
logging.info("rendering languages: %s", ", ".join(langs))
|
||||
|
||||
# Every component renders with its own environment, so their filters
|
||||
# never interfere with each other.
|
||||
iss_path_env = create_environment()
|
||||
register_general_filters(iss_path_env)
|
||||
isl_path_env = create_environment()
|
||||
register_general_filters(isl_path_env)
|
||||
iss_template_env = create_environment()
|
||||
register_general_filters(iss_template_env)
|
||||
register_inno_filters(iss_template_env)
|
||||
isl_template_env = create_environment()
|
||||
register_general_filters(isl_template_env)
|
||||
register_inno_filters(isl_template_env)
|
||||
|
||||
iss_out = opts.out_dir / render_path(
|
||||
iss_path_env, opts.in_iss_path, _InnoIssPathContext()
|
||||
)
|
||||
render(
|
||||
iss_template_env,
|
||||
opts.in_iss_template,
|
||||
_InnoIssTemplateContext(source, langs),
|
||||
iss_out,
|
||||
)
|
||||
|
||||
for pack, lang in zip(ordered, langs):
|
||||
isl_out = opts.out_dir / render_path(
|
||||
isl_path_env, opts.in_isl_path, _InnoIslPathContext(lang)
|
||||
)
|
||||
render(
|
||||
isl_template_env,
|
||||
opts.in_isl_template,
|
||||
_InnoIslTemplateContext(pack),
|
||||
isl_out,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from ...filters import (
|
||||
register_general_filters,
|
||||
register_plist_info_filters,
|
||||
register_plist_strings_filters,
|
||||
)
|
||||
from ...langmap import plist_lang_map
|
||||
from ...manifest import load_manifest
|
||||
from ...pofile import (
|
||||
LanguagePack,
|
||||
build_default_pack,
|
||||
resolve_glob_files,
|
||||
resolve_translations,
|
||||
)
|
||||
from ...render import create_environment, render, render_path
|
||||
|
||||
_PLIST_DEST = "plist"
|
||||
_PO_DEST = "po"
|
||||
_PLIST_TEMPLATE_DEST = "plist_template"
|
||||
_STRINGS_TEMPLATE_DEST = "strings_template"
|
||||
_PLIST_PATH_DEST = "plist_path"
|
||||
_STRINGS_PATH_DEST = "strings_path"
|
||||
_OUTPUT_DEST = "output"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlistRenderOpts:
|
||||
in_manifest: Path
|
||||
"""The path to input manifest file including translation strings."""
|
||||
in_po: list[Path]
|
||||
"""The path to input PO files to read, each may be a glob pattern."""
|
||||
in_plist_template: Path
|
||||
"""The path to the input user-provided Liquid for the Info.plist file."""
|
||||
in_strings_template: Path
|
||||
"""The path to the input user-provided Liquid for the InfoPlist.strings files."""
|
||||
in_plist_path: str
|
||||
"""The Liquid template resolving the output path of the Info.plist file."""
|
||||
in_strings_path: str
|
||||
"""The Liquid template resolving the output path of every InfoPlist.strings file."""
|
||||
out_dir: Path
|
||||
"""The path of the output folder holding every rendered file."""
|
||||
|
||||
|
||||
def register(parser: ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--manifest",
|
||||
dest=_PLIST_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path to input manifest file including translation strings.",
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--po",
|
||||
dest=_PO_DEST,
|
||||
action="extend",
|
||||
nargs="+",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The PO files to read, each may be a glob pattern.",
|
||||
metavar="GLOB",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plist-template",
|
||||
dest=_PLIST_TEMPLATE_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path to the input user-provided Liquid for the Info.plist file.",
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strings-template",
|
||||
dest=_STRINGS_TEMPLATE_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help=(
|
||||
"The path to the input user-provided Liquid for the "
|
||||
"InfoPlist.strings files."
|
||||
),
|
||||
metavar="FILE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plist-path",
|
||||
dest=_PLIST_PATH_DEST,
|
||||
action="store",
|
||||
type=str,
|
||||
required=True,
|
||||
help=(
|
||||
"The Liquid template resolving the output path of the Info.plist "
|
||||
"file, relative to the output folder."
|
||||
),
|
||||
metavar="TEMPLATE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strings-path",
|
||||
dest=_STRINGS_PATH_DEST,
|
||||
action="store",
|
||||
type=str,
|
||||
required=True,
|
||||
help=(
|
||||
"The Liquid template resolving the output path of every "
|
||||
"InfoPlist.strings file, relative to the output folder. It is "
|
||||
"rendered once per language with the language tag exposed "
|
||||
"as 'lang'."
|
||||
),
|
||||
metavar="TEMPLATE",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
dest=_OUTPUT_DEST,
|
||||
action="store",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="The path of the output folder holding every rendered file.",
|
||||
metavar="DIR",
|
||||
)
|
||||
|
||||
|
||||
def parse(args: dict[str, Any]) -> PlistRenderOpts:
|
||||
return PlistRenderOpts(
|
||||
args[_PLIST_DEST],
|
||||
args[_PO_DEST],
|
||||
args[_PLIST_TEMPLATE_DEST],
|
||||
args[_STRINGS_TEMPLATE_DEST],
|
||||
args[_PLIST_PATH_DEST],
|
||||
args[_STRINGS_PATH_DEST],
|
||||
args[_OUTPUT_DEST],
|
||||
)
|
||||
|
||||
|
||||
class _PlistStringsView:
|
||||
"""Route template string key lookups to their resolved text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].text
|
||||
|
||||
|
||||
class _PlistSourcesView:
|
||||
"""Route template string key lookups to their manifest source text."""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The language pack the lookups route to."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return self.__pack.translations[key].entry.msgid
|
||||
|
||||
|
||||
class _PlistInfoPathContext:
|
||||
"""The Liquid-facing context of the Info.plist path template.
|
||||
|
||||
The context is empty: the Info.plist path is typically the plain
|
||||
literal 'Contents/Info.plist', and referencing any variable fails
|
||||
the render.
|
||||
"""
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ()
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _PlistStringsPathContext:
|
||||
"""The Liquid-facing context of one InfoPlist.strings path template
|
||||
rendering.
|
||||
|
||||
It is rendered once per language, with the language tag of the
|
||||
InfoPlist.strings file being placed exposed as ``lang``.
|
||||
"""
|
||||
|
||||
__lang: str
|
||||
"""The BCP 47 language tag of the InfoPlist.strings file being placed."""
|
||||
|
||||
def __init__(self, lang: str):
|
||||
self.__lang = lang
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("lang",)
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
if key == "lang":
|
||||
return self.__lang
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _PlistStringsTemplateContext:
|
||||
"""The Liquid-facing top-level context of every InfoPlist.strings
|
||||
file rendering.
|
||||
|
||||
The members of the current language are exposed at the top level.
|
||||
The structure is identical for every language; only the data differs.
|
||||
The class also serves as the value of ``default`` inside the Info.plist
|
||||
template context, where it exposes the source language the same way.
|
||||
"""
|
||||
|
||||
__pack: LanguagePack
|
||||
"""The wrapped language pack."""
|
||||
__strings: _PlistStringsView
|
||||
"""The view of the pack's resolved translations."""
|
||||
__sources: _PlistSourcesView
|
||||
"""The view of the pack's manifest source texts."""
|
||||
__lang: str
|
||||
"""The BCP 47 language tag of the pack's language."""
|
||||
|
||||
def __init__(self, pack: LanguagePack):
|
||||
self.__pack = pack
|
||||
self.__strings = _PlistStringsView(pack)
|
||||
self.__sources = _PlistSourcesView(pack)
|
||||
self.__lang = plist_lang_map.convert(pack.lang).value
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the exposed template variables."""
|
||||
return ("name", "lang", "strings", "sources")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "name":
|
||||
return self.__pack.lang.value
|
||||
case "lang":
|
||||
return self.__lang
|
||||
case "strings":
|
||||
return self.__strings
|
||||
case "sources":
|
||||
return self.__sources
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _PlistInfoTemplateContext:
|
||||
"""The Liquid-facing top-level context of the Info.plist file rendering."""
|
||||
|
||||
__default: _PlistStringsTemplateContext
|
||||
"""The view of the manifest's source language."""
|
||||
__langs: tuple[str, ...]
|
||||
"""The BCP 47 language tags of every language, source language first."""
|
||||
|
||||
def __init__(self, source: LanguagePack, langs: tuple[str, ...]):
|
||||
self.__default = _PlistStringsTemplateContext(source)
|
||||
self.__langs = langs
|
||||
|
||||
def keys(self) -> tuple[str, ...]:
|
||||
"""Return the names of the top-level template variables."""
|
||||
return ("default", "langs")
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
match key:
|
||||
case "default":
|
||||
return self.__default
|
||||
case "langs":
|
||||
return self.__langs
|
||||
case _:
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def run(opts: PlistRenderOpts) -> None:
|
||||
manifest = load_manifest(opts.in_manifest)
|
||||
packs = resolve_translations(manifest, resolve_glob_files(opts.in_po))
|
||||
# The source language always leads the language list and always gets
|
||||
# an InfoPlist.strings file: its .lproj folder is the fallback
|
||||
# localization of the bundle.
|
||||
source = packs.pop(manifest.source_language, None)
|
||||
if source is None:
|
||||
source = build_default_pack(manifest, manifest.source_language)
|
||||
ordered = [source, *packs.values()]
|
||||
langs = tuple(plist_lang_map.convert(pack.lang).value for pack in ordered)
|
||||
logging.info("rendering languages: %s", ", ".join(langs))
|
||||
|
||||
# Every component renders with its own environment, so their filters
|
||||
# never interfere with each other.
|
||||
plist_path_env = create_environment()
|
||||
register_general_filters(plist_path_env)
|
||||
strings_path_env = create_environment()
|
||||
register_general_filters(strings_path_env)
|
||||
plist_template_env = create_environment()
|
||||
register_general_filters(plist_template_env)
|
||||
register_plist_info_filters(plist_template_env)
|
||||
strings_template_env = create_environment()
|
||||
register_general_filters(strings_template_env)
|
||||
register_plist_strings_filters(strings_template_env)
|
||||
|
||||
plist_out = opts.out_dir / render_path(
|
||||
plist_path_env, opts.in_plist_path, _PlistInfoPathContext()
|
||||
)
|
||||
render(
|
||||
plist_template_env,
|
||||
opts.in_plist_template,
|
||||
_PlistInfoTemplateContext(source, langs),
|
||||
plist_out,
|
||||
)
|
||||
|
||||
for pack, lang in zip(ordered, langs):
|
||||
strings_out = opts.out_dir / render_path(
|
||||
strings_path_env,
|
||||
opts.in_strings_path,
|
||||
_PlistStringsPathContext(lang),
|
||||
)
|
||||
render(
|
||||
strings_template_env,
|
||||
opts.in_strings_template,
|
||||
_PlistStringsTemplateContext(pack),
|
||||
strings_out,
|
||||
)
|
||||
@@ -130,3 +130,58 @@ def register_msix_resources_filters(env: Environment) -> None:
|
||||
"""
|
||||
env.add_filter("xml_escape", _xml_escape)
|
||||
env.add_filter("json_escape", _json_escape)
|
||||
|
||||
|
||||
def _plist_strings_escape(value: str) -> str:
|
||||
"""Escape a string for use inside an InfoPlist.strings value.
|
||||
|
||||
The escape sequences of the strings file format are produced: ``\\``
|
||||
becomes ``\\\\``, ``"`` becomes ``\\"``, linefeed, carriage return and
|
||||
tab become ``\\n``, ``\\r`` and ``\\t``. Non-ASCII characters carry
|
||||
over unchanged, as strings files are UTF-8 by definition.
|
||||
"""
|
||||
return (
|
||||
value.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
)
|
||||
|
||||
|
||||
def register_plist_info_filters(env: Environment) -> None:
|
||||
"""Register the Liquid filters for the Info.plist file rendering.
|
||||
|
||||
:param env: The environment to register the filters on.
|
||||
"""
|
||||
env.add_filter("xml_escape", _xml_escape)
|
||||
|
||||
|
||||
def register_plist_strings_filters(env: Environment) -> None:
|
||||
"""Register the Liquid filters for the InfoPlist.strings file rendering.
|
||||
|
||||
:param env: The environment to register the filters on.
|
||||
"""
|
||||
env.add_filter("plist_strings_escape", _plist_strings_escape)
|
||||
|
||||
|
||||
def _inno_escape(value: str) -> str:
|
||||
"""Escape a string for use inside an Inno Setup message value.
|
||||
|
||||
Linefeed and carriage return become ``%n``. The ``%1``..``%9``
|
||||
argument placeholders and a literal ``%`` are left untouched: the
|
||||
``%%`` escape is only defined for messages that take arguments, so
|
||||
managing ``%`` is the translator's responsibility.
|
||||
"""
|
||||
return value.replace("\n", "%n").replace("\r", "%n")
|
||||
|
||||
|
||||
def register_inno_filters(env: Environment) -> None:
|
||||
"""Register the Liquid filters for Inno Setup file rendering.
|
||||
|
||||
Both the .iss and the .isl file renderings share the same message
|
||||
value escaping, so one registration function serves both.
|
||||
|
||||
:param env: The environment to register the filters on.
|
||||
"""
|
||||
env.add_filter("inno_escape", _inno_escape)
|
||||
|
||||
@@ -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
|
||||
@@ -369,6 +369,17 @@ class MsixLang(Bcp47Lang):
|
||||
"""
|
||||
|
||||
|
||||
class PlistLang(Bcp47Lang):
|
||||
"""
|
||||
Represents a BCP 47 language tag, as required by the language folder
|
||||
names of macOS bundle localizations (e.g. ``en.lproj``,
|
||||
``zh-Hans.lproj``).
|
||||
|
||||
See :class:`Bcp47Lang` for the accepted grammar, the validation
|
||||
rules and the serialization policy.
|
||||
"""
|
||||
|
||||
|
||||
class WinLcid:
|
||||
"""
|
||||
Represents a Windows language identifier (LANGID), the 2-byte language
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from ..langid import PlistLang, PoLang
|
||||
from . import bcp47
|
||||
|
||||
|
||||
def convert(lang: PoLang) -> PlistLang:
|
||||
"""
|
||||
Convert a PO file language to a BCP 47 language tag.
|
||||
|
||||
The tag is composed and validated by :func:`metaglot.langmap.bcp47.convert`
|
||||
on behalf of :class:`PlistLang`.
|
||||
|
||||
:param lang: The PO file language.
|
||||
:return: The BCP 47 language tag for the language.
|
||||
:raises ValueError: If the composed tag is not a valid BCP 47 tag.
|
||||
"""
|
||||
return bcp47.convert(lang, PlistLang)
|
||||
@@ -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