Skip to content

Commit

Permalink
feat(i18n): allow custom interpolation options for i18n (#4727)
Browse files Browse the repository at this point in the history
* feat(i18n): allow custom interpolation options for i18n

Allow for specifying custom interpolation options for i18n.
This is useful for custom interpolation patterns, such as `{{` and `}}`.
Tests added, and scoped to the i18n module.

Closes #4726

* test(i18n): undo describe scoping

Unwrapped the tests from the describe blocks, so the diff is cleaner.
Prefixed new tests names so they don't collide, also in commented section with begin/end.

#4726

* docs(i18n): add docs for interpolation settings

#4726

* docs(i18n): fix example

#4246
  • Loading branch information
zleight1 authored May 29, 2024
1 parent cd2e4c5 commit dadc58a
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 14 deletions.
32 changes: 32 additions & 0 deletions docs/src/pages/guide/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,38 @@ Here is an example that show cases interpolation for different cases:

<LiveExample client:visible id="vee-validate-v4-i18n-interpolation" />

#### Custom Interpolation Prefix and Suffix

You also have the ability to customize the `prefix` and `suffix` of the interpolated values, by default they are `{` and `}` respectively.

Configure them by passing a third parameter of `InterpolateOptions` to the `localize` function:

```js
import { defineRule, configure } from 'vee-validate';
import { between } from '@vee-validate/rules';
import { localize } from '@vee-validate/i18n';

// Define the rule globally
defineRule('between', between);

configure({
// Generates an English message locale generator
generateMessage: localize(
'en',
{
messages: {
// use double `{{` and `}}` i18next-like curly braces for the interpolated values, instead of the default `{` and `}`
between: `The {{field}} field must be between 0:{{min}} and 1:{{max}}`,
},
},
{
prefix: '{{',
suffix: '}}',
},
),
});
```

### Loading Locale From CDN

If you are using Vue.js over CDN, you can still load the locale files without having to copy them to your code, the `@vee-validate/i18n` library exposes a `loadLocaleFromURL` function that accepts a URL to a locale file.
Expand Down
40 changes: 31 additions & 9 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isCallable, FieldValidationMetaInfo, ValidationMessageGenerator, merge } from '../../shared';
import { InterpolateOptions } from '../../shared/types';
import { interpolate } from './utils';

export { FieldValidationMetaInfo };
Expand All @@ -18,15 +19,21 @@ class Dictionary {
public locale: string;

private container: RootI18nDictionary;
private interpolateOptions: InterpolateOptions;

public constructor(locale: string, dictionary: RootI18nDictionary) {
public constructor(
locale: string,
dictionary: RootI18nDictionary,
interpolateOptions: InterpolateOptions = { prefix: '{', suffix: '}' },
) {
this.container = {};
this.locale = locale;
this.interpolateOptions = interpolateOptions;
this.merge(dictionary);
}

public resolve(ctx: FieldValidationMetaInfo) {
return this.format(this.locale, ctx);
public resolve(ctx: FieldValidationMetaInfo, interpolateOptions?: InterpolateOptions) {
return this.format(this.locale, ctx, interpolateOptions);
}

public getLocaleDefault(locale: string, field: string): string | ValidationMessageGenerator | undefined {
Expand All @@ -41,14 +48,16 @@ class Dictionary {
return this.container[locale]?.names?.[name] || name;
}

public format(locale: string, ctx: FieldValidationMetaInfo) {
public format(locale: string, ctx: FieldValidationMetaInfo, interpolateOptions?: InterpolateOptions) {
let message!: ValidationMessageTemplate | undefined;
const { rule, form, label, name } = ctx;
const fieldName = this.resolveLabel(locale, name, label);

if (!rule) {
message = this.getLocaleDefault(locale, name) || `${fieldName} is not valid`;
return isCallable(message) ? message(ctx) : interpolate(message, { ...form, field: fieldName });
return isCallable(message)
? message(ctx)
: interpolate(message, { ...form, field: fieldName }, interpolateOptions ?? this.interpolateOptions);
}

// find if specific message for that field was specified.
Expand All @@ -59,7 +68,11 @@ class Dictionary {

return isCallable(message)
? message(ctx)
: interpolate(message, { ...form, field: fieldName, params: rule.params });
: interpolate(
message,
{ ...form, field: fieldName, params: rule.params },
interpolateOptions ?? this.interpolateOptions,
);
}

public merge(dictionary: RootI18nDictionary) {
Expand All @@ -71,10 +84,19 @@ const DICTIONARY: Dictionary = new Dictionary('en', {});

function localize(dictionary: RootI18nDictionary): ValidationMessageGenerator;
function localize(locale: string, dictionary?: PartialI18nDictionary): ValidationMessageGenerator;

function localize(locale: string | RootI18nDictionary, dictionary?: PartialI18nDictionary) {
function localize(
locale: string,
dictionary?: PartialI18nDictionary,
interpolateOptions?: InterpolateOptions,
): ValidationMessageGenerator;

function localize(
locale: string | RootI18nDictionary,
dictionary?: PartialI18nDictionary,
interpolateOptions?: InterpolateOptions,
) {
const generateMessage: ValidationMessageGenerator = ctx => {
return DICTIONARY.resolve(ctx);
return DICTIONARY.resolve(ctx, interpolateOptions);
};

if (typeof locale === 'string') {
Expand Down
16 changes: 11 additions & 5 deletions packages/i18n/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { InterpolateOptions } from '../../shared/types';

/**
* Replaces placeholder values in a string with their actual values
*/
export function interpolate(template: string, values: Record<string, any>): string {
return template.replace(/(\d:)?{([^}]+)}/g, function (_, param, placeholder): string {
export function interpolate(template: string, values: Record<string, any>, options: InterpolateOptions): string {
const { prefix, suffix } = options;

const regExp = new RegExp(`([0-9]:)?${prefix}([^${suffix}]+)${suffix}`, 'g');

return template.replace(regExp, function (_, param, placeholder): string {
if (!param || !values.params) {
return placeholder in values
? values[placeholder]
: values.params && placeholder in values.params
? values.params[placeholder]
: `{${placeholder}}`;
: `${prefix}${placeholder}${suffix}`;
}

// Handles extended object params format
if (!Array.isArray(values.params)) {
return placeholder in values.params ? values.params[placeholder] : `{${placeholder}}`;
return placeholder in values.params ? values.params[placeholder] : `${prefix}${placeholder}${suffix}`;
}

// Extended Params exit in the format of `paramIndex:{paramName}` where the index is optional
const paramIndex = Number(param.replace(':', ''));

return paramIndex in values.params ? values.params[paramIndex] : `${param}{${placeholder}}`;
return paramIndex in values.params ? values.params[paramIndex] : `${param}${prefix}${placeholder}${suffix}`;
});
}
Loading

0 comments on commit dadc58a

Please sign in to comment.