Skip to content

Commit 6f24b62

Browse files
authored
fix: improvement processor for linked and pluralization (#1023)
* fix: improvement processor for linkd and pluralization * updates * revert * fix for windows * fix timezone setting failure
1 parent 8a2a5a2 commit 6f24b62

File tree

18 files changed

+353
-64
lines changed

18 files changed

+353
-64
lines changed

e2e/bridge/components/datetime-format.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
describe(`bridge: datetime format component`, () => {
22
beforeAll(async () => {
3+
await page.emulateTimezone('UTC')
34
await page.goto(
45
`http://localhost:8080/examples/bridge/composition/components/datetime-format.html`
56
)

e2e/components/datetime-format.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
;['composition', 'legacy'].forEach(pattern => {
22
describe(`${pattern}`, () => {
33
beforeAll(async () => {
4+
await page.emulateTimezone('UTC')
45
await page.goto(
56
`http://localhost:8080/examples/${pattern}/components/datetime-format.html`
67
)

e2e/datetime.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
;['composition', 'legacy'].forEach(pattern => {
22
describe(`${pattern}`, () => {
33
beforeAll(async () => {
4+
await page.emulateTimezone('UTC')
45
await page.goto(`http://localhost:8080/examples/${pattern}/datetime.html`)
56
})
67

jest.e2e.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ module.exports = {
135135

136136
// The test environment that will be used for testing
137137
// testEnvironment: 'node',
138-
testEnvironment: './jest-e2e-environment.js',
138+
// testEnvironment: './jest-e2e-environment.js',
139139

140140
// Options that will be passed to the testEnvironment
141141
// testEnvironmentOptions: {},

packages/core-base/src/context.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,37 @@ export const DEFAULT_LOCALE = 'en-US'
260260

261261
export const MISSING_RESOLVE_VALUE = ''
262262

263+
const capitalize = (str: string) =>
264+
`${str.charAt(0).toLocaleUpperCase()}${str.substr(1)}`
265+
263266
function getDefaultLinkedModifiers<
264267
Message = string
265268
>(): LinkedModifiers<Message> {
266269
return {
267-
upper: (val: Message): MessageType<Message> =>
268-
(isString(val) ? val.toUpperCase() : val) as MessageType<Message>,
269-
lower: (val: Message): MessageType<Message> =>
270-
(isString(val) ? val.toLowerCase() : val) as MessageType<Message>,
271-
// prettier-ignore
272-
capitalize: (val: Message): MessageType<Message> =>
273-
(isString(val)
274-
? `${val.charAt(0).toLocaleUpperCase()}${val.substr(1)}`
275-
: val) as MessageType<Message>
270+
upper: (val: Message, type: string): MessageType<Message> => {
271+
// prettier-ignore
272+
return type === 'text' && isString(val)
273+
? val.toUpperCase()
274+
: type === 'vnode' && isObject(val) && '__v_isVNode' in val
275+
? (val as any).children.toUpperCase()
276+
: val
277+
},
278+
lower: (val: Message, type: string): MessageType<Message> => {
279+
// prettier-ignore
280+
return type === 'text' && isString(val)
281+
? val.toLowerCase()
282+
: type === 'vnode' && isObject(val) && '__v_isVNode' in val
283+
? (val as any).children.toLowerCase()
284+
: val
285+
},
286+
capitalize: (val: Message, type: string): MessageType<Message> => {
287+
// prettier-ignore
288+
return (type === 'text' && isString(val)
289+
? capitalize(val)
290+
: type === 'vnode' && isObject(val) && '__v_isVNode' in val
291+
? capitalize( (val as any).children)
292+
: val) as MessageType<Message>
293+
}
276294
}
277295
}
278296

packages/core-base/src/runtime.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
toDisplayString,
55
isObject,
66
isString,
7+
isArray,
78
isPlainObject
89
} from '@intlify/shared'
910
import { HelperNameMap } from '@intlify/message-compiler'
@@ -73,7 +74,10 @@ export type PluralizationProps = {
7374
count?: number
7475
}
7576

76-
export type LinkedModify<T = string> = (value: T) => MessageType<T>
77+
export type LinkedModify<T = string> = (
78+
value: T,
79+
type: string
80+
) => MessageType<T>
7781
/** @VueI18nGeneral */
7882
export type LinkedModifiers<T = string> = { [key: string]: LinkedModify<T> }
7983

@@ -93,12 +97,19 @@ export interface MessageContextOptions<T = string, N = {}> {
9397
processor?: MessageProcessor<T>
9498
}
9599

100+
export interface LinkedOptions {
101+
type?: string
102+
modifier?: string
103+
}
104+
96105
// TODO: list and named type definition more improvements
97106
export interface MessageContext<T = string> {
98107
list(index: number): unknown
99108
named(key: string): unknown
100109
plural(messages: T[]): T
101110
linked(key: Path, modifier?: string): MessageType<T>
111+
linked(key: Path, modifier?: string, type?: string): MessageType<T>
112+
linked(key: Path, optoins?: LinkedOptions): MessageType<T>
102113
message(key: Path): MessageFunction<T>
103114
type: string
104115
interpolate: MessageInterpolate<T>
@@ -167,8 +178,9 @@ export function createMessageContext<T = string, N = {}>(
167178
isFunction(options.pluralRules[locale])
168179
? pluralDefault
169180
: undefined
170-
const plural = (messages: T[]): T =>
171-
messages[pluralRule(pluralIndex, messages.length, orgPluralRule)]
181+
const plural = (messages: T[]): T => {
182+
return messages[pluralRule(pluralIndex, messages.length, orgPluralRule)]
183+
}
172184

173185
const _list = options.list || []
174186
const list = (index: number): unknown => _list[index]
@@ -208,16 +220,38 @@ export function createMessageContext<T = string, N = {}>(
208220
? options.processor.interpolate
209221
: (DEFAULT_INTERPOLATE as unknown as MessageInterpolate<T>)
210222

211-
const linked = (key: Path, modifier?: string): MessageType<T> => {
212-
const msg = message(key)(ctx)
213-
return isString(modifier) ? _modifier(modifier)(msg as T) : msg
214-
}
215-
216223
const type =
217224
isPlainObject(options.processor) && isString(options.processor.type)
218225
? options.processor.type
219226
: DEFAULT_MESSAGE_DATA_TYPE
220227

228+
const linked = (key: Path, ...args: unknown[]): MessageType<T> => {
229+
const [arg1, arg2] = args
230+
let type = 'text'
231+
let modifier = ''
232+
if (args.length === 1) {
233+
if (isObject(arg1)) {
234+
modifier = arg1.modifier || modifier
235+
type = arg1.type || type
236+
} else if (isString(arg1)) {
237+
modifier = arg1 || modifier
238+
}
239+
} else if (args.length === 2) {
240+
if (isString(arg1)) {
241+
modifier = arg1 || modifier
242+
}
243+
if (isString(arg2)) {
244+
type = arg2 || type
245+
}
246+
}
247+
let msg = message(key)(ctx)
248+
// The message in vnode resolved with linked are returned as an array by processor.nomalize
249+
if (type === 'vnode' && isArray(msg) && modifier) {
250+
msg = msg[0]
251+
}
252+
return modifier ? _modifier(modifier)(msg as T, type) : msg
253+
}
254+
221255
const ctx = {
222256
[HelperNameMap.LIST]: list,
223257
[HelperNameMap.NAMED]: named,

packages/core-base/test/helper.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// eumlate VNode interface on intlify
2+
// ref: github.com/vuejs/core/blob/2f07e3460bf51bc1b083f3d03b3d192e042d2d75/packages/runtime-core/src/vnode.ts#L131-L217
3+
4+
type VNodeChildAtom =
5+
| VNode
6+
| string
7+
| number
8+
| boolean
9+
| null
10+
| undefined
11+
| void
12+
13+
type VNodeArrayChildren = Array<VNodeArrayChildren | VNodeChildAtom>
14+
15+
type VNodeNormalizedChildren =
16+
| string
17+
| VNodeArrayChildren
18+
// | RawSlots
19+
| null
20+
21+
export interface VNode {
22+
__v_isVNode: true
23+
children: VNodeNormalizedChildren
24+
[field: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any
25+
}
26+
27+
export function createTextNode(key: string): VNode {
28+
return {
29+
__v_isVNode: true,
30+
children: key
31+
}
32+
}

packages/core-base/test/translate.test.ts

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ jest.mock('@intlify/shared', () => ({
55
...jest.requireActual<object>('@intlify/shared'),
66
warn: jest.fn()
77
}))
8-
import { warn } from '@intlify/shared'
8+
import { warn, isString, isNumber, isBoolean } from '@intlify/shared'
99

1010
import { createCoreContext as context, NOT_REOSLVED } from '../src/context'
1111
import { translate } from '../src/translate'
@@ -18,8 +18,12 @@ import {
1818
import { compileToFunction } from '../src/compile'
1919
import { fallbackWithLocaleChain } from '../src/fallbacker'
2020
import { resolveValue } from '../src/resolver'
21+
import { createTextNode } from './helper'
2122

2223
import type { MessageContext } from '../src/runtime'
24+
import type { VNode } from './helper'
25+
import type { MessageType, MessageProcessor } from '../src/runtime'
26+
import type { PickupKeys } from '../src/types/utils'
2327

2428
beforeEach(() => {
2529
registerMessageCompiler(compileToFunction)
@@ -810,4 +814,152 @@ test('fallback context', () => {
810814

811815
expect(translate(ctx, 'hi')).toEqual('hi! hello man!')
812816
})
817+
818+
describe('processor', () => {
819+
// VNode processor
820+
function normalize(
821+
values: MessageType<string | VNode>[]
822+
): MessageType<VNode>[] {
823+
return values.map(val =>
824+
isString(val) || isNumber(val) || isBoolean(val)
825+
? createTextNode(String(val))
826+
: val
827+
)
828+
}
829+
const interpolate = (val: unknown): MessageType<VNode> => val as VNode
830+
const processor = {
831+
normalize,
832+
interpolate,
833+
type: 'vnode'
834+
} as MessageProcessor<VNode>
835+
836+
test('basic', () => {
837+
const ctx = context<VNode>({
838+
locale: 'en',
839+
messages: {
840+
en: { hi: 'hi kazupon !' }
841+
}
842+
})
843+
ctx.processor = processor
844+
expect(
845+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
846+
ctx,
847+
'hi'
848+
)
849+
).toEqual([{ __v_isVNode: true, children: 'hi kazupon !' }])
850+
})
851+
852+
test('list', () => {
853+
const ctx = context<VNode>({
854+
locale: 'en',
855+
messages: {
856+
en: { hi: 'hi {0} !', nest: { foo: '' } }
857+
}
858+
})
859+
ctx.processor = processor
860+
expect(
861+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
862+
ctx,
863+
'hi',
864+
['kazupon']
865+
)
866+
).toEqual([
867+
{ __v_isVNode: true, children: 'hi ' },
868+
{ __v_isVNode: true, children: 'kazupon' },
869+
{ __v_isVNode: true, children: ' !' }
870+
])
871+
})
872+
873+
test('named', () => {
874+
const ctx = context<VNode>({
875+
locale: 'en',
876+
messages: {
877+
en: { hi: 'hi {name} !' }
878+
}
879+
})
880+
ctx.processor = processor
881+
expect(
882+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
883+
ctx,
884+
'hi',
885+
{ name: 'kazupon' }
886+
)
887+
).toEqual([
888+
{ __v_isVNode: true, children: 'hi ' },
889+
{ __v_isVNode: true, children: 'kazupon' },
890+
{ __v_isVNode: true, children: ' !' }
891+
])
892+
})
893+
894+
test('linked', () => {
895+
const ctx = context<VNode>({
896+
locale: 'en',
897+
messages: {
898+
en: {
899+
name: 'kazupon',
900+
hi: 'hi @.upper:name !'
901+
}
902+
}
903+
})
904+
ctx.processor = processor
905+
expect(
906+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
907+
ctx,
908+
'hi'
909+
)
910+
).toEqual([
911+
{ __v_isVNode: true, children: 'hi ' },
912+
{ __v_isVNode: true, children: 'KAZUPON' },
913+
{ __v_isVNode: true, children: ' !' }
914+
])
915+
})
916+
917+
test('plural', () => {
918+
const ctx = context<VNode>({
919+
locale: 'en',
920+
messages: {
921+
en: { apple: 'no apples | one apple | {count} apples from {name}' }
922+
}
923+
})
924+
ctx.processor = processor
925+
expect(
926+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
927+
ctx,
928+
'apple',
929+
0
930+
)
931+
).toEqual([{ __v_isVNode: true, children: 'no apples' }])
932+
expect(
933+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
934+
ctx,
935+
'apple',
936+
1
937+
)
938+
).toEqual([{ __v_isVNode: true, children: 'one apple' }])
939+
expect(
940+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
941+
ctx,
942+
'apple',
943+
10
944+
)
945+
).toEqual([
946+
{ __v_isVNode: true, children: '10' },
947+
{ __v_isVNode: true, children: ' apples from ' },
948+
undefined
949+
])
950+
expect(
951+
translate<typeof ctx, string, PickupKeys<typeof ctx.messages>, VNode>(
952+
ctx,
953+
'apple',
954+
{ count: 20, name: 'kazupon' },
955+
10
956+
)
957+
).toEqual([
958+
{ __v_isVNode: true, children: '20' },
959+
{ __v_isVNode: true, children: ' apples from ' },
960+
{ __v_isVNode: true, children: 'kazupon' }
961+
])
962+
})
963+
})
964+
813965
/* eslint-enable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */

packages/message-compiler/src/generator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ function generateLinkedNode(generator: CodeGenerator, node: LinkedNode): void {
150150
if (node.modifier) {
151151
generator.push(`, `)
152152
generateNode(generator, node.modifier)
153+
generator.push(`, _type`)
154+
} else {
155+
generator.push(`, undefined, _type`)
153156
}
154157
generator.push(`)`)
155158
}

0 commit comments

Comments
 (0)