diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0dcf319..3b5c208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,15 @@ jobs: - uses: actions/checkout@v6 - name: 'Test ICU version ${{ matrix.icu_version }}' run: 'make DOCKER_TEST_ENV=rust_icu_testenv-${{ matrix.icu_version}} docker-test' + test-examples: + runs-on: ubuntu-latest + strategy: + matrix: + icu_version: [74, 76, 77] + steps: + - uses: actions/checkout@v6 + - name: 'Build and run examples on ICU version ${{ matrix.icu_version }}' + run: 'make DOCKER_TEST_ENV=rust_icu_testenv-${{ matrix.icu_version }} RUST_ICU_MAJOR_VERSION_NUMBER=${{ matrix.icu_version }} docker-test-example' test-with-features: runs-on: ubuntu-latest strategy: diff --git a/Cargo.toml b/Cargo.toml index 421e196..5bf7ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,11 @@ members = [ "rust_icu_utext", "rust_icu_utrans", ] +# Examples are standalone crates that build against the workspace crates via +# `path` dependencies. They are excluded from the workspace so the feature +# matrix used in CI (`cargo test --no-default-features --features ...`) does +# not have to know about them. +exclude = [ + "examples/hello_world", +] resolver = "2" diff --git a/Makefile b/Makefile index a9df912..4548530 100644 --- a/Makefile +++ b/Makefile @@ -89,6 +89,36 @@ docker-test-current: ${DOCKER_REPO}/rust_icu_testenv-current:${USED_BUILDENV_VERSION} .PHONY: docker-test-current +# Builds and runs the standalone examples (examples/*) inside the dockerized +# test environment. The example crates are excluded from the workspace, so the +# regular docker-test target does not touch them; this target gives the +# presubmits a way to confirm the documented example actually compiles, runs and +# prints the expected output. It overrides the image entrypoint and drives +# cargo directly because the examples are built with default features, unlike +# the feature-matrix runs. +docker-test-example: + mkdir -p ${CARGO_TARGET_DIR} + echo top_dir: ${TOP_DIR} + echo pwd: $(shell pwd) + docker run ${TTY} \ + --user=${UID}:${GID} \ + --volume=${TOP_DIR}:/src/rust_icu \ + --volume=${CARGO_TARGET_DIR}:/build/cargo \ + --volume=${LOGNAME_HOME}/.cargo:/usr/local/cargo \ + --env="RUST_ICU_MAJOR_VERSION_NUMBER=${RUST_ICU_MAJOR_VERSION_NUMBER}" \ + --env="RUST_BACKTRACE=full" \ + --entrypoint="/bin/bash" \ + ${DOCKER_REPO}/${DOCKER_TEST_ENV}:${USED_BUILDENV_VERSION} \ + "-l" "-c" "set -eo pipefail; \ + for example in /src/rust_icu/examples/*/; do \ + ( cd \"$$example\" \ + && env LD_LIBRARY_PATH=/usr/local/lib \ + cargo test --target-dir=/build/cargo \ + && env LD_LIBRARY_PATH=/usr/local/lib \ + cargo run --target-dir=/build/cargo ); \ + done" +.PHONY: docker-test-example + # Test with the homebrew version of icu4c on macOS, running bindgen natively # using the ICU headers provided by Homebrew and the clang toolchain from Xcode. # Two passes: dynamic linking (default) and static linking. diff --git a/README.md b/README.md index 710f007..2a2161f 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,27 @@ Crate [rust_icu_utext](https://crates.io/crates/rust_icu_utext) | Text operations. Implements [`utext.h`](https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/utext_8h.html) C API header from the ICU library. [rust_icu_utrans](https://crates.io/crates/rust_icu_utrans) | Transliteration support. Implements [`utrans.h`](https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/utrans_8h.html) C API header from the ICU library. +# Hello, World! + +A complete, runnable "Hello, World!" lives in +[`examples/hello_world`](examples/hello_world). It shows locale-aware +[MessageFormat](http://userguide.icu-project.org/formatparse/messages) string +formatting (MessageFormat is *not* XLIFF; it is a pattern-based template +language built into ICU). Run it with: + +```sh +cd examples/hello_world +cargo run +# => Hello, World! +``` + +The example's [`Cargo.toml`](examples/hello_world/Cargo.toml) lists the +dependencies you need (`rust_icu_common`, `rust_icu_uloc`, `rust_icu_umsg`, +`rust_icu_ustring`), and [`src/main.rs`](examples/hello_world/src/main.rs) +contains the code. See the +[`rust_icu_umsg` crate docs](https://docs.rs/rust_icu_umsg) for more advanced +patterns (numbers, dates, plural rules, etc.). + # Limitations The generated rust language binding methods of today limit the availability of diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml new file mode 100644 index 0000000..cb0d521 --- /dev/null +++ b/examples/hello_world/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "hello_world" +version = "0.0.0" +edition = "2018" +license = "Apache-2.0" +publish = false +description = "Minimal rust_icu MessageFormat 'Hello, World!' example." + +# This example is intentionally kept out of the top-level workspace (see the +# `exclude` entry in ../../Cargo.toml) so it can be built on its own: +# +# cd examples/hello_world +# cargo run +# +# The dependencies below use `path` so the example always builds against the +# crates in this repository. A downstream user would instead depend on the +# published crates, e.g.: +# +# [dependencies] +# rust_icu_common = "5.6" +# rust_icu_uloc = "5.6" +# rust_icu_umsg = "5.6" +# rust_icu_ustring = "5.6" + +[dependencies] +rust_icu_common = { path = "../../rust_icu_common" } +rust_icu_uloc = { path = "../../rust_icu_uloc" } +rust_icu_umsg = { path = "../../rust_icu_umsg" } +rust_icu_ustring = { path = "../../rust_icu_ustring" } diff --git a/examples/hello_world/README.md b/examples/hello_world/README.md new file mode 100644 index 0000000..4b25c52 --- /dev/null +++ b/examples/hello_world/README.md @@ -0,0 +1,50 @@ +# `rust_icu` Hello, World! + +A minimal, runnable example of locale-aware +[MessageFormat](http://userguide.icu-project.org/formatparse/messages) string +formatting with `rust_icu`. `MessageFormat` is a pattern-based template +language built into ICU that lets you embed formatted numbers, dates, and +plurals directly inside a message string. It is *not* XLIFF. + +## Run it + +You need a working ICU installation, the same as for the rest of `rust_icu` +(see the [top-level README](../../README.md)). Then: + +```sh +cd examples/hello_world +cargo run +``` + +Expected output: + +```text +Hello, World! +``` + +## Verified by presubmits + +This example is not just documentation. The project's CI builds and runs it on +every push and pull request, across the same ICU versions as the rest of the +test matrix. The `make docker-test-example` target builds the crate inside the +dockerized test environment, runs `cargo test` (which asserts that the formatted +output equals `Hello, World!`), and then runs the binary with `cargo run`. If +the example ever stops compiling or stops producing the expected output, CI +fails. + +You can reproduce the presubmit locally with: + +```sh +make DOCKER_TEST_ENV=rust_icu_testenv-77 RUST_ICU_MAJOR_VERSION_NUMBER=77 docker-test-example +``` + +## What it shows + +- [`Cargo.toml`](Cargo.toml) — the dependencies required to format a message: + `rust_icu_common`, `rust_icu_uloc`, `rust_icu_umsg`, and `rust_icu_ustring`. +- [`src/main.rs`](src/main.rs) — building a `UMessageFormat` from a pattern and + a locale, then formatting a positional argument with the + [`message_format!`](https://docs.rs/rust_icu_umsg) macro. + +See the [`rust_icu_umsg` crate docs](https://docs.rs/rust_icu_umsg) for more +advanced patterns (numbers, dates, plural rules, etc.). diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs new file mode 100644 index 0000000..ae143bc --- /dev/null +++ b/examples/hello_world/src/main.rs @@ -0,0 +1,63 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A minimal "Hello, World!" example for `rust_icu`. +//! +//! It uses [`rust_icu_umsg`] (ICU `MessageFormat`) to format a locale-aware +//! message. `MessageFormat` is a pattern-based template language built into +//! ICU; it is *not* XLIFF. Run it with: +//! +//! ```text +//! cargo run +//! ``` +//! +//! The formatting logic lives in [`greet`] so it can be exercised by a unit +//! test (see the bottom of this file). That test is what lets the project's +//! presubmits confirm the example actually produces the expected output. + +use rust_icu_common as common; +use rust_icu_uloc as uloc; +use rust_icu_umsg::{self as umsg, message_format}; +use rust_icu_ustring as ustring; +use std::convert::TryFrom; + +/// Formats a locale-aware "Hello, World!" using ICU `MessageFormat`. +fn greet() -> Result { + // Choose a locale. Formatting of numbers, dates and plurals is driven by + // the locale's ICU data. + let loc = uloc::ULoc::try_from("en-US")?; + + // A MessageFormat pattern. `{0}` is the first positional argument. + let pattern = ustring::UChar::try_from("Hello, {0}!")?; + let fmt = umsg::UMessageFormat::try_from(&pattern, &loc)?; + + // Bind a value to each positional parameter and format the message. + let name = ustring::UChar::try_from("World")?; + message_format!(fmt, { name => String }) +} + +fn main() -> Result<(), common::Error> { + println!("{}", greet()?); // Hello, World! + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::greet; + + #[test] + fn formats_hello_world() { + assert_eq!(greet().expect("formatting should succeed"), "Hello, World!"); + } +}