Skip to content

Commit

Permalink
feat: add support for vinejs messages provider
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Sep 8, 2023
1 parent 75e1405 commit 118cfa6
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@types/luxon": "^3.3.2",
"@types/negotiator": "^0.6.1",
"@types/node": "^20.5.9",
"@vinejs/vine": "^1.6.0",
"c8": "^8.0.1",
"copyfiles": "^2.4.1",
"del-cli": "^5.1.0",
Expand Down
12 changes: 10 additions & 2 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Emitter } from '@adonisjs/core/events'
import type { I18nManager } from './i18n_manager.js'
import { Formatter } from './formatters/values_formatter.js'
import type { MissingTranslationEventPayload } from './types/main.js'
import { I18nMessagesProvider } from './i18n_messages_provider.js'

/**
* I18n exposes the APIs to format values and translate messages
Expand Down Expand Up @@ -41,6 +42,13 @@ export class I18n extends Formatter {
return this.#i18nManager.getFallbackLocaleFor(this.locale)
}

/**
* Creates a messages provider for VineJS
*/
createMessagesProvider(prefix: string = 'validator.shared') {
return new I18nMessagesProvider(prefix, this)
}

constructor(
locale: string,
emitter: Emitter<{ 'i18n:missing:translation': MissingTranslationEventPayload } & any>,
Expand All @@ -67,7 +75,7 @@ export class I18n extends Formatter {
/**
* Returns the message for a given identifier
*/
#getMessage(identifier: string): { message: string; isFallback: boolean } | null {
resolveIdentifier(identifier: string): { message: string; isFallback: boolean } | null {
let message = this.localeTranslations[identifier]

/**
Expand Down Expand Up @@ -117,7 +125,7 @@ export class I18n extends Formatter {
* Formats a message using the messages formatter
*/
formatMessage(identifier: string, data?: Record<string, any>, fallbackMessage?: string): string {
const message = this.#getMessage(identifier)
const message = this.resolveIdentifier(identifier)

if (!message) {
this.#notifyForMissingTranslation(identifier, false)
Expand Down
87 changes: 87 additions & 0 deletions src/i18n_messages_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* @adonisjs/i18n
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import string from '@poppinss/utils/string'
import type { FieldContext, MessagesProviderContact } from '@vinejs/vine/types'
import type { I18n } from './i18n.js'

/**
* VineJS messages provider to read validation messages
* from translations
*/
export class I18nMessagesProvider implements MessagesProviderContact {
/**
* The validation messages prefix to use when reading translations.
*/
#messagesPrefix: string

/**
* The validation fields prefix to use when reading translations.
*/
#fieldsPrefix: string

/**
* Reference to i18n for formatting messages
*/
#i18n: I18n

constructor(prefix: string, i18n: I18n) {
this.#fieldsPrefix = `${prefix}.fields`
this.#messagesPrefix = `${prefix}.messages`
this.#i18n = i18n
}

getMessage(
defaultMessage: string,
rule: string,
field: FieldContext,
meta?: Record<string, any>
) {
/**
* Translating field name
*/
let fieldName = field.name
const translatedFieldName = this.#i18n.resolveIdentifier(`${this.#fieldsPrefix}.${field.name}`)
if (translatedFieldName) {
fieldName = this.#i18n.formatRawMessage(translatedFieldName.message)
}

/**
* 1st priority is given to the field messages
*/
const fieldMessage = this.#i18n.resolveIdentifier(
`${this.#messagesPrefix}.${field.wildCardPath}.${rule}`
)
if (fieldMessage) {
return this.#i18n.formatRawMessage(fieldMessage.message, {
field: fieldName,
...meta,
})
}

/**
* 2nd priority is for rule messages
*/
const ruleMessage = this.#i18n.resolveIdentifier(`${this.#messagesPrefix}.${rule}`)
if (ruleMessage) {
return this.#i18n.formatRawMessage(ruleMessage.message, {
field: fieldName,
...meta,
})
}

/**
* Fallback to default message
*/
return string.interpolate(defaultMessage, {
field: fieldName,
...meta,
})
}
}
119 changes: 119 additions & 0 deletions tests/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { I18n } from '../src/i18n.js'
import { I18nManager } from '../src/i18n_manager.js'
import { defineConfig } from '../src/define_config.js'
import type { MissingTranslationEventPayload } from '../src/types/main.js'
import vine from '@vinejs/vine'

const app = new AppFactory().create(new URL('./', import.meta.url), () => {})
const emitter = new Emitter<{ 'i18n:missing:translation': MissingTranslationEventPayload }>(app)
Expand Down Expand Up @@ -177,3 +178,121 @@ test.group('I18n', () => {
assert.isTrue(i18n.hasFallbackMessage('messages.greeting'))
})
})

test.group('I18n | validator messages provider', () => {
test('provide validation message', async ({ fs, assert }) => {
assert.plan(1)

await fs.createJson('resources/lang/en/validator.json', {
shared: {
messages: {
'title.required': 'Post title is required',
'required': 'The {field} is needed',
},
},
})

const i18nManager = new I18nManager(
emitter,
defineConfig({
loaders: {
fs: {
enabled: true,
location: join(fs.basePath, 'resources/lang'),
},
},
})
)

await i18nManager.loadTranslations()
const i18n = new I18n('en', emitter, i18nManager)

const schema = vine.object({
title: vine.string(),
description: vine.string(),
tags: vine.enum(['programming']),
})

try {
await vine.validate({
schema,
data: { tags: '' },
messagesProvider: i18n.createMessagesProvider(),
})
} catch (error) {
assert.deepEqual(error.messages, [
{
field: 'title',
message: 'Post title is required',
rule: 'required',
},
{
field: 'description',
message: 'The description is needed',
rule: 'required',
},
{
field: 'tags',
message: 'The selected tags is invalid',
rule: 'enum',
meta: {
choices: ['programming'],
},
},
])
}
})

test('provide field translations', async ({ fs, assert }) => {
assert.plan(1)

await fs.createJson('resources/lang/en/validator.json', {
shared: {
fields: {
title: 'Post title',
description: 'Post description',
},
messages: {
required: 'The {field} is needed',
},
},
})

const i18nManager = new I18nManager(
emitter,
defineConfig({
loaders: {
fs: {
enabled: true,
location: join(fs.basePath, 'resources/lang'),
},
},
})
)

await i18nManager.loadTranslations()
const i18n = new I18n('en', emitter, i18nManager)

const schema = vine.object({
title: vine.string(),
description: vine.string(),
})

try {
await vine.validate({ schema, data: {}, messagesProvider: i18n.createMessagesProvider() })
} catch (error) {
assert.deepEqual(error.messages, [
{
field: 'title',
message: 'The Post title is needed',
rule: 'required',
},
{
field: 'description',
message: 'The Post description is needed',
rule: 'required',
},
])
}
})
})

0 comments on commit 118cfa6

Please sign in to comment.