mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 01:07:56 +00:00
Compare commits
2 Commits
version-bu
...
fix/i18n-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f7b00a60 | ||
|
|
dfac4e0b31 |
@@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
appendWorkflowJsonExt,
|
||||
ensureWorkflowSuffix,
|
||||
escapeVueI18nMessageSyntax,
|
||||
formatLocalizedMediumDate,
|
||||
formatLocalizedNumber,
|
||||
getFilePathSeparatorVariants,
|
||||
@@ -478,49 +477,4 @@ describe('formatUtil', () => {
|
||||
expect(formatLocalizedMediumDate('not a date', 'en')).toBe('—')
|
||||
})
|
||||
})
|
||||
|
||||
describe('escapeVueI18nMessageSyntax', () => {
|
||||
it('escapes a literal @ that would break linked-message compilation', () => {
|
||||
expect(
|
||||
escapeVueI18nMessageSyntax('clips (tagged @Audio1-3 in the prompt)')
|
||||
).toBe("clips (tagged {'@'}Audio1-3 in the prompt)")
|
||||
})
|
||||
|
||||
it('escapes @ in an email address', () => {
|
||||
expect(escapeVueI18nMessageSyntax('support@comfy.org')).toBe(
|
||||
"support{'@'}comfy.org"
|
||||
)
|
||||
})
|
||||
|
||||
it('escapes interpolation braces', () => {
|
||||
expect(escapeVueI18nMessageSyntax('size {w}x{h}')).toBe(
|
||||
"size {'{'}w{'}'}x{'{'}h{'}'}"
|
||||
)
|
||||
})
|
||||
|
||||
it('escapes the plural pipe separator', () => {
|
||||
expect(escapeVueI18nMessageSyntax('foreground | background')).toBe(
|
||||
"foreground {'|'} background"
|
||||
)
|
||||
})
|
||||
|
||||
it('escapes the modulo percent so it cannot re-form %{', () => {
|
||||
expect(escapeVueI18nMessageSyntax('50%{done}')).toBe(
|
||||
"50{'%'}{'{'}done{'}'}"
|
||||
)
|
||||
})
|
||||
|
||||
it('escapes every occurrence in a single pass', () => {
|
||||
expect(escapeVueI18nMessageSyntax('@a @b @c')).toBe(
|
||||
"{'@'}a {'@'}b {'@'}c"
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves strings without syntax characters unchanged', () => {
|
||||
expect(escapeVueI18nMessageSyntax('no special chars here')).toBe(
|
||||
'no special chars here'
|
||||
)
|
||||
expect(escapeVueI18nMessageSyntax('')).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -178,40 +178,6 @@ export function normalizeI18nKey(key: string) {
|
||||
return typeof key === 'string' ? key.replace(/\./g, '_') : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters that vue-i18n's message compiler treats as syntax in message text,
|
||||
* so plain text has to escape them to render verbatim through `t()`/`st()`:
|
||||
*
|
||||
* - `@` starts a linked-message reference (`@:key`); malformed usage throws
|
||||
* `Invalid linked format`.
|
||||
* - `{` / `}` delimit interpolation (`{name}`, `{'literal'}`); an unbalanced
|
||||
* brace throws `Unterminated/Unbalanced closing brace`.
|
||||
* - `|` separates plural branches, so `a | b` silently renders as one branch.
|
||||
* - `%` forms modulo interpolation when immediately followed by `{` (`%{name}`);
|
||||
* it must be escaped too, otherwise escaping a following `{` re-forms `%{`.
|
||||
*
|
||||
* The set is a build-inlined `const enum` (`TokenChars`) in
|
||||
* `@intlify/message-compiler` and is not exported, so it is hardcoded here.
|
||||
*
|
||||
* @see https://vue-i18n.intlify.dev/guide/essentials/syntax (Special Characters, Literal interpolation)
|
||||
* @see https://vue-i18n.intlify.dev/guide/essentials/pluralization
|
||||
*/
|
||||
const VUE_I18N_SYNTAX_CHARS = /[@{}|%]/g
|
||||
|
||||
/**
|
||||
* Escapes vue-i18n message-syntax characters as literal interpolations (`{'x'}`)
|
||||
* so arbitrary text renders verbatim instead of being parsed as syntax. This is
|
||||
* the only escape vue-i18n supports; see {@link VUE_I18N_SYNTAX_CHARS}.
|
||||
*
|
||||
* Only apply to values read through the compiler (`t()`/`st()`). Values read raw
|
||||
* via `tm()`/`stRaw()` (e.g. node tooltips) must be left untouched, or the
|
||||
* literal `{'x'}` would surface to users. Apply exactly once to raw text: the
|
||||
* escape output itself contains `{`/`}`, so it is not idempotent.
|
||||
*/
|
||||
export function escapeVueI18nMessageSyntax(text: string): string {
|
||||
return text.replace(VUE_I18N_SYNTAX_CHARS, (char) => `{'${char}'}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a dynamic prompt in the format {opt1|opt2|{optA|optB}|} and randomly replaces groups. Supports C style comments.
|
||||
* @param input The dynamic prompt to process
|
||||
|
||||
@@ -3,11 +3,9 @@ import * as fs from 'fs'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
|
||||
import {
|
||||
escapeVueI18nMessageSyntax,
|
||||
normalizeI18nKey
|
||||
} from '@/utils/formatUtil'
|
||||
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
|
||||
import type { WidgetLabels } from './nodeDefLocaleSerializer'
|
||||
import { serializeNodeDefLocales } from './nodeDefLocaleSerializer'
|
||||
|
||||
const localePath = './src/locales/en/main.json'
|
||||
const nodeDefsPath = './src/locales/en/nodeDefs.json'
|
||||
@@ -17,10 +15,6 @@ interface WidgetInfo {
|
||||
label?: string
|
||||
}
|
||||
|
||||
interface WidgetLabels {
|
||||
[key: string]: Record<string, { name: string }>
|
||||
}
|
||||
|
||||
test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
// Mock view route
|
||||
await comfyPage.page.route('**/view**', async (route) => {
|
||||
@@ -47,26 +41,6 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
}
|
||||
)
|
||||
|
||||
const allDataTypesLocale = Object.fromEntries(
|
||||
nodeDefs
|
||||
.flatMap((nodeDef) => {
|
||||
const inputDataTypes = Object.values(nodeDef.inputs).map(
|
||||
(inputSpec) => inputSpec.type
|
||||
)
|
||||
const outputDataTypes = nodeDef.outputs.map(
|
||||
(outputSpec) => outputSpec.type
|
||||
)
|
||||
const allDataTypes = [...inputDataTypes, ...outputDataTypes].flatMap(
|
||||
(type: string) => type.split(',')
|
||||
)
|
||||
return allDataTypes.map((dataType) => [
|
||||
normalizeI18nKey(dataType),
|
||||
escapeVueI18nMessageSyntax(dataType)
|
||||
])
|
||||
})
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
)
|
||||
|
||||
async function extractWidgetLabels() {
|
||||
const nodeLabels: WidgetLabels = {}
|
||||
|
||||
@@ -95,14 +69,10 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
[nodeDef.name, nodeDef.display_name, inputNames]
|
||||
)
|
||||
|
||||
// Format runtime widgets
|
||||
const runtimeWidgets = Object.fromEntries(
|
||||
Object.entries(widgetsMappings)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) => [
|
||||
normalizeI18nKey(key),
|
||||
{ name: value ? escapeVueI18nMessageSyntax(value) : value }
|
||||
])
|
||||
.map(([key, name]) => [key, { name }])
|
||||
)
|
||||
|
||||
if (Object.keys(runtimeWidgets).length > 0) {
|
||||
@@ -121,97 +91,8 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
}
|
||||
|
||||
const nodeDefLabels = await extractWidgetLabels()
|
||||
|
||||
function extractInputs(nodeDef: ComfyNodeDefImpl) {
|
||||
const inputs = Object.fromEntries(
|
||||
Object.values(nodeDef.inputs).flatMap((input) => {
|
||||
const name =
|
||||
input.name === undefined
|
||||
? undefined
|
||||
: escapeVueI18nMessageSyntax(input.name)
|
||||
const tooltip = input.tooltip
|
||||
|
||||
if (name === undefined && tooltip === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
normalizeI18nKey(input.name),
|
||||
{
|
||||
name,
|
||||
tooltip
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
)
|
||||
return Object.keys(inputs).length > 0 ? inputs : undefined
|
||||
}
|
||||
|
||||
function extractOutputs(nodeDef: ComfyNodeDefImpl) {
|
||||
const outputs = Object.fromEntries(
|
||||
nodeDef.outputs.flatMap((output, i) => {
|
||||
// Ignore data types if they are already translated in allDataTypesLocale.
|
||||
const name =
|
||||
output.name === undefined || output.name in allDataTypesLocale
|
||||
? undefined
|
||||
: escapeVueI18nMessageSyntax(output.name)
|
||||
const tooltip = output.tooltip
|
||||
|
||||
if (name === undefined && tooltip === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
[
|
||||
i.toString(),
|
||||
{
|
||||
name,
|
||||
tooltip
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
)
|
||||
return Object.keys(outputs).length > 0 ? outputs : undefined
|
||||
}
|
||||
|
||||
const allNodeDefsLocale = Object.fromEntries(
|
||||
nodeDefs
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((nodeDef) => {
|
||||
const inputs = {
|
||||
...extractInputs(nodeDef),
|
||||
...(nodeDefLabels[nodeDef.name] ?? {})
|
||||
}
|
||||
|
||||
return [
|
||||
normalizeI18nKey(nodeDef.name),
|
||||
{
|
||||
display_name: escapeVueI18nMessageSyntax(
|
||||
nodeDef.display_name ?? nodeDef.name
|
||||
),
|
||||
description: nodeDef.description
|
||||
? escapeVueI18nMessageSyntax(nodeDef.description)
|
||||
: undefined,
|
||||
inputs: Object.keys(inputs).length > 0 ? inputs : undefined,
|
||||
outputs: extractOutputs(nodeDef)
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const allNodeCategoriesLocale = Object.fromEntries(
|
||||
nodeDefs.flatMap((nodeDef) =>
|
||||
nodeDef.category
|
||||
.split('/')
|
||||
.map((category) => [
|
||||
normalizeI18nKey(category),
|
||||
escapeVueI18nMessageSyntax(category)
|
||||
])
|
||||
)
|
||||
)
|
||||
const { dataTypes, nodeCategories, nodeDefinitions } =
|
||||
serializeNodeDefLocales(nodeDefs, nodeDefLabels)
|
||||
|
||||
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
|
||||
fs.writeFileSync(
|
||||
@@ -219,13 +100,13 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
|
||||
JSON.stringify(
|
||||
{
|
||||
...locale,
|
||||
dataTypes: allDataTypesLocale,
|
||||
nodeCategories: allNodeCategoriesLocale
|
||||
dataTypes,
|
||||
nodeCategories
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
fs.writeFileSync(nodeDefsPath, JSON.stringify(allNodeDefsLocale, null, 2))
|
||||
fs.writeFileSync(nodeDefsPath, JSON.stringify(nodeDefinitions, null, 2))
|
||||
})
|
||||
|
||||
130
scripts/nodeDefLocaleSerializer.test.ts
Normal file
130
scripts/nodeDefLocaleSerializer.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { serializeNodeDefLocales } from './nodeDefLocaleSerializer'
|
||||
|
||||
function render(message: string): string {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: { value: message } }
|
||||
})
|
||||
return i18n.global.t('value')
|
||||
}
|
||||
|
||||
describe('serializeNodeDefLocales', () => {
|
||||
it('escapes compiled fields and preserves raw tooltips', () => {
|
||||
const syntax = '@ $ {value} | 50%{done}'
|
||||
const inputName = `Input ${syntax}`
|
||||
const outputName = `Output ${syntax}`
|
||||
const dataType = `TYPE ${syntax}`
|
||||
const category = `Category ${syntax}`
|
||||
const nodeDef = {
|
||||
name: 'Test.Node',
|
||||
display_name: `Display ${syntax}`,
|
||||
description: `Description ${syntax}`,
|
||||
category,
|
||||
inputs: {
|
||||
input: {
|
||||
name: inputName,
|
||||
type: dataType,
|
||||
tooltip: `Input tooltip ${syntax}`
|
||||
}
|
||||
},
|
||||
outputs: [
|
||||
{
|
||||
name: outputName,
|
||||
type: 'OTHER',
|
||||
tooltip: `Output tooltip ${syntax}`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const { dataTypes, nodeCategories, nodeDefinitions } =
|
||||
serializeNodeDefLocales([nodeDef], {
|
||||
'Test.Node': {
|
||||
'Runtime.Widget': { name: `Widget ${syntax}` }
|
||||
}
|
||||
})
|
||||
const serializedNode = nodeDefinitions.Test_Node
|
||||
const serializedInput =
|
||||
serializedNode.inputs['Input @ $ {value} | 50%{done}']
|
||||
const serializedOutput = serializedNode.outputs['0']
|
||||
|
||||
expect(render(serializedNode.display_name)).toBe(nodeDef.display_name)
|
||||
expect(render(serializedNode.description)).toBe(nodeDef.description)
|
||||
expect(render(serializedInput.name)).toBe(inputName)
|
||||
expect(render(serializedOutput.name)).toBe(outputName)
|
||||
expect(render(serializedNode.inputs.Runtime_Widget.name)).toBe(
|
||||
`Widget ${syntax}`
|
||||
)
|
||||
expect(render(dataTypes[dataType])).toBe(dataType)
|
||||
expect(render(nodeCategories[category])).toBe(category)
|
||||
expect(serializedInput.tooltip).toBe(nodeDef.inputs.input.tooltip)
|
||||
expect(serializedOutput.tooltip).toBe(nodeDef.outputs[0].tooltip)
|
||||
})
|
||||
|
||||
it('preserves locale shapes and ordering', () => {
|
||||
const { dataTypes, nodeCategories, nodeDefinitions } =
|
||||
serializeNodeDefLocales(
|
||||
[
|
||||
{
|
||||
name: 'Z.Node',
|
||||
description: '',
|
||||
category: 'group/sub.group',
|
||||
inputs: {
|
||||
omitted: { type: 'Z.TYPE' },
|
||||
tooltipOnly: { type: 'A_TYPE', tooltip: 'raw @ tooltip' }
|
||||
},
|
||||
outputs: [
|
||||
{ name: 'A_TYPE', type: 'A_TYPE' },
|
||||
{ name: 'Custom.Output', type: 'Z.TYPE' },
|
||||
{ tooltip: 'raw output @ tooltip', type: 'Z.TYPE' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'A.Node',
|
||||
category: 'group',
|
||||
inputs: {},
|
||||
outputs: []
|
||||
}
|
||||
],
|
||||
{
|
||||
'Z.Node': {
|
||||
'Runtime.Widget': { name: 'Runtime.Label' }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(dataTypes).toEqual({
|
||||
A_TYPE: 'A_TYPE',
|
||||
Z_TYPE: 'Z.TYPE'
|
||||
})
|
||||
expect(nodeCategories).toEqual({
|
||||
group: 'group',
|
||||
sub_group: 'sub.group'
|
||||
})
|
||||
expect(nodeDefinitions).toEqual({
|
||||
A_Node: {
|
||||
display_name: 'A.Node',
|
||||
description: undefined,
|
||||
inputs: undefined,
|
||||
outputs: undefined
|
||||
},
|
||||
Z_Node: {
|
||||
display_name: 'Z.Node',
|
||||
description: undefined,
|
||||
inputs: {
|
||||
'': { name: undefined, tooltip: 'raw @ tooltip' },
|
||||
Runtime_Widget: { name: 'Runtime.Label' }
|
||||
},
|
||||
outputs: {
|
||||
1: { name: 'Custom.Output', tooltip: undefined },
|
||||
2: { name: undefined, tooltip: 'raw output @ tooltip' }
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(Object.keys(dataTypes)).toEqual(['A_TYPE', 'Z_TYPE'])
|
||||
expect(Object.keys(nodeDefinitions)).toEqual(['A_Node', 'Z_Node'])
|
||||
})
|
||||
})
|
||||
127
scripts/nodeDefLocaleSerializer.ts
Normal file
127
scripts/nodeDefLocaleSerializer.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
interface LocalizableInput {
|
||||
type: string
|
||||
name?: string
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
interface LocalizableOutput {
|
||||
type: string
|
||||
name?: string
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
interface LocalizableNodeDef {
|
||||
category: string
|
||||
inputs: Record<string, LocalizableInput>
|
||||
name: string
|
||||
outputs: LocalizableOutput[]
|
||||
description?: string
|
||||
display_name?: string
|
||||
}
|
||||
|
||||
export type WidgetLabels = Record<
|
||||
string,
|
||||
Record<string, { name: string | undefined }>
|
||||
>
|
||||
|
||||
const VUE_I18N_SYNTAX_CHARS = /[@${}|%]/g
|
||||
|
||||
function escapeMessage(text: string): string {
|
||||
return text.replace(VUE_I18N_SYNTAX_CHARS, (char) => `{'${char}'}`)
|
||||
}
|
||||
|
||||
export function serializeNodeDefLocales(
|
||||
nodeDefs: readonly LocalizableNodeDef[],
|
||||
widgetLabels: WidgetLabels = {}
|
||||
) {
|
||||
const dataTypes = Object.fromEntries(
|
||||
nodeDefs
|
||||
.flatMap((nodeDef) => [
|
||||
...Object.values(nodeDef.inputs).map(({ type }) => type),
|
||||
...nodeDef.outputs.map(({ type }) => type)
|
||||
])
|
||||
.flatMap((type) => type.split(','))
|
||||
.map((dataType) => [normalizeI18nKey(dataType), escapeMessage(dataType)])
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
)
|
||||
|
||||
function serializeInputs(nodeDef: LocalizableNodeDef) {
|
||||
const inputs = Object.fromEntries(
|
||||
Object.values(nodeDef.inputs).flatMap(({ name, tooltip }) => {
|
||||
if (name === undefined && tooltip === undefined) return []
|
||||
|
||||
return [
|
||||
[
|
||||
normalizeI18nKey(name ?? ''),
|
||||
{
|
||||
name: name === undefined ? undefined : escapeMessage(name),
|
||||
tooltip
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
)
|
||||
return Object.keys(inputs).length > 0 ? inputs : undefined
|
||||
}
|
||||
|
||||
function serializeOutputs(nodeDef: LocalizableNodeDef) {
|
||||
const outputs = Object.fromEntries(
|
||||
nodeDef.outputs.flatMap(({ name, tooltip }, index) => {
|
||||
const serializedName =
|
||||
name === undefined || name in dataTypes
|
||||
? undefined
|
||||
: escapeMessage(name)
|
||||
if (serializedName === undefined && tooltip === undefined) return []
|
||||
|
||||
return [[index.toString(), { name: serializedName, tooltip }]]
|
||||
})
|
||||
)
|
||||
return Object.keys(outputs).length > 0 ? outputs : undefined
|
||||
}
|
||||
|
||||
function serializeWidgetLabels(nodeName: string) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(widgetLabels[nodeName] ?? {}).map(([name, label]) => [
|
||||
normalizeI18nKey(name),
|
||||
{
|
||||
name: label.name === undefined ? undefined : escapeMessage(label.name)
|
||||
}
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
const nodeDefinitions = Object.fromEntries(
|
||||
[...nodeDefs]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((nodeDef) => {
|
||||
const inputs = {
|
||||
...serializeInputs(nodeDef),
|
||||
...serializeWidgetLabels(nodeDef.name)
|
||||
}
|
||||
|
||||
return [
|
||||
normalizeI18nKey(nodeDef.name),
|
||||
{
|
||||
display_name: escapeMessage(nodeDef.display_name ?? nodeDef.name),
|
||||
description: nodeDef.description
|
||||
? escapeMessage(nodeDef.description)
|
||||
: undefined,
|
||||
inputs: Object.keys(inputs).length > 0 ? inputs : undefined,
|
||||
outputs: serializeOutputs(nodeDef)
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const nodeCategories = Object.fromEntries(
|
||||
nodeDefs.flatMap(({ category }) =>
|
||||
category
|
||||
.split('/')
|
||||
.map((part) => [normalizeI18nKey(part), escapeMessage(part)])
|
||||
)
|
||||
)
|
||||
|
||||
return { dataTypes, nodeCategories, nodeDefinitions }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { escapeVueI18nMessageSyntax } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Node descriptions are compiled by vue-i18n via `t()`/`st()`, which parses
|
||||
* `@ { } | %` as message syntax — a literal `@` even crashes the compiler with
|
||||
* `Invalid linked format` (this broke the whole app after the 1.47.7 locale
|
||||
* sync). `collect-i18n-node-defs.ts` escapes such values with
|
||||
* `escapeVueI18nMessageSyntax` before writing them; this guards that the escaped
|
||||
* output actually compiles and renders the original literal text.
|
||||
*/
|
||||
describe('escapeVueI18nMessageSyntax output is compiled safely by vue-i18n', () => {
|
||||
const compile = (message: string) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: { value: message } }
|
||||
})
|
||||
return i18n.global.t('value')
|
||||
}
|
||||
|
||||
it.for([
|
||||
'clips (tagged @Audio1-3 in the prompt)',
|
||||
'support@comfy.org',
|
||||
'resolution {width}x{height}',
|
||||
'foreground | background',
|
||||
'50%{done}',
|
||||
'all of @ { } | % together',
|
||||
'no special chars here'
|
||||
])('renders %s as the original literal text', (raw) => {
|
||||
expect(compile(escapeVueI18nMessageSyntax(raw))).toBe(raw)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user