A library for providing type-safe internationalized messages on TypeScript + React + Vite environment.
Have you ever thought that it would be nice to be able to specify strictly typed parameters when generating and outputting internationalized messages in React?
You can use Interpolation String when formatting message strings, but you can't use this because it makes it difficult to internationalize messages. On the other hand, the usual string formatting functions do not check the arguments and types given.
This package allows you to manage internationalized messages in a JSON file and specify parameters based on a type-safe definition. For example, the following message file could be prepared:
{
"WELCOME_USER": "Welcome {firstName} {lastName}! Age: {age:number}",
}
You can use a React component to display this message:
{/* The provider */}
<TypedMessageProvider messages={localeMessages[locale]}>
{/* Place the component that formatted string */}
<TypedMessage
message={messages.WELCOME_USER}
params={{ firstName: "John ", lastName: "Doe", age: 25 }} />
</TypedMessageProvider>
Moreover, the format parameter object you specify for params
is typed by TypeScript, so you will never get lost or specify the wrong type for a parameter name or type.
Of course, you can also use it as a hook function instead of a React component:
// Use message getter function
const getMessage = useTypedMessage();
// To format with the parameters
const formatted = getMessage(
messages.WELCOME_USER,
{ firstName: "Jane", lastName: "Smith", age: 30 });
And if the parameter order changes depending on the locale, there is no need to make any changes to the code. Parameter type extraction is done automatically by the Vite plugin. This means you only need to edit the message file for each locale!
- Completely Type-Safe - Compile-time validation of message keys with TypeScript
- Hot Reload Support - Automatic detection of locale file changes during development
- Automatic Fallback Message Aggregation - Specify fallback messages for when a message is not found
- Parameterized Messages - Dynamic message formatting using placeholders (type-safe)
- Vite Optimized - Automatic code generation via Vite plugin
npm install typed-message
Add typedMessagePlugin()
to your vite.config.ts
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { typedMessagePlugin } from 'typed-message/vite'
export default defineConfig({
plugins: [
react(),
typedMessagePlugin({
localeDir: 'locale', // Directory containing JSON files
outputPath: 'src/generated/messages.ts' // Path for generated file
})
]
})
Create a locale
directory in your project root and place JSON files in it.
fallback.json
is referenced when messages cannot be found in other locale files.
Prepare a message file based on the language name. Examples in English and Japanese are shown below:
locale/en.json
{
"WELCOME_MESSAGE": "Welcome!!",
"BUTTON_SUBMIT": "Submit!!"
}
locale/ja.json
{
"WELCOME_MESSAGE": "ようこそ!!",
"BUTTON_SUBMIT": "送信する"
}
The fallback locale is optional, but having it ready allows you to use it as a hard-coded safe message in case a message file is not provided:
locale/fallback.json
{
"WELCOME_MESSAGE": "Welcome",
"BUTTON_SUBMIT": "Submit"
}
You can use placeholder syntax {variableName}
and type specification {variableName:type}
. Example below:
locale/en.json
{
"WELCOME_USER": "Welcome {firstName} {lastName}! Age: {age:number}",
"ITEM_COUNT": "You have {count:number} {itemType} in your cart",
"FORMATTED_DATE": "Today: {date:date}, Temperature: {temp:number}°C"
}
locale/ja.json
{
"WELCOME_USER": "こんにちは {firstName} {lastName}さん、{age:number}歳ですね",
"ITEM_COUNT": "{itemType}が{count:number}個あります",
"FORMATTED_DATE": "今日は{date:date}、気温は{temp:number}度です"
}
locale/fallback.json
{
"WELCOME_USER": "Hello {firstName} {lastName}, you are {age:number} years old!",
"ITEM_COUNT": "You have {count:number} {itemType}",
"FORMATTED_DATE": "Today is {date:date}, temperature is {temp:number}°C"
}
Supported types:
string
- String (default, type specification can be omitted)number
- Numberboolean
- Booleandate
- Date object
If you are doing a manual build, please build it.
The Vite plugin is installed correctly, just edit the message file and the src/generated/messages.ts
file should be generated automatically!
Now that you are ready, all you have to do is use the message.
Use React components to embed messages directly into elements.
Before the TypedMessage
component, a TypedMessageProvider
provider is required.
This provider receives the message dictionary from an external source and makes the message transformation happen.
The following example is a language-switching UI, which enables message switching between Japanese and English:
import React, { useState } from 'react'
import { TypedMessageProvider, TypedMessage } from 'typed-message'
import { messages } from './generated/messages'
// Import locale dictionaries
import enMessages from '../locale/en.json'
import jaMessages from '../locale/ja.json'
const App = () => {
const [locale, setLocale] = useState('en')
const localeMessages = {
en: enMessages,
ja: jaMessages
}
return (
<TypedMessageProvider messages={localeMessages[locale]}>
<div>
{/* Non-parameterized messages */}
<h1>
<TypedMessage message={messages.WELCOME_MESSAGE} />
</h1>
<button>
<TypedMessage message={messages.BUTTON_SUBMIT} />
</button>
{/* Parameterized messages - Type-checked by TypeScript! */}
<p>
<TypedMessage
message={messages.WELCOME_USER}
params={{ firstName: "John", lastName: "Doe", age: 25 }}
/>
</p>
<p>
<TypedMessage
message={messages.ITEM_COUNT}
params={{ count: 3, itemType: "books" }}
/>
</p>
<p>
<TypedMessage
message={messages.FORMATTED_DATE}
params={{ date: new Date(), temp: 23 }}
/>
</p>
{/* Language switcher */}
<select onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="ja">Japanese</option>
</select>
</div>
</TypedMessageProvider>
)
}
export default App
You can freely decide how to supply message dictionaries to TypedMessageProvider
. In the above example, we used TypeScript import
to insert the JSON dictionary directly on the source code, but there are many other possible methods, such as downloading from an external server and setting up.
import React, { useState } from 'react'
import { TypedMessageProvider, useTypedMessage } from 'typed-message'
import { messages } from './generated/messages'
// Import locale dictionaries
import enMessages from '../locale/en.json'
import jaMessages from '../locale/ja.json'
const MyComponent = () => {
const getMessage = useTypedMessage();
return (
<div>
{/* Non-parameterized messages */}
<h1>{getMessage(messages.WELCOME_MESSAGE)}</h1>
<button>{getMessage(messages.BUTTON_SUBMIT)}</button>
{/* Parameterized messages - Type-safe object format */}
<p>{getMessage(messages.WELCOME_USER, { firstName: "Jane", lastName: "Smith", age: 30 })}</p>
<p>{getMessage(messages.ITEM_COUNT, { count: 5, itemType: "apples" })}</p>
<p>{getMessage(messages.FORMATTED_DATE, { date: new Date(), temp: 18 })}</p>
</div>
)
}
const App = () => {
const [locale, setLocale] = useState('en');
const localeMessages = {
en: enMessages,
ja: jaMessages
}
return (
<TypedMessageProvider messages={localeMessages[locale]}>
<MyComponent />
{/* Language switcher */}
<select onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="ja">Japanese</option>
</select>
</TypedMessageProvider>
)
}
export default App
A React context provider that provides message dictionaries. It internally creates a message retrieval function and manages fallback processing.
Property | Type | Description |
---|---|---|
messages |
Record<string, string> (optional) |
Message dictionary. Defaults to empty object if omitted |
children |
React.ReactNode |
Child elements |
<TypedMessageProvider messages={{ HELLO: "Hello" }}>
{/* Child components */}
</TypedMessageProvider>
A React component for displaying messages. Uses TypeScript overloads to handle both non-parameterized and parameterized messages in a type-safe manner.
Property | Type | Description |
---|---|---|
message |
SimpleMessageItem |
Message item to display |
Property | Type | Description |
---|---|---|
message |
MessageItem<T> |
Message item to display |
params |
T |
Parameters to pass to the message (object format) |
{/* Non-parameterized message */}
<TypedMessage message={messages.WELCOME_MESSAGE} />
{/* Parameterized message */}
<TypedMessage
message={messages.WELCOME_USER}
params={{ firstName: "John", lastName: "Doe", age: 25 }}
/>
TypeScript automatically infers the necessity and types of params
:
// ✅ Correct usage
<TypedMessage message={simpleMessage} />
<TypedMessage message={paramMessage} params={{ name: "value1", count: 42 }} />
// ❌ Compile errors
<TypedMessage message={simpleMessage} params={{ invalid: "param" }} />
<TypedMessage message={paramMessage} /> // params required
<TypedMessage message={paramMessage} params={{ wrong: "types" }} />
A Vite plugin that generates TypeScript code from JSON. It detects placeholders and automatically generates types for parameterized messages.
Property | Type | Default | Description |
---|---|---|---|
localeDir |
string |
'locale' |
Directory containing JSON files |
outputPath |
string |
'src/generated/messages.ts' |
Path for generated file |
fallbackPriorityOrder |
string[] |
['en', 'fallback'] |
Priority order for fallback values (fallback to later elements in array) |
import { typedMessagePlugin } from 'typed-message/vite'
typedMessagePlugin({
localeDir: 'locale',
outputPath: 'src/generated/messages.ts',
// Priority order: search messages in ja.json, en.json, fallback.json order
fallbackPriorityOrder: ['ja', 'en', 'fallback']
})
The fallbackPriorityOrder
option controls the priority order of fallback messages:
import { typedMessagePlugin } from 'typed-message/vite'
typedMessagePlugin({
localeDir: 'locale',
outputPath: 'src/generated/messages.ts',
// Priority order: search messages in ja.json, en.json, fallback.json order
fallbackPriorityOrder: ['ja', 'en', 'fallback']
})
- Fallback messages are searched towards the last element of the array
- Files not included in the array are processed in alphabetical order
- When the same key exists in multiple files, the value from the higher priority file is used as the fallback message
Types for generated message items.
interface SimpleMessageItem {
key: string;
fallback: string;
}
interface MessageItem<T extends Record<string, any>> {
key: string;
fallback: string;
}
key
: Key to search in the locale dictionaryfallback
: The fallback message template with placeholder syntax
A hook to get the message retrieval function from TypedMessageProvider. This function takes message items, searches for messages in the dictionary, and uses the fallback template when not found.
const getMessage = useTypedMessage()
// Non-parameterized messages
const simpleResult = getMessage(simpleMessage)
// Parameterized messages
const paramResult = getMessage(paramMessage, { name: "John", age: 30 })
The Vite plugin automatically validates placeholder types across different locale files and provides warnings when inconsistencies are detected.
When the same placeholder name is used with different types across locale files, the plugin will:
- Generate JSDoc warnings in the generated TypeScript code
- Display console warnings during build time
- Use explicit types over implicit
string
types when available
Example of type conflicts:
locale/fallback.json
{
"USER_MESSAGE": "User {userId} has {balance:number} points and status {isActive:boolean}"
}
locale/en.json
{
"USER_MESSAGE": "User {userId:number} has {balance:number} points and status {isActive:boolean}"
}
locale/ja.json
{
"USER_MESSAGE": "ユーザー{userId:boolean}の残高は{balance:string}ポイント、ステータスは{isActive:number}です"
}
Generated TypeScript code with warnings:
/**
* Warning: Placeholder types do not match across locales
* - userId: fallback.json: string, en.json: number, ja.json: boolean
* - balance: fallback.json: number, en.json: number, ja.json: string
* - isActive: fallback.json: boolean, en.json: boolean, ja.json: number
*/
USER_MESSAGE: {
key: "USER_MESSAGE",
fallback: "User {userId:number} has {balance:number} points and status {isActive:boolean}"
} as MessageItem<{ userId: number; balance: number; isActive: boolean }>
- All types match: No warnings generated
- Implicit vs explicit types: Explicit types (e.g.,
:number
) take precedence over implicitstring
types - Type conflicts: Plugin uses the first explicit type found in priority order and generates warnings
When locale files contain invalid JSON syntax, the plugin will:
- Continue processing other valid files
- Generate JSDoc warnings listing invalid files
- Display console warnings with error details
Example with invalid files:
Generated TypeScript code:
/**
* Warning: Failed to load the following locale files
* - broken.json
* - invalid-syntax.json
* These files are not included in the generated code.
*/
export const messages = {
// ... only messages from valid files
} as const;
This feature helps maintain type safety and consistency across your internationalization setup while providing clear feedback when issues are detected.
With the object-based parameter system, placeholders can appear in any order in different locale files:
locale/en.json
{
"USER_INFO": "Hello {firstName} {lastName}, you are {age:number} years old!"
}
locale/ja.json
{
"USER_INFO": "こんにちは {lastName} {firstName}さん、あなたは{age:number}歳です!"
}
Both will work correctly with the same parameter object:
<TypedMessage
message={messages.USER_INFO}
params={{ firstName: "太郎", lastName: "田中", age: 25 }}
/>
Results:
- English: "Hello 太郎 田中, you are 25 years old!"
- Japanese: "こんにちは 田中 太郎さん、あなたは25歳です!"
If a placeholder is missing from a locale file, it will be gracefully ignored:
locale/en.json
{
"PARTIAL_MESSAGE": "Hello {firstName}, welcome!"
}
<TypedMessage
message={messages.PARTIAL_MESSAGE}
params={{ firstName: "John", lastName: "Doe", age: 30 }}
/>
Result: "Hello John, welcome!" (unused parameters are ignored)
MIT License. See LICENSE for details.