Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.5.0 - 2026-10-13

### Added
- `build-intl` reads a JSON config via `-c` / `--config` with `supportedLocales` and `compileTo`
- `.arb` files declare `@@locale`, and an optional `@@prefix` prefixes every key in that file
- Translation inputs can be `.arb` files or directories
- Warnings for inconsistent keys and inconsistent placeholder types
- Placeholder types other than `string`, `int`, and `float` are errors. The build reports every error and does not write the output file when any error is present
- Placeholder types are no longer inferred from the variable name. A placeholder without a type warns and is read as `string`

### Changed
- Locale identity comes from `@@locale` instead of the file name
- Key prefixes come from `@@prefix` instead of nested directories
- Omitted config fields default to locales `en-US` and `de-DE`, `compileTo` `typescript`, export name `appTranslation`, and output file `./i18n/translations.ts`
- Placeholder types are `string`, `int`, and `float`
- The config accepts `name`, `outputFile`, and `translationFiles`
- `build-intl` accepts multiple file or directory paths, overwrites the output file, and `--dry-run` prints that diff
- `-c` / `--config` is optional. Without it, the build uses the same defaults as a config with omitted fields
- `int` and `float` placeholders are `number` parameters. `int` values are rounded before they are inserted into the translation string. `float` values are inserted as given

## [0.4.0] - 2025-11-26

### Added
Expand Down
51 changes: 41 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ helpwaves package for internationalization that creates localized and typesafe t
Create a `.arb` file with your translations:
```json
{
"@@locale": "en-US",
"priceInfo": "The price is {price}{currency, select, usd{$USD} eur{€} other{}}.",
"@priceInfo": {
"placeholders": {
"price": {
"type": "number"
"type": "float"
},
"currency": {}
}
Expand Down Expand Up @@ -43,6 +44,16 @@ t("priceInfo", { price, currency })
npm install -D @helpwave/internationalization
```
#### Create local ARB files
Each file sets its locale with `@@locale`. An optional `@@prefix` prefixes every key in that file, so a prefix of `test` makes `name` available as `test.name`.

```json
{
"@@locale": "en-US",
"@@prefix": "test",
"name": "Name"
}
```

The file structure could look like this:
```
/locales
Expand All @@ -53,23 +64,43 @@ The file structure could look like this:
-> en-US.arb
```

Then run:
A JSON config file is optional. Pass it with `-c` or `--config`. Every field is optional:
```json
{
"supportedLocales": ["en-US", "de-DE"],
"compileTo": "typescript",
"name": "appTranslation",
"outputFile": "./i18n/translations.ts",
"translationFiles": ["./locales", "./shared/locales"]
}
```

Without a config file, and for any omitted field, `supportedLocales`, `compileTo`, `name`, and `outputFile` use the values above. `translationFiles` defaults to an empty list; the sample lists two paths to show that more than one is allowed. `supportedLocales` is the set of locales the project expects. Every placeholder must declare a type of `string`, `int`, or `float`. A missing type is a warning and is read as `string`. Any other type is an error. The build reports every error it finds, and it does not write the output file when any error is present. `compileTo` currently accepts `typescript`. `-n` overrides `name`, and `-o` overrides `outputFile`.

`int` and `float` are `number` parameters in the generated functions. `TranslationGen.round` rounds `int` values before they are inserted into the translation string. `float` values are inserted as given.

Pass one or more `.arb` files or directories. Every `.arb` file under a directory is included. Paths on the command replace `translationFiles` when you provide any.

```bash
npx build-intl
npx build-intl ./locales ./shared/locales
npx build-intl --config ./intl.config.json ./locales ./shared/locales
```

By default `./locales` will be translated to `./i18n`, the input directory and output file are configurable with:
An existing output file is overwritten. `--dry-run` prints the diff and does not write the file.

```
Usage: i18n-compile [options]
Usage: build-intl [options] [location ...]

Options:
-i, --in <dir> Input directory containing .arb files
-o, --out <file> Output file (e.g. ./i18n/translations.ts)
-f, --force Overwrite output without prompt
-c, --config <path> JSON configuration file (optional)
-o, --out <file> Output file (overrides config outputFile)
-n, --name <name> Exported translation name (overrides config name)
--dry-run Print the output diff without writing the file
-h, --help Show this help message
`)
```

Keys or placeholder types that differ between locales are reported as warnings.


## Tests
The lexer, parser and compiler are all tested with jest, see [our tests](/tests)
Expand All @@ -80,7 +111,7 @@ Example translation files and the resulting translation can be found in the [exa
Rebuild the examples:
```bash
npm run build
node dist/scripts/compile-arb.js --force -i ./examples/locales -o ./examples/translations/translations.ts -n "exampleTranslation"
node dist/scripts/build-intl.js --config ./examples/intl.config.json
```

React hook example:
Expand Down
7 changes: 7 additions & 0 deletions examples/intl.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"supportedLocales": ["de-DE", "en-US", "fr-FR"],
"compileTo": "typescript",
"name": "exampleTranslation",
"outputFile": "./examples/translations/translations.ts",
"translationFiles": ["./examples/locales"]
}
9 changes: 5 additions & 4 deletions examples/locales/de-DE.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"@@locale": "de-DE",
"userGreeting": "{gender, select, male{Hallo, {name}!} female{Hallo, {name}!} other{Hallo, Person!}}",
"@userGreeting": {
"placeholders": {
Expand Down Expand Up @@ -34,15 +35,15 @@
"type": "string"
},
"count": {
"type": "number"
"type": "int"
}
}
},
"priceInfo": "Der Preis beträgt {price}{currency, select, usd{$USD} eur{€} other{}}.",
"@priceInfo": {
"placeholders": {
"price": {
"type": "number"
"type": "float"
},
"currency": {}
}
Expand All @@ -61,7 +62,7 @@
"placeholders": {
"gender": {},
"count": {
"type": "number"
"type": "int"
}
}
},
Expand All @@ -70,7 +71,7 @@
"@nWard": {
"placeholders": {
"count": {
"type": "number"
"type": "int"
}
}
},
Expand Down
7 changes: 4 additions & 3 deletions examples/locales/en-US.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"@@locale": "en-US",
"userGreeting": "{gender, select, male{Hello, {name}!} female{Hello, {name}!} other{Hello, person!}}",
"@userGreeting": {
"placeholders": {
Expand Down Expand Up @@ -34,15 +35,15 @@
"type": "string"
},
"count": {
"type": "number"
"type": "int"
}
}
},
"priceInfo": "The price is {price}{currency, select, usd{$USD} eur{€} other{}}.",
"@priceInfo": {
"placeholders": {
"price": {
"type": "number"
"type": "float"
},
"currency": {}
}
Expand All @@ -61,7 +62,7 @@
"placeholders": {
"gender": {},
"count": {
"type": "number"
"type": "int"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/locales/fr-FR.arb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"@@locale": "fr-FR",
"hello": "Bonjour",
"goodbye": "Au revoir",
"thankYou": "Merci",
Expand Down
4 changes: 3 additions & 1 deletion examples/locales/nested/de-DE.arb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"@@locale": "de-DE",
"@@prefix": "nested",
"nested": "Geschachtelt",
"itemCount": "{count, plural, =0{Keine Elemente} =1{Ein Element} other{{count} Elemente}}",
"@itemCount": {
"placeholders": {
"count": {
"type": "number"
"type": "int"
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion examples/locales/nested/en-US.arb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"@@locale": "en-US",
"@@prefix": "nested",
"nested": "Nested",
"itemCount": "{count, plural, =0{No items} =1{One item} other{{count} items}}",
"@itemCount": {
"placeholders": {
"count": {
"type": "number"
"type": "int"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/locales/nested/fr-FR.arb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{
"@@locale": "fr-FR"
}
24 changes: 12 additions & 12 deletions examples/translations/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
return TranslationGen.resolvePlural(count, {
'=0': `Keine Elemente`,
'=1': `Ein Element`,
'other': `${count} Elemente`,
'other': `${TranslationGen.round(count)} Elemente`,
})
},
'nested.nested': `Geschachtelt`,
Expand All @@ -63,24 +63,24 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
'male': TranslationGen.resolvePlural(count, {
'=0': `Keine Nachrichten`,
'=1': `Eine Nachricht`,
'other': `${count} Nachrichten`,
'other': `${TranslationGen.round(count)} Nachrichten`,
}),
'female': TranslationGen.resolvePlural(count, {
'=0': `Keine Nachrichten`,
'=1': `Eine Nachricht`,
'other': `${count} Nachrichten`,
'other': `${TranslationGen.round(count)} Nachrichten`,
}),
'other': TranslationGen.resolvePlural(count, {
'=0': `Keine Nachrichten`,
'=1': `Eine Nachricht`,
'other': `${count} Nachrichten`,
'other': `${TranslationGen.round(count)} Nachrichten`,
}),
})
},
'nWard': ({ count }): string => {
return TranslationGen.resolvePlural(count, {
'=1': `${count} Station`,
'other': `${count} Stationen`,
'=1': `${TranslationGen.round(count)} Station`,
'other': `${TranslationGen.round(count)} Stationen`,
})
},
'passwordStrength': ({ strength }): string => {
Expand Down Expand Up @@ -123,7 +123,7 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
_out += TranslationGen.resolvePlural(count, {
'=0': `keine neuen Nachrichten`,
'=1': `eine neue Nachricht`,
'other': `${count} neue Nachrichten`,
'other': `${TranslationGen.round(count)} neue Nachrichten`,
})
_out += `.`
return _out
Expand All @@ -150,7 +150,7 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
return TranslationGen.resolvePlural(count, {
'=0': `No items`,
'=1': `One item`,
'other': `${count} items`,
'other': `${TranslationGen.round(count)} items`,
})
},
'nested.nested': `Nested`,
Expand All @@ -159,17 +159,17 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
'male': TranslationGen.resolvePlural(count, {
'=0': `No messages`,
'=1': `One message`,
'other': `${count} messages`,
'other': `${TranslationGen.round(count)} messages`,
}),
'female': TranslationGen.resolvePlural(count, {
'=0': `No messages`,
'=1': `One message`,
'other': `${count} messages`,
'other': `${TranslationGen.round(count)} messages`,
}),
'other': TranslationGen.resolvePlural(count, {
'=0': `No messages`,
'=1': `One message`,
'other': `${count} messages`,
'other': `${TranslationGen.round(count)} messages`,
}),
})
},
Expand Down Expand Up @@ -212,7 +212,7 @@ export const exampleTranslation: Translation<ExampleTranslationLocales, Partial<
_out += TranslationGen.resolvePlural(count, {
'=0': `no new messages`,
'=1': `one new message`,
'other': `${count} new messages`,
'other': `${TranslationGen.round(count)} new messages`,
})
_out += `.`
return _out
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"url": "git+https://github.com/helpwave/internationlization.git"
},
"license": "MPL-2.0",
"version": "0.4.0",
"version": "0.5.0",
"type": "module",
"files": [
"dist"
Expand All @@ -19,7 +19,7 @@
"module": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"build-intl": "dist/scripts/compile-arb.js"
"build-intl": "dist/scripts/build-intl.js"
},
"exports": {
".": {
Expand All @@ -31,7 +31,8 @@
"clean": "rm -rf dist",
"build": "tsup",
"lint": "eslint .",
"test": "jest"
"test": "jest",
"build-intl": "node dist/scripts/build-intl.js"
},
"devDependencies": {
"@helpwave/eslint-config": "0.0.7",
Expand Down
Loading
Loading