Skip to content

Commit

Permalink
feat: message function (#972)
Browse files Browse the repository at this point in the history
* feat: message function

* add docs
  • Loading branch information
kazupon committed Aug 13, 2020
1 parent c21decc commit 2ea18e0
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 17 deletions.
7 changes: 6 additions & 1 deletion decls/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ declare var Intl: any;

declare type Path = string;
declare type Locale = string;
declare type MessageContext = {
list: (index: number) => mixed,
named: (key: string) => mixed
}
declare type MessageFunction = (ctx: MessageContext) => string
declare type FallbackLocale = string | string[] | false | { [locale: string]: string[] };
declare type LocaleMessage = string | LocaleMessageObject | LocaleMessageArray;
declare type LocaleMessage = string | MessageFunction | LocaleMessageObject | LocaleMessageArray;
declare type LocaleMessageObject = { [key: Path]: LocaleMessage };
declare type LocaleMessageArray = Array<LocaleMessage>;
declare type LocaleMessages = { [key: Locale]: LocaleMessageObject };
Expand Down
34 changes: 25 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isArray,
isBoolean,
isString,
isFunction,
looseClone,
remove,
includes,
Expand Down Expand Up @@ -188,7 +189,7 @@ export default class VueI18n {
paths.pop()
}
})
} else if (Array.isArray(message)) {
} else if (isArray(message)) {
message.forEach((item, index) => {
if (isPlainObject(item)) {
paths.push(`[${index}]`)
Expand Down Expand Up @@ -382,16 +383,16 @@ export default class VueI18n {
if (!message) { return null }

const pathRet: PathValue = this._path.getPathValue(message, key)
if (Array.isArray(pathRet) || isPlainObject(pathRet)) { return pathRet }
if (isArray(pathRet) || isPlainObject(pathRet)) { return pathRet }

let ret: mixed
if (isNull(pathRet)) {
/* istanbul ignore else */
if (isPlainObject(message)) {
ret = message[key]
if (!isString(ret)) {
if (!(isString(ret) || isFunction(ret))) {
if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(`Value of key '${key}' is not a string!`)
warn(`Value of key '${key}' is not a string or function !`)
}
return null
}
Expand All @@ -400,18 +401,18 @@ export default class VueI18n {
}
} else {
/* istanbul ignore else */
if (isString(pathRet)) {
if (isString(pathRet) || isFunction(pathRet)) {
ret = pathRet
} else {
if (process.env.NODE_ENV !== 'production' && !this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(`Value of key '${key}' is not a string!`)
warn(`Value of key '${key}' is not a string or function!`)
}
return null
}
}

// Check for the existence of links within the translated string
if (ret.indexOf('@:') >= 0 || ret.indexOf('@.') >= 0) {
if (isString(ret) && (ret.indexOf('@:') >= 0 || ret.indexOf('@.') >= 0)) {
ret = this._link(locale, message, ret, host, 'raw', values, visitedLinkStack)
}

Expand Down Expand Up @@ -476,7 +477,7 @@ export default class VueI18n {
}
translated = this._warnDefault(
locale, linkPlaceholder, translated, host,
Array.isArray(values) ? values : [values],
isArray(values) ? values : [values],
interpolateMode
)

Expand All @@ -495,7 +496,22 @@ export default class VueI18n {
return ret
}

_render (message: string, interpolateMode: string, values: any, path: string): any {
_createMessageContext (values: any): MessageContext {
const _list = isArray(values) ? values : []
const _named = isObject(values) ? values : {}
const list = (index: number): mixed => _list[index]
const named = (key: string): mixed => _named[key]
return {
list,
named
}
}

_render (message: string | MessageFunction, interpolateMode: string, values: any, path: string): any {
if (isFunction(message)) {
return message(this._createMessageContext(values))
}

let ret = this._formatter.interpolate(message, values, path)

// If the custom formatter refuses to work - apply the default one
Expand Down
2 changes: 1 addition & 1 deletion src/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ function parse (path: Path): ?Array<string> {
}
}

export type PathValue = PathValueObject | PathValueArray | string | number | boolean | null
export type PathValue = PathValueObject | PathValueArray | Function | string | number | boolean | null
export type PathValueObject = { [key: string]: PathValue }
export type PathValueArray = Array<PathValue>

Expand Down
12 changes: 8 additions & 4 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,15 @@ export function isNull (val: mixed): boolean {
return val === null || val === undefined
}

export function isFunction (val: mixed): boolean %checks {
return typeof val === 'function'
}

export function parseArgs (...args: Array<mixed>): Object {
let locale: ?string = null
let params: mixed = null
if (args.length === 1) {
if (isObject(args[0]) || Array.isArray(args[0])) {
if (isObject(args[0]) || isArray(args[0])) {
params = args[0]
} else if (typeof args[0] === 'string') {
locale = args[0]
Expand All @@ -81,7 +85,7 @@ export function parseArgs (...args: Array<mixed>): Object {
locale = args[0]
}
/* istanbul ignore if */
if (isObject(args[1]) || Array.isArray(args[1])) {
if (isObject(args[1]) || isArray(args[1])) {
params = args[1]
}
}
Expand Down Expand Up @@ -137,8 +141,8 @@ export function looseEqual (a: any, b: any): boolean {
const isObjectB: boolean = isObject(b)
if (isObjectA && isObjectB) {
try {
const isArrayA: boolean = Array.isArray(a)
const isArrayB: boolean = Array.isArray(b)
const isArrayA: boolean = isArray(a)
const isArrayB: boolean = isArray(b)
if (isArrayA && isArrayB) {
return a.length === b.length && a.every((e: any, i: number): boolean => {
return looseEqual(e, b[i])
Expand Down
55 changes: 55 additions & 0 deletions test/unit/message_function.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
describe('message function', () => {
describe('simple', () => {
it('should be worked', () => {
i18n = new VueI18n({
locale: 'ja',
fallbackLocale: 'en',
messages: {
en: {
hello: (ctx) => 'hello'
},
ja: {
hello: (ctx) => 'こんにちは!'
}
}
})
assert.strictEqual(i18n.t('hello'), 'こんにちは!')
})
})

describe('list argument', () => {
it('should be worked', () => {
i18n = new VueI18n({
locale: 'ja',
fallbackLocale: 'en',
messages: {
en: {
hello: (ctx) => `hello, ${ctx.list(0)}!`
},
ja: {
hello: (ctx) => `こんにちは、${ctx.list(0)}!`
}
}
})
assert.strictEqual(i18n.t('hello', ['kazupon']), 'こんにちは、kazupon!')
})
})

describe('named argument', () => {
it('should be worked', () => {
i18n = new VueI18n({
locale: 'ja',
fallbackLocale: 'en',
messages: {
en: {
hello: (ctx) => `hello, ${ctx.named('name')}!`
},
ja: {
hello: (ctx) => `こんにちは、${ctx.named('name')}!`
}
}
})
assert.strictEqual(i18n.t('hello', { name: 'kazupon' }), 'こんにちは、kazupon!')
})
})
})
9 changes: 8 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ declare namespace VueI18n {
type FallbackLocale = string | string[] | false | { [locale: string]: string[] }
type Values = any[] | { [key: string]: any };
type Choice = number;
type LocaleMessage = string | LocaleMessageObject | LocaleMessageArray;
interface MessageContext {
list(index: number): unknown
named(key: string): unknown
}
type MessageFunction = (ctx: MessageContext) => string;
type LocaleMessage = string | MessageFunction | LocaleMessageObject | LocaleMessageArray;
interface LocaleMessageObject { [key: string]: LocaleMessage; }
interface LocaleMessageArray { [index: number]: LocaleMessage; }
interface LocaleMessages { [key: string]: LocaleMessageObject; }
Expand Down Expand Up @@ -123,6 +128,8 @@ export type Locale = VueI18n.Locale;
export type FallbackLocale = VueI18n.FallbackLocale;
export type Values = VueI18n.Values;
export type Choice = VueI18n.Choice;
export type MessageContext = VueI18n.MessageContext;
export type MessageFunction = VueI18n.MessageFunction;
export type LocaleMessage = VueI18n.LocaleMessage;
export type LocaleMessageObject = VueI18n.LocaleMessageObject;
export type LocaleMessageArray = VueI18n.LocaleMessageArray;
Expand Down
1 change: 1 addition & 0 deletions vuepress/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.exports = {
themeConfig: {
repo: 'kazupon/vue-i18n',
editLinks: true,
sidebarDepth: 3,
docsDir: 'vuepress',
locales: {
'/': {
Expand Down
106 changes: 105 additions & 1 deletion vuepress/guide/messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ Locale Messages syntax below:
type LocaleMessages = { [key: Locale]: LocaleMessageObject };
type LocaleMessageObject = { [key: Path]: LocaleMessage };
type LocaleMessageArray = LocaleMessage[];
type LocaleMessage = string | LocaleMessageObject | LocaleMessageArray;
type MessageContext = {
list: (index: number) => mixed,
named: (key: string) => mixed
};
type MessageFunction = (ctx: MessageContext) => string;
type LocaleMessage = string | MessageFunction | LocaleMessageObject | LocaleMessageArray;
type Locale = string;
type Path = string;
```
Expand Down Expand Up @@ -186,3 +191,102 @@ Output:
```html
<p>There's a reason, you lost, DIO.</p>
```

## Message Function

vue-i18n recommends using the string base on list or named format as locale messages when translating messages.

There are situations however, where you really need the full programmatic power of JavaScript, due to the complex language syntax. So instead of string-based messages, you can use the **message function**.

The following is a message function that returns a simple greeting:

```js
const messages = {
en: {
greeting: (ctx) => 'hello!'
}
}
```

The use of the message function is very easy! You just specify the key of the message function with `$t` or `t`:

```html
<p>{{ $t('greeting') }}</p>
```

Output is the below:

```html
<p>hello!</p>
```

The message function outputs the message of the return value of the message function.

### Named formatting

vue-i18n supports [named formatting](./formatting.md#named-formatting) as a string-based message format. vue-i18n interpolate the parameter values with `$t` or `t`, and it can be output it.

You can do the same thing with the message function by using **message context**:

here is the example of greeting:

```js
const messages = {
en: {
greeting: (ctx) => `hello, ${ctx.named('name')}!`
}
}
```

Template:

```html
<p>{{ $t('greeting', { name: 'DIO' }) }}</p>
```

Output is the below:

```html
<p>hello, DIO!</p>
```

The message context has a named function. You need to specify the key that resolves the value specified with the named of `$t` or `t`.

### List formatting

The use of the list format is similar to the named format described above.

vue-i18n supports [list formatting](./formatting.md#list-formatting) as a string-based message format. vue-i18n interpolate the parameter values with `$t` or `t`, and it can be output it.

You can do the same thing with the message function by using message context:

here is the example of greeting:

```js
const messages = {
en: {
greeting: (ctx) => `hello, ${ctx.list(0)}!`
}
}
```

Template:

```html
<p>{{ $t('greeting', ['DIO']) }}</p>
```

Output is the below:

```html
<p>hello, DIO!</p>
```

The message context has a list function. You need to specify the index that resolves the value specified with the list of `$t` or `t`.

### Limitation

In the message function, the following functions, which are provided on a string basis, are not available via a message context:

- Linked locale messages
- Pluralization

0 comments on commit 2ea18e0

Please sign in to comment.