Skip to content

Commit

Permalink
feat: provide validator messages via translations
Browse files Browse the repository at this point in the history
  • Loading branch information
Harminder Virk authored and Harminder Virk committed Oct 12, 2021
1 parent 6b1044c commit 9b76e12
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 44 deletions.
22 changes: 20 additions & 2 deletions adonis-typings/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ declare module '@ioc:Adonis/Addons/I18n' {
import { DateTime } from 'luxon'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'

/**
* Wildcard callback for the validator messages
*/
export type ValidatorWildcardCallback = (
field: string,
rule: string,
arrayExpressionPointer?: string,
args?: any
) => string

/**
* Number formatting options
*/
Expand Down Expand Up @@ -175,17 +185,25 @@ declare module '@ioc:Adonis/Addons/I18n' {
*/
switchLocale(locale: string): void

/**
* Returns a wildcard function to format validation
* failure messages
*/
validatorMessages(messagesPrefix?: string): {
'*': ValidatorWildcardCallback
}

/**
* Format a message using its identifier. The message from the
* fallback language is used when the message from current
* locale is missing.
*/
formatMessage(identifier: string, data: Record<string, any>): string
formatMessage(identifier: string, data?: Record<string, any>): string

/**
* Format a raw message
*/
formatRawMessage(message: string, data: Record<string, any>): string
formatRawMessage(message: string, data?: Record<string, any>): string
}

/**
Expand Down
48 changes: 32 additions & 16 deletions providers/I18nProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
* file that was distributed with this source code.
*/

import { ApplicationContract } from '@ioc:Adonis/Core/Application'
import { I18nManager } from '../src/I18nManager'
import { viewBindings } from '../src/Bindings/View'
import { contextBindings } from '../src/Bindings/Context'
import { validatorBindings } from '../src/Bindings/Validator'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'

export default class I18nProvider {
constructor(protected application: ApplicationContract) {}
Expand All @@ -30,23 +33,36 @@ export default class I18nProvider {
* helper
*/
public boot() {
this.application.container.withBindings(
['Adonis/Core/HttpContext', 'Adonis/Addons/I18n'],
(Context, I18n) => {
Context.getter('i18n', () => I18n.locale(I18n.defaultLocale), true)
}
)
const I18n = this.application.container.resolveBinding('Adonis/Addons/I18n')

/**
* Share I18n instance with the HTTP context
*/
this.application.container.withBindings(['Adonis/Core/HttpContext'], (Context) => {
contextBindings(Context, I18n)
})

/**
* Add required globals to the template engine
*/
this.application.container.withBindings(['Adonis/Core/View'], (View) => {
View.global('t', function (...args: any[]) {
if (!this.i18n) {
throw new Error(
'Cannot locate "i18n" object. Make sure your are sharing it with the view inside the "DetectUserLocale" middleware'
)
}

return this.i18n.formatMessage(...args)
})
viewBindings(View, I18n)
})

/**
* Hook into validator to provide default validation messages
*/
this.application.container.withBindings(['Adonis/Core/Validator'], ({ validator }) => {
validatorBindings(validator, I18n)
})
}

/**
* Hook into start lifecycle to load all translation
* messages
*/
public async start() {
const I18n = this.application.container.resolveBinding('Adonis/Addons/I18n')
await I18n.loadTranslations()
}
}
21 changes: 21 additions & 0 deletions src/Bindings/Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* @adonisjs/i18n
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { I18nManagerContract } from '@ioc:Adonis/Addons/I18n'
import { HttpContextConstructorContract } from '@ioc:Adonis/Core/HttpContext'

/**
* Shares the i18n with the HTTP context as a getter
*/
export function contextBindings(
Context: HttpContextConstructorContract,
I18n: I18nManagerContract
) {
Context.getter('i18n', () => I18n.locale(I18n.defaultLocale), true)
}
24 changes: 24 additions & 0 deletions src/Bindings/Validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* @adonisjs/i18n
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { validator } from '@ioc:Adonis/Core/Validator'
import { I18nManagerContract } from '@ioc:Adonis/Addons/I18n'

/**
* Registers a hook to deliver default messages to the validator.
*/
export function validatorBindings(Validator: typeof validator, I18n: I18nManagerContract) {
Validator.messages((ctx) => {
if (ctx && 'i18n' in ctx === true) {
return ctx.i18n.validatorMessages()
}

return I18n.locale(I18n.defaultLocale).validatorMessages()
})
}
32 changes: 32 additions & 0 deletions src/Bindings/View.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* @adonisjs/i18n
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { ViewContract } from '@ioc:Adonis/Core/View'
import { I18nManagerContract } from '@ioc:Adonis/Addons/I18n'

/**
* Registers the "t" helper and the i18n instance for the default
* locale.
*
* HTTP requests can share the request specific i18n with the template
* to overwrite the default one
*/
export function viewBindings(View: ViewContract, I18n: I18nManagerContract) {
/**
* The "i18n" is a reference to the default locale instance.
*/
View.global('i18n', I18n.locale(I18n.defaultLocale))

/**
* The "t" helper to translate messages within the template
*/
View.global('t', function (...args: any[]) {
return this.i18n.formatMessage(...args)
})
}
120 changes: 94 additions & 26 deletions src/I18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

import { LoggerContract } from '@ioc:Adonis/Core/Logger'
import { EmitterContract } from '@ioc:Adonis/Core/Event'
import { I18nContract, I18nManagerContract } from '@ioc:Adonis/Addons/I18n'
import {
I18nContract,
I18nManagerContract,
ValidatorWildcardCallback,
} from '@ioc:Adonis/Addons/I18n'
import { Formatter } from '../Formatters/Core'

/**
Expand All @@ -29,6 +33,15 @@ export class I18n extends Formatter implements I18nContract {
*/
private fallbackTranslations: Record<string, string>

constructor(
public locale: string,
private emitter: EmitterContract,
private logger: LoggerContract,
private i18nManager: I18nManagerContract
) {
super(locale)
}

/**
* Lazy load messages. Doing this as i18n class usually results in switchLocale
* during real world use cases
Expand All @@ -42,13 +55,33 @@ export class I18n extends Formatter implements I18nContract {
}
}

constructor(
public locale: string,
private emitter: EmitterContract,
private logger: LoggerContract,
private i18nManager: I18nManagerContract
) {
super(locale)
/**
* Returns the message for a given identifier
*/
private getMessage(identifier: string, emitAlways = true): string | null {
let message = this.localeTranslations[identifier]
if (message) {
return message
}

message = this.fallbackTranslations[identifier]

/**
* If emit always is true, then we will notify about the
* missing translation.
*
* Otherwise we only notify then the fallback message
* exists.
*/
if (emitAlways || !message) {
this.emitter.emit('i18n:missing:translation', {
locale: this.locale,
identifier,
hasFallback: !!message,
})
}

return message || null
}

/**
Expand All @@ -62,27 +95,62 @@ export class I18n extends Formatter implements I18nContract {
}

/**
* Formats a message using the messages formatter
* Returns a wildcard function to format validation
* failure messages
*/
public formatMessage(identifier: string, data: Record<string, any>): string {
this.lazyLoadMessages()
let message = this.localeTranslations[identifier]
public validatorMessages(messagesPrefix: string = 'validator.shared'): {
'*': ValidatorWildcardCallback
} {
return {
'*': (field, rule, arrayExpressionPointer, options) => {
this.lazyLoadMessages()

/**
* Attempt to read message from the fallback messages
*/
if (!message) {
message = this.fallbackTranslations[identifier]
const fieldRuleMessage = this.getMessage(`${messagesPrefix}.${field}.${rule}`, false)
const data = { field, rule, options }

/**
* Notify user about the missing translation
*/
this.emitter.emit('i18n:missing:translation', {
locale: this.locale,
identifier,
hasFallback: !!message,
})
/**
* The first priority is give to the field + rule message
*/
if (fieldRuleMessage) {
return this.formatRawMessage(fieldRuleMessage, data)
}

/**
* If array expression pointer exists, then the 2nd priority
* is given to the array expression pointer
*/
if (arrayExpressionPointer) {
const arrayExpressionPointerMessage = this.getMessage(
`${messagesPrefix}.${arrayExpressionPointer}.${rule}`,
false
)
if (arrayExpressionPointerMessage) {
return this.formatRawMessage(arrayExpressionPointerMessage, data)
}
}

/**
* Find if there is a message for the validation rule
*/
const ruleMessage = this.getMessage(`${messagesPrefix}.${rule}`, false)
if (ruleMessage) {
return this.formatRawMessage(ruleMessage, data)
}

/**
* Otherwise fallback to a standard english string
*/
return `${rule} validation failed on ${field}`
},
}
}

/**
* Formats a message using the messages formatter
*/
public formatMessage(identifier: string, data?: Record<string, any>): string {
this.lazyLoadMessages()
const message = this.getMessage(identifier)

/**
* Return translation missing string when there is no fallback
Expand All @@ -98,7 +166,7 @@ export class I18n extends Formatter implements I18nContract {
/**
* Formats a message using the messages formatter
*/
public formatRawMessage(message: string, data: Record<string, any>): string {
public formatRawMessage(message: string, data?: Record<string, any>): string {
return this.i18nManager.getFormatter().format(message, this.locale, data)
}
}
Loading

0 comments on commit 9b76e12

Please sign in to comment.