Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improved localization API #343

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
30 changes: 20 additions & 10 deletions demo/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import messages from 'public/_locales/en/messages.json';

export default defineBackground(() => {
console.log(browser.runtime.id);
logId();
Expand All @@ -8,20 +6,32 @@ export default defineBackground(() => {
chrome: __IS_CHROME__,
firefox: __IS_FIREFOX__,
manifestVersion: __MANIFEST_VERSION__,
messages,
});

// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
browser.runtime.getURL('/icon-128.png');

console.log([
// @ts-expect-error: browser.i18n should only accept known message names
browser.i18n.getMessage('test'),
browser.i18n.getMessage('promptForName'),
browser.i18n.getMessage('hello', ['Aaron']),
browser.i18n.getMessage('bye', ['Aaron']),
browser.i18n.getMessage('@@extension_id'),
browser.i18n.getMessage('nItems'),
]);

// @ts-expect-error: should only accept known message names
browser.i18n.getMessage('test');
browser.i18n.getMessage('prompt_for_name');
browser.i18n.getMessage('hello', 'Aaron');
browser.i18n.getMessage('bye', ['Aaron']);
browser.i18n.getMessage('@@extension_id');
console.log([
// @ts-expect-error: i18n should only accept known message names
i18n.t('test'),
i18n.t('promptForName'),
i18n.t('hello', ['Aaron']),
i18n.t('bye', ['Aaron']),
i18n.t('@@extension_id'),
i18n.tp('nItems', 0, ['0']),
]);

console.log('WXT MODE:', {
MODE: import.meta.env.MODE,
Expand Down
21 changes: 21 additions & 0 deletions demo/src/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
promptForName: What's your name?
hello:
message: Hello, $USER$
description: Greet the user
placeholders:
user:
content: $1
example: Paul
bye:
message: Goodbye, $USER$. Come back to $OUR_SITE$ soon!
description: Say goodbye to the user
placeholders:
our_site:
content: Example.com
user:
content: $1
example: Paul
nItems:
0: 0 items
1: 1 item
n: $1 items
29 changes: 0 additions & 29 deletions demo/src/public/_locales/en/messages.json

This file was deleted.

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default defineConfig({
{ text: 'Storage', link: '/guide/storage.md' },
{ text: 'Assets', link: '/guide/assets.md' },
{ text: 'Content Script UI', link: '/guide/content-script-ui.md' },
{ text: 'Localization', link: '/guide/localization.md' },
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
{ text: 'Vite', link: '/guide/vite.md' },
Expand Down
159 changes: 159 additions & 0 deletions docs/guide/localization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
outline: deep
---

# Localization

WXT includes a util, [`i18n`](/api/wxt/i18n/), that provides a type-safe feature-rich alternative to `browser.i18n.getMessage`.

It is available automatically when you create a messages file under the `locales/` directory, and add a `default_locale` to the manifest:

```
<srcDir>
└─ locales/
├─ en.json
├─ es.json5
├─ fr.yml
├─ de.yaml
└─ ...
```

> You can use JSON, JSON5, or YAML formats.

```ts
// wxt.config.ts
export default defineConfig({
manifest: {
default_locale: 'en',
},
});
```

## Message File Format

```yml
# Use plain strings
simpleMessage: Hello world!

# Nest strings in objects
popup:
overview:
title: Nested text

# Plural form support
items:
1: 1 item
n: $1 items
# Optionally include a custom string for 0
cartSize:
0: Empty
1: 1 item
n: $1 items

# Or stick with the standard web extension format (with a message, description, and placeholders)
manifestMessage:
message: $THIS$ is translated
description: This is not-translated, helps translators
placeholder:
this:
content: This
```

:::tip
`locales/<code>.json` is 100% compatible with the standard web extension localization format (`_locales/<code>/messages.json`). If you have existing messages files, just move them into the `locales/` directory.
:::

## Usage

`i18n` is auto-imported, but can be manually imported from `wxt/i18n`.

```ts
import { i18n } from 'wxt/i18n';
```

### Basic Usage

You can access messages by their name:

```yml
helloWorld: Hello world!
```

```ts
i18n.t('helloWorld'); // "Hello world!"
```

Nested messages are combined into one string using an `_`.

```yml
popup:
overview:
title: Hello world!
```

```ts
i18n.t('popup_overview_title'); // "Hello world!"
```

If a message is in the standard web extension format, don't include a `_message`, even if it's nested.

```yml
helloWorld:
message: Hello world!
description: Some description
popup:
overview:
title:
message: Nested Title
```

```ts
i18n.t('helloWorld'); // "Hello world!"
i18n.t('popup_overview_title'); // "Nested Title"
```

### Substitutions

To insert a custom string into a translation, pass an array of values as the second parameter of the `i18n.t` function:

```yml
hello: Hello, $1, my name is $2.
```

```ts
i18n.t('hello', ['Aaron', 'Mark']); // "Hello Aaron, my name is Mark."
```

### Plural Form

When getting the translation for text with a plural form, use the `i18n.tp` function.

```yml
friends:
0: I have no friends.
1: I have a friend.
n: I have many friends.
```

```ts
i18n.tp('friends', 0); // "I have no friends."
i18n.tp('friends', 1); // "I have one friend."
i18n.tp('friends', 2); // "I have many friends."
```

The first number is the `count`. It is what decides which form will be used.

Substitutions are not required. But usually, a plural form will look something like this:

```yml
items:
1: 1 item
n: $1 items
```

```ts
i18n.tp('items', 0, ['0']); // "0 items"
i18n.tp('items', 1, ['1']); // "1 item"
i18n.tp('items', 2, ['2']); // "2 items"
i18n.tp('items', 3, ['3']); // "3 items"
```
3 changes: 2 additions & 1 deletion docs/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"../src/browser.ts",
"../src/sandbox",
"../src/storage.ts",
"../src/testing"
"../src/testing",
"../src/i18n"
],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"out": "./api",
Expand Down
1 change: 1 addition & 0 deletions e2e/tests/auto-imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('Auto Imports', () => {
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
const i18n: typeof import('wxt/i18n')['i18n']
const storage: typeof import('wxt/storage')['storage']
}
"
Expand Down