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', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy