Skip to content

Commit 1540767

Browse files
authored
feat: escap html parameter options (#139)
1 parent 34a31a8 commit 1540767

File tree

8 files changed

+115
-1
lines changed

8 files changed

+115
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ Note: the replacement value **must be boolean literals** and cannot be strings,
228228
- [x] dateTimeFormats
229229
- [x] numberFormats
230230
- [x] warnHtmlMessage
231+
- [x] escapeParameter
231232
- methods
232233
- [x] t
233234
- [x] getLocaleMessages
@@ -263,6 +264,7 @@ Note: the replacement value **must be boolean literals** and cannot be strings,
263264
- [x] formatFallbackMessages
264265
- [x] preserveDirectiveContent
265266
- [x] warnHtmlInMessage
267+
- [x] escapeParameterHtml
266268
- [x] postTranslation
267269
- [x] componentInstanceCreatedListener
268270
- [x] t

src/composer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export interface ComposerOptions<Message = VueMessageType> {
139139
fallbackFormat?: boolean
140140
postTranslation?: PostTranslationHandler<Message>
141141
warnHtmlMessage?: boolean
142+
escapeParameter?: boolean
142143
}
143144

144145
/**
@@ -183,6 +184,7 @@ export interface Composer<
183184
fallbackRoot: boolean
184185
fallbackFormat: boolean
185186
warnHtmlMessage: boolean
187+
escapeParameter: boolean
186188
// methods
187189
t(key: Path): string
188190
t(key: Path, plural: number): string
@@ -469,6 +471,8 @@ export function createComposer<
469471
? options.warnHtmlMessage
470472
: true
471473

474+
let _escapeParameter = !!options.escapeParameter
475+
472476
// custom linked modifiers
473477
// prettier-ignore
474478
const _modifiers = __root
@@ -509,6 +513,7 @@ export function createComposer<
509513
unresolving: true,
510514
postTranslation: _postTranslation === null ? undefined : _postTranslation,
511515
warnHtmlMessage: _warnHtmlMessage,
516+
escapeParameter: _escapeParameter,
512517
__datetimeFormatters: isPlainObject(_context)
513518
? ((_context as unknown) as RuntimeInternalContext).__datetimeFormatters
514519
: undefined,
@@ -933,6 +938,13 @@ export function createComposer<
933938
_warnHtmlMessage = val
934939
_context.warnHtmlMessage = val
935940
},
941+
get escapeParameter(): boolean {
942+
return _escapeParameter
943+
},
944+
set escapeParameter(val: boolean) {
945+
_escapeParameter = val
946+
_context.escapeParameter = val
947+
},
936948
// methods
937949
t,
938950
d,

src/core/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export interface RuntimeOptions<Message = string> {
9292
postTranslation?: PostTranslationHandler<Message>
9393
processor?: MessageProcessor<Message>
9494
warnHtmlMessage?: boolean
95+
escapeParameter?: boolean
9596
messageCompiler?: MessageCompiler<Message>
9697
onWarn?: (msg: string, err?: Error) => void
9798
}
@@ -124,6 +125,7 @@ export interface RuntimeTranslationContext<Messages = {}, Message = string>
124125
postTranslation: PostTranslationHandler<Message> | null
125126
processor: MessageProcessor<Message> | null
126127
warnHtmlMessage: boolean
128+
escapeParameter: boolean
127129
messageCompiler: MessageCompiler<Message>
128130
}
129131

@@ -245,6 +247,7 @@ export function createRuntimeContext<
245247
const warnHtmlMessage = isBoolean(options.warnHtmlMessage)
246248
? options.warnHtmlMessage
247249
: true
250+
const escapeParameter = !!options.escapeParameter
248251
const messageCompiler = isFunction(options.messageCompiler)
249252
? options.messageCompiler
250253
: compile
@@ -275,6 +278,7 @@ export function createRuntimeContext<
275278
postTranslation,
276279
processor,
277280
warnHtmlMessage,
281+
escapeParameter,
278282
messageCompiler,
279283
onWarn,
280284
__datetimeFormatters,

src/core/translate.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ import {
3434
isEmptyObject,
3535
generateFormatCacheKey,
3636
generateCodeFrame,
37+
escapeHtml,
3738
inBrowser,
3839
mark,
39-
measure
40+
measure,
41+
isObject
4042
} from '../utils'
4143
import { DevToolsTimelineEvents } from '../debugger/constants'
4244

@@ -83,6 +85,9 @@ const isMessageFunction = <T>(val: unknown): val is MessageFunction<T> =>
8385
*
8486
* // suppress localize fallback warning option, override context.fallbackWarn
8587
* translate(context, 'foo.bar', { name: 'kazupon' }, { fallbackWarn: false })
88+
*
89+
* // escape parameter option, override context.escapeParameter
90+
* translate(context, 'foo.bar', { name: 'kazupon' }, { escapeParameter: true })
8691
*/
8792

8893
/** @internal */
@@ -94,6 +99,7 @@ export type TranslateOptions = {
9499
locale?: Locale
95100
missingWarn?: boolean
96101
fallbackWarn?: boolean
102+
escapeParameter?: boolean
97103
}
98104

99105
// `translate` function overloads
@@ -210,6 +216,10 @@ export function translate<Messages, Message = string>(
210216
? options.fallbackWarn
211217
: context.fallbackWarn
212218

219+
const escapeParameter = isBoolean(options.escapeParameter)
220+
? options.escapeParameter
221+
: context.escapeParameter
222+
213223
// prettier-ignore
214224
const defaultMsgOrKey: string =
215225
isString(options.default) || isBoolean(options.default) // default by function option
@@ -222,6 +232,9 @@ export function translate<Messages, Message = string>(
222232
const enableDefaultMsg = fallbackFormat || defaultMsgOrKey !== ''
223233
const locale = isString(options.locale) ? options.locale : context.locale
224234

235+
// escape params
236+
escapeParameter && escapeParams(options)
237+
225238
// resolve message format
226239
// eslint-disable-next-line prefer-const
227240
let [format, targetLocale, message] = resolveMessageFormat(
@@ -289,6 +302,20 @@ export function translate<Messages, Message = string>(
289302
return postTranslation ? postTranslation(messaged) : messaged
290303
}
291304

305+
function escapeParams(options: TranslateOptions) {
306+
if (isArray(options.list)) {
307+
options.list = options.list.map(item =>
308+
isString(item) ? escapeHtml(item) : item
309+
)
310+
} else if (isObject(options.named)) {
311+
Object.keys(options.named).forEach(key => {
312+
if (isString(options.named![key])) {
313+
options.named![key] = escapeHtml(options.named![key] as string)
314+
}
315+
})
316+
}
317+
}
318+
292319
function resolveMessageFormat<Messages, Message>(
293320
context: RuntimeTranslationContext<Messages, Message>,
294321
key: string,

src/legacy.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export interface VueI18nOptions {
9090
formatFallbackMessages?: boolean
9191
preserveDirectiveContent?: boolean
9292
warnHtmlInMessage?: WarnHtmlInMessageLevel
93+
escapeParameterHtml?: boolean
9394
sharedMessages?: LocaleMessages<VueMessageType>
9495
pluralizationRules?: PluralizationRules
9596
postTranslation?: PostTranslationHandler<VueMessageType>
@@ -124,6 +125,7 @@ export interface VueI18n<
124125
formatFallbackMessages: boolean
125126
sync: boolean
126127
warnHtmlInMessage: WarnHtmlInMessageLevel
128+
escapeParameterHtml: boolean
127129
preserveDirectiveContent: boolean
128130
// methods
129131
t(key: Path): TranslateResult
@@ -230,6 +232,7 @@ function convertComposerOptions<
230232
const warnHtmlMessage = isString(options.warnHtmlInMessage)
231233
? options.warnHtmlInMessage !== 'off'
232234
: true
235+
const escapeParameter = !!options.escapeParameterHtml
233236
const inheritLocale = isBoolean(options.sync) ? options.sync : true
234237

235238
if (__DEV__ && options.formatter) {
@@ -271,6 +274,7 @@ function convertComposerOptions<
271274
pluralRules: pluralizationRules,
272275
postTranslation,
273276
warnHtmlMessage,
277+
escapeParameter,
274278
inheritLocale,
275279
__i18n,
276280
__root
@@ -430,6 +434,14 @@ export function createVueI18n<
430434
composer.warnHtmlMessage = val !== 'off'
431435
},
432436

437+
// escapeParameterHtml
438+
get escapeParameterHtml(): boolean {
439+
return composer.escapeParameter
440+
},
441+
set escapeParameterHtml(val: boolean) {
442+
composer.escapeParameter = val
443+
},
444+
433445
// preserveDirectiveContent
434446
get preserveDirectiveContent(): boolean {
435447
__DEV__ &&

src/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ export const getGlobalThis = (): any => {
108108
)
109109
}
110110

111+
export function escapeHtml(rawText: string): string {
112+
return rawText
113+
.replace(/&/g, '&amp;')
114+
.replace(/</g, '&lt;')
115+
.replace(/>/g, '&gt;')
116+
.replace(/"/g, '&quot;')
117+
.replace(/'/g, '&apos;')
118+
}
119+
111120
/* eslint-enable */
112121

113122
/**

test/core/context.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ describe('postTranslation', () => {
163163
})
164164
})
165165

166+
describe('escapeParameter', () => {
167+
test('default', () => {
168+
const ctx = context({})
169+
expect(ctx.escapeParameter).toEqual(false)
170+
})
171+
172+
test('specify', () => {
173+
const ctx = context({ escapeParameter: true })
174+
expect(ctx.escapeParameter).toEqual(true)
175+
})
176+
})
177+
166178
describe('getLocaleChain', () => {
167179
let ctx: RuntimeContext<unknown, unknown, unknown, string>
168180
beforeEach(() => {

test/core/translate.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,42 @@ describe('warnHtmlMessage', () => {
611611
})
612612
})
613613

614+
describe('escapeParameter', () => {
615+
test('context option', () => {
616+
const ctx = context({
617+
locale: 'en',
618+
warnHtmlMessage: false,
619+
escapeParameter: true,
620+
messages: {
621+
en: {
622+
hello: 'hello, {name}!'
623+
}
624+
}
625+
})
626+
627+
expect(translate(ctx, 'hello', { name: '<b>kazupon</b>' })).toEqual(
628+
'hello, &lt;b&gt;kazupon&lt;/b&gt;!'
629+
)
630+
})
631+
632+
test('override with params', () => {
633+
const ctx = context({
634+
locale: 'en',
635+
warnHtmlMessage: false,
636+
escapeParameter: false,
637+
messages: {
638+
en: {
639+
hello: 'hello, {0}!'
640+
}
641+
}
642+
})
643+
644+
expect(
645+
translate(ctx, 'hello', ['<b>kazupon</b>'], { escapeParameter: true })
646+
).toEqual('hello, &lt;b&gt;kazupon&lt;/b&gt;!')
647+
})
648+
})
649+
614650
describe('error', () => {
615651
test(errorMessages[CoreErrorCodes.INVALID_ARGUMENT], () => {
616652
const ctx = context({

0 commit comments

Comments
 (0)