element will have a different range.
+ const isGjsWrapper =
+ templateRange &&
+ node.tag === 'template' &&
+ node.range[0] === templateRange[0] &&
+ node.range[1] === templateRange[1];
+
+ if (!isGjsWrapper) {
+ elementStack.push(node.tag);
+ }
+ },
+ 'GlimmerElementNode:exit'(node) {
+ const isGjsWrapper =
+ templateRange &&
+ node.tag === 'template' &&
+ node.range[0] === templateRange[0] &&
+ node.range[1] === templateRange[1];
+
+ if (!isGjsWrapper) {
+ elementStack.pop();
+ }
+ },
+
+ GlimmerTextNode(node) {
+ if (!node.loc) {
+ return;
+ }
+
+ const attrParent = isInAttrNode(node);
+ if (attrParent) {
+ // Check if this attribute should be checked
+ const attrName = attrParent.name;
+ const tag = currentElementNode?.tag;
+ const isGlobal = config.globalAttributes.includes(attrName);
+ const isElement =
+ tag &&
+ config.elementAttributes[tag] &&
+ config.elementAttributes[tag].includes(attrName);
+
+ if (isGlobal || isElement) {
+ const desc = ` in \`${attrName}\` ${attrName.startsWith('@') ? 'argument' : 'attribute'}`;
+ checkAndLog(node, desc);
+ }
+ } else {
+ checkAndLog(node, '');
+ }
+ },
+
+ GlimmerMustacheStatement(node) {
+ const inAttr = isInAttrNode(node);
+
+ // Check the path itself (StringLiteral path)
+ if (!inAttr && node.path) {
+ checkAndLog(node.path, '');
+ }
+
+ if (isPageTitleHelper(node)) {
+ for (const param of node.params || []) {
+ checkAndLog(param, '');
+ }
+ } else if (isIfHelper(node) && !inAttr) {
+ const [, maybeTrue, maybeFalse] = node.params || [];
+ if (maybeTrue) {
+ checkAndLog(maybeTrue, '');
+ }
+ if (maybeFalse) {
+ checkAndLog(maybeFalse, '');
+ }
+ } else if (isUnlessHelper(node) && !inAttr) {
+ const [, maybeFalse, maybeTrue] = node.params || [];
+ if (maybeTrue) {
+ checkAndLog(maybeTrue, '');
+ }
+ if (maybeFalse) {
+ checkAndLog(maybeFalse, '');
+ }
+ } else if (isStringOnlyConcatHelper(node) && !inAttr) {
+ if (node.params?.[0]) {
+ checkAndLog(node.params[0], '');
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-bare-strings.js b/tests/lib/rules/template-no-bare-strings.js
new file mode 100644
index 0000000000..83bab47096
--- /dev/null
+++ b/tests/lib/rules/template-no-bare-strings.js
@@ -0,0 +1,467 @@
+const rule = require('../../../lib/rules/template-no-bare-strings');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+
+ruleTester.run('template-no-bare-strings', rule, {
+ valid: [
+ '{{t "hello.world"}}',
+ '&
',
+ '
',
+ {
+ code: 'Welcome
',
+ options: [{ allowlist: ['Welcome'] }],
+ },
+
+ '',
+ '',
+ '',
+ '',
+ '',
+ '{{unless @a @b}}',
+ '{{t "howdy"}}',
+ '',
+ '{{t "foo"}}',
+ '{{t "foo"}}, {{t "bar"}} ({{length}})',
+ '(),.&+-=*/#%!?:[]{}',
+ '(),.& ',
+ '—–',
+ '',
+ '',
+ ' fdff sf sf f
',
+ '',
+ '',
+ ' fdff sf sf aaa
f
',
+ '',
+ '',
+ `
+`,
+ '',
+ '{{page-title}}',
+ '{{page-title (t "foo")}}',
+ '{{page-title @model.foo}}',
+ '{{page-title this.model.foo}}',
+ '{{page-title this.model.foo " - " this.model.bar}}',
+ ],
+
+ invalid: [
+ {
+ code: 'Hello World
',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: 'Some text content here
',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+
+ {
+ code: '{{unless true "asd"}}',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '{{unless @b "b"}}',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '{{concat "foo" "bar"}}
',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '{{unless true "Yes" "No"}}
',
+ output: null,
+ errors: [{ messageId: 'bareString' }, { messageId: 'bareString' }],
+ },
+ {
+ code: '{{if true "Yes" "No"}}
',
+ output: null,
+ errors: [{ messageId: 'bareString' }, { messageId: 'bareString' }],
+ },
+ {
+ code: '{{"Hello!"}}
',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: `
+ howdy`,
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: `
+ 1234
+
`,
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: `Bady
+
+
`,
+ output: null,
+ errors: [{ messageId: 'bareString' }, { messageId: 'bareString' }],
+ },
+ {
+ code: '{{page-title "foo"}}',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ code: '{{page-title "foo" " - " "bar"}}',
+ output: null,
+ errors: [{ messageId: 'bareString' }, { messageId: 'bareString' }],
+ },
+ {
+ filename: 'template.gjs',
+ code: '',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ {
+ filename: 'template.gts',
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'bareString' }],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-bare-strings', rule, {
+ valid: [
+ '',
+ '',
+ '',
+ '',
+ '',
+ '{{unless @a @b}}',
+ '{{t "howdy"}}',
+ '',
+ '{{t "foo"}}',
+ '{{t "foo"}}, {{t "bar"}} ({{length}})',
+ '(),.&+-=*/#%!?:[]{}',
+ '(),.& ',
+ '—–',
+ '{{! template-lint-disable no-bare-strings }}',
+ '{{! template-lint-disable }}',
+ '',
+ '',
+ ' fdff sf sf f
',
+ '',
+ '',
+ ' fdff sf sf aaa
f
',
+ ' fdff sf sf f ',
+ ' fdff sf sf aaa
f ',
+ '',
+ '
',
+ '',
+ '',
+ `
+`,
+ '',
+ '{{page-title}}',
+ '{{page-title (t "foo")}}',
+ '{{page-title @model.foo}}',
+ '{{page-title this.model.foo}}',
+ '{{page-title this.model.foo " - " this.model.bar}}',
+ ' ',
+ `
+ {{translate "greeting"}}`,
+ `
+ {{translate "greeting"}},`,
+ '& ×',
+ '
',
+ '',
+ '',
+ '
',
+ // Custom allowlist options.
+ {
+ code: ' ',
+ options: [{ allowlist: [';'] }],
+ },
+ {
+ code: ' ',
+ options: [[';']],
+ },
+ {
+ code: '\nfoo',
+ options: [['foo']],
+ },
+ {
+ code: 'tarzan!\t\n tarzan!',
+ options: [['tarzan!']],
+ },
+ {
+ code: '4 × 3=12',
+ options: [['&', '×', '4', '3=12']],
+ },
+ {
+ code: 'Tom & Jerry',
+ options: [['&', '×', 'Tom', 'Jerry']],
+ },
+ {
+ code: 'howdy',
+ options: [{ allowlist: ['howdy'] }],
+ },
+ {
+ code: '\u20B9',
+ options: [['\u20B9']],
+ },
+ {
+ code: '₹',
+ options: [['₹']],
+ },
+ {
+ code: '{{t "foo"}} / "{{name}}"',
+ options: [['/', '"']],
+ },
+ // Custom ignoredElements.
+ {
+ code: 'some style',
+ options: [{ ignoredElements: ['mj-style'] }],
+ },
+ // Allowlist covers attribute content.
+ {
+ code: '',
+ options: [['X']],
+ },
+ ],
+ invalid: [
+ {
+ code: '{{unless true "asd"}}',
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: '{{unless @b "b"}}',
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: '{{concat "foo" "bar"}}
',
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: '{{unless true "Yes" "No"}}
',
+ output: null,
+ errors: [
+ { message: 'Non-translated string used' },
+ { message: 'Non-translated string used' },
+ ],
+ },
+ {
+ code: '{{if true "Yes" "No"}}
',
+ output: null,
+ errors: [
+ { message: 'Non-translated string used' },
+ { message: 'Non-translated string used' },
+ ],
+ },
+ {
+ code: '{{"Hello!"}}
',
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: `
+ howdy`,
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: `
+ 1234
+
`,
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `title` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `placeholder` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `placeholder` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `placeholder` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `placeholder` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `@placeholder` argument' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `@placeholder` argument' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `aria-label` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `aria-placeholder` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `aria-roledescription` attribute' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `aria-valuetext` attribute' }],
+ },
+ {
+ code: `Bady
+
+
`,
+ output: null,
+ errors: [
+ { message: 'Non-translated string used' },
+ { message: 'Non-translated string used in `placeholder` attribute' },
+ ],
+ },
+ {
+ code: '{{page-title "foo"}}',
+ output: null,
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ {
+ code: '{{page-title "foo" " - " "bar"}}',
+ output: null,
+ errors: [
+ { message: 'Non-translated string used' },
+ { message: 'Non-translated string used' },
+ ],
+ },
+ {
+ code: '{{t "foo"}} / error / ("{{name}}")',
+ output: null,
+ errors: [
+ { message: 'Non-translated string used' },
+ { message: 'Non-translated string used' },
+ ],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Non-translated string used in `placeholder` attribute' }],
+ },
+ // Custom globalAttributes config.
+ {
+ code: '',
+ output: null,
+ options: [{ globalAttributes: ['data-foo'] }],
+ errors: [{ message: 'Non-translated string used in `data-foo` attribute' }],
+ },
+ // Custom elementAttributes config.
+ {
+ code: '
',
+ output: null,
+ options: [{ elementAttributes: { img: ['data-alt'] } }],
+ errors: [{ message: 'Non-translated string used in `data-alt` attribute' }],
+ },
+ // Custom allowlist — only non-allowlisted text triggers error.
+ {
+ code: '{{t "foo"}} / error / ("{{name}}")',
+ output: null,
+ options: [{ allowlist: ['/', '"'] }],
+ errors: [{ message: 'Non-translated string used' }],
+ },
+ ],
+});
From 4a1b63a0490f8e772d50e8518cdf4efa1a1913ca Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Fri, 13 Mar 2026 15:22:00 -0400
Subject: [PATCH 2/2] PR Feedback
---
lib/rules/template-no-bare-strings.js | 18 ++++++++++++------
tests/lib/rules/template-no-bare-strings.js | 18 ++++++++++++++++++
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/lib/rules/template-no-bare-strings.js b/lib/rules/template-no-bare-strings.js
index 7ec2714c8b..fe26269f34 100644
--- a/lib/rules/template-no-bare-strings.js
+++ b/lib/rules/template-no-bare-strings.js
@@ -175,24 +175,30 @@ module.exports = {
},
create(context) {
+ const filename = context.filename ?? context.getFilename();
+ const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
+
const rawConfig = context.options[0];
let config;
+ // In strict mode (gjs/gts), Input/Textarea could be custom components —
+ // prefer false negatives over false positives, matching ember-template-lint.
+ const defaultElementAttributes = isStrictMode
+ ? DEFAULT_ELEMENT_ATTRIBUTES
+ : mergeObjects(DEFAULT_ELEMENT_ATTRIBUTES, BUILTIN_COMPONENT_ATTRIBUTES);
+
if (Array.isArray(rawConfig)) {
config = {
allowlist: sanitizeConfigArray([...rawConfig, ...DEFAULT_ALLOWLIST]),
globalAttributes: [...DEFAULT_GLOBAL_ATTRIBUTES],
- elementAttributes: mergeObjects(DEFAULT_ELEMENT_ATTRIBUTES, BUILTIN_COMPONENT_ATTRIBUTES),
+ elementAttributes: defaultElementAttributes,
ignoredElements: [...IGNORED_ELEMENTS],
};
} else if (rawConfig && typeof rawConfig === 'object') {
config = {
allowlist: sanitizeConfigArray([...(rawConfig.allowlist || []), ...DEFAULT_ALLOWLIST]),
globalAttributes: [...(rawConfig.globalAttributes || []), ...DEFAULT_GLOBAL_ATTRIBUTES],
- elementAttributes: mergeObjects(
- rawConfig.elementAttributes,
- mergeObjects(DEFAULT_ELEMENT_ATTRIBUTES, BUILTIN_COMPONENT_ATTRIBUTES)
- ),
+ elementAttributes: mergeObjects(rawConfig.elementAttributes, defaultElementAttributes),
ignoredElements: [
...sanitizeConfigArray(rawConfig.ignoredElements || []),
...IGNORED_ELEMENTS,
@@ -202,7 +208,7 @@ module.exports = {
config = {
allowlist: [...DEFAULT_ALLOWLIST],
globalAttributes: [...DEFAULT_GLOBAL_ATTRIBUTES],
- elementAttributes: mergeObjects(DEFAULT_ELEMENT_ATTRIBUTES, BUILTIN_COMPONENT_ATTRIBUTES),
+ elementAttributes: defaultElementAttributes,
ignoredElements: [...IGNORED_ELEMENTS],
};
}
diff --git a/tests/lib/rules/template-no-bare-strings.js b/tests/lib/rules/template-no-bare-strings.js
index 83bab47096..6223ef9949 100644
--- a/tests/lib/rules/template-no-bare-strings.js
+++ b/tests/lib/rules/template-no-bare-strings.js
@@ -37,6 +37,24 @@ ruleTester.run('template-no-bare-strings', rule, {
' fdff sf sf aaa
f
',
'',
'',
+ // In GJS/GTS (strict mode), Input/Textarea could be custom components —
+ // not checked to avoid false positives, matching ember-template-lint behavior.
+ {
+ filename: 'template.gjs',
+ code: '',
+ },
+ {
+ filename: 'template.gjs',
+ code: '',
+ },
+ {
+ filename: 'template.gjs',
+ code: '',
+ },
+ {
+ filename: 'template.gts',
+ code: '',
+ },
`
`,
'',