From 2d12488395687642cf72abbe900f03fd17779b3c Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 14 Feb 2025 11:57:19 +0200 Subject: [PATCH 1/2] Add support for mapping translation parameters --- __tests__/TranslationContext.spec.ts | 39 ++++++++++++++++++++++++++++ src/TranslationContext.ts | 25 +++++++++++++----- src/index.ts | 14 ++++++---- src/types/index.d.ts | 10 ++++++- 4 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 __tests__/TranslationContext.spec.ts diff --git a/__tests__/TranslationContext.spec.ts b/__tests__/TranslationContext.spec.ts new file mode 100644 index 00000000..a56bab5c --- /dev/null +++ b/__tests__/TranslationContext.spec.ts @@ -0,0 +1,39 @@ +import { FluentBundle, FluentResource } from '@fluent/bundle' +import { describe, expect, it, vi } from 'vitest' + +import { ref } from 'vue-demi' + +import { TranslationContext } from '../src/TranslationContext' + +describe('translationContext', () => { + it('should format a message', () => { + const bundle = new FluentBundle('en-US', { useIsolating: false }) + bundle.addResource(new FluentResource('hello = Hello!')) + + const context = new TranslationContext(ref([bundle]), { warnMissing: vi.fn(), parseMarkup: vi.fn() }) + expect(context.format('hello')).toBe('Hello!') + }) + + it('should format a message with a value', () => { + const bundle = new FluentBundle('en-US', { useIsolating: false }) + bundle.addResource(new FluentResource('hello = Hello {$name}!')) + + const context = new TranslationContext(ref([bundle]), { warnMissing: vi.fn(), parseMarkup: vi.fn() }) + expect(context.format('hello', { name: 'John' })).toBe('Hello John!') + }) + + it('should format a message with a value and custom types', () => { + const bundle = new FluentBundle('en-US', { useIsolating: false }) + bundle.addResource(new FluentResource('hello = Hello {$name} it is {$date}!')) + + const context = new TranslationContext(ref([bundle]), { + warnMissing: vi.fn(), + parseMarkup: vi.fn(), + mapVariable: (variable) => { + if (variable instanceof Date) + return variable.toLocaleDateString('en-UK') + }, + }) + expect(context.format('hello', { name: 'John', date: new Date(0) })).toBe('Hello John it is 01/01/1970!') + }) +}) diff --git a/src/TranslationContext.ts b/src/TranslationContext.ts index fdbe0a0a..49d7a9a1 100644 --- a/src/TranslationContext.ts +++ b/src/TranslationContext.ts @@ -1,5 +1,6 @@ import type { FluentBundle, FluentVariable } from '@fluent/bundle' import type { Message, Pattern } from '@fluent/bundle/esm/ast' +import type { TypesConfig } from 'src' import type { Ref } from 'vue-demi' import type { TranslationContextOptions } from './types' import { mapBundleSync } from '@fluent/sequence' @@ -38,10 +39,20 @@ export class TranslationContext { bundle: FluentBundle, key: string, message: Pattern, - value?: Record, + value?: Record, ): string { const errors: Error[] = [] - const formatted = bundle.formatPattern(message, value, errors) + + const mappedValue = value + if (mappedValue != null && this.options.mapVariable != null) { + for (const [key, variable] of Object.entries(mappedValue)) { + const mappedVariable = this.options.mapVariable(variable) + if (mappedVariable != null) + mappedValue[key] = mappedVariable + } + } + + const formatted = bundle.formatPattern(message, mappedValue, errors) for (const error of errors) warn(`Error when formatting message with key [${key}]`, error) @@ -52,7 +63,7 @@ export class TranslationContext { private _format( context: FluentBundle | null, message: Message | null, - value?: Record, + value?: Record, ): string | null { if (context === null || message === null || message.value === null) return null @@ -60,7 +71,7 @@ export class TranslationContext { return this.formatPattern(context, message.id, message.value, value) } - format = (key: string, value?: Record): string => { + format = (key: string, value?: Record): string => { const context = this.getBundle(key) const message = this.getMessage(context, key) return this._format(context, message, value) ?? key @@ -69,7 +80,7 @@ export class TranslationContext { private _formatAttrs( context: FluentBundle | null, message: Message | null, - value?: Record, + value?: Record, ): Record | null { if (context === null || message === null) return null @@ -81,13 +92,13 @@ export class TranslationContext { return result } - formatAttrs = (key: string, value?: Record): Record => { + formatAttrs = (key: string, value?: Record): Record => { const context = this.getBundle(key) const message = this.getMessage(context, key) return this._formatAttrs(context, message, value) ?? {} } - formatWithAttrs = (key: string, value?: Record): TranslationWithAttrs => { + formatWithAttrs = (key: string, value?: Record): TranslationWithAttrs => { const context = this.getBundle(key) const message = this.getMessage(context, key) diff --git a/src/index.ts b/src/index.ts index 6dca610f..c2d65de0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,20 +16,24 @@ import './types/volar' export { useFluent } from './composition' +export interface TypesConfig { + customVariableTypes: never +} + export interface FluentVue { /** Current negotiated fallback chain of languages */ bundles: Iterable - format: (key: string, value?: Record) => string + format: (key: string, value?: Record) => string - formatAttrs: (key: string, value?: Record) => Record + formatAttrs: (key: string, value?: Record) => Record - formatWithAttrs: (key: string, value?: Record) => TranslationWithAttrs + formatWithAttrs: (key: string, value?: Record) => TranslationWithAttrs mergedWith: (extraTranslations?: Record) => TranslationContext - $t: (key: string, value?: Record) => string - $ta: (key: string, value?: Record) => Record + $t: (key: string, value?: Record) => string + $ta: (key: string, value?: Record) => Record install: InstallFunction } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index fa376ccd..c5749aa6 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,4 +1,5 @@ -import type { FluentBundle } from '@fluent/bundle' +import type { FluentBundle, FluentVariable } from '@fluent/bundle' +import type { TypesConfig } from 'src' type SimpleNode = Pick @@ -28,11 +29,18 @@ export interface FluentVueOptions { * @default 'span' */ componentTag?: string | false + + /** + * Function that converts a custom value to a FluentVariable. + * This is useful for adding support for types that are not supported by fluent.js. + */ + mapVariable?: (value: TypesConfig['customTypes'] | FluentVariable) => FluentVariable | undefined } export interface TranslationContextOptions { warnMissing: (key: string) => void parseMarkup: (markup: string) => SimpleNode[] + mapVariable?: (value: TypesConfig['customTypes'] | FluentVariable) => FluentVariable | undefined } export interface ResolvedOptions extends TranslationContextOptions { From 43ebd528384942e258e21beb13816fb8a393a8d0 Mon Sep 17 00:00:00 2001 From: Ivan Demchuk Date: Fri, 14 Feb 2025 12:52:55 +0200 Subject: [PATCH 2/2] Fix actually passing config to the TranslationContext --- __tests__/vue/plugin.spec.ts | 24 ++++++++++++++++++++++++ src/util/options.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/__tests__/vue/plugin.spec.ts b/__tests__/vue/plugin.spec.ts index 8001e69d..4309d90a 100644 --- a/__tests__/vue/plugin.spec.ts +++ b/__tests__/vue/plugin.spec.ts @@ -101,4 +101,28 @@ describe('vue integration', () => { '
Hello, \u{2068}John\u{2069}!
Hello from child component, \u{2068}Alice\u{2069}
\n
', ) }) + + it('allows specifying custom variable mapping function', () => { + // Arrange + const fluent = createFluentVue({ + bundles: [bundle], + mapVariable: (variable) => { + if (typeof variable === 'string') + return variable.toUpperCase() + }, + }) + + const component = { + data: () => ({ + name: 'John', + }), + template: '
{{ $t("message", { name }) }}
', + } + + // Act + const mounted = mountWithFluent(fluent, component) + + // Assert + expect(mounted.html()).toEqual('
Hello, \u{2068}JOHN\u{2069}!
') + }) }) diff --git a/src/util/options.ts b/src/util/options.ts index 4473267a..f4f2fd89 100644 --- a/src/util/options.ts +++ b/src/util/options.ts @@ -29,6 +29,7 @@ export function resolveOptions(options: FluentVueOptions): ResolvedOptions { return { warnMissing: getWarnMissing(options), parseMarkup: options.parseMarkup ?? defaultMarkupParser, + mapVariable: options.mapVariable, globalFormatName: options.globals?.functions?.format ?? '$t', globalFormatAttrsName: options.globals?.functions?.formatAttrs ?? '$ta', directiveName: options.globals?.directive ?? 't',