From 0999dd8f14af481f02fef2a6e5cfff8016f60f04 Mon Sep 17 00:00:00 2001 From: Paritosh Maurya Date: Sat, 26 Apr 2025 22:16:30 +0530 Subject: [PATCH 1/3] Added support for part option in Added test cases for the same --- packages/vue-i18n-core/src/composer.ts | 12 +-- .../vue-i18n-core/test/composer.test-d.ts | 14 ++-- packages/vue-i18n-core/test/composer.test.ts | 77 +++++++++++++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/packages/vue-i18n-core/src/composer.ts b/packages/vue-i18n-core/src/composer.ts index f696e53aa..8256be2bc 100644 --- a/packages/vue-i18n-core/src/composer.ts +++ b/packages/vue-i18n-core/src/composer.ts @@ -1207,7 +1207,7 @@ export interface ComposerNumberFormatting< | Key | ResourceKeys | NumberOptions - ): string + ): string | Intl.NumberFormatPart[] /** * Number Formatting * @@ -1229,7 +1229,7 @@ export interface ComposerNumberFormatting< | ResourceKeys | NumberOptions, locale: Locales - ): string + ): string | Intl.NumberFormatPart[] } /** @@ -2290,14 +2290,14 @@ export function createComposer(options: any = {}): any { } // n - function n(...args: unknown[]): string { - return wrapWithDeps<{}, string>( - context => Reflect.apply(number, null, [context, ...args]) as string, + function n(...args: unknown[]): string | Intl.NumberFormatPart[] { + return wrapWithDeps<{}, string | Intl.NumberFormatPart[]>( + context => Reflect.apply(number, null, [context, ...args]), () => parseNumberArgs(...args), 'number format', root => Reflect.apply(root.n, root, [...args]), () => MISSING_RESOLVE_VALUE, - val => isString(val) + val => isString(val) || isArray(val) ) } diff --git a/packages/vue-i18n-core/test/composer.test-d.ts b/packages/vue-i18n-core/test/composer.test-d.ts index 1e05f5461..97d0f156a 100644 --- a/packages/vue-i18n-core/test/composer.test-d.ts +++ b/packages/vue-i18n-core/test/composer.test-d.ts @@ -353,15 +353,15 @@ test('strict composer with direct options', () => { strictDirectComposer.d(new Date(), 'custom' as any) ).toEqualTypeOf() expectTypeOf(strictDirectComposer.n(1)).toEqualTypeOf() - expectTypeOf( - strictDirectComposer.n(1, 'currency', 'zh') - ).toEqualTypeOf() + expectTypeOf(strictDirectComposer.n(1, 'currency', 'zh')).toEqualTypeOf< + string | Intl.NumberFormatPart[] + >() expectTypeOf( strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) - ).toEqualTypeOf() - expectTypeOf( - strictDirectComposer.n(1, 'custom' as any) - ).toEqualTypeOf() + ).toEqualTypeOf() + expectTypeOf(strictDirectComposer.n(1, 'custom' as any)).toEqualTypeOf< + string | Intl.NumberFormatPart[] + >() // const noOptionsComposer = createComposer({ missingWarn: true }) const noOptionsComposer = createComposer({ locale: 'en' }) diff --git a/packages/vue-i18n-core/test/composer.test.ts b/packages/vue-i18n-core/test/composer.test.ts index 59ff15c30..74f541098 100644 --- a/packages/vue-i18n-core/test/composer.test.ts +++ b/packages/vue-i18n-core/test/composer.test.ts @@ -1213,6 +1213,83 @@ describe('n', () => { }) expect(n(0.99, { key: 'percent' })).toEqual('') }) + + test('part formating with n', () => { + const { n } = createComposer({ + locale: 'en-US', + fallbackLocale: ['ja-JP'], + numberFormats: { + 'en-US': { + currency: { + style: 'currency', + currency: 'USD', + currencyDisplay: 'symbol' + }, + decimal: { + style: 'decimal', + useGrouping: true + } + }, + 'ja-JP': { + currency: { + style: 'currency', + currency: 'JPY' /*, currencyDisplay: 'symbol'*/ + }, + numeric: { + style: 'decimal', + useGrouping: false + }, + percent: { + style: 'percent', + useGrouping: true + } + } + } + }) + expect(n(0.99, { key: 'currency', part: true })).toEqual([ + { type: 'currency', value: '$' }, + { type: 'integer', value: '0' }, + { type: 'decimal', value: '.' }, + { type: 'fraction', value: '99' } + ]) + expect( + n(10100, { + key: 'currency', + locale: 'ja-JP', + currency: 'EUR', + part: true + }) + ).toEqual([ + { type: 'currency', value: '€' }, + { type: 'integer', value: '10' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '100' }, + { type: 'decimal', value: '.' }, + { type: 'fraction', value: '00' } + ]) + // expect(n(12145281111, 'decimal', 'ja-JP')).toEqual([]) + expect(n(12145281000, { key: 'percent', part: true })).toEqual([ + { type: 'integer', value: '1' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '214' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '528' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '100' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '000' }, + { type: 'percentSign', value: '%' } + ]) + expect(n(12145281111, { key: 'decimal', part: true })).toEqual([ + { type: 'integer', value: '12' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '145' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '281' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '111' } + ]) + }) }) describe('tm', () => { From feb8aab1c7a16b626ce7c77260a7021e6161ac57 Mon Sep 17 00:00:00 2001 From: Paritosh Maurya Date: Sun, 27 Apr 2025 19:23:44 +0530 Subject: [PATCH 2/3] Updated function defination for --- packages/vue-i18n/src/vue.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vue-i18n/src/vue.d.ts b/packages/vue-i18n/src/vue.d.ts index c3a53f405..2fe1a8722 100644 --- a/packages/vue-i18n/src/vue.d.ts +++ b/packages/vue-i18n/src/vue.d.ts @@ -846,7 +846,10 @@ declare module 'vue' { * * @returns formatted value */ - $n(value: number, options: NumberOptions): string + $n( + value: number, + options: OptionsType + ): OptionsType['part'] extends true ? Intl.NumberFormatPart[] : string /** * Locale messages getter * From 42bc36c8a96d6fa99504e96010c202efd66f9bef Mon Sep 17 00:00:00 2001 From: mauryapari Date: Sun, 11 May 2025 22:16:43 +0530 Subject: [PATCH 3/3] Added examples --- docs/guide/essentials/syntax.md | 69 +++++++++++++++++++- packages/vue-i18n-core/test/composer.test.ts | 30 +++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/guide/essentials/syntax.md b/docs/guide/essentials/syntax.md index 1ad631c75..f000ac6c2 100644 --- a/docs/guide/essentials/syntax.md +++ b/docs/guide/essentials/syntax.md @@ -237,10 +237,77 @@ It’s `en` locale that has hierarchical structure in the object. The `message.snake` has `snake case`. The `message.custom_modifier` has `custom modifiers example: @.snakeCase:{'message.snake'}`, and it’s linked to the locale messages key, which is interpolated with literal. :::tip NOTE -You can use the interpolations (Named, List, and Literal) for the key of Linked messages. +You can use the interpolations (Named, List, and Literal) for the key of Linked messages shown below. ::: +This example shows the use of modifiers (`@.lower`, `@.upper`, `@.capitalize`) combined with named, list, and literal interpolations. + + +```js +const messages = { + en: { + message: { + greeting: "Hello, @.lower:{'message.name'}! You have {count} new messages.", + name:"{name}" + }, + + welcome: "Welcome, @.upper:{'name'}! Today is @.capitalize:{'day'}.", + name: '{0}', + day: '{1}', + + literalMessage: "This is an email: foo{'@'}@.lower:domain", + domain: 'SHOUTING' + } +} +``` +### Named interpolation with modifier + +In `message.greeting`, we use a named interpolation for `{count}` and link to `message.name`, applying the .lower modifier. + +The key `message.name` contains `{name}`, which will be interpolated with the passed `name` param. + +The `message.greeting` is linked to the locale message key `message.name`. + +```html +

{{ $t('message.greeting', { name: 'Alice', count: 5 }) }}

+``` +As result, the below + +```html +

Hello, alice! You have 5 new messages.

+``` + +### List interpolation with modifier + +In this case, the values for `{0}` and `{1}` are passed as an array. The keys `name` and `day` are resolved using list interpolation and transformed with modifiers. + +```html +

{{ $t('welcome', ['bob', 'MONDAY']) }}

+``` + +As result, the below + +```html +

Welcome, BOB! Today is Monday.

+``` + +### Literal interpolation with modifier + +In this example, we use a literal string inside the message and apply the `.lower` modifier. + +```html +

{{ $t('literalMessage') }}

+``` + +Here, the modifier is applied to the content inside `domain`, and the `@` is preserved as literal output. + +As result, the below + +```html +

This is an email: foo@shouting

+``` + ## Special Characters The following characters used in the message format syntax are processed by the compiler as special characters: diff --git a/packages/vue-i18n-core/test/composer.test.ts b/packages/vue-i18n-core/test/composer.test.ts index 5c5636aae..11300b7dd 100644 --- a/packages/vue-i18n-core/test/composer.test.ts +++ b/packages/vue-i18n-core/test/composer.test.ts @@ -359,6 +359,36 @@ describe('modifiers', () => { expect(t('hi')).toEqual('hi hello-world') }) + test('Modifiers with Named, List and Literal Interpolation', () => { + const { t } = createComposer({ + locale: 'en', + messages: { + en: { + message: { + greeting: + "Hello, @.lower:{'message.name'}! You have {count} new messages.", + name: '{name}' + }, + + welcome: "Welcome, @.upper:{'name'}! Today is @.capitalize:{'day'}.", + name: '{0}', + day: '{1}', + + literalMessage: "This is an email: foo{'@'}@.lower:domain", + domain: 'SHOUTING' + } + } + }) + + expect(t('message.greeting', { name: 'Alice', count: 5 })).toEqual( + 'Hello, alice! You have 5 new messages.' + ) + expect(t('welcome', ['bob', 'monday'])).toEqual( + 'Welcome, BOB! Today is Monday.' + ) + expect(t('literalMessage')).toEqual('This is an email: foo@shouting') + }) + test('pascal case', () => { const _modifiers = { snakeCase: (str: VueMessageType) =>