Skip to content

Add support for mapping translation parameters #920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions __tests__/TranslationContext.spec.ts
Original file line number Diff line number Diff line change
@@ -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!')
})
})
24 changes: 24 additions & 0 deletions __tests__/vue/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,28 @@ describe('vue integration', () => {
'<div>Hello, \u{2068}John\u{2069}!<div>Hello from child component, \u{2068}Alice\u{2069}</div>\n</div>',
)
})

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: '<div>{{ $t("message", { name }) }}</div>',
}

// Act
const mounted = mountWithFluent(fluent, component)

// Assert
expect(mounted.html()).toEqual('<div>Hello, \u{2068}JOHN\u{2069}!</div>')
})
})
25 changes: 18 additions & 7 deletions src/TranslationContext.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -38,10 +39,20 @@ export class TranslationContext {
bundle: FluentBundle,
key: string,
message: Pattern,
value?: Record<string, FluentVariable>,
value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>,
): 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)
Expand All @@ -52,15 +63,15 @@ export class TranslationContext {
private _format(
context: FluentBundle | null,
message: Message | null,
value?: Record<string, FluentVariable>,
value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>,
): string | null {
if (context === null || message === null || message.value === null)
return null

return this.formatPattern(context, message.id, message.value, value)
}

format = (key: string, value?: Record<string, FluentVariable>): string => {
format = (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>): string => {
const context = this.getBundle(key)
const message = this.getMessage(context, key)
return this._format(context, message, value) ?? key
Expand All @@ -69,7 +80,7 @@ export class TranslationContext {
private _formatAttrs(
context: FluentBundle | null,
message: Message | null,
value?: Record<string, FluentVariable>,
value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>,
): Record<string, string> | null {
if (context === null || message === null)
return null
Expand All @@ -81,13 +92,13 @@ export class TranslationContext {
return result
}

formatAttrs = (key: string, value?: Record<string, FluentVariable>): Record<string, string> => {
formatAttrs = (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>): Record<string, string> => {
const context = this.getBundle(key)
const message = this.getMessage(context, key)
return this._formatAttrs(context, message, value) ?? {}
}

formatWithAttrs = (key: string, value?: Record<string, FluentVariable>): TranslationWithAttrs => {
formatWithAttrs = (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>): TranslationWithAttrs => {
const context = this.getBundle(key)
const message = this.getMessage(context, key)

Expand Down
14 changes: 9 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FluentBundle>

format: (key: string, value?: Record<string, FluentVariable>) => string
format: (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>) => string

formatAttrs: (key: string, value?: Record<string, FluentVariable>) => Record<string, string>
formatAttrs: (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>) => Record<string, string>

formatWithAttrs: (key: string, value?: Record<string, FluentVariable>) => TranslationWithAttrs
formatWithAttrs: (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>) => TranslationWithAttrs

mergedWith: (extraTranslations?: Record<string, FluentResource>) => TranslationContext

$t: (key: string, value?: Record<string, FluentVariable>) => string
$ta: (key: string, value?: Record<string, FluentVariable>) => Record<string, string>
$t: (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>) => string
$ta: (key: string, value?: Record<string, FluentVariable | TypesConfig['customVariableTypes']>) => Record<string, string>

install: InstallFunction
}
Expand Down
10 changes: 9 additions & 1 deletion src/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<Node, 'nodeType' | 'textContent' | 'nodeValue'>

Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/util/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down