Open rchinerman opened 5 years ago
I'm interested in this feature too. From what I can, to implement it you'd need to look for withTranslation
and useTranslation
within the parseFuncFromString
and parseTransFromString
methods in https://github.com/i18next/i18next-scanner/blob/master/src/parser.js.
The tricky bit is if you have multiple withTranslation
or useTranslation
calls in a single file, it'd be hard to know which namespace belongs to which part of the file.
No improvements on this? When scanning the files, i18next-scanner
adds all keys to the defaultNs
, and the other namespaces are empty. Am I missing something or is it not possible to use i18next-scanner
when using i18-next
with hooks: useTranslation(ns)
?
My config file is this one:
const fs = require("fs");
const _ = require("lodash");
const eol = require("eol");
const VirtualFile = require("vinyl");
const flattenObjectKeys = require("i18next-scanner/lib/flatten-object-keys")
.default;
const omitEmptyObject = require("i18next-scanner/lib/omit-empty-object")
.default;
function getFileJSON(resPath) {
try {
return JSON.parse(
fs
.readFileSync(fs.realpathSync(path.join("src", resPath)))
.toString("utf-8")
);
} catch (e) {
return {};
}
}
function flush(done) {
const { parser } = this;
const { options } = parser;
// Flush to resource store
const resStore = parser.get({ sort: options.sort });
const { jsonIndent } = options.resource;
const lineEnding = String(options.resource.lineEnding).toLowerCase();
Object.keys(resStore).forEach(lng => {
const namespaces = resStore[lng];
Object.keys(namespaces).forEach(ns => {
let obj = namespaces[ns];
const resPath = parser.formatResourceSavePath(lng, ns);
// if not defaultLng then Get, Merge & removeUnusedKeys of old JSON content
if (lng !== options.defaultLng) {
let resContent = getFileJSON(resPath);
if (options.removeUnusedKeys) {
const namespaceKeys = flattenObjectKeys(obj);
const resContentKeys = flattenObjectKeys(resContent);
const unusedKeys = _.differenceWith(
resContentKeys,
namespaceKeys,
_.isEqual
);
for (let i = 0; i < unusedKeys.length; ++i) {
_.unset(resContent, unusedKeys[i]);
}
resContent = omitEmptyObject(resContent);
}
obj = { ...obj, ...resContent };
}
let text = `${JSON.stringify(obj, null, jsonIndent)}\n`;
if (lineEnding === "auto") {
text = eol.auto(text);
} else if (lineEnding === "\r\n" || lineEnding === "crlf") {
text = eol.crlf(text);
} else if (lineEnding === "\n" || lineEnding === "lf") {
text = eol.lf(text);
} else if (lineEnding === "\r" || lineEnding === "cr") {
text = eol.cr(text);
} else {
// Defaults to LF
text = eol.lf(text);
}
this.push(
new VirtualFile({
path: resPath,
contents: Buffer.from(text)
})
);
});
});
done();
}
module.exports = {
input: [
"src/**/*.{js,jsx}",
// Use ! to filter out files or directories
"!src/**/*.spec.{js,jsx}",
"!src/i18n/**",
"!**/node_modules/**",
"!build/**"
],
output: "./src",
options: {
debug: false,
removeUnusedKeys: true,
sort: true,
func: {
list: ["i18next.t", "i18n.t", "t"],
extensions: [".js", ".jsx"]
},
lngs: ["en", "es", "pt"],
ns: ["main", "navbar", "signup", "rate-provider"],
defaultNs: "main",
defaultLng: "en",
resource: {
loadPath: "locales/{{lng}}/{{ns}}.json",
savePath: "locales/{{lng}}/{{ns}}.json"
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: function(ns, value) {
return value;
}
},
keySeparator: false // key separator
},
flush
};
@otaviobps @rchinerman I came up with a config to solve this problem.
For <Trans>
, my files look like this:
<Trans i18nKey="view:dontateDaiFromInterest">
Donate {{ donateNum }} DAI from interest
</Trans>
Which will output in the "view" namespace as desired
{
"dontateDaiFromInterest": "Donate <1>{{donateNum}}</1> DAI from interest"
}
For const { t } = useTranslation("namespace")
I had to search for useTranslation
in the file and find the namespace as @jamesknelson suggested
Here is my final config. The relevant parts are mostly in parser.parseFuncFromString
and parser.parseTransFromString
.
var fs = require('fs');
var chalk = require('chalk');
var { languages } = require('./language-list.js');
const STRING_NOT_TRANSLATED = '';
const DEFAULT_NS = 'namespace-undefined'
module.exports = {
input: [
'src/**/*.{js,jsx}',
'styleguide/**/*.{js,jsx}',
// Use ! to filter out files or directories
'!src/i18n/**',
'!**/node_modules/**',
'!**/dist/**'
],
output: './',
options: {
debug: true,
removeUnusedKeys: true,
plural: true,
func: {
list: ['i18next.t', 'i18n.t', 't'],
extensions: ['.js', '.jsx']
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: ['.js', '.jsx'],
fallbackKey: function(ns, value) {
return value;
},
acorn: {
ecmaVersion: 10, // defaults to 10
sourceType: 'module' // defaults to 'module'
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
}
},
lngs: languages.map(language => language.code),
ns: ['component', 'foundation', 'ui-kit', 'view'],
defaultLng: 'en',
defaultNs: DEFAULT_NS,
defaultValue: STRING_NOT_TRANSLATED,
resource: {
loadPath: 'src/i18n/locales/{{lng}}/{{ns}}.json',
savePath: 'src/i18n/locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n'
},
nsSeparator: false, // namespace separator
keySeparator: false, // key separator
interpolation: {
prefix: '{{',
suffix: '}}'
}
},
transform: function customTransform(file, enc, done) {
'use strict';
const parser = this.parser;
const content = fs.readFileSync(file.path, enc);
let ns;
const match = content.match(/useTranslation\(.+\)/);
if (match) ns = match[0].split(/(\'|\")/)[2];
let count = 0;
parser.parseFuncFromString(content, { list: ['t'] }, function(
key,
options
) {
parser.set(
key,
Object.assign({}, options, {
ns: ns ? ns : DEFAULT_NS,
nsSeparator: false,
keySeparator: false
})
);
++count;
});
parser.parseTransFromString(
content,
{ component: 'Trans', i18nKey: 'i18nKey' },
function(key, options) {
parser.set(
key.split(':')[1],
Object.assign({}, options, {
ns: key.split(':')[0],
nsSeparator: false,
keySeparator: false
})
);
++count;
}
);
if (count > 0) {
console.log(
`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(
JSON.stringify(file.relative)
)}`
);
}
done();
}
};
The only remaining issue is that I cannot override the defaultNs
setting. Each item will appear in both the desired namespace, and the default one. Idk how to solve this so I just made the default something called "namespace-undefined" because its trash. Happy to hear your ideas!!
I added a couple lines to the above fix to allow using multiple namespaces with useTranslation()
, as described here.
If you want to do this:
const { t } = useTranslation(["foundation", "app"])
// then
t("app:someKey")
t("foundation:anotherKey")
Then add these lines in my fix above
parser.parseFuncFromString(content, { list: ['t'] }, function(
key,
options
) {
// New code to handle multiple namespaces like t("app:someKey")
let thisNs = ns;
if (/.+:.+/.test(key)) {
[thisNs, key] = key.split(':');
}
parser.set(
key,
Object.assign({}, options, {
ns: thisNs ? thisNs : defaultNs,
// ...
@pi0neerpat thanks for your work to get namespace awareness with useTranslation from i18next-scanner.
Could we get this merged in the code to automatically use this parser with react hooks?
Also, I found a small issue with the following code
const Home: React.FC = () => {
const { t } = useTranslation("custom_namespace");
return (
<div className={container}>
{t("logged_in")}
</div>
);
};
i18next-scanner with the custom parser will still add this "logged_in" key to the default namespace in addition to the "custom_namespace" one.
@AdrienLemaire
I think the problem here is the double parsing of strings. In the first case, it's done by the default func
and in the second run it is done by the custom parser. You can solve it by omitting the func
parameter.
I'm using useTranslation()
hook like this:
...
const { t } = useTranslation(['navigation', 'common', 'test'])
const options = [
...(isActivated
? [
{
icon: documents,
title: isMobile ? t('Docs') : t('common:Documents'),
path: '/documents',
},
{ icon: archive, title: t('test:Archive'), path: '/archive' },
...
and in this case I want to add all translations by default to the first namespace navigation
and to others if this is explicitly defined by pointing the namespace in the t
function.
So here's the working configuration:
var fs = require('fs');
var chalk = require('chalk');
module.exports = {
input: [
'app/**/*.{js,jsx}',
// Use ! to filter out files or directories
'!app/**/*.spec.{js,jsx}',
'!app/i18n/**',
'!**/node_modules/**',
],
output: './',
options: {
debug: true,
lngs: ['en-US','bg-BG'],
ns: [
'navigation',
'common',
'test'
],
defaultLng: (lng, ns, key) => lng,
defaultNs: 'common',
defaultKey: (lng, ns, key) => `${ns}-${key}`,
defaultValue: (lng, ns, key) => lng === 'en-US'? key : '__STRING_NOT_TRANSLATED__',
resource: {
loadPath: 'app/public/locales/{{lng}}/{{ns}}.json',
savePath: 'app/public/locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n'
},
interpolation: {
prefix: '{{',
suffix: '}}'
}
},
transform: function customTransform(file, enc, done) {
'use strict';
const parser = this.parser;
const content = fs.readFileSync(file.path, enc);
let ns;
const match = content.match(/useTranslation\(.+\)/);
if (match) ns = match[0].split(/(\'|\")/)[2];
let count = 0;
parser.parseFuncFromString(content, { list: ['t'] }, function(
key,
options
) {
parser.set(
key,
Object.assign({}, options, {
ns: ns ? ns : DEFAULT_NS,
nsSeparator: ':',
keySeparator: '.'
})
);
++count;
});
if (count > 0) {
console.log(
`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(
JSON.stringify(file.relative)
)}`
);
}
done();
}
};
NB: I'm running i18next-scanner
outside of my react app directory.
In react-i18next, the default namespace can be either the namespace defined during init, or the first namespace of the array in the arguments (
withTranslation([namespace1, namespace2])
oruseTranslation([namespace1, namespace2])
) .As far as I can tell, i18next-scanner only supports the namespace defined in the init. Would it be possible to support this? Or am I doing something incorrect in my config and this is already supported?
Version
Configuration