Bazel rules for FormatJS - Internationalize your web apps on the client & server.
- Extract messages from source files (TypeScript, JavaScript, JSX, TSX, Rust, Python)
- 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
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>",
)The platform-specific CLI selected by the registered toolchain is directly runnable:
bazel run @rules_formatjs//cli -- --versionThe same label can be used in executable or tool attributes. Apply the execution transition when declaring a Starlark attribute:
"_formatjs_cli": attr.label(
default = Label("@rules_formatjs//cli"),
executable = True,
cfg = "exec",
)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", "src/**/*.py"]),
out = "en.json",
id_interpolation_pattern = "[sha512:contenthash:base64:6]",
)Python extraction recognizes define_message(...) calls with literal id,
default_message, and optional description keyword arguments:
from example.i18n import define_message
GREETING = define_message(
id="python.greeting",
default_message="Hello, {name}!",
description="Greeting shown on the account page",
)Compile extracted messages for optimized runtime:
load("@rules_formatjs//formatjs:defs.bzl", "formatjs_compile")
formatjs_compile(
name = "messages_compiled",
src = ":messages_extracted",
out = "en-compiled.json",
ast = True, # Compile to AST for better performance
)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 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_messagesload("@rules_formatjs//formatjs:defs.bzl", "formatjs_extract", "formatjs_compile", "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_compile(
name = "compile_en",
src = ":extract_en",
out = "lang/en-compiled.json",
ast = True,
)
# Compile translations
formatjs_compile(
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"],
)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:
name(required): Target namesrcs(required): List of source files to extract fromout(optional): Output JSON file (defaults toname + ".json")id_interpolation_pattern(optional): Pattern for message ID generation- Example:
"[sha512:contenthash:base64:6]"for content-based IDs
- Example:
extract_from_format_message_call(optional): Extract fromformatMessage()callsadditional_component_names(optional): Additional component names to extract fromadditional_function_names(optional): Additional function names to extract fromignore(optional): 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
Compile extracted messages for optimized runtime.
Attributes:
name(required): Target namesrc(required): Source JSON file with extracted messagesout(optional): Output compiled JSON file (defaults toname + ".json")ast(optional): Compile to AST format for better runtime performanceformat(optional): Input format (simple,crowdin,smartling,transifex)
Verify that translations are complete and correctly formatted.
Attributes:
name(required): Target namereference(required): Reference messages file (typically the base language)translations(required): List of translation files to verify
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>
);
}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
The formatjs_aggregate_aspect collects and merges extracted messages from a target and all its dependencies into a single JSON file using jq.
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_messagesOutput: 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.
The library also includes demonstration aspects in formatjs/aspects.bzl:
bazel build //path/to:target \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_stats_aspect \
--output_groups=message_statsbazel build //path/to:target \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_validator_aspect \
--output_groups=message_validationUse message_collector_aspect when a consumer needs original catalogs for a
custom merge (for example, preserving every declaration location for shared IDs):
bazel build //app:main \
--aspects=@rules_formatjs//formatjs:aspects.bzl%message_collector_aspect \
--output_groups=all_messagesThe collector follows deps and srcs, collecting FormatjsExtractInfo from
reachable targets. It uses transitive depsets to deduplicate shared catalogs,
registers no merge actions, and requires no jq toolchain. An empty graph returns
an empty all_messages output group. Raw source files still need an extraction
target; attaching this aspect does not run extraction on arbitrary sources.
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...
return [OutputGroupInfo(...)]
my_aspect = aspect(
implementation = _my_aspect_impl,
doc = "My custom aspect for formatjs_extract",
)rules_formatjs uses a native Rust CLI toolchain that is automatically downloaded for your platform. The toolchain supports:
- macOS (Apple Silicon)
- Linux (x86_64 and ARM64)
- Windows (x86_64)
The toolchain is registered automatically when you add the MODULE.bazel dependency. Binaries are fetched from GitHub releases and cached by Bazel.
See the examples directory for complete working examples:
- examples/simple - Basic message extraction, compilation, and verification
- examples/aggregate - Aggregating messages from multiple modules
See CONTRIBUTING.md for development instructions.
Apache License 2.0