diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index a1d98db8..3ecd246a 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -49,6 +49,17 @@ export default class JavascriptLexer extends BaseLexer {
return keys
}
+ setKeyPrefixes(keys) {
+ if (this.keyPrefix) {
+ return keys.map((key) => ({
+ ...key,
+ keyPrefix: this.keyPrefix,
+ }))
+ }
+
+ return keys
+ }
+
extract(content, filename = '__default.js') {
const keys = []
@@ -117,8 +128,25 @@ export default class JavascriptLexer extends BaseLexer {
node.arguments.length
) {
const { text, elements } = node.arguments[0]
+
+ // useTranslation
if (text) {
this.defaultNamespace = text
+ const optionsArgument = node.arguments[1]
+
+ if (
+ optionsArgument &&
+ optionsArgument.kind === ts.SyntaxKind.ObjectLiteralExpression
+ ) {
+ const node = optionsArgument.properties.find(
+ (p) => p.name.escapedText === 'keyPrefix'
+ )
+ if (node != null) {
+ const keyPrefixValue = node.initializer.text
+ this.keyPrefix = keyPrefixValue
+ }
+ }
+ // withTranslation
} else if (elements && elements.length) {
this.defaultNamespace = elements[0].text
}
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index d9ace4e5..b8a6ab79 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -55,7 +55,10 @@ export default class JsxLexer extends JavascriptLexer {
)
parseTree(sourceFile)
- return this.setNamespaces(keys)
+ const keysWithNamespace = this.setNamespaces(keys)
+ const keysWithPrefixes = this.setKeyPrefixes(keysWithNamespace)
+
+ return keysWithPrefixes
}
jsxExtractor(node, sourceText) {
diff --git a/src/transform.js b/src/transform.js
index 039eaf96..819b3a2f 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -46,7 +46,7 @@ export default class i18nTransform extends Transform {
this.options.i18nextOptions = {
...options.i18nextOptions,
pluralSeparator: this.options.pluralSeparator,
- nsSeparator: this.options.namespaceSeparator
+ nsSeparator: this.options.namespaceSeparator,
}
if (this.options.keySeparator === false) {
@@ -108,6 +108,11 @@ export default class i18nTransform extends Transform {
for (const entry of entries) {
let key = entry.key
+
+ if (entry.keyPrefix) {
+ key = entry.keyPrefix + this.options.keySeparator + key
+ }
+
const parts = key.split(this.options.namespaceSeparator)
// make sure we're not pulling a 'namespace' out of a default value
diff --git a/test/gulp/gulp.test.js b/test/gulp/gulp.test.js
index ee961b07..802e45cf 100644
--- a/test/gulp/gulp.test.js
+++ b/test/gulp/gulp.test.js
@@ -48,6 +48,18 @@ describe('gulp plugin', function () {
assert.strictEqual(error.code, 'ENOENT')
}
+ const enKeyPrefix = await fs.readJson(
+ path.resolve(__dirname, './locales/en/key-prefix.json')
+ )
+
+ try {
+ await fs.readJson(
+ path.resolve(__dirname, './locales/en/key-prefix_old.json')
+ )
+ } catch (error) {
+ assert.strictEqual(error.code, 'ENOENT')
+ }
+
const enTranslation = await fs.readJson(
path.resolve(__dirname, './locales/en/translation.json')
)
@@ -113,6 +125,12 @@ describe('gulp plugin', function () {
'test-1': '',
'test-2': '',
})
+ assert.deepEqual(enKeyPrefix, {
+ 'test-prefix': {
+ foo: '',
+ bar: '',
+ },
+ })
assert.deepEqual(enTranslation, {
fifth: 'bar',
fifth_male: '',
diff --git a/test/parser.test.js b/test/parser.test.js
index 11ebcb3e..38f38cc1 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -380,6 +380,35 @@ describe('parser', () => {
i18nextParser.end(fakeFile)
})
+ it('applies useTranslation keyPrefix globally', (done) => {
+ let result
+ const i18nextParser = new i18nTransform()
+ const fakeFile = new Vinyl({
+ contents: fs.readFileSync(
+ path.resolve(__dirname, 'templating/keyPrefix-hook.jsx')
+ ),
+ path: 'file.jsx',
+ })
+ const expected = {
+ 'test-prefix': {
+ foo: '',
+ bar: '',
+ },
+ }
+
+ i18nextParser.on('data', (file) => {
+ if (file.relative.endsWith(path.normalize('en/key-prefix.json'))) {
+ result = JSON.parse(file.contents)
+ }
+ })
+ i18nextParser.on('end', () => {
+ assert.deepEqual(result, expected)
+ done()
+ })
+
+ i18nextParser.end(fakeFile)
+ })
+
it('handles escaped single and double quotes', (done) => {
let result
const i18nextParser = new i18nTransform()
diff --git a/test/templating/keyPrefix-hook.jsx b/test/templating/keyPrefix-hook.jsx
new file mode 100644
index 00000000..cd2d3584
--- /dev/null
+++ b/test/templating/keyPrefix-hook.jsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import { useTranslation, Trans } from 'react-i18next'
+
+// This will have test-namespace even though it comes before useTranslation during parsing
+const Component = () => {
+ return