babel-plugin-i18next-extract
Advanced tools
Comparing version 0.1.0-rc to 0.1.0-rc.1
@@ -851,3 +851,4 @@ import i18next from 'i18next'; | ||
keyAsDefaultValueForDerivedKeys: coalesce(opts.keyAsDefaultValueForDerivedKeys, true), | ||
exporterJsonSpace: coalesce(opts.exporterJsonSpace, 2), | ||
discardOldKeys: coalesce(opts.discardOldKeys, false), | ||
jsonSpace: coalesce(opts.jsonSpace, 2), | ||
}; | ||
@@ -858,7 +859,76 @@ } | ||
/** | ||
* Thrown when an error happens during the translations export. | ||
*/ | ||
class ExportError extends Error { | ||
} | ||
/** | ||
* This creates a new empty cache for the exporter. | ||
* | ||
* The cache is required by the exporter and is used to merge the translations | ||
* from the original translation file. It will be mutated by the exporter | ||
* and the same instance must be given untouched across export calls. | ||
*/ | ||
function createExporterCache() { | ||
return { | ||
originalTranslationFiles: {}, | ||
currentTranslationFiles: {}, | ||
}; | ||
} | ||
/** | ||
* Take a deep translation file and flatten it. | ||
* This is to simplify merging keys later on. | ||
* | ||
* @param deep Deep translation file. | ||
*/ | ||
function flattenTranslationFile(deep) { | ||
const result = {}; | ||
for (const [k, v] of Object.entries(deep)) { | ||
if (typeof v === 'object' && v !== null) { | ||
// Nested case, we must recurse | ||
const flat = flattenTranslationFile(v); | ||
for (const [flatK, flatV] of Object.entries(flat)) { | ||
result[JSON.stringify([k, ...JSON.parse(flatK)])] = flatV; | ||
} | ||
} | ||
else { | ||
// Leaf, just set the value. | ||
result[JSON.stringify([k])] = v; | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* Take a flat translation file and make it deep. | ||
* | ||
* This is to make a flat translation file ready to be exported to a file. | ||
* | ||
* @param flat Flat translation file as returned by flattenTranslationFile | ||
*/ | ||
function unflattenTranslationFile(flat) { | ||
const result = {}; | ||
for (const [k, v] of Object.entries(flat)) { | ||
const keyPath = JSON.parse(k); | ||
const cleanKey = keyPath.pop(); | ||
const error = new ExportError(`${PLUGIN_NAME}: Couldn't export translations. Key "${keyPath}" ` + | ||
`has a conflict.`); | ||
let obj = result; | ||
for (const p of keyPath) { | ||
const currentValue = obj[p]; | ||
if (typeof currentValue === 'string' || currentValue === null) { | ||
throw error; | ||
} | ||
obj = obj[p] = currentValue || {}; | ||
} | ||
const currentValue = obj[cleanKey]; | ||
if (typeof currentValue === 'object' && currentValue !== null) { | ||
throw error; | ||
} | ||
obj[cleanKey] = v; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Load translation given a file. If the file is not found, default to empty | ||
* object | ||
* object. | ||
* @param filePath Path of the JSON translation file to load. | ||
@@ -876,15 +946,41 @@ */ | ||
} | ||
const obj = JSON.parse(content); | ||
return obj; | ||
return flattenTranslationFile(JSON.parse(content)); | ||
} | ||
/** | ||
* Exports all given translation keyr as JSON. | ||
* Create a new translationFile with a key added to it. | ||
* | ||
* @param translationFile The translation file to add the key to. | ||
* @param key The translation key to add. | ||
* @param locale Current locale being exported | ||
* @param config Configuration (that will help setting the proper default | ||
* value) | ||
*/ | ||
function addKeyToTranslationFile(translationFile, key, locale, config) { | ||
// compute the default value | ||
let defaultValue = config.defaultValue; | ||
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true || | ||
(Array.isArray(config.keyAsDefaultValue) && | ||
config.keyAsDefaultValue.includes(locale)); | ||
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys; | ||
if (keyAsDefaultValueEnabled && | ||
(keyAsDefaultValueForDerivedKeys || !key.isDerivedKey)) { | ||
defaultValue = key.cleanKey; | ||
} | ||
return { | ||
[JSON.stringify([...key.keyPath, key.cleanKey])]: defaultValue, | ||
...translationFile, | ||
}; | ||
} | ||
/** | ||
* Exports all given translation keys as JSON. | ||
* | ||
* @param keys: translation keys to export | ||
* @param locale: the locale to export | ||
* @param config: plugin configuration | ||
* @param cache: cache instance to use (see createExporterCache) | ||
*/ | ||
function exportTranslationKeys(keys, locale, config) { | ||
function exportTranslationKeys(keys, locale, config, cache) { | ||
const keysPerFilepath = {}; | ||
for (const key of keys) { | ||
// Figure out in which path each key should go. | ||
const filePath = config.outputPath | ||
@@ -896,38 +992,36 @@ .replace('{{locale}}', locale) | ||
for (const [filePath, keysForFilepath] of Object.entries(keysPerFilepath)) { | ||
let obj = {}; | ||
obj = loadTranslationFile(filePath); | ||
const originalObject = obj; | ||
if (!(filePath in cache.originalTranslationFiles)) { | ||
// Cache original translation file so that we don't loose it across babel | ||
// passes. | ||
cache.originalTranslationFiles[filePath] = loadTranslationFile(filePath); | ||
} | ||
let translationFile = cache.currentTranslationFiles[filePath] || {}; | ||
for (const k of keysForFilepath) { | ||
// resolve key path | ||
for (const p of k.keyPath) { | ||
let value = obj[p]; | ||
if (value === undefined) { | ||
value = obj[p] = {}; | ||
} | ||
if (typeof value === 'string' || value === null) { | ||
throw new ExportError(`${PLUGIN_NAME}: Couldn't export translations. Key "${k.key}" ` + | ||
`has conflict.`); | ||
} | ||
obj = value; | ||
} | ||
// The key was already exported. | ||
if (obj[k.cleanKey] !== undefined) { | ||
continue; | ||
} | ||
// Set the default values for the path | ||
let defaultValue = config.defaultValue; | ||
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true || | ||
(Array.isArray(config.keyAsDefaultValue) && | ||
config.keyAsDefaultValue.includes(locale)); | ||
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys; | ||
if (keyAsDefaultValueEnabled && | ||
(keyAsDefaultValueForDerivedKeys || !k.isDerivedKey)) { | ||
defaultValue = k.cleanKey; | ||
} | ||
obj[k.cleanKey] = defaultValue; | ||
translationFile = addKeyToTranslationFile(translationFile, k, locale, config); | ||
} | ||
cache.currentTranslationFiles[filePath] = translationFile; | ||
let deepTranslationFile; | ||
if (config.discardOldKeys) { | ||
const originalTranslationFile = cache.originalTranslationFiles[filePath]; | ||
const alreadyTranslated = Object.keys(originalTranslationFile) | ||
.filter(k => k in translationFile) | ||
.reduce((accumulator, k) => ({ | ||
...accumulator, | ||
[k]: originalTranslationFile[k], | ||
}), {}); | ||
deepTranslationFile = unflattenTranslationFile({ | ||
...translationFile, | ||
...alreadyTranslated, | ||
}); | ||
} | ||
else { | ||
deepTranslationFile = unflattenTranslationFile({ | ||
...translationFile, | ||
...cache.originalTranslationFiles[filePath], | ||
}); | ||
} | ||
// Finally do the export | ||
const directoryPath = path.dirname(filePath); | ||
fs.mkdirSync(directoryPath, { recursive: true }); | ||
fs.writeFileSync(filePath, JSON.stringify(originalObject, null, config.exporterJsonSpace), { | ||
fs.writeFileSync(filePath, JSON.stringify(deepTranslationFile, null, config.jsonSpace), { | ||
encoding: 'utf8', | ||
@@ -1152,5 +1246,2 @@ }); | ||
// We have to store which nodes were extracted because the plugin might be called multiple times | ||
// by Babel and the state would be lost across calls. | ||
const extractedNodes = new WeakSet(); | ||
/** | ||
@@ -1169,9 +1260,10 @@ * Handle the extraction. | ||
const lineNumber = (path.node.loc && path.node.loc.start.line) || '???'; | ||
const extractState = state.I18NextExtract; | ||
const collect = (keys) => { | ||
for (const key of keys) { | ||
if (extractedNodes.has(key.nodePath.node)) { | ||
if (extractState.extractedNodes.has(key.nodePath.node)) { | ||
// The node was already extracted. Skip it. | ||
continue; | ||
} | ||
extractedNodes.add(key.nodePath.node); | ||
extractState.extractedNodes.add(key.nodePath.node); | ||
state.I18NextExtract.extractedKeys.push(key); | ||
@@ -1222,2 +1314,8 @@ } | ||
api.assertVersion(7); | ||
// We have to store which nodes were extracted because the visitor might be | ||
// called multiple times by Babel and the state would be lost across calls. | ||
const extractedNodes = new WeakSet(); | ||
// This is a cache for the exporter to keep track of the translation files. | ||
// It must remain global and persist across transpiled files. | ||
const exporterCache = createExporterCache(); | ||
return { | ||
@@ -1229,2 +1327,4 @@ pre() { | ||
commentHints: [], | ||
extractedNodes, | ||
exporterCache, | ||
}; | ||
@@ -1241,3 +1341,3 @@ }, | ||
], Array()); | ||
exportTranslationKeys(derivedKeys, locale, extractState.config); | ||
exportTranslationKeys(derivedKeys, locale, extractState.config, extractState.exporterCache); | ||
} | ||
@@ -1244,0 +1344,0 @@ }, |
186
lib/index.js
@@ -855,3 +855,4 @@ 'use strict'; | ||
keyAsDefaultValueForDerivedKeys: coalesce(opts.keyAsDefaultValueForDerivedKeys, true), | ||
exporterJsonSpace: coalesce(opts.exporterJsonSpace, 2), | ||
discardOldKeys: coalesce(opts.discardOldKeys, false), | ||
jsonSpace: coalesce(opts.jsonSpace, 2), | ||
}; | ||
@@ -862,7 +863,76 @@ } | ||
/** | ||
* Thrown when an error happens during the translations export. | ||
*/ | ||
class ExportError extends Error { | ||
} | ||
/** | ||
* This creates a new empty cache for the exporter. | ||
* | ||
* The cache is required by the exporter and is used to merge the translations | ||
* from the original translation file. It will be mutated by the exporter | ||
* and the same instance must be given untouched across export calls. | ||
*/ | ||
function createExporterCache() { | ||
return { | ||
originalTranslationFiles: {}, | ||
currentTranslationFiles: {}, | ||
}; | ||
} | ||
/** | ||
* Take a deep translation file and flatten it. | ||
* This is to simplify merging keys later on. | ||
* | ||
* @param deep Deep translation file. | ||
*/ | ||
function flattenTranslationFile(deep) { | ||
const result = {}; | ||
for (const [k, v] of Object.entries(deep)) { | ||
if (typeof v === 'object' && v !== null) { | ||
// Nested case, we must recurse | ||
const flat = flattenTranslationFile(v); | ||
for (const [flatK, flatV] of Object.entries(flat)) { | ||
result[JSON.stringify([k, ...JSON.parse(flatK)])] = flatV; | ||
} | ||
} | ||
else { | ||
// Leaf, just set the value. | ||
result[JSON.stringify([k])] = v; | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* Take a flat translation file and make it deep. | ||
* | ||
* This is to make a flat translation file ready to be exported to a file. | ||
* | ||
* @param flat Flat translation file as returned by flattenTranslationFile | ||
*/ | ||
function unflattenTranslationFile(flat) { | ||
const result = {}; | ||
for (const [k, v] of Object.entries(flat)) { | ||
const keyPath = JSON.parse(k); | ||
const cleanKey = keyPath.pop(); | ||
const error = new ExportError(`${PLUGIN_NAME}: Couldn't export translations. Key "${keyPath}" ` + | ||
`has a conflict.`); | ||
let obj = result; | ||
for (const p of keyPath) { | ||
const currentValue = obj[p]; | ||
if (typeof currentValue === 'string' || currentValue === null) { | ||
throw error; | ||
} | ||
obj = obj[p] = currentValue || {}; | ||
} | ||
const currentValue = obj[cleanKey]; | ||
if (typeof currentValue === 'object' && currentValue !== null) { | ||
throw error; | ||
} | ||
obj[cleanKey] = v; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Load translation given a file. If the file is not found, default to empty | ||
* object | ||
* object. | ||
* @param filePath Path of the JSON translation file to load. | ||
@@ -880,15 +950,41 @@ */ | ||
} | ||
const obj = JSON.parse(content); | ||
return obj; | ||
return flattenTranslationFile(JSON.parse(content)); | ||
} | ||
/** | ||
* Exports all given translation keyr as JSON. | ||
* Create a new translationFile with a key added to it. | ||
* | ||
* @param translationFile The translation file to add the key to. | ||
* @param key The translation key to add. | ||
* @param locale Current locale being exported | ||
* @param config Configuration (that will help setting the proper default | ||
* value) | ||
*/ | ||
function addKeyToTranslationFile(translationFile, key, locale, config) { | ||
// compute the default value | ||
let defaultValue = config.defaultValue; | ||
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true || | ||
(Array.isArray(config.keyAsDefaultValue) && | ||
config.keyAsDefaultValue.includes(locale)); | ||
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys; | ||
if (keyAsDefaultValueEnabled && | ||
(keyAsDefaultValueForDerivedKeys || !key.isDerivedKey)) { | ||
defaultValue = key.cleanKey; | ||
} | ||
return { | ||
[JSON.stringify([...key.keyPath, key.cleanKey])]: defaultValue, | ||
...translationFile, | ||
}; | ||
} | ||
/** | ||
* Exports all given translation keys as JSON. | ||
* | ||
* @param keys: translation keys to export | ||
* @param locale: the locale to export | ||
* @param config: plugin configuration | ||
* @param cache: cache instance to use (see createExporterCache) | ||
*/ | ||
function exportTranslationKeys(keys, locale, config) { | ||
function exportTranslationKeys(keys, locale, config, cache) { | ||
const keysPerFilepath = {}; | ||
for (const key of keys) { | ||
// Figure out in which path each key should go. | ||
const filePath = config.outputPath | ||
@@ -900,38 +996,36 @@ .replace('{{locale}}', locale) | ||
for (const [filePath, keysForFilepath] of Object.entries(keysPerFilepath)) { | ||
let obj = {}; | ||
obj = loadTranslationFile(filePath); | ||
const originalObject = obj; | ||
if (!(filePath in cache.originalTranslationFiles)) { | ||
// Cache original translation file so that we don't loose it across babel | ||
// passes. | ||
cache.originalTranslationFiles[filePath] = loadTranslationFile(filePath); | ||
} | ||
let translationFile = cache.currentTranslationFiles[filePath] || {}; | ||
for (const k of keysForFilepath) { | ||
// resolve key path | ||
for (const p of k.keyPath) { | ||
let value = obj[p]; | ||
if (value === undefined) { | ||
value = obj[p] = {}; | ||
} | ||
if (typeof value === 'string' || value === null) { | ||
throw new ExportError(`${PLUGIN_NAME}: Couldn't export translations. Key "${k.key}" ` + | ||
`has conflict.`); | ||
} | ||
obj = value; | ||
} | ||
// The key was already exported. | ||
if (obj[k.cleanKey] !== undefined) { | ||
continue; | ||
} | ||
// Set the default values for the path | ||
let defaultValue = config.defaultValue; | ||
const keyAsDefaultValueEnabled = config.keyAsDefaultValue === true || | ||
(Array.isArray(config.keyAsDefaultValue) && | ||
config.keyAsDefaultValue.includes(locale)); | ||
const keyAsDefaultValueForDerivedKeys = config.keyAsDefaultValueForDerivedKeys; | ||
if (keyAsDefaultValueEnabled && | ||
(keyAsDefaultValueForDerivedKeys || !k.isDerivedKey)) { | ||
defaultValue = k.cleanKey; | ||
} | ||
obj[k.cleanKey] = defaultValue; | ||
translationFile = addKeyToTranslationFile(translationFile, k, locale, config); | ||
} | ||
cache.currentTranslationFiles[filePath] = translationFile; | ||
let deepTranslationFile; | ||
if (config.discardOldKeys) { | ||
const originalTranslationFile = cache.originalTranslationFiles[filePath]; | ||
const alreadyTranslated = Object.keys(originalTranslationFile) | ||
.filter(k => k in translationFile) | ||
.reduce((accumulator, k) => ({ | ||
...accumulator, | ||
[k]: originalTranslationFile[k], | ||
}), {}); | ||
deepTranslationFile = unflattenTranslationFile({ | ||
...translationFile, | ||
...alreadyTranslated, | ||
}); | ||
} | ||
else { | ||
deepTranslationFile = unflattenTranslationFile({ | ||
...translationFile, | ||
...cache.originalTranslationFiles[filePath], | ||
}); | ||
} | ||
// Finally do the export | ||
const directoryPath = path.dirname(filePath); | ||
fs.mkdirSync(directoryPath, { recursive: true }); | ||
fs.writeFileSync(filePath, JSON.stringify(originalObject, null, config.exporterJsonSpace), { | ||
fs.writeFileSync(filePath, JSON.stringify(deepTranslationFile, null, config.jsonSpace), { | ||
encoding: 'utf8', | ||
@@ -1156,5 +1250,2 @@ }); | ||
// We have to store which nodes were extracted because the plugin might be called multiple times | ||
// by Babel and the state would be lost across calls. | ||
const extractedNodes = new WeakSet(); | ||
/** | ||
@@ -1173,9 +1264,10 @@ * Handle the extraction. | ||
const lineNumber = (path.node.loc && path.node.loc.start.line) || '???'; | ||
const extractState = state.I18NextExtract; | ||
const collect = (keys) => { | ||
for (const key of keys) { | ||
if (extractedNodes.has(key.nodePath.node)) { | ||
if (extractState.extractedNodes.has(key.nodePath.node)) { | ||
// The node was already extracted. Skip it. | ||
continue; | ||
} | ||
extractedNodes.add(key.nodePath.node); | ||
extractState.extractedNodes.add(key.nodePath.node); | ||
state.I18NextExtract.extractedKeys.push(key); | ||
@@ -1226,2 +1318,8 @@ } | ||
api.assertVersion(7); | ||
// We have to store which nodes were extracted because the visitor might be | ||
// called multiple times by Babel and the state would be lost across calls. | ||
const extractedNodes = new WeakSet(); | ||
// This is a cache for the exporter to keep track of the translation files. | ||
// It must remain global and persist across transpiled files. | ||
const exporterCache = createExporterCache(); | ||
return { | ||
@@ -1233,2 +1331,4 @@ pre() { | ||
commentHints: [], | ||
extractedNodes, | ||
exporterCache, | ||
}; | ||
@@ -1245,3 +1345,3 @@ }, | ||
], Array()); | ||
exportTranslationKeys(derivedKeys, locale, extractState.config); | ||
exportTranslationKeys(derivedKeys, locale, extractState.config, extractState.exporterCache); | ||
} | ||
@@ -1248,0 +1348,0 @@ }, |
@@ -14,3 +14,4 @@ export interface Config { | ||
keyAsDefaultValueForDerivedKeys: boolean; | ||
exporterJsonSpace: string | number; | ||
discardOldKeys: boolean; | ||
jsonSpace: string | number; | ||
} | ||
@@ -17,0 +18,0 @@ /** |
import { TranslationKey } from './keys'; | ||
import { Config } from './config'; | ||
/** | ||
* Thrown when an error happens during the translations export. | ||
*/ | ||
export declare class ExportError extends Error { | ||
} | ||
/** | ||
* Exports all given translation keyr as JSON. | ||
* Flat version of TranslationFile. This is mainly used to simplify merging | ||
* translations. | ||
*/ | ||
interface FlatTranslationFile { | ||
[keyPathAsJSON: string]: string | null; | ||
} | ||
/** | ||
* An instance of exporter cache. | ||
* | ||
* See createExporterCache for details. | ||
*/ | ||
export interface ExporterCache { | ||
originalTranslationFiles: { | ||
[path: string]: FlatTranslationFile; | ||
}; | ||
currentTranslationFiles: { | ||
[path: string]: FlatTranslationFile; | ||
}; | ||
} | ||
/** | ||
* This creates a new empty cache for the exporter. | ||
* | ||
* The cache is required by the exporter and is used to merge the translations | ||
* from the original translation file. It will be mutated by the exporter | ||
* and the same instance must be given untouched across export calls. | ||
*/ | ||
export declare function createExporterCache(): ExporterCache; | ||
/** | ||
* Exports all given translation keys as JSON. | ||
* | ||
* @param keys: translation keys to export | ||
* @param locale: the locale to export | ||
* @param config: plugin configuration | ||
* @param cache: cache instance to use (see createExporterCache) | ||
*/ | ||
export default function exportTranslationKeys(keys: TranslationKey[], locale: string, config: Config): void; | ||
export default function exportTranslationKeys(keys: TranslationKey[], locale: string, config: Config, cache: ExporterCache): void; | ||
export {}; |
import * as BabelCore from '@babel/core'; | ||
import * as BabelTypes from '@babel/types'; | ||
import { CommentHint } from './comments'; | ||
import { ExtractedKey } from './keys'; | ||
import { Config } from './config'; | ||
import { ExporterCache } from './exporter'; | ||
export interface VisitorState { | ||
@@ -14,4 +16,6 @@ file: any; | ||
config: Config; | ||
extractedNodes: WeakSet<BabelTypes.Node>; | ||
exporterCache: ExporterCache; | ||
} | ||
export default function (api: BabelCore.ConfigAPI): BabelCore.PluginObj<VisitorState>; | ||
export {}; |
{ | ||
"name": "babel-plugin-i18next-extract", | ||
"version": "0.1.0-rc", | ||
"version": "0.1.0-rc.1", | ||
"description": "Statically extract translation keys from i18next application.", | ||
@@ -5,0 +5,0 @@ "repository": { |
164
README.md
@@ -12,4 +12,18 @@ # babel-plugin-i18next-extract | ||
babel-plugin-i18next-extract is a [Babel Plugin](https://babeljs.io/docs/en/plugins/) that will | ||
traverse your JavaScript/Typescript code in order to find i18next translation keys. | ||
traverse your Javascript/Typescript code in order to find i18next translation keys. | ||
## Features | ||
- ☑️ Keys extraction in [JSON v3 format](https://www.i18next.com/misc/json-format). | ||
- ☑️ Detection of `i18next.t()` function calls. | ||
- ☑️ Full [react-i18next](https://react.i18next.com/) support. | ||
- ☑️ Plurals support. | ||
- ☑️ Contexts support. | ||
- ☑️ Namespace detection. | ||
- ☑️ Disable extraction on a specific file sections or lines using [comment hints]( | ||
#comment-hints). | ||
- ☑️ Overwrite namespaces, plurals and contexts on-the-fly using [comment hints]( | ||
#comment-hints). | ||
- [… and more?](./CONTRIBUTING.md) | ||
## Installation | ||
@@ -28,3 +42,3 @@ | ||
If you already use [Babel](https://babeljs.io), chances are you already have an babel | ||
configuration (e.g. a `.babelrc` file). Just add declare the plugin and you're good to go: | ||
configuration (e.g. a `.babelrc` file). Just declare the plugin and you're good to go: | ||
@@ -40,3 +54,3 @@ ```javascript | ||
You can also specify additional [configuration options](#configuration) to the plugin: | ||
You may want to specify additional [configuration options](#configuration): | ||
@@ -52,3 +66,3 @@ ```javascript | ||
Once you are set up, you can build your app normally or run Babel through [Babel CLI]( | ||
Once the plugin is setup, you can build your app normally or run Babel through [Babel CLI]( | ||
https://babeljs.io/docs/en/babel-cli): | ||
@@ -64,85 +78,7 @@ | ||
Extracted translations should land in the `extractedTranslations/` directory. Magic huh? | ||
Extracted translations should land in the `extractedTranslations/` directory by default. | ||
If you don't have a babel configuration yet, you can follow the [Configure Babel]( | ||
https://babeljs.io/docs/en/configuration) documentation to try setting it up. | ||
https://babeljs.io/docs/en/configuration) documentation to get started. | ||
## Usage with create-react-app | ||
[create-react-app](https://github.com/facebook/create-react-app) doesn't let you modify the babel | ||
configuration. Fortunately, it's still possible to use this plugin without ejecting. First of all, | ||
install Babel CLI: | ||
```bash | ||
yarn add --dev @babel/cli | ||
# or | ||
npm add --save-dev @babel/cli | ||
``` | ||
Create a minimal `.babelrc` that uses the `react-app` babel preset (DO NOT install it, it's already | ||
shipped with CRA): | ||
```javascript | ||
{ | ||
"presets": ["react-app"], | ||
"plugins": ["i18next-extract"] | ||
} | ||
``` | ||
You should then be able to extract your translations using the CLI: | ||
```bash | ||
# NODE_ENV must be specified for react-app preset to work properly | ||
NODE_ENV=development yarn run babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}' | ||
``` | ||
To simplify the extraction, you can add a script to your `package.json`: | ||
```javascript | ||
"scripts": { | ||
[…] | ||
"i18n-extract": "NODE_ENV=development babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'", | ||
[…] | ||
} | ||
``` | ||
And then just run: | ||
```bash | ||
yarn run i18n-extract | ||
# or | ||
npm run i18n-extract | ||
``` | ||
## Features | ||
- [x] Translation extraction in [JSON v3 format](https://www.i18next.com/misc/json-format). | ||
- [x] Detection of `i18next.t()` function calls. | ||
- [x] Plural forms support: | ||
- [x] Keys derivation depending on the locale. | ||
- [x] Automatic detection from `i18next.t` function calls. | ||
- [x] Automatic detection from `react-i18next` properties. | ||
- [x] Manual detection from [comment hints](#comment-hints). | ||
- [x] Contexts support: | ||
- [x] Naïve implementation with default contexts. | ||
- [x] Automatic detection from `i18next.t` function calls. | ||
- [x] Automatic detection from `react-i18next` properties. | ||
- [x] Manual detection from [comment hints](#comment-hints). | ||
- [x] [react-i18next](https://react.i18next.com/) support: | ||
- [x] `Trans` component support (with plural forms, contexts and namespaces). | ||
- [x] `useTranslation` hook support (with plural forms, contexts and namespaces). | ||
- [x] `Translation` render prop support (with plural forms, contexts and namespaces). | ||
- [x] Namespace inference from `withTranslation` HOC. | ||
- [x] Namespace inference: | ||
- [x] Depending on the key value. | ||
- [x] Depending on the `t()` function options. | ||
- [x] Depending on the `ns` property in `Translation` render prop. | ||
- [x] Depending on the `ns` attribute in the `Trans` component. | ||
- [x] Explicitely disable extraction on a specific file sections or lines using [comment hints](#comment-hints). | ||
- [ ] [… and more?](./CONTRIBUTING.md) | ||
## Configuration | ||
@@ -164,3 +100,4 @@ | ||
| keyAsDefaultValueForDerivedKeys | `boolean` | If false and `keyAsDefaultValue` is enabled, don't use derived keys (plural forms or contexts) as default value. `defaultValue` will be used instead. | `true` | | ||
| exporterJsonSpace | `number` | Number of indentation space to use in extracted JSON files. | 2 | | ||
| discardOldKeys | `boolean` | When set to `true`, keys that no longer exist are removed from the JSON files. By default, new keys will be added to the JSON files and never removed. | `false` | | ||
| jsonSpace | `number` | Number of indentation space to use in extracted JSON files. | `2` | | ||
@@ -241,7 +178,58 @@ ## Comment hints | ||
## Usage with create-react-app | ||
[create-react-app](https://github.com/facebook/create-react-app) doesn't let you modify the babel | ||
configuration. Fortunately, it's still possible to use this plugin without ejecting. First of all, | ||
install Babel CLI: | ||
```bash | ||
yarn add --dev @babel/cli | ||
# or | ||
npm add --save-dev @babel/cli | ||
``` | ||
Create a minimal `.babelrc` that uses the `react-app` babel preset (DO NOT install it, it's already | ||
shipped with CRA): | ||
```javascript | ||
{ | ||
"presets": ["react-app"], | ||
"plugins": ["i18next-extract"] | ||
} | ||
``` | ||
You should then be able to extract your translations using the CLI: | ||
```bash | ||
# NODE_ENV must be specified for react-app preset to work properly | ||
NODE_ENV=development yarn run babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}' | ||
``` | ||
To simplify the extraction, you can add a script to your `package.json`: | ||
```javascript | ||
"scripts": { | ||
[…] | ||
"i18n-extract": "NODE_ENV=development babel -f .babelrc 'src/**/*.{js,jsx,ts,tsx}'", | ||
[…] | ||
} | ||
``` | ||
And then just run: | ||
```bash | ||
yarn run i18n-extract | ||
# or | ||
npm run i18n-extract | ||
``` | ||
## Gotchas | ||
The plugin tries to be smart, but can't do magic. i18next has a runtime unlike this plugin which | ||
must guess everything statically. For instance, you may want to disable extraction on dynamic | ||
keys: | ||
The plugin tries to be a little smart, but can't do magic. i18next has a runtime unlike this | ||
plugin which must guess everything statically. For instance, you may want to disable extraction | ||
on dynamic keys: | ||
@@ -248,0 +236,0 @@ ```javascript |
125827
2987
251