Bazel rules for FormatJS - Internationalize your web apps with Bazel build integration.
Features#
- Extract messages from source files (TypeScript, JavaScript, JSX, TSX)
- Compile messages for optimized runtime performance
- Verify translations for completeness
- Aggregate messages across multiple modules
- Native Rust CLI toolchain for fast builds
- Type-safe message extraction and compilation
- Custom Bazel aspects for advanced analysis
Installation#
Add rules_formatjs to your MODULE.bazel:
bazel_dep(name = "rules_formatjs", version = "0.1.0")
Or use a specific commit from GitHub:
git_override(
module_name = "rules_formatjs",
remote = "https://github.com/formatjs/rules_formatjs.git",
commit = "<commit-sha>",
)
Usage#
Extract Messages#
Extract internationalized messages from your source code:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_extract")
formatjs_extract(
name = "messages_extracted",
srcs = glob(["src/**/*.tsx", "src/**/*.ts"]),
out = "en.json",
id_interpolation_pattern = "[sha512:contenthash:base64:6]",
)
Compile Messages#
Compile extracted messages for optimized runtime:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_library")
formatjs_library(
name = "messages_compiled",
src = ":messages_extracted",
out = "en-compiled.json",
ast = True, # Compile to AST for better performance
)
Verify Translations#
Verify that translations are complete and correctly formatted:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_verify")
formatjs_verify(
name = "verify_translations",
reference = ":messages_extracted", # Base language
translations = [
"translations/es.json",
"translations/fr.json",
],
)
Aggregate Messages#
Aggregate messages from multiple modules into a single file:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_extract", "formatjs_aggregate")
# Extract from multiple modules
formatjs_extract(name = "module1_msgs", srcs = ["module1/**/*.tsx"])
formatjs_extract(name = "module2_msgs", srcs = ["module2/**/*.tsx"])
formatjs_extract(name = "module3_msgs", srcs = ["module3/**/*.tsx"])
# Create aggregation target
formatjs_aggregate(
name = "all_messages",
deps = [":module1_msgs", ":module2_msgs", ":module3_msgs"],
)
Then build with the aspect:
bazel build //:all_messages \
--aspects=@rules_formatjs//formatjs:aggregate.bzl%formatjs_aggregate_aspect \
--output_groups=aggregated_messages
Complete Example#
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_extract", "formatjs_library", "formatjs_verify")
# Extract messages from source files
formatjs_extract(
name = "extract_en",
srcs = glob([
"src/**/*.ts",
"src/**/*.tsx",
]),
out = "lang/en.json",
id_interpolation_pattern = "[sha512:contenthash:base64:6]",
extract_from_format_message_call = True,
)
# Compile for production
formatjs_library(
name = "compile_en",
src = ":extract_en",
out = "lang/en-compiled.json",
ast = True,
)
# Compile translations
formatjs_library(
name = "compile_es",
src = "translations/es.json",
out = "lang/es-compiled.json",
ast = True,
)
# Verify translations are complete
formatjs_verify(
name = "verify_es",
reference = ":extract_en",
translations = ["translations/es.json"],
)
API Reference#
formatjs_extract#
Extract messages from source files. This is a custom Bazel rule (not a macro), which means you can attach aspects to it for advanced build graph analysis.
Attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Target name |
srcs | label_list | Yes | List of source files to extract from |
out | string | No | Output JSON file (defaults to name + ".json") |
id_interpolation_pattern | string | No | Pattern for message ID generation (e.g., "[sha512:contenthash:base64:6]") |
extract_from_format_message_call | bool | No | Extract from formatMessage() calls |
additional_component_names | string_list | No | Additional component names to extract from (see Wrapper Components Guide) |
additional_function_names | string_list | No | Additional function names to extract from (see Wrapper Components Guide) |
ignore | string_list | No | List of glob patterns to ignore |
Providers:
DefaultInfo: Contains the extracted messages JSON fileFormatjsExtractInfo: Custom provider with:messages: File containing extracted messagessrcs: Depset of source filesid_interpolation_pattern: Pattern used for ID generation
Example with wrapper components:
formatjs_extract(
name = "extract_with_custom_components",
srcs = glob(["src/**/*.tsx"]),
out = "messages.json",
id_interpolation_pattern = "[sha512:contenthash:base64:6]",
additional_component_names = ["CustomButton", "TranslatableText"],
additional_function_names = ["t", "$formatMessage"],
)
formatjs_library#
Compile extracted messages for optimized runtime.
Attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Target name |
src | label | Yes | Source JSON file with extracted messages |
out | string | No | Output compiled JSON file (defaults to name + ".json") |
ast | bool | No | Compile to AST format for better runtime performance (recommended) |
format | string | No | Input format: simple, crowdin, smartling, or transifex |
Example:
formatjs_library(
name = "compile_messages",
src = ":extract_en",
out = "en-compiled.json",
ast = True,
format = "simple",
)
formatjs_verify#
Verify that translations are complete and correctly formatted.
Attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Target name |
reference | label | Yes | Reference messages file (typically the base language) |
translations | label_list | Yes | List of translation files to verify |
Example:
formatjs_verify(
name = "verify_translations",
reference = ":extract_en",
translations = [
"translations/es.json",
"translations/fr.json",
"translations/de.json",
],
)
formatjs_aggregate#
Create an aggregation target for collecting messages from multiple modules.
Attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Target name |
deps | label_list | Yes | List of formatjs_extract targets to aggregate |
Example:
formatjs_aggregate(
name = "all_messages",
deps = [
":module1_msgs",
":module2_msgs",
":module3_msgs",
],
)
Build with the aggregation aspect:
bazel build //:all_messages \
--aspects=@rules_formatjs//formatjs:aggregate.bzl%formatjs_aggregate_aspect \
--output_groups=aggregated_messages
Output: bazel-bin/all_messages_aggregated_messages.json containing all merged messages.
Merge Strategy: Uses jq to merge JSON objects. Later files overwrite earlier ones for duplicate keys.
Integration with React#
Use compiled messages with react-intl:
import {createIntl, createIntlCache, RawIntlProvider} from 'react-intl'
import messages from './lang/en-compiled.json'
const cache = createIntlCache()
const intl = createIntl(
{
locale: 'en',
messages,
},
cache
)
function App() {
return <RawIntlProvider value={intl}>{/* Your app */}</RawIntlProvider>
}
Advanced: Using Aspects#
Since formatjs_extract is a custom rule, you can attach Bazel aspects to it for advanced analysis and transformations. This is useful for:
- Collecting statistics about extracted messages
- Validating message format and completeness
- Aggregating messages across multiple targets
- Custom reporting and analysis
Message Aggregation Aspect#
The formatjs_aggregate_aspect collects and merges extracted messages from a target and all its dependencies into a single JSON file using jq.
Example:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_extract", "formatjs_aggregate")
# Extract from multiple modules
formatjs_extract(name = "module1_msgs", srcs = ["module1/**/*.tsx"])
formatjs_extract(name = "module2_msgs", srcs = ["module2/**/*.tsx"])
formatjs_extract(name = "module3_msgs", srcs = ["module3/**/*.tsx"])
# Create aggregation target
formatjs_aggregate(
name = "all_messages",
deps = [":module1_msgs", ":module2_msgs", ":module3_msgs"],
)
Then build with the aspect:
bazel build //:all_messages \
--aspects=@rules_formatjs//formatjs:aggregate.bzl%formatjs_aggregate_aspect \
--output_groups=aggregated_messages
Built-in Example Aspects#
The library includes demonstration aspects in formatjs/aspects.bzl:
Message Statistics#
Collect statistics about extracted messages:
bazel build //path/to:target \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_stats_aspect \
--output_groups=message_stats
Message Validation#
Validate message format and structure:
bazel build //path/to:target \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_validator_aspect \
--output_groups=message_validation
Message Collection#
Collect messages across all dependencies:
bazel build //path/to:target \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_collector_aspect \
--output_groups=all_messages
Creating Custom Aspects#
You can create custom aspects to analyze and transform extracted messages:
load("@rules_formatjs//formatjs:defs.bzl", "FormatjsExtractInfo")
def _my_aspect_impl(target, ctx):
if FormatjsExtractInfo not in target:
return []
info = target[FormatjsExtractInfo]
# Access extracted message file
messages_file = info.messages
# Access source files
src_files = info.srcs.to_list()
# Perform custom analysis...
# For example:
# - Count messages by type
# - Validate message IDs
# - Check for missing translations
# - Generate reports
return [OutputGroupInfo(...)]
my_aspect = aspect(
implementation = _my_aspect_impl,
doc = "My custom aspect for formatjs_extract",
)
Toolchain#
rules_formatjs uses a native Rust CLI toolchain that is automatically downloaded for your platform. The toolchain supports:
- macOS (Apple Silicon and Intel)
- Linux (x86_64)
The toolchain is registered automatically when you add the MODULE.bazel dependency. Binaries are fetched from GitHub releases and cached by Bazel.
Performance Benefits#
The native Rust CLI is significantly faster than the Node.js-based tools:
- 20.90x faster message extraction in the checked-in Rust vs TypeScript CLI benchmark
- No Node.js runtime required for supported extraction, compilation, and verification workflows
- Minimal memory footprint
- Instant startup - no JavaScript runtime initialization
- CI/CD friendly - fast builds in continuous integration
The native CLI parallelizes per-file work with Rayon. By default, Rayon uses the
available logical CPU count. Set RAYON_NUM_THREADS to cap worker threads when
running under constrained CI executors.
The Rust CLI used by these rules covers the native toolchain's supported
FormatJS workflows. Node.js-only behavior from @formatjs/cli, such as
framework template extraction for Vue, Svelte, Handlebars, Glimmer, GTS, and
GJS, is not available in the standalone native binary. Custom JavaScript
formatter files for compilation are deprecated in the Node.js CLI and are not
supported by the standalone native binary.
Common Workflows#
Development Workflow#
- Extract messages from source files
- Compile for development (with AST for better performance)
- Verify translations are complete
# BUILD.bazel
formatjs_extract(
name = "extract_dev",
srcs = glob(["src/**/*.tsx"]),
out = "messages.json",
id_interpolation_pattern = "[sha512:contenthash:base64:6]",
)
formatjs_library(
name = "compile_dev",
src = ":extract_dev",
ast = True,
)
formatjs_verify(
name = "verify_dev",
reference = ":extract_dev",
translations = glob(["translations/*.json"]),
)
Production Workflow#
- Extract from all modules
- Aggregate across the entire application
- Compile with AST optimization
- Verify all translations
# Multiple module extraction
formatjs_extract(name = "auth_msgs", srcs = glob(["auth/**/*.tsx"]))
formatjs_extract(name = "dashboard_msgs", srcs = glob(["dashboard/**/*.tsx"]))
formatjs_extract(name = "settings_msgs", srcs = glob(["settings/**/*.tsx"]))
# Aggregate all messages
formatjs_aggregate(
name = "all_app_messages",
deps = [":auth_msgs", ":dashboard_msgs", ":settings_msgs"],
)
# Compile with AST
formatjs_library(
name = "compile_prod",
src = ":all_app_messages",
out = "app-messages.json",
ast = True,
)
# Verify all translations
formatjs_verify(
name = "verify_all",
reference = ":all_app_messages",
translations = glob(["translations/*.json"]),
)
Build the aggregated messages:
bazel build //:all_app_messages \
--aspects=@rules_formatjs//formatjs:aggregate.bzl%formatjs_aggregate_aspect \
--output_groups=aggregated_messages
CI/CD Integration#
Add verification to your CI pipeline:
# Extract and verify in one command
bazel test //...
# Or run specific verification targets
bazel test //:verify_all //:verify_es //:verify_fr
Examples#
Complete working examples are available in the rules_formatjs repository:
- examples/simple - Basic message extraction, compilation, and verification
- examples/aggregate - Aggregating messages from multiple modules
Comparison with Other Tools#
| Feature | Bazel Rules | CLI | Babel Plugin | TS Transformer |
|---|---|---|---|---|
| Build System Integration | ✅ Native | ❌ External | ❌ External | ❌ External |
| Incremental Builds | ✅ Yes | ❌ No | ⚠️ Partial | ⚠️ Partial |
| Caching | ✅ Yes | ❌ No | ⚠️ Limited | ⚠️ Limited |
| Message Aggregation | ✅ Native | ⚠️ Manual | ❌ No | ❌ No |
| Translation Verification | ✅ Native | ✅ Yes | ❌ No | ❌ No |
| AST Compilation | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Performance | ✅ Rust | ✅ Rust | ⚠️ Node.js | ⚠️ Node.js |
| Multi-module Support | ✅ Aspects | ⚠️ Manual | ❌ No | ❌ No |
| Custom Analysis | ✅ Aspects | ❌ No | ⚠️ Limited | ⚠️ Limited |
Troubleshooting#
Messages not being extracted#
- Verify your
srcsglob pattern matches your files - Check that you're using standard prop names (see Wrapper Components Guide)
- Ensure
extract_from_format_message_callis enabled if usingformatMessage() - Add custom components/functions using
additional_component_namesandadditional_function_names
Build errors#
- Check that
rules_formatjsis properly installed in MODULE.bazel - Verify the toolchain is registered (should happen automatically)
- Check Bazel version compatibility (requires Bazel 6.0+)
Aggregation not working#
- Ensure you're using the correct aspect:
@rules_formatjs//formatjs:aggregate.bzl%formatjs_aggregate_aspect - Verify all deps are
formatjs_extracttargets - Check that
jqis available on your system - Use
--output_groups=aggregated_messagesflag
Slow builds#
- Enable remote caching in your
.bazelrc - Use
formatjs_aggregatewith aspects instead of manual merging - Ensure the Rust toolchain is being used (check Bazel logs)
- Consider splitting large source sets into multiple extract targets
Resources#
- FormatJS Documentation
- FormatJS CLI
- React Intl
- Wrapper Components Guide
- Bazel
- rules_formatjs GitHub Repository
See Also#
- CLI Documentation - Command-line tools
- Babel Plugin Documentation - Babel integration
- TypeScript Transformer Documentation - TypeScript integration
- Message Extraction Guide - General extraction concepts