diff --git a/src/rules/no-expression-in-message.ts b/src/rules/no-expression-in-message.ts
index f8ca987..d2446b9 100644
--- a/src/rules/no-expression-in-message.ts
+++ b/src/rules/no-expression-in-message.ts
@@ -16,6 +16,8 @@ export const rule = createRule({
},
messages: {
default: 'Should be ${variable}, not ${object.property} or ${myFunction()}',
+ multiplePlaceholders:
+ 'Invalid placeholder: Expected an object with a single key-value pair, but found multiple keys',
},
schema: [
{
@@ -29,27 +31,43 @@ export const rule = createRule({
defaultOptions: [],
create: function (context) {
- const linguiMacroFunctionNames = ['plural', 'select', 'selectOrdinal']
+ const linguiMacroFunctionNames = ['plural', 'select', 'selectOrdinal', 'ph']
function checkExpressionsInTplLiteral(node: TSESTree.TemplateLiteral) {
- node.expressions.forEach((expression) => {
- if (expression.type === TSESTree.AST_NODE_TYPES.Identifier) {
- return
- }
+ node.expressions.forEach((expression) => checkExpression(expression))
+ }
+
+ function checkExpression(expression: TSESTree.Expression) {
+ if (expression.type === TSESTree.AST_NODE_TYPES.Identifier) {
+ return
+ }
+
+ const isCallToLinguiMacro =
+ expression.type === TSESTree.AST_NODE_TYPES.CallExpression &&
+ expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
+ linguiMacroFunctionNames.includes(expression.callee.name)
+
+ if (isCallToLinguiMacro) {
+ return
+ }
- const isCallToLinguiMacro =
- expression.type === TSESTree.AST_NODE_TYPES.CallExpression &&
- expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier &&
- linguiMacroFunctionNames.includes(expression.callee.name)
+ const isExplicitLabel = expression.type === TSESTree.AST_NODE_TYPES.ObjectExpression
- if (isCallToLinguiMacro) {
+ if (isExplicitLabel) {
+ // there can be only one key in the object
+ if (expression.properties.length === 1) {
return
}
-
context.report({
node: expression,
- messageId: 'default',
+ messageId: 'multiplePlaceholders',
})
+ return
+ }
+
+ context.report({
+ node: expression,
+ messageId: 'default',
})
}
@@ -76,6 +94,16 @@ export const rule = createRule({
return checkExpressionsInTplLiteral(node)
}
+ if (node.type === TSESTree.AST_NODE_TYPES.ObjectExpression) {
+ // Hello {{name: obj.prop}}
+ return checkExpression(node)
+ }
+
+ if (node.type === TSESTree.AST_NODE_TYPES.CallExpression) {
+ // Hello {ph({name: obj.prop})}
+ return checkExpression(node)
+ }
+
if (node.type !== TSESTree.AST_NODE_TYPES.Identifier) {
context.report({
node,
diff --git a/tests/src/rules/no-expression-in-message.test.ts b/tests/src/rules/no-expression-in-message.test.ts
index 0cbb875..85fe59d 100644
--- a/tests/src/rules/no-expression-in-message.test.ts
+++ b/tests/src/rules/no-expression-in-message.test.ts
@@ -87,6 +87,18 @@ ruleTester.run(name, rule, {
name: 'Strings as children are preserved',
code: '{"hello {count, plural, one {world} other {worlds}}"}',
},
+ {
+ code: 't`hello ${{name: obj.prop}}`',
+ },
+ {
+ code: 't`hello ${ph({name: obj.prop})}`',
+ },
+ {
+ code: 'hello {{name: obj.prop}}',
+ },
+ {
+ code: 'hello {ph({name: obj.prop})}',
+ },
],
invalid: [
{
@@ -131,5 +143,21 @@ ruleTester.run(name, rule, {
code: 't`hello ${func()}?`',
errors: [{ messageId: 'default' }],
},
+ {
+ code: 't`hello ${{name: obj.foo, surname: obj.bar}}`',
+ errors: [{ messageId: 'multiplePlaceholders' }],
+ },
+ {
+ code: 't`hello ${greeting({name: obj.prop})}`',
+ errors: [{ messageId: 'default' }],
+ },
+ {
+ code: 'hello {{name: obj.foo, surname: obj.bar}}',
+ errors: [{ messageId: 'multiplePlaceholders' }],
+ },
+ {
+ code: 'hello {greeting({name: obj.prop})}',
+ errors: [{ messageId: 'default' }],
+ },
],
})