From d44886cb99a91845477b6f0d277d7ba3d6b9652a Mon Sep 17 00:00:00 2001 From: Paritosh Maurya Date: Mon, 28 Apr 2025 15:51:48 +0530 Subject: [PATCH 1/2] Back ported part option functionality for () --- packages/vue-i18n-core/src/composer.ts | 15 ++- .../vue-i18n-core/test/composer.test-d.ts | 14 +-- packages/vue-i18n-core/test/composer.test.ts | 109 +++++++++++++++--- packages/vue-i18n/src/vue.d.ts | 5 +- 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/packages/vue-i18n-core/src/composer.ts b/packages/vue-i18n-core/src/composer.ts index 1a80faa7d..41fbb19b6 100644 --- a/packages/vue-i18n-core/src/composer.ts +++ b/packages/vue-i18n-core/src/composer.ts @@ -1209,7 +1209,7 @@ export interface ComposerNumberFormatting< | Key | ResourceKeys | NumberOptions - ): string + ): string | Intl.NumberFormatPart[] /** * Number Formatting * @@ -1231,7 +1231,7 @@ export interface ComposerNumberFormatting< | ResourceKeys | NumberOptions, locale: Locales - ): string + ): string | Intl.NumberFormatPart[] } /** @@ -2287,14 +2287,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..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 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..e663c80ab 100644 --- a/packages/vue-i18n/src/vue.d.ts +++ b/packages/vue-i18n/src/vue.d.ts @@ -865,7 +865,10 @@ declare module 'vue' { * * @returns formatted value */ - $n(value: number, options: NumberOptions): string + $n( + value: number, + options: OptionsType + ): OptionsType['type'] extends true ? Intl.NumberFormatPart[] : string /** * Locale messages getter * From 5ec1164413bdb2c84fade8352b58a67cc46524be Mon Sep 17 00:00:00 2001 From: Paritosh Maurya Date: Mon, 28 Apr 2025 18:38:55 +0530 Subject: [PATCH 2/2] Added type safety --- packages/vue-i18n-core/src/composer.ts | 18 +++++++++++---- .../vue-i18n-core/test/composer.test-d.ts | 23 +++++++++++++++++-- packages/vue-i18n/src/vue.d.ts | 19 ++++++++++++--- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/vue-i18n-core/src/composer.ts b/packages/vue-i18n-core/src/composer.ts index 41fbb19b6..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 | Intl.NumberFormatPart[] + ): 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 | Intl.NumberFormatPart[] + ): Return } /** diff --git a/packages/vue-i18n-core/test/composer.test-d.ts b/packages/vue-i18n-core/test/composer.test-d.ts index 97d0f156a..fd8b7eb9b 100644 --- a/packages/vue-i18n-core/test/composer.test-d.ts +++ b/packages/vue-i18n-core/test/composer.test-d.ts @@ -357,8 +357,27 @@ test('strict composer with direct options', () => { string | Intl.NumberFormatPart[] >() expectTypeOf( - strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) - ).toEqualTypeOf() + strictDirectComposer.n(1, 'currency', 'zh') + ).toEqualTypeOf() + expectTypeOf( + strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) + ).toEqualTypeOf() + expectTypeOf( + strictDirectComposer.n(1, { key: 'currency', locale: 'en' }) + ).toEqualTypeOf() + expectTypeOf( + 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[] >() diff --git a/packages/vue-i18n/src/vue.d.ts b/packages/vue-i18n/src/vue.d.ts index e663c80ab..b84917eb3 100644 --- a/packages/vue-i18n/src/vue.d.ts +++ b/packages/vue-i18n/src/vue.d.ts @@ -865,10 +865,23 @@ declare module 'vue' { * * @returns formatted value */ - $n( + $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: OptionsType - ): OptionsType['type'] extends true ? Intl.NumberFormatPart[] : string + options: NumberOptions + ): Return /** * Locale messages getter *