This example demonstrates how to use the formatjs_aggregate_aspect to collect and merge extracted messages from multiple modules into a single JSON file.
This is a standalone Bazel workspace with the following structure:
aggregate/
├── MODULE.bazel # Bazel workspace configuration
├── BUILD.bazel # Root build file
├── module1/
│ ├── BUILD.bazel # Module 1 build configuration
│ └── Messages.tsx # Module 1 messages
├── module2/
│ ├── BUILD.bazel # Module 2 build configuration
│ └── Messages.tsx # Module 2 messages
├── module3/
│ ├── BUILD.bazel # Module 3 build configuration (depends on module1)
│ └── Messages.tsx # Module 3 messages (includes some common messages)
└── app/
├── BUILD.bazel # Application build configuration
├── App.tsx # Application that uses all modules
└── expected.json # Expected aggregated messages for testing
- Transitive Dependency Collection: Module 3 depends on Module 1, and the app only depends on Module 2 and Module 3. The aspect automatically traverses the dependency graph to collect messages from Module 1 transitively, demonstrating proper dependency graph traversal.
- Application-Level Aggregation: The
formatjs_aggregatetarget is at the app level, collecting messages from direct and transitive dependencies - Automated Testing: Includes a snapshot test that verifies the aggregated output contains all expected messages, including those from Module 1 (which is only a transitive dependency)
Extract messages from each module separately:
cd examples/aggregate
bazel build //module1:messages
bazel build //module2:messages
bazel build //module3:messagesThe formatjs_aggregate rule automatically applies the aspect to collect and merge all messages:
bazel build //app:all_messagesThis will create a single JSON file containing all messages from all three modules. The rule automatically:
- Applies the
formatjs_aggregate_aspectto traverse dependencies - Collects messages from
module2andmodule3(direct deps) - Transitively collects messages from
module1(via module3's dependency) - Merges all messages into a single JSON file using jq
Verify that the aggregation works correctly:
bazel test //app:aggregation_testThis test uses snapshot testing with write_source_files to verify the aggregated output matches the expected fixture.
If you intentionally change the messages, update the snapshot:
bazel run //app:update_all_messages_fixtureThe aggregated file will be located at:
bazel-bin/app/all_messages.json
It will contain all messages from all three modules merged into a single JSON object:
{
"module1.title": {
"defaultMessage": "Module 1 Title",
"description": "Title for module 1"
},
"module1.description": {
"defaultMessage": "This is the first module",
"description": "Description for module 1"
},
"module2.title": {
"defaultMessage": "Module 2 Title",
"description": "Title for module 2"
},
"module2.description": {
"defaultMessage": "This is the second module",
"description": "Description for module 2"
},
"module3.title": {
"defaultMessage": "Module 3 Title",
"description": "Title for module 3"
},
"module3.description": {
"defaultMessage": "This is the third module",
"description": "Description for module 3"
},
"common.save": {
"defaultMessage": "Save",
"description": "Common save button"
},
"common.cancel": {
"defaultMessage": "Cancel",
"description": "Common cancel button"
}
}- Extraction: Each
formatjs_extractrule extracts messages from its source files - Dependency Graph: Module3 depends on Module1, and the app depends on Module2 and Module3
- Aspect Application: The
formatjs_aggregaterule appliesformatjs_aggregate_aspectto its deps - Transitive Collection: The aspect traverses the dependency graph, collecting messages from:
- Module2 (direct dependency)
- Module3 (direct dependency)
- Module1 (transitive dependency via Module3)
- Aggregation: All message files are collected using
FormatjsExtractInfoandFormatjsAggregateInfoproviders - Merging:
jqmerges all JSON files into a single file using object multiplication
The merge strategy uses jq's reduce .[] as $item ({}; . * $item) which:
- Starts with an empty object
{} - Iterates through all message files
- Merges each file into the result using
*(object multiplication) - Later files overwrite earlier ones for duplicate keys
This aggregation is useful for:
- Monorepo Translation: Collect all messages from multiple packages/modules
- Build-time Validation: Ensure no duplicate message IDs across modules
- Translation Workflows: Generate a single file for translators
- CI/CD: Check for message changes across the entire codebase
- Bundle Optimization: Create optimized message bundles per locale