diff --git a/packages/vue-i18n-core/src/composer.ts b/packages/vue-i18n-core/src/composer.ts index 1a80faa7d..94ffbe4e7 100644 --- a/packages/vue-i18n-core/src/composer.ts +++ b/packages/vue-i18n-core/src/composer.ts @@ -1203,13 +1203,18 @@ export interface ComposerNumberFormatting< * * @returns Formatted value */ - ( + < + Key extends string = string, + Return extends string | Intl.NumberFormatPart[] = + | string + | Intl.NumberFormatPart[] + >( value: number, keyOrOptions: | Key | ResourceKeys | NumberOptions - ): string + ): Return /** * Number Formatting * @@ -1224,14 +1229,19 @@ export interface ComposerNumberFormatting< * * @returns Formatted value */ - ( + < + Key extends string = string, + Return extends string | Intl.NumberFormatPart[] = + | string + | Intl.NumberFormatPart[] + >( value: number, keyOrOptions: | Key | ResourceKeys | NumberOptions, locale: Locales - ): string + ): Return } /** @@ -2287,14 +2297,17 @@ 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]) as + | string + | Intl.NumberFormatPart[], () => 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..fd8b7eb9b 100644 --- a/packages/vue-i18n-core/test/composer.test-d.ts +++ b/packages/vue-i18n-core/test/composer.test-d.ts @@ -353,15 +353,34 @@ 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< + string | Intl.NumberFormatPart[] + >() + expectTypeOf( + strictDirectComposer.n(1, 'currency', 'zh') + ).toEqualTypeOf() expectTypeOf( - strictDirectComposer.n(1, 'currency', 'zh') + strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) ).toEqualTypeOf() expectTypeOf( - strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) + strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) ).toEqualTypeOf() expectTypeOf( - strictDirectComposer.n(1, 'custom' as any) + strictDirectComposer.n(1, { + key: 'currency', + locale: 'en', + part: true + }) + ).toEqualTypeOf() + expectTypeOf(strictDirectComposer.n(1, 'currency')).toEqualTypeOf< + string | Intl.NumberFormatPart[] + >() + expectTypeOf( + strictDirectComposer.n(1, 'currency') ).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 6b474ff8e..5b589fca4 100644 --- a/packages/vue-i18n-core/test/composer.test.ts +++ b/packages/vue-i18n-core/test/composer.test.ts @@ -5,6 +5,7 @@ // utils import * as shared from '@intlify/shared' +import { pluralRules as _pluralRules } from './helper' vi.mock('@intlify/shared', async () => { const actual = await vi.importActual('@intlify/shared') return { @@ -12,34 +13,33 @@ vi.mock('@intlify/shared', async () => { warn: vi.fn() } }) -import { pluralRules as _pluralRules } from './helper' import { + compile, + fallbackWithLocaleChain, + Locale, + MessageContext, + MessageFunction, + Path, + PathValue, + registerLocaleFallbacker, + registerMessageCompiler, + registerMessageResolver, + resolveValue +} from '@intlify/core-base' +import { createVNode, nextTick, Text, watch, watchEffect } from 'vue' +import { + ComposerOptions, createComposer, MissingHandler, - ComposerOptions, VueMessageType } from '../src/composer' import { - TranslateVNodeSymbol, + DatetimePartsSymbol, NumberPartsSymbol, - DatetimePartsSymbol + TranslateVNodeSymbol } from '../src/symbols' import { getWarnMessage, I18nWarnCodes } from '../src/warnings' -import { watch, watchEffect, nextTick, Text, createVNode } from 'vue' -import { - Locale, - compile, - registerMessageCompiler, - resolveValue, - registerMessageResolver, - fallbackWithLocaleChain, - registerLocaleFallbacker, - MessageContext, - Path, - PathValue, - MessageFunction -} from '@intlify/core-base' beforeEach(() => { registerMessageCompiler(compile) @@ -1202,6 +1202,79 @@ describe('n', () => { }) expect(n(0.99, { key: 'percent' })).toEqual('') }) + + test('part formatting 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', + part: true + }) + ).toEqual([ + { type: 'currency', value: '¥' }, + { type: 'integer', value: '10' }, + { type: 'group', value: ',' }, + { type: 'integer', value: '100' } + ]) + 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', () => { diff --git a/packages/vue-i18n/src/vue.d.ts b/packages/vue-i18n/src/vue.d.ts index 25d5f7059..b84917eb3 100644 --- a/packages/vue-i18n/src/vue.d.ts +++ b/packages/vue-i18n/src/vue.d.ts @@ -865,7 +865,23 @@ declare module 'vue' { * * @returns formatted value */ - $n(value: number, options: NumberOptions): string + $n< + Key extends string, + Return extends string | Intl.NumberFormatPart[] = + | string + | Intl.NumberFormatPart[], + DefinedNumberFormat extends + RemovedIndexResources = RemovedIndexResources, + Keys = IsEmptyObject extends false + ? PickupFormatPathKeys<{ + [K in keyof DefinedNumberFormat]: DefinedNumberFormat[K] + }> + : never, + ResourceKeys extends Keys = IsNever extends false ? Keys : never + >( + value: number, + options: NumberOptions + ): Return /** * Locale messages getter *