Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ permissions:
contents: write
id-token: write

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
group: publication-${{ github.workflow }}-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: true
Expand Down Expand Up @@ -62,7 +65,7 @@ jobs:
echo "Working tree is clean"

- name: Set up uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
Expand Down Expand Up @@ -242,7 +245,7 @@ jobs:

- name: Store PyPI distributions
if: matrix.python-version == '3.14'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-dist
path: publish-dist/
Expand All @@ -251,7 +254,7 @@ jobs:

- name: Store GitHub release assets
if: matrix.python-version == '3.14'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-assets
path: release-assets-dist/
Expand All @@ -264,7 +267,7 @@ jobs:
timeout-minutes: 30
if: github.event_name == 'workflow_dispatch' && inputs.publish_to_testpypi
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pypi-dist
path: dist/
Expand Down Expand Up @@ -295,7 +298,7 @@ jobs:
ref: ${{ inputs.release_tag }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -383,7 +386,7 @@ jobs:
- name: Make scripts executable
run: chmod +x ./scripts/*.sh

- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets
path: dist/
Expand Down Expand Up @@ -445,7 +448,7 @@ jobs:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -547,7 +550,7 @@ jobs:
# available on GitHub Actions runners by default.
# GitHub Actions runners ship with Python 3.12 as their system Python as of 2025.
- name: Set up Python 3.12
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
group: test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -34,7 +37,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
Expand Down
86 changes: 83 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
afad: "3.5"
version: "0.163.0"
version: "0.164.0"
domain: CHANGELOG
updated: "2026-04-22"
updated: "2026-04-23"
route:
keywords: [changelog, release notes, version history, breaking changes, migration, fixed, what's new]
questions: ["what changed in version X?", "what are the breaking changes?", "what was fixed in the latest release?", "what is the release history?"]
Expand All @@ -15,13 +15,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.164.0] - 2026-04-23
### Changed

- **Release publication now stages PyPI uploads separately from GitHub Release assets.** The
publish workflow now uploads only the wheel and source distribution to PyPI-facing jobs while
keeping the `.sha256` receipt in the GitHub Release asset path, and the maintainer runbook now
documents rerunning `workflow_dispatch` against an existing tag to recover a partial release
without moving or recreating the tag.
- **GitHub Actions now opt into Node 24 and pin the Node 24-capable immutable action releases.**
The `Test` and `Build and Publish` workflows now set
`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true`, move `astral-sh/setup-uv` to its
official `v8.1.0` commit pin, and update `actions/setup-python`,
`actions/upload-artifact`, and `actions/download-artifact` to their current
Node 24-native immutable releases.
- **Parser-only facades now expose the zero-dependency helpers that actually work without Babel.**
`ftllexengine`, `ftllexengine.runtime`, and `ftllexengine.localization` now keep
`CacheConfig`, `FluentNumber`, `fluent_function`, `make_fluent_number`,
`PathResourceLoader`, `FallbackInfo`, `ResourceLoadResult`, `LoadSummary`, and
`CacheAuditLogEntry` importable in parser-only installs while gating only the
Babel-backed bundle/orchestration facades behind the runtime extra.
- **Public locale-aware entry points now reject unknown locales instead of silently formatting as `en_US`.**
`FluentBundle`, `FluentLocalization`, `number_format()`, `datetime_format()`, and
`currency_format()` now validate locale identifiers against Babel/CLDR up front so
a misconfigured locale cannot claim one public value while rendering with another.
- **`clear_module_caches()` now validates selector names and no longer masks internal import failures as missing Babel.**
Unknown cache component names raise `ValueError`, parser-only skips are driven by
Babel availability instead of broad `ImportError` swallowing, and broken runtime
imports now surface their real exception path.
- **Parser-only optional facades now behave like truly absent names during feature probing.**
Babel-backed names such as `ftllexengine.FluentBundle`,
`ftllexengine.localization.FluentLocalization`, and parser-only-hidden
`ftllexengine.runtime.number_format()` now return `False` from `hasattr()` and
honor `getattr(..., default)` instead of raising during routine capability
checks, while direct access still carries explicit runtime-install guidance.
- **The Babel availability gate no longer rewrites broken Babel imports into misleading “install Babel” errors.**
`core.babel_compat._check_babel_available()` now treats only a genuinely missing
top-level `babel` package as parser-only mode; internal `ImportError` failures
from a broken Babel install now surface unchanged for diagnosis instead of being
collapsed into `BabelImportError`.
- **Parser-only `ftllexengine.runtime` no longer advertises locale-formatting helpers that cannot run without Babel.**
`create_default_registry()`, `get_shared_registry()`, `number_format()`,
`datetime_format()`, `currency_format()`, and `select_plural_category()` are
now gated with the same full-runtime boundary as the bundle classes instead of
remaining importable-but-immediately-failing in parser-only installs.
- **Documentation now uses consistent parser-only vs full-runtime terminology and fixes Babel requirement scope.**
README and guide pages now distinguish the two install modes explicitly,
document that unknown locales fail fast at public formatting boundaries, and
stop implying that all ISO helpers require Babel when
`get_currency_decimal_digits()` works from embedded tables.
- **Core and runtime reference docs now match the current localization and cache contracts.**
The reference set now describes `FluentLocalization` as eager-load plus
demand-driven bundle materialization, documents the exact
`clear_module_caches()` selector names, and correctly describes
`CacheAuditLogEntry` as the public alias of `WriteLogEntry` rather than a
separate dataclass.
- **Quick-reference and example index docs now steer users toward the actual default behavior.**
The quick reference labels full-runtime vs parser-only install commands,
removes an unnecessary `strict=False` from the multi-locale example, and the
examples index now calls out the parser-only helper facades shown by
`examples/parser_only.py`.
- **Locale, contributing, release, and fuzzing docs now match the shipped workflows more precisely.**
The locale guide now documents `get_system_locale()` as a fallback-returning
helper instead of implying a default-path `ValueError`, contributing docs no
longer claim the examples rely on local stubs, the release runbook now keeps
pre-flight worktrees detached until the gates pass, explicitly distinguishes
bootstrap-path staged diffs from the full PR diff against `origin/main`, and
the Atheris docs now state clearly that `--list` inspects stored crashes and
findings instead of enumerating target names.
- **Shipped examples now assert their own contracts, and the example runner enforces those contracts.**
`examples/parser_only.py` now demonstrates the real top-level `validate_resource()`
surface plus warning-only versus invalid validation semantics, `examples/ftl_linter.py`
now detects attribute-only no-value messages and undefined term references as advertised,
and `scripts/run_examples.py` now rejects examples that exit `0` but miss their registered
output-contract markers.
- **Diagnostics docs now describe the real structural parser-annotation contract.**
`ParserAnnotation` is now a documented public diagnostics export, and the
diagnostics reference plus regression tests now bind `ValidationResult.annotations`
to `tuple[ParserAnnotation, ...]` instead of implying a single concrete `Annotation`
implementation.
- **Additional god-files are now split, and architecture budgets reach beyond `src/`.**
`introspection.iso` now delegates lookup, validation, and cache clearing into
focused modules instead of carrying all ISO logic in one file, the
`runtime.bundle` property test monolith is now partitioned into focused test
modules, and `tests/test_architecture_contract.py` now enforces explicit line
budgets for the previously ungoverned test, fuzz, and script hotspots as well
as the remaining large internal utility modules.

## [0.163.0] - 2026-04-22

Expand Down Expand Up @@ -6897,6 +6976,7 @@ Both validators are re-exported from `ftllexengine.introspection` and the root
[0.29.0]: https://github.com/resoltico/ftllexengine/releases/tag/v0.29.0
[0.28.1]: https://github.com/resoltico/ftllexengine/releases/tag/v0.28.1
[0.28.0]: https://github.com/resoltico/ftllexengine/releases/tag/v0.28.0
[Unreleased]: https://github.com/resoltico/FTLLexEngine/compare/v0.163.0...HEAD
[Unreleased]: https://github.com/resoltico/FTLLexEngine/compare/v0.164.0...HEAD
[0.164.0]: https://github.com/resoltico/FTLLexEngine/compare/v0.163.0...v0.164.0
[0.163.0]: https://github.com/resoltico/FTLLexEngine/compare/v0.162.0...v0.163.0
[0.162.0]: https://github.com/resoltico/FTLLexEngine/compare/v0.161.0...v0.162.0
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
afad: "3.5"
version: "0.163.0"
version: "0.164.0"
domain: CONTRIBUTING
updated: "2026-04-22"
updated: "2026-04-23"
route:
keywords: [contributing, development, uv, lint, test, fuzz, benchmark, release, virtualenv]
questions: ["how do I set up development?", "how do I run lint and tests?", "how do I work on fuzzing?", "how do I prepare a release?"]
Expand Down Expand Up @@ -56,7 +56,8 @@ Useful variants:
- `./scripts/benchmark.sh`
- `./scripts/fuzz_hypofuzz.sh`
- `./scripts/fuzz_hypofuzz.sh --deep --time 300`
- `./scripts/fuzz_atheris.sh --list`
- `./scripts/fuzz_atheris.sh numbers --time 60`
- `./scripts/fuzz_atheris.sh --list` to inspect stored crashes and finding artifacts

## Documentation Work

Expand All @@ -77,7 +78,7 @@ Expectations:

## Type Checking Examples

The `examples/` directory has its own `mypy.ini` and local stubs.
The `examples/` directory has its own `mypy.ini` and does not rely on local stub overlays.

```bash
uv run mypy --config-file examples/mypy.ini examples
Expand Down
2 changes: 1 addition & 1 deletion PATENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
afad: "3.5"
version: "0.163.0"
version: "0.164.0"
domain: LEGAL
updated: "2026-04-22"
route:
Expand Down
51 changes: 37 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ assert errors == ()
assert result == "500 bags of Ethiopian coffee"
```

Unknown locales raise `ValueError` on `FluentBundle`,
`FluentLocalization`, `number_format()`, `datetime_format()`, and
`currency_format()` rather than silently formatting with a fallback locale.

> `use_isolating=False` removes Unicode bidi isolation markers from output, making strings suitable for direct comparison and logging. The default `use_isolating=True` wraps each placeable in U+2068/U+2069 markers for correct bidirectional text rendering in UI contexts.

**Parse user input back to Python types:**
Expand Down Expand Up @@ -110,6 +114,9 @@ Or with pip:
pip install ftllexengine[babel]
```

This is the **full runtime** install: locale-aware formatting, localization orchestration,
bidirectional parsing, and Babel-backed ISO helpers.

**Requirements**: Python >= 3.13 | Babel >= 2.18

<details>
Expand All @@ -121,16 +128,32 @@ uv add ftllexengine

Or: `pip install ftllexengine`

**Works without Babel:**
**Available in parser-only installs:**
- FTL syntax parsing (`parse_ftl()`, `serialize_ftl()`)
- AST manipulation and transformation
- Validation and message introspection
- Zero-dependency runtime helpers such as `CacheConfig`, `FluentNumber`,
`FunctionRegistry`, `fluent_function`, and `make_fluent_number`
- Zero-dependency localization loading types such as `PathResourceLoader`,
`FallbackInfo`, `ResourceLoadResult`, and `LoadSummary`
- Embedded ISO 4217 decimal precision lookup via `get_currency_decimal_digits()`

**Requires Babel:**
**Requires the full runtime install:**
- `FluentBundle` (locale-aware formatting)
- `AsyncFluentBundle`
- `FluentLocalization` (multi-locale fallback)
- `LocalizationBootConfig`
- Runtime formatter and registry helpers such as `number_format()`,
`datetime_format()`, `currency_format()`, `select_plural_category()`,
`create_default_registry()`, and `get_shared_registry()`
- Bidirectional parsing (numbers, dates, currency)
- ISO territory and currency lookups
- Localized ISO territory/currency metadata lookups and ISO code validation helpers

Public formatting and localization entry points reject unknown locales
instead of silently falling back to `en_US`.
Parser-only facade probes such as `hasattr(ftllexengine.runtime, "number_format")`
and `getattr(ftllexengine, "FluentBundle", None)` treat Babel-backed names
as absent instead of raising during feature detection.

</details>

Expand Down Expand Up @@ -636,17 +659,17 @@ Alice's invoices format correctly: JPY 28,125,000 in Tokyo, $187,500.00 in New Y

## Architecture at a Glance

| Component | What It Does | Requires Babel? |
|:----------|:-------------|:----------------|
| **Syntax** — `ftllexengine.syntax` | FTL parser, AST, serializer, visitor pattern | No |
| **Runtime** — `ftllexengine.runtime` | `FluentBundle`, message resolution, thread-safe formatting, built-in functions (`NUMBER`, `CURRENCY`, `DATETIME`) | Yes |
| **Localization** — `ftllexengine.localization` | `FluentLocalization` multi-locale fallback chains; `LocalizationBootConfig` strict-mode production boot | Yes |
| **Parsing** — `ftllexengine.parsing` | Bidirectional parsing: numbers, dates, currency back to Python types | Yes |
| **Introspection** — `ftllexengine.introspection` | Message-variable/function extraction, ISO 3166/4217 territory and currency data | Partial |
| **Analysis** — `ftllexengine.analysis` | Dependency-graph helpers such as `detect_cycles()` | No |
| **Validation** — `ftllexengine.validation` | Resource validation, unresolved-reference checks, semantic checks | No |
| **Diagnostics** — `ftllexengine.diagnostics` | Structured error types, error codes, formatting | No |
| **Integrity** — `ftllexengine.integrity` | BLAKE2b checksums, strict mode, immutable exceptions | No |
| Component | What It Does | Install Mode |
|:----------|:-------------|:-------------|
| **Syntax** — `ftllexengine.syntax` | FTL parser, AST, serializer, visitor pattern | Parser-only install |
| **Runtime** — `ftllexengine.runtime` | `FluentBundle`, message resolution, thread-safe formatting, built-in functions (`NUMBER`, `CURRENCY`, `DATETIME`), plus zero-dependency helper types | Mixed: parser-only helpers + full-runtime formatters |
| **Localization** — `ftllexengine.localization` | `FluentLocalization` multi-locale fallback chains; `LocalizationBootConfig` strict-mode production boot; zero-dependency loading types | Mixed: parser-only loading types + full-runtime orchestration |
| **Parsing** — `ftllexengine.parsing` | Bidirectional parsing: numbers, dates, currency back to Python types | Full runtime install |
| **Introspection** — `ftllexengine.introspection` | Message-variable/function extraction, AST reference analysis, and ISO helpers; localized territory/currency metadata needs the full runtime while `get_currency_decimal_digits()` uses embedded tables | Mixed: parser-only helpers + full-runtime localized metadata |
| **Analysis** — `ftllexengine.analysis` | Dependency-graph helpers such as `detect_cycles()` | Parser-only install |
| **Validation** — `ftllexengine.validation` | Resource validation, unresolved-reference checks, semantic checks | Parser-only install |
| **Diagnostics** — `ftllexengine.diagnostics` | Structured error types, error codes, formatting | Parser-only install |
| **Integrity** — `ftllexengine.integrity` | BLAKE2b checksums, strict mode, immutable exceptions | Parser-only install |

---

Expand Down
7 changes: 4 additions & 3 deletions docs/CUSTOM_FUNCTIONS_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
afad: "3.5"
version: "0.163.0"
version: "0.164.0"
domain: CUSTOM_FUNCTIONS
updated: "2026-04-22"
updated: "2026-04-23"
route:
keywords: [custom functions, fluent_function, FunctionRegistry, locale injection, add_function]
questions: ["how do I add a custom function?", "how does locale injection work?", "should I use a registry or add_function?"]
Expand All @@ -11,14 +11,15 @@ route:
# Custom Functions Guide

**Purpose**: Add domain-specific functions to `FluentBundle` or `FluentLocalization`.
**Prerequisites**: Familiarity with `FluentBundle.format_pattern()` and FTL function calls.
**Prerequisites**: Full runtime install (`ftllexengine[babel]`) plus familiarity with `FluentBundle.format_pattern()` and FTL function calls.

## Overview

FTLLexEngine supports two patterns:

- `bundle.add_function("NAME", func)` for one bundle or one localization object.
- `FunctionRegistry` for reusable or shared function sets.
- This guide assumes the full runtime because the examples attach functions to `FluentBundle` and use `create_default_registry()`.

FTL uses uppercase function names by convention. Python callables can keep normal snake_case parameter names; the bridge maps FTL camelCase named arguments onto Python snake_case parameters automatically.

Expand Down
Loading