diff --git a/schema.json b/schema.json index 30c8b9db..ece1b10c 100644 --- a/schema.json +++ b/schema.json @@ -74,6 +74,10 @@ "items": { "type": "string" } + }, + "removeOtherKeys": { + "description": "Remove keys which are not present in the import.", + "type": "boolean" } } }, @@ -136,6 +140,78 @@ "type": ["string", "null"] } } + }, + "sync": { + "type": "object", + "properties": { + "backup": { + "description": "Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state.", + "type": "string" + }, + "continueOnWarning": { + "description": "Continue the sync regardless of whether warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.", + "type": "boolean" + }, + "removeUnused": { + "description": "Delete unused keys from the Tolgee project", + "type": "boolean" + } + } + }, + "tag": { + "type": "object", + "properties": { + "filterExtracted": { + "description": "Extract keys from code and filter by it.", + "type": "boolean" + }, + "filterNotExtracted": { + "description": "Extract keys from code and filter them out.", + "type": "boolean" + }, + "filterTag": { + "description": "Filter only keys with tag. Use * as a wildcard.", + "type": "array", + "items": { + "type": "string" + } + }, + "filterNoTag": { + "description": "Filter only keys without tag. Use * as a wildcard.", + "type": "array", + "items": { + "type": "string" + } + }, + "tag": { + "description": "Add tag to filtered keys.", + "type": "array", + "items": { + "type": "string" + } + }, + "tagOther": { + "description": "Tag keys which are not filtered.", + "type": "array", + "items": { + "type": "string" + } + }, + "untag": { + "description": "Remove tag from filtered keys. Use * as a wildcard.", + "type": "array", + "items": { + "type": "string" + } + }, + "untagOther": { + "description": "Remove tag from keys which are not filtered. Use * as a wildcard.", + "type": "array", + "items": { + "type": "string" + } + } + } } }, "$defs": { diff --git a/src/commands/push.ts b/src/commands/push.ts index ce81579b..fa15d184 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -245,6 +245,6 @@ export default (config: Schema) => new Option( '--remove-other-keys', 'Remove keys which are not present in the import.' - ).default(false) + ).default(config.push?.removeOtherKeys) ) .action(pushHandler(config)); diff --git a/src/commands/sync/sync.ts b/src/commands/sync/sync.ts index 10b7f9fe..5f3af338 100644 --- a/src/commands/sync/sync.ts +++ b/src/commands/sync/sync.ts @@ -1,5 +1,5 @@ import type { BaseOptions } from '../../options.js'; -import { Command } from 'commander'; +import { Command, Option } from 'commander'; import ansi from 'ansi-colors'; import { @@ -21,7 +21,7 @@ import { } from '../../client/TolgeeClient.js'; type Options = BaseOptions & { - backup?: string; + backup?: string | false; removeUnused?: boolean; continueOnWarning?: boolean; yes?: boolean; @@ -211,21 +211,29 @@ export default (config: Schema) => .description( 'Synchronizes the keys in your code project and in the Tolgee project, by creating missing keys and optionally deleting unused ones. For a dry-run, use `tolgee compare`.' ) - .option( - '-B, --backup ', - 'Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state.' + .addOption( + new Option( + '-B, --backup ', + 'Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state.' + ).default(config.sync?.backup ?? false) ) - .option( - '--continue-on-warning', - 'Set this flag to continue the sync if warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.' + .addOption( + new Option( + '--continue-on-warning', + 'Set this flag to continue the sync if warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.' + ).default(config.sync?.continueOnWarning ?? false) ) - .option( - '-Y, --yes', - 'Skip prompts and automatically say yes to them. You will not be asked for confirmation before creating/deleting keys.' + .addOption( + new Option( + '-Y, --yes', + 'Skip prompts and automatically say yes to them. You will not be asked for confirmation before creating/deleting keys.' + ).default(false) ) - .option( - '--remove-unused', - 'Also delete unused keys from the Tolgee project.' + .addOption( + new Option( + '--remove-unused', + 'Delete unused keys from the Tolgee project.' + ).default(config.sync?.removeUnused ?? false) ) .option( '--tag-new-keys ', diff --git a/src/commands/tag.ts b/src/commands/tag.ts index d8d2679d..293b2993 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -71,40 +71,47 @@ export default (config: Schema) => new Option( '--filter-extracted', 'Extract keys from code and filter by it.' - ) + ).default(config.tag?.filterExtracted) ) .addOption( new Option( '--filter-not-extracted', 'Extract keys from code and filter them out.' - ) + ).default(config.tag?.filterNotExtracted) ) .addOption( new Option( '--filter-tag ', 'Filter only keys with tag. Use * as a wildcard.' - ) + ).default(config.tag?.filterTag) ) .addOption( new Option( '--filter-no-tag ', 'Filter only keys without tag. Use * as a wildcard.' + ).default(config.tag?.filterNoTag) + ) + .addOption( + new Option('--tag ', 'Add tag to filtered keys.').default( + config.tag?.tag ) ) - .addOption(new Option('--tag ', 'Add tag to filtered keys.')) .addOption( - new Option('--tag-other ', 'Tag keys which are not filtered.') + new Option( + '--tag-other ', + 'Tag keys which are not filtered.' + ).default(config.tag?.tagOther) ) .addOption( new Option( '--untag ', 'Remove tag from filtered keys. Use * as a wildcard.' - ) + ).default(config.tag?.untag) ) .addOption( new Option( '--untag-other ', 'Remove tag from keys which are not filtered. Use * as a wildcard.' - ) + ).default(config.tag?.untagOther) ) .action(tagHandler(config)); diff --git a/src/schema.d.ts b/src/schema.d.ts index 4499beb5..b55524bf 100644 --- a/src/schema.d.ts +++ b/src/schema.d.ts @@ -119,6 +119,10 @@ export interface Schema { * Specify tags that will be added to newly created keys. */ tagNewKeys?: string[]; + /** + * Remove keys which are not present in the import. + */ + removeOtherKeys?: boolean; }; pull?: { /** @@ -162,6 +166,54 @@ export interface Schema { */ delimiter?: string | null; }; + sync?: { + /** + * Store translation files backup (only translation files, not states, comments, tags, etc.). If something goes wrong, the backup can be used to restore the project to its previous state. + */ + backup?: string; + /** + * Continue the sync regardless of whether warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync. + */ + continueOnWarning?: boolean; + /** + * Delete unused keys from the Tolgee project + */ + removeUnused?: boolean; + }; + tag?: { + /** + * Extract keys from code and filter by it. + */ + filterExtracted?: boolean; + /** + * Extract keys from code and filter them out. + */ + filterNotExtracted?: boolean; + /** + * Filter only keys with tag. Use * as a wildcard. + */ + filterTag?: string[]; + /** + * Filter only keys without tag. Use * as a wildcard. + */ + filterNoTag?: string[]; + /** + * Add tag to filtered keys. + */ + tag?: string[]; + /** + * Tag keys which are not filtered. + */ + tagOther?: string[]; + /** + * Remove tag from filtered keys. Use * as a wildcard. + */ + untag?: string[]; + /** + * Remove tag from keys which are not filtered. Use * as a wildcard. + */ + untagOther?: string[]; + }; } export interface FileMatch { path: Path; diff --git a/test/__fixtures__/nestedArrayKeysProject/tolgee.config.json b/test/__fixtures__/nestedArrayKeysProject/tolgee.config.json index e20e73bc..7435e64d 100644 --- a/test/__fixtures__/nestedArrayKeysProject/tolgee.config.json +++ b/test/__fixtures__/nestedArrayKeysProject/tolgee.config.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../schema.json", + "$schema": "../../../schema.json", "pull": { "delimiter": "." }, diff --git a/test/__fixtures__/nestedKeysProject/tolgee.config.flat.json b/test/__fixtures__/nestedKeysProject/tolgee.config.flat.json deleted file mode 100644 index eb2c9ebe..00000000 --- a/test/__fixtures__/nestedKeysProject/tolgee.config.flat.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "pull": { - "delimiter": null - }, - "push": { - "files": [ - { - "path": "./en.json", - "language": "en" - }, - { - "path": "./fr.json", - "language": "fr" - } - ] - } -} diff --git a/test/__fixtures__/nestedKeysProject/tolgee.config.nested.json b/test/__fixtures__/nestedKeysProject/tolgee.config.nested.json deleted file mode 100644 index 29d3f3f3..00000000 --- a/test/__fixtures__/nestedKeysProject/tolgee.config.nested.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "pull": { - "delimiter": "." - }, - "push": { - "files": [ - { - "path": "./en.json", - "language": "en" - }, - { - "path": "./fr.json", - "language": "fr" - } - ] - } -} diff --git a/test/__fixtures__/parserDetection/config.mixed.json b/test/__fixtures__/parserDetection/config.mixed.json deleted file mode 100644 index 06c29f86..00000000 --- a/test/__fixtures__/parserDetection/config.mixed.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.{ts?(x),svelte,vue}"] -} diff --git a/test/__fixtures__/parserDetection/config.parser.json b/test/__fixtures__/parserDetection/config.parser.json deleted file mode 100644 index 6e5a81c9..00000000 --- a/test/__fixtures__/parserDetection/config.parser.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.{ts,js}"], - "parser": "react" -} diff --git a/test/__fixtures__/parserDetection/config.react.json b/test/__fixtures__/parserDetection/config.react.json deleted file mode 100644 index af67ab42..00000000 --- a/test/__fixtures__/parserDetection/config.react.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.ts?(x)"] -} diff --git a/test/__fixtures__/parserDetection/config.svelte.json b/test/__fixtures__/parserDetection/config.svelte.json deleted file mode 100644 index 808239c9..00000000 --- a/test/__fixtures__/parserDetection/config.svelte.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.{svelte,ts,js}"] -} diff --git a/test/__fixtures__/parserDetection/config.unknown.json b/test/__fixtures__/parserDetection/config.unknown.json deleted file mode 100644 index 6d0b56cc..00000000 --- a/test/__fixtures__/parserDetection/config.unknown.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.{ts,js}"] -} diff --git a/test/__fixtures__/parserDetection/config.vue.json b/test/__fixtures__/parserDetection/config.vue.json deleted file mode 100644 index 36bcdb4f..00000000 --- a/test/__fixtures__/parserDetection/config.vue.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.{vue,ts,js}"] -} diff --git a/test/__fixtures__/parserOptions/config.react.default.json b/test/__fixtures__/parserOptions/config.react.default.json deleted file mode 100644 index d7a8c6bd..00000000 --- a/test/__fixtures__/parserOptions/config.react.default.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.tsx"], - "strictNamespace": false, - "defaultNamespace": "default" -} diff --git a/test/__fixtures__/parserOptions/config.react.json b/test/__fixtures__/parserOptions/config.react.json deleted file mode 100644 index 41521589..00000000 --- a/test/__fixtures__/parserOptions/config.react.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.tsx"] -} diff --git a/test/__fixtures__/parserOptions/config.react.noStrict.json b/test/__fixtures__/parserOptions/config.react.noStrict.json deleted file mode 100644 index c8b4e7a4..00000000 --- a/test/__fixtures__/parserOptions/config.react.noStrict.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.tsx"], - "strictNamespace": false -} diff --git a/test/__fixtures__/parserOptions/config.svelte.default.json b/test/__fixtures__/parserOptions/config.svelte.default.json deleted file mode 100644 index da8b1e47..00000000 --- a/test/__fixtures__/parserOptions/config.svelte.default.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.svelte"], - "strictNamespace": false, - "defaultNamespace": "default" -} diff --git a/test/__fixtures__/parserOptions/config.svelte.json b/test/__fixtures__/parserOptions/config.svelte.json deleted file mode 100644 index 49060f0d..00000000 --- a/test/__fixtures__/parserOptions/config.svelte.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.svelte"] -} diff --git a/test/__fixtures__/parserOptions/config.svelte.noStrict.json b/test/__fixtures__/parserOptions/config.svelte.noStrict.json deleted file mode 100644 index 4c2d7d29..00000000 --- a/test/__fixtures__/parserOptions/config.svelte.noStrict.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.svelte"], - "strictNamespace": false -} diff --git a/test/__fixtures__/parserOptions/config.vue.default.json b/test/__fixtures__/parserOptions/config.vue.default.json deleted file mode 100644 index d08d1e3c..00000000 --- a/test/__fixtures__/parserOptions/config.vue.default.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.vue"], - "strictNamespace": false, - "defaultNamespace": "default" -} diff --git a/test/__fixtures__/parserOptions/config.vue.json b/test/__fixtures__/parserOptions/config.vue.json deleted file mode 100644 index 40456805..00000000 --- a/test/__fixtures__/parserOptions/config.vue.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.vue"] -} diff --git a/test/__fixtures__/parserOptions/config.vue.noStrict.json b/test/__fixtures__/parserOptions/config.vue.noStrict.json deleted file mode 100644 index 7511926d..00000000 --- a/test/__fixtures__/parserOptions/config.vue.noStrict.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "patterns": ["./code/*.vue"], - "strictNamespace": false -} diff --git a/test/__fixtures__/tagsProject/config.json b/test/__fixtures__/tagsProject/config.json index ac205af4..e0a2e9cd 100644 --- a/test/__fixtures__/tagsProject/config.json +++ b/test/__fixtures__/tagsProject/config.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../schema.json", + "$schema": "../../../schema.json", "apiUrl": "http://localhost:22222", "patterns": [ "./react.tsx" diff --git a/test/__fixtures__/updatedProject1/tolgeerc.mjs b/test/__fixtures__/updatedProject1/tolgeerc.mjs deleted file mode 100644 index 0ee7df2c..00000000 --- a/test/__fixtures__/updatedProject1/tolgeerc.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export default { - push: { - files: [ - { - path: './en.json', - language: 'en', - }, - { - path: './fr.json', - language: 'fr', - }, - ], - }, -}; diff --git a/test/__fixtures__/updatedProject2WithConflicts/.tolgeerc b/test/__fixtures__/updatedProject2WithConflicts/.tolgeerc deleted file mode 100644 index 49427f8e..00000000 --- a/test/__fixtures__/updatedProject2WithConflicts/.tolgeerc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "../../../schema.json", - "push": { - "files": [ - { - "path": "./en.json", - "language": "en" - }, - { - "path": "./fr.json", - "language": "fr" - } - ] - } -} diff --git a/test/__fixtures__/updatedProject3/.tolgeerc b/test/__fixtures__/updatedProject3/.tolgeerc deleted file mode 100644 index 22a3900c..00000000 --- a/test/__fixtures__/updatedProject3/.tolgeerc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "../../../../schema.json", - "push": { - "files": [ - { - "path": "./i18n/en.json", - "language": "en" - }, - { - "path": "./i18n/fr.json", - "language": "fr" - }, - { - "path": "./i18n/drinks/en.json", - "language": "en", - "namespace": "drinks" - }, - { - "path": "./i18n/drinks/fr.json", - "language": "fr", - "namespace": "drinks" - }, - { - "path": "./i18n/food/en.json", - "language": "en", - "namespace": "food" - }, - { - "path": "./i18n/food/fr.json", - "language": "fr", - "namespace": "food" - } - ], - "forceMode": "OVERRIDE" - } -} diff --git a/test/__fixtures__/updatedProject3/i18n/drinks/en.json b/test/__fixtures__/updatedProject3/drinks/en.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/drinks/en.json rename to test/__fixtures__/updatedProject3/drinks/en.json diff --git a/test/__fixtures__/updatedProject3/i18n/drinks/fr.json b/test/__fixtures__/updatedProject3/drinks/fr.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/drinks/fr.json rename to test/__fixtures__/updatedProject3/drinks/fr.json diff --git a/test/__fixtures__/updatedProject3/i18n/en.json b/test/__fixtures__/updatedProject3/en.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/en.json rename to test/__fixtures__/updatedProject3/en.json diff --git a/test/__fixtures__/updatedProject3/i18n/food/en.json b/test/__fixtures__/updatedProject3/food/en.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/food/en.json rename to test/__fixtures__/updatedProject3/food/en.json diff --git a/test/__fixtures__/updatedProject3/i18n/food/fr.json b/test/__fixtures__/updatedProject3/food/fr.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/food/fr.json rename to test/__fixtures__/updatedProject3/food/fr.json diff --git a/test/__fixtures__/updatedProject3/i18n/fr.json b/test/__fixtures__/updatedProject3/fr.json similarity index 100% rename from test/__fixtures__/updatedProject3/i18n/fr.json rename to test/__fixtures__/updatedProject3/fr.json diff --git a/test/__fixtures__/updatedProject3DeprecatedKeys/.tolgeerc b/test/__fixtures__/updatedProject3DeprecatedKeys/.tolgeerc deleted file mode 100644 index 2d26d45c..00000000 --- a/test/__fixtures__/updatedProject3DeprecatedKeys/.tolgeerc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "../../../schema.json", - "push": { - "files": [ - { - "path": "./en.json", - "language": "en" - }, - { - "path": "./fr.json", - "language": "fr" - }, - { - "path": "./drinks/en.json", - "language": "en", - "namespace": "drinks" - }, - { - "path": "./drinks/fr.json", - "language": "fr", - "namespace": "drinks" - } - ] - } -} diff --git a/test/__fixtures__/validTolgeeRc/withPaths.json b/test/__fixtures__/validTolgeeRc/withPaths.json index 3ffe301b..6b15e128 100644 --- a/test/__fixtures__/validTolgeeRc/withPaths.json +++ b/test/__fixtures__/validTolgeeRc/withPaths.json @@ -1,5 +1,5 @@ { - "$schema": "../../../../schema.json", + "$schema": "../../../schema.json", "apiUrl": "https://app.tolgee.io", "projectId": 1337, "delimiter": "testDelimiter", diff --git a/test/__fixtures__/validTolgeeRc/withProjectIdString.json b/test/__fixtures__/validTolgeeRc/withProjectIdString.json index a9345a58..4392ce2a 100644 --- a/test/__fixtures__/validTolgeeRc/withProjectIdString.json +++ b/test/__fixtures__/validTolgeeRc/withProjectIdString.json @@ -1,4 +1,4 @@ { - "$schema": "../../../../schema.json", + "$schema": "../../../schema.json", "projectId": "1337" } diff --git a/test/e2e/detector.test.ts b/test/e2e/detector.test.ts index a9260960..ea20d118 100644 --- a/test/e2e/detector.test.ts +++ b/test/e2e/detector.test.ts @@ -1,51 +1,69 @@ import { fileURLToPathSlash } from './utils/toFilePath.js'; import { run } from './utils/run.js'; import { join } from 'path'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url); const PROJECT = fileURLToPathSlash(new URL('./parserDetection', FIXTURES_PATH)); -const CONFIG_REACT = join(PROJECT, 'config.react.json'); -const CONFIG_SVELTE = join(PROJECT, 'config.svelte.json'); -const CONFIG_VUE = join(PROJECT, 'config.vue.json'); -const CONFIG_UNKNOWN = join(PROJECT, 'config.unknown.json'); -const CONFIG_PARSER = join(PROJECT, 'config.parser.json'); -const CONFIG_MIXED = join(PROJECT, 'config.mixed.json'); - describe('parser detection from file extensions', () => { + afterEach(async () => { + await removeTmpFolder(); + }); + it('detects react', async () => { - const out = await run(['--config', CONFIG_REACT, 'extract', 'print']); + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.ts?(x)')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(0); expect(out.stdout).toContain('line 10: hello-react'); expect(out.stdout).toContain('line 4: hello-unknown'); }); it('detects svelte', async () => { - const out = await run(['--config', CONFIG_SVELTE, 'extract', 'print']); + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{svelte,ts,js}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(0); expect(out.stdout).toContain('line 4: hello-unknown'); expect(out.stdout).toContain('line 5: hello-svelte'); }); it('detects vue', async () => { - const out = await run(['--config', CONFIG_VUE, 'extract', 'print']); + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{vue,ts,js}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(0); expect(out.stdout).toContain('line 4: hello-unknown'); expect(out.stdout).toContain('line 6: hello-vue'); }); it('fails on unknown', async () => { - const out = await run(['--config', CONFIG_UNKNOWN, 'extract', 'print']); + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{ts,js}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(1); expect(out.stdout).toContain( "Couldn't detect which framework is used, use '--parser' or 'config.parser' option" ); }); - it('passes unknown on supplied parser in options', async () => { + it('passes unknown on supplied parser in options (args)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{ts,js}')], + }); + const out = await run([ '--config', - CONFIG_UNKNOWN, + configFile, '--parser', 'react', 'extract', @@ -55,24 +73,37 @@ describe('parser detection from file extensions', () => { expect(out.stdout).toContain('line 4: hello-unknown'); }); - it('passes when parser specified in config', async () => { - const out = await run(['--config', CONFIG_PARSER, 'extract', 'print']); + it('passes unknown on supplied parser in options (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + parser: 'react', + patterns: [join(PROJECT, './code/*.{ts,js}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(0); expect(out.stdout).toContain('line 4: hello-unknown'); }); it('fails when multiple parsers are possible', async () => { - const out = await run(['--config', CONFIG_MIXED, 'extract', 'print']); + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{ts?(x),svelte,vue}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); expect(out.code).toBe(1); expect(out.stdout).toContain( "Detected multiple possible frameworks used (react, vue, svelte), use '--parser' or 'config.parser' options" ); }); - it('passes multiple when parser supplied as option', async () => { + it('passes multiple when parser supplied as option (args)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, './code/*.{ts?(x),svelte,vue}')], + }); + const out = await run([ '--config', - CONFIG_MIXED, + configFile, '--parser', 'react', 'extract', @@ -82,4 +113,16 @@ describe('parser detection from file extensions', () => { expect(out.stdout).toContain('line 10: hello-react'); expect(out.stdout).toContain('line 4: hello-unknown'); }); + + it('passes multiple when parser supplied as option (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + parser: 'react', + patterns: [join(PROJECT, './code/*.{ts?(x),svelte,vue}')], + }); + + const out = await run(['--config', configFile, 'extract', 'print']); + expect(out.code).toBe(0); + expect(out.stdout).toContain('line 10: hello-react'); + expect(out.stdout).toContain('line 4: hello-unknown'); + }); }); diff --git a/test/e2e/extractCustom.test.ts b/test/e2e/extractCustom.test.ts index 857d28bb..59d2bb11 100644 --- a/test/e2e/extractCustom.test.ts +++ b/test/e2e/extractCustom.test.ts @@ -1,5 +1,6 @@ import { fileURLToPathSlash } from './utils/toFilePath.js'; import { run } from './utils/run.js'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url); const FAKE_PROJECT = new URL('./customExtractors/', FIXTURES_PATH); @@ -11,31 +12,68 @@ const TS_EXTRACTOR = fileURLToPathSlash( new URL('./extract-ts.ts', FAKE_PROJECT) ); -it('successfully uses a custom extractor written in JS', async () => { - const out = await run( - ['extract', 'print', '--extractor', JS_EXTRACTOR, '--patterns', TEST_FILE], - undefined, - 50e3 - ); - - console.log(out.stdout); - expect(out.code).toBe(0); - expect(out.stdout).toContain('STR_CRUDE'); - expect(out.stdout).toContain('STR_WORKING'); - expect(out.stdout).toContain('STR_NEW'); - expect(out.stdout).toContain('STR_PRODUCTION'); -}, 60e3); - -it('successfully uses a custom extractor written in TS', async () => { - const out = await run( - ['extract', 'print', '--extractor', TS_EXTRACTOR, '--patterns', TEST_FILE], - undefined, - 50e3 - ); - - expect(out.code).toBe(0); - expect(out.stdout).toContain('STR_CRUDE'); - expect(out.stdout).toContain('STR_WORKING'); - expect(out.stdout).toContain('STR_NEW'); - expect(out.stdout).toContain('STR_PRODUCTION'); -}, 60e3); +describe('custom extractor', () => { + it('successfully uses a custom extractor written in JS (arg)', async () => { + afterEach(async () => { + await removeTmpFolder(); + }); + + const out = await run( + [ + 'extract', + 'print', + '--extractor', + JS_EXTRACTOR, + '--patterns', + TEST_FILE, + ], + undefined, + 50e3 + ); + + expect(out.code).toBe(0); + expect(out.stdout).toContain('STR_CRUDE'); + expect(out.stdout).toContain('STR_WORKING'); + expect(out.stdout).toContain('STR_NEW'); + expect(out.stdout).toContain('STR_PRODUCTION'); + }, 60e3); + + it('successfully uses a custom extractor written in JS (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + extractor: JS_EXTRACTOR, + patterns: [TEST_FILE], + }); + const out = await run( + ['-c', configFile, 'extract', 'print'], + undefined, + 50e3 + ); + + expect(out.code).toBe(0); + expect(out.stdout).toContain('STR_CRUDE'); + expect(out.stdout).toContain('STR_WORKING'); + expect(out.stdout).toContain('STR_NEW'); + expect(out.stdout).toContain('STR_PRODUCTION'); + }, 60e3); + + it('successfully uses a custom extractor written in TS', async () => { + const out = await run( + [ + 'extract', + 'print', + '--extractor', + TS_EXTRACTOR, + '--patterns', + TEST_FILE, + ], + undefined, + 50e3 + ); + + expect(out.code).toBe(0); + expect(out.stdout).toContain('STR_CRUDE'); + expect(out.stdout).toContain('STR_WORKING'); + expect(out.stdout).toContain('STR_NEW'); + expect(out.stdout).toContain('STR_PRODUCTION'); + }, 60e3); +}); diff --git a/test/e2e/extractorOptions.test.ts b/test/e2e/extractorOptions.test.ts index 2d44d1aa..d1ff519d 100644 --- a/test/e2e/extractorOptions.test.ts +++ b/test/e2e/extractorOptions.test.ts @@ -1,10 +1,17 @@ import { fileURLToPathSlash } from './utils/toFilePath.js'; import { run } from './utils/run.js'; import { join } from 'path'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url); const PROJECT = fileURLToPathSlash(new URL('./parserOptions', FIXTURES_PATH)); +const PATTERNS = { + react: './code/*.tsx', + svelte: './code/*.svelte', + vue: './code/*.vue', +}; + function lns(input: string) { return input .split('\n') @@ -13,99 +20,119 @@ function lns(input: string) { .join('\n'); } -describe.each(['react', 'svelte', 'vue'])('extractor options', (parser) => { - it('gives warning when strict', async () => { - const config = join(PROJECT, `config.${parser}.json`); - const out = await run(['--config', config, 'extract', 'print']); - expect(out.code).toBe(0); - expect(lns(out.stdout)).toContain( - lns(` +describe.each(['react', 'svelte', 'vue'] as const)( + 'extractor options', + (parser) => { + afterEach(async () => { + await removeTmpFolder(); + }); + + it('gives warning when strict', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, PATTERNS[parser])], + }); + const out = await run(['--config', configFile, 'extract', 'print']); + expect(out.code).toBe(0); + expect(lns(out.stdout)).toContain( + lns(` line 4: Expected source of \`t\` function (useTranslate or getTranslate)`) - ); - expect(lns(out.stdout)).toContain( - lns(` + ); + expect(lns(out.stdout)).toContain( + lns(` line 5: key2 namespace: custom`) - ); - expect(lns(out.stdout)).toContain( - lns(` + ); + expect(lns(out.stdout)).toContain( + lns(` line 9: key3 namespace: namespace`) - ); - }); + ); + }); - it('no warning when not strict', async () => { - const config = join(PROJECT, `config.${parser}.json`); - const out = await run([ - '--config', - config, - '--no-strict-namespace', - 'extract', - 'print', - ]); - expect(out.code).toBe(0); - expect(lns(out.stdout)).toContain( - lns(` + it('no warning when not strict', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, PATTERNS[parser])], + }); + const out = await run([ + '--config', + configFile, + '--no-strict-namespace', + 'extract', + 'print', + ]); + expect(out.code).toBe(0); + expect(lns(out.stdout)).toContain( + lns(` line 4: key1 line 5: key2 namespace: custom line 9: key3 namespace: namespace`) - ); - }); + ); + }); - it('no warning when not strict (config)', async () => { - const config = join(PROJECT, `config.${parser}.noStrict.json`); + it('no warning when not strict (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, PATTERNS[parser])], + strictNamespace: false, + }); - const out = await run(['--config', config, 'extract', 'print']); - expect(out.code).toBe(0); - expect(lns(out.stdout)).toContain( - lns(` + const out = await run(['--config', configFile, 'extract', 'print']); + expect(out.code).toBe(0); + expect(lns(out.stdout)).toContain( + lns(` line 4: key1 line 5: key2 namespace: custom line 9: key3 namespace: namespace`) - ); - }); + ); + }); - it('default namespace used', async () => { - const config = join(PROJECT, `config.${parser}.json`); + it('default namespace used', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, PATTERNS[parser])], + }); - const out = await run([ - '--config', - config, - '--no-strict-namespace', - '--default-namespace', - 'default', - 'extract', - 'print', - ]); - expect(out.code).toBe(0); - expect(lns(out.stdout)).toContain( - lns(` + const out = await run([ + '--config', + configFile, + '--no-strict-namespace', + '--default-namespace', + 'default', + 'extract', + 'print', + ]); + expect(out.code).toBe(0); + expect(lns(out.stdout)).toContain( + lns(` line 4: key1 namespace: default line 5: key2 namespace: custom line 9: key3 namespace: namespace`) - ); - }); + ); + }); - it('default namespace used (config)', async () => { - const config = join(PROJECT, `config.${parser}.default.json`); + it('default namespace used (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + patterns: [join(PROJECT, PATTERNS[parser])], + strictNamespace: false, + defaultNamespace: 'default', + }); - const out = await run(['--config', config, 'extract', 'print']); - expect(out.code).toBe(0); - expect(lns(out.stdout)).toContain( - lns(` + const out = await run(['--config', configFile, 'extract', 'print']); + expect(out.code).toBe(0); + expect(lns(out.stdout)).toContain( + lns(` line 4: key1 namespace: default line 5: key2 namespace: custom line 9: key3 namespace: namespace`) - ); - }); -}); + ); + }); + } +); diff --git a/test/e2e/login.test.ts b/test/e2e/login.test.ts index da261720..d0010fe1 100644 --- a/test/e2e/login.test.ts +++ b/test/e2e/login.test.ts @@ -1,6 +1,6 @@ import { tmpdir } from 'os'; import { join } from 'path'; -import { rm, readFile, mkdtemp, writeFile } from 'fs/promises'; +import { rm, readFile } from 'fs/promises'; import { run } from './utils/run.js'; import { TolgeeClient } from '#cli/client/TolgeeClient.js'; import { PROJECT_1 } from './utils/api/project1.js'; @@ -10,11 +10,12 @@ import { createProjectWithClient, deleteProject, } from './utils/api/common.js'; -import { Schema } from '#cli/schema.js'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; const AUTH_FILE_PATH = join(tmpdir(), '.tolgee-e2e', 'authentication.json'); afterEach(async () => { + await removeTmpFolder(); try { await rm(AUTH_FILE_PATH); } catch (e: any) { @@ -122,11 +123,4 @@ describe('Project 1', () => { const out = await run(['-c', configFile, 'pull']); expect(out.code).toBe(0); }); - - async function createTmpFolderWithConfig(config: Schema) { - const tempFolder = await mkdtemp(join(tmpdir(), 'cli-project-')); - const configFile = join(tempFolder, '.tolgeerc.json'); - await writeFile(configFile, JSON.stringify(config, null, 2)); - return { tempFolder, configFile }; - } }); diff --git a/test/e2e/pull.test.ts b/test/e2e/pull.test.ts index 2ece49a3..1fddaf31 100644 --- a/test/e2e/pull.test.ts +++ b/test/e2e/pull.test.ts @@ -1,7 +1,12 @@ import { fileURLToPath } from 'url'; import { mkdir, readFile, rm, writeFile } from 'fs/promises'; import { readFileSync } from 'fs'; -import { TMP_FOLDER, setupTemporaryFolder } from './utils/tmp.js'; +import { + TMP_FOLDER, + createTmpFolderWithConfig, + removeTmpFolder, + setupTemporaryFolder, +} from './utils/tmp.js'; import { run } from './utils/run.js'; import './utils/toMatchContentsOf.js'; import { dirname, join } from 'path'; @@ -28,14 +33,6 @@ const PROJECT_3_DATA_ONLY_FOOD = fileURLToPath( new URL('./tolgeeImportData/test3-only-food', FIXTURES_PATH) ); -const NESTED_KEYS_PROJECT_FLAT_CONFIG = fileURLToPath( - new URL('./nestedKeysProject/tolgee.config.flat.json', FIXTURES_PATH) -); - -const NESTED_KEYS_PROJECT_NESTED_CONFIG = fileURLToPath( - new URL('./nestedKeysProject/tolgee.config.nested.json', FIXTURES_PATH) -); - let client: TolgeeClient; let pak: string; @@ -70,15 +67,29 @@ describe('Project 1', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); - it('pulls strings from Tolgee', async () => { + it('pulls strings from Tolgee with --path', async () => { const out = await run(['pull', '--api-key', pak, '--path', TMP_FOLDER]); expect(out.code).toBe(0); await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_1_DATA); }); + it('pulls strings from Tolgee with config', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_1_DATA); + }); + it('does empty existing folder if asked to (arg)', async () => { await mkdir(TMP_FOLDER); const existingFile = join(TMP_FOLDER, 'test'); @@ -95,6 +106,22 @@ describe('Project 1', () => { expect(out.code).toBe(0); await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_1_DATA); }); + + it('does empty existing folder if asked to (config)', async () => { + await mkdir(TMP_FOLDER); + const existingFile = join(TMP_FOLDER, 'test'); + await writeFile(existingFile, 'test'); + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + emptyDir: true, + }, + }); + const out = await run(['-c', configFile, 'pull']); + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_1_DATA); + }); }); describe('Project 3', () => { @@ -105,6 +132,7 @@ describe('Project 3', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); it('pulls strings with all namespaces from Tolgee', async () => { @@ -114,7 +142,7 @@ describe('Project 3', () => { await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_3_DATA); }); - it('pulls strings only from the specified namespaces', async () => { + it('pulls strings only from the specified namespaces (arg)', async () => { const namespaceFolder = 'food'; const out = await run([ 'pull', @@ -130,6 +158,20 @@ describe('Project 3', () => { await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_3_DATA_ONLY_FOOD); }); + it('pulls strings only from the specified namespaces (config)', async () => { + const namespaceFolder = 'food'; + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + namespaces: [namespaceFolder], + }, + }); + const out = await run(['-c', configFile, 'pull']); + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_3_DATA_ONLY_FOOD); + }); + it('keeps existing files in folders', async () => { await mkdir(TMP_FOLDER); const existingFile = join(TMP_FOLDER, 'food', 'test'); @@ -144,7 +186,7 @@ describe('Project 3', () => { await expect(TMP_FOLDER).toMatchStructureOf(PROJECT_3_DATA); }); - it('filters by languages', async () => { + it('filters by languages (arg)', async () => { await mkdir(TMP_FOLDER); const out = await run([ 'pull', @@ -165,7 +207,27 @@ describe('Project 3', () => { └── en.json`); }); - it('filters by namespace', async () => { + it('filters by languages (config)', async () => { + await mkdir(TMP_FOLDER); + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + languages: ['en'], + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchStructure(` +├── drinks/ +| └── en.json +├── en.json +└── food/ + └── en.json`); + }); + + it('filters by namespace (arg)', async () => { await mkdir(TMP_FOLDER); const out = await run([ 'pull', @@ -184,7 +246,25 @@ describe('Project 3', () => { └── fr.json`); }); - it('filters by tag', async () => { + it('filters by namespace (config)', async () => { + await mkdir(TMP_FOLDER); + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + namespaces: ['food'], + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchStructure(` +└── food/ + ├── en.json + └── fr.json`); + }); + + it('filters by tag (arg)', async () => { await prepareTags(client); await mkdir(TMP_FOLDER); const out = await run([ @@ -208,7 +288,31 @@ describe('Project 3', () => { expect(content).toEqual({ soda: 'Soda' }); }); - it('filters negatively by tag', async () => { + it('filters by tag (config)', async () => { + await prepareTags(client); + await mkdir(TMP_FOLDER); + + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + tags: ['soda_tag'], + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchStructure(` +└── drinks/ + ├── en.json + └── fr.json`); + + const content = (await import(join(TMP_FOLDER, 'drinks', 'en.json'))) + .default; + expect(content).toEqual({ soda: 'Soda' }); + }); + + it('filters negatively by tag (arg)', async () => { await prepareTags(client); await mkdir(TMP_FOLDER); const out = await run([ @@ -237,7 +341,36 @@ describe('Project 3', () => { expect(content).toEqual({ water: 'Water' }); }); - it('honors files template structure', async () => { + it('filters negatively by tag (config)', async () => { + await prepareTags(client); + await mkdir(TMP_FOLDER); + + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + excludeTags: ['soda_tag'], + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchStructure(` +├── drinks/ +| ├── en.json +| └── fr.json +├── en.json +├── food/ +| ├── en.json +| └── fr.json +└── fr.json`); + + const content = (await import(join(TMP_FOLDER, 'drinks', 'en.json'))) + .default; + expect(content).toEqual({ water: 'Water' }); + }); + + it('honors files template structure (arg)', async () => { await prepareTags(client); await mkdir(TMP_FOLDER); const out = await run([ @@ -261,6 +394,35 @@ describe('Project 3', () => { | ├── lang-en.json | └── lang-fr.json ├── lang-en.json +└── lang-fr.json`); + const content = (await import(join(TMP_FOLDER, 'drinks', 'lang-en.json'))) + .default; + expect(content).toEqual({ water: 'Water' }); + }); + + it('honors files template structure (config)', async () => { + await prepareTags(client); + await mkdir(TMP_FOLDER); + + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + excludeTags: ['soda_tag'], + fileStructureTemplate: '{namespace}/lang-{languageTag}.{extension}', + }, + }); + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchStructure(` +├── drinks/ +| ├── lang-en.json +| └── lang-fr.json +├── food/ +| ├── lang-en.json +| └── lang-fr.json +├── lang-en.json └── lang-fr.json`); const content = (await import(join(TMP_FOLDER, 'drinks', 'lang-en.json'))) .default; @@ -279,13 +441,14 @@ describe('Nested keys project', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); - it('pulls flat structure with config delmiter: null', async () => { + it('pulls flat structure with delimiter (arg)', async () => { const out = await run([ 'pull', - '-c', - NESTED_KEYS_PROJECT_FLAT_CONFIG, + // simulating empty string e.g. `--delimiter ""`, which somehow can't be passed here + '--delimiter=', '--api-key', pak, '--path', @@ -298,16 +461,15 @@ describe('Nested keys project', () => { }); }); - it('pulls flat structure with delimiter in parameter', async () => { - const out = await run([ - 'pull', - // simulating empty string e.g. `--delimiter ""`, which somehow can't be passed here - '--delimiter=', - '--api-key', - pak, - '--path', - TMP_FOLDER, - ]); + it('pulls flat structure with delmiter: null (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + delimiter: null, + path: TMP_FOLDER, + }, + }); + const out = await run(['-c', configFile, 'pull']); expect(out.code).toBe(0); expect(readJsonFile(join(TMP_FOLDER, 'en.json'))).toEqual({ @@ -315,11 +477,11 @@ describe('Nested keys project', () => { }); }); - it('pulls nested structure with delmiter: "."', async () => { + it('pulls nested structure with delimiter (arg)', async () => { const out = await run([ 'pull', - '-c', - NESTED_KEYS_PROJECT_NESTED_CONFIG, + '--delimiter', + '.', '--api-key', pak, '--path', @@ -332,16 +494,16 @@ describe('Nested keys project', () => { }); }); - it('pulls nested structure with delimiter in parameter', async () => { - const out = await run([ - 'pull', - '--delimiter', - '.', - '--api-key', - pak, - '--path', - TMP_FOLDER, - ]); + it('pulls nested structure with delimiter (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + delimiter: '.', + }, + }); + + const out = await run(['-c', configFile, 'pull']); expect(out.code).toBe(0); expect(readJsonFile(join(TMP_FOLDER, 'en.json'))).toEqual({ @@ -349,7 +511,7 @@ describe('Nested keys project', () => { }); }); - it('pulls nested structure with arrays', async () => { + it('pulls nested structure with arrays (arg)', async () => { const out = await run([ 'pull', '--support-arrays', @@ -366,6 +528,24 @@ describe('Nested keys project', () => { nested: { keyboard: 'Keyboard' }, }); }); + + it('pulls nested structure with arrays (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + pull: { + path: TMP_FOLDER, + delimiter: '.', + supportArrays: true, + }, + }); + + const out = await run(['-c', configFile, 'pull']); + + expect(out.code).toBe(0); + expect(readJsonFile(join(TMP_FOLDER, 'en.json'))).toEqual({ + nested: { keyboard: 'Keyboard' }, + }); + }); }); describe('Nested array keys project', () => { @@ -379,9 +559,10 @@ describe('Nested array keys project', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); - it('pulls nested structure with arrays', async () => { + it('pulls nested structure with arrays for json (arg)', async () => { const out = await run([ 'pull', '--support-arrays', @@ -408,29 +589,33 @@ describe('Nested array keys project', () => { }); }); - it('pulls nested structure with arrays', async () => { - const out = await run([ - 'pull', - '--support-arrays', - '--format', - 'YAML_ICU', - '--delimiter', - '.', - '--api-key', - pak, - '--path', - TMP_FOLDER, - ]); + it('pulls nested structure with arrays for json (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + format: 'JSON_ICU', + pull: { + path: TMP_FOLDER, + delimiter: '.', + supportArrays: true, + }, + }); + + const out = await run(['-c', configFile, 'pull']); expect(out.code).toBe(0); - expect(readFileSync(join(TMP_FOLDER, 'en.yaml')).toString()).toContain( - `nested: -- keyboard: "Keyboard 0" -- keyboard: "Keyboard 1"` - ); + expect(readJsonFile(join(TMP_FOLDER, 'en.json'))).toEqual({ + nested: [ + { + keyboard: 'Keyboard 0', + }, + { + keyboard: 'Keyboard 1', + }, + ], + }); }); - it('pulls nested structure with arrays', async () => { + it('pulls nested structure with arrays for yaml', async () => { const out = await run([ 'pull', '--support-arrays', diff --git a/test/e2e/push.test.ts b/test/e2e/push.test.ts index 0ff0807b..aaadcf86 100644 --- a/test/e2e/push.test.ts +++ b/test/e2e/push.test.ts @@ -11,21 +11,37 @@ import { TolgeeClient } from '#cli/client/TolgeeClient.js'; import { PROJECT_1 } from './utils/api/project1.js'; import { PROJECT_3 } from './utils/api/project3.js'; import { PROJECT_2 } from './utils/api/project2.js'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; +import { FileMatch } from '#cli/schema.js'; const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url); -const PROJECT_1_CONFIG = fileURLToPath( - new URL('./updatedProject1/tolgeerc.mjs', FIXTURES_PATH) -); -const PROJECT_2_CONFIG = fileURLToPath( - new URL('./updatedProject2WithConflicts/.tolgeerc', FIXTURES_PATH) -); -const PROJECT_3_CONFIG = fileURLToPath( - new URL('./updatedProject3/.tolgeerc', FIXTURES_PATH) -); -const PROJECT_3_DEPRECATED_CONFIG = fileURLToPath( - new URL('./updatedProject3DeprecatedKeys/.tolgeerc', FIXTURES_PATH) + +const PROJECT_1_DIR = new URL('./updatedProject1/', FIXTURES_PATH); + +const PROJECT_2_DIR = new URL('./updatedProject2WithConflicts/', FIXTURES_PATH); +const PROJECT_3_DIR = new URL('./updatedProject3/', FIXTURES_PATH); +const PROJECT_3_DEPRECATED_DIR = new URL( + './updatedProject3DeprecatedKeys/', + FIXTURES_PATH ); +function pushFilesConfig(base: URL, namespaces: string[] = ['']) { + const result: FileMatch[] = []; + for (const ns of namespaces) { + result.push({ + path: fileURLToPath(new URL(`./${ns}/en.json`, base)), + language: 'en', + namespace: ns, + }); + result.push({ + path: fileURLToPath(new URL(`./${ns}/fr.json`, base)), + language: 'fr', + namespace: ns, + }); + } + return result; +} + let client: TolgeeClient; let pak: string; @@ -36,12 +52,16 @@ describe('project 1', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); it('pushes updated strings to Tolgee', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { files: pushFilesConfig(PROJECT_1_DIR) }, + }); const out = await run([ '--config', - PROJECT_1_CONFIG, + configFile, '--api-key', pak, 'push', @@ -74,10 +94,14 @@ describe('project 1', () => { }); }); - it('pushes only selected languages', async () => { + it('pushes only selected languages (args)', async () => { + const config = { + push: { files: pushFilesConfig(PROJECT_1_DIR) }, + }; + const { configFile } = await createTmpFolderWithConfig(config); const out = await run([ '--config', - PROJECT_1_CONFIG, + configFile, '--api-key', pak, 'push', @@ -107,6 +131,39 @@ describe('project 1', () => { }, }); }); + + it('pushes only selected languages (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + push: { + files: pushFilesConfig(PROJECT_1_DIR), + languages: ['fr'], + }, + }); + const out = await run(['--config', configFile, 'push']); + + expect(out.code).toBe(0); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { search: 'wire' }, + }, + }); + expect(keys.data?.page?.totalElements).toBe(2); + + const stored = tolgeeDataToDict(keys.data); + expect(stored).toEqual({ + wired: { + __ns: null, + fr: 'Filaire', + }, + wireless: { + __ns: null, + fr: 'Sans-fil', + }, + }); + }); }); describe('project 3', () => { @@ -118,16 +175,17 @@ describe('project 3', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); it('pushes to Tolgee with correct namespaces', async () => { - const out = await run([ - '--config', - PROJECT_3_CONFIG, - '--api-key', - pak, - 'push', - ]); + const { configFile } = await createTmpFolderWithConfig({ + push: { + files: pushFilesConfig(PROJECT_3_DIR, ['', 'drinks', 'food']), + forceMode: 'OVERRIDE', + }, + }); + const out = await run(['--config', configFile, '--api-key', pak, 'push']); expect(out.code).toBe(0); const keys = await client.GET('/v2/projects/{projectId}/translations', { @@ -154,10 +212,16 @@ describe('project 3', () => { }); }); - it('pushes only selected namespaces and languages', async () => { + it('pushes only selected namespaces and languages (args)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { + files: pushFilesConfig(PROJECT_3_DIR, ['', 'drinks', 'food']), + forceMode: 'OVERRIDE', + }, + }); const out = await run([ '--config', - PROJECT_3_CONFIG, + configFile, 'push', '--api-key', pak, @@ -187,14 +251,50 @@ describe('project 3', () => { }); }); - it('removes other keys', async () => { + it('pushes only selected namespaces and languages (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + push: { + files: pushFilesConfig(PROJECT_3_DIR, ['', 'drinks', 'food']), + forceMode: 'OVERRIDE', + namespaces: ['drinks'], + }, + }); + const out = await run(['--config', configFile, 'push']); + expect(out.code).toBe(0); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { filterKeyName: ['water', 'glass'] }, + }, + }); + + expect(keys.data?.page?.totalElements).toBe(1); + + const stored = tolgeeDataToDict(keys.data); + expect(stored).toEqual({ + water: { + __ns: 'drinks', + en: 'Dihydrogen monoxide', + fr: 'Monoxyde de dihydrogène', + }, + }); + }); + + it('removes other keys (args)', async () => { const pakWithDelete = await createPak(client, [ ...DEFAULT_SCOPES, 'keys.delete', ]); + const { configFile } = await createTmpFolderWithConfig({ + push: { + files: pushFilesConfig(PROJECT_3_DEPRECATED_DIR, ['', 'drinks']), + }, + }); const out = await run([ '--config', - PROJECT_3_DEPRECATED_CONFIG, + configFile, 'push', '--api-key', pakWithDelete, @@ -219,6 +319,39 @@ describe('project 3', () => { 'water', ]); }); + + it('removes other keys (config)', async () => { + const pakWithDelete = await createPak(client, [ + ...DEFAULT_SCOPES, + 'keys.delete', + ]); + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pakWithDelete, + push: { + files: pushFilesConfig(PROJECT_3_DEPRECATED_DIR, ['', 'drinks']), + removeOtherKeys: true, + }, + }); + const out = await run(['--config', configFile, 'push']); + + expect(out.code).toEqual(0); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + }, + }); + + const stored = tolgeeDataToDict(keys.data); + + expect(Object.keys(stored)).toEqual([ + 'table', + 'chair', + 'plate', + 'fork', + 'water', + ]); + }); }); describe('project 2', () => { @@ -228,12 +361,16 @@ describe('project 2', () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); it('does not push strings to Tolgee if there are conflicts', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { files: pushFilesConfig(PROJECT_2_DIR) }, + }); const out = await run([ '--config', - PROJECT_2_CONFIG, + configFile, 'push', '--api-key', pak, @@ -261,10 +398,13 @@ describe('project 2', () => { }); }); - it('does preserve the remote strings when using KEEP', async () => { + it('does preserve the remote strings when using KEEP (args)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { files: pushFilesConfig(PROJECT_2_DIR) }, + }); const out = await run([ '--config', - PROJECT_2_CONFIG, + configFile, 'push', '--api-key', pak, @@ -298,9 +438,45 @@ describe('project 2', () => { }); }); + it('does preserve the remote strings when using KEEP (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + push: { files: pushFilesConfig(PROJECT_2_DIR), forceMode: 'KEEP' }, + }); + const out = await run(['--config', configFile, 'push']); + + expect(out.code).toBe(0); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { filterKeyName: ['cat-name', 'fox-name'] }, + }, + }); + + expect(keys.data?.page?.totalElements).toBe(2); + + const stored = tolgeeDataToDict(keys.data); + expect(stored).toEqual({ + 'cat-name': { + __ns: null, + en: 'Cat', + fr: 'Chat', + }, + 'fox-name': { + __ns: null, + en: 'Fox', + fr: 'Renard', + }, + }); + }); + it('asks for confirmation when there are conflicts', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { files: pushFilesConfig(PROJECT_2_DIR) }, + }); const out = await runWithStdin( - ['--config', PROJECT_2_CONFIG, 'push', '--api-key', pak], + ['--config', configFile, 'push', '--api-key', pak], 'OVERRIDE' ); @@ -330,10 +506,13 @@ describe('project 2', () => { }); }); - it('does override the remote strings when using OVERRIDE', async () => { + it('does override the remote strings when using OVERRIDE (args)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + push: { files: pushFilesConfig(PROJECT_2_DIR) }, + }); const out = await run([ '--config', - PROJECT_2_CONFIG, + configFile, 'push', '--api-key', pak, @@ -365,4 +544,36 @@ describe('project 2', () => { }, }); }); + + it('does override the remote strings when using OVERRIDE (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + push: { files: pushFilesConfig(PROJECT_2_DIR), forceMode: 'OVERRIDE' }, + }); + const out = await run(['--config', configFile, 'push']); + + expect(out.code).toBe(0); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { filterKeyName: ['cat-name', 'fox-name'] }, + }, + }); + expect(keys.data?.page?.totalElements).toBe(2); + + const stored = tolgeeDataToDict(keys.data); + expect(stored).toEqual({ + 'cat-name': { + __ns: null, + en: 'Kitty', + fr: 'Chaton', + }, + 'fox-name': { + __ns: null, + en: 'Fox', + fr: 'Renard', + }, + }); + }); }); diff --git a/test/e2e/sync.test.ts b/test/e2e/sync.test.ts index d41e56f2..f2cced3f 100644 --- a/test/e2e/sync.test.ts +++ b/test/e2e/sync.test.ts @@ -1,6 +1,11 @@ import { fileURLToPath } from 'url'; import { fileURLToPathSlash } from './utils/toFilePath.js'; -import { TMP_FOLDER, setupTemporaryFolder } from './utils/tmp.js'; +import { + TMP_FOLDER, + createTmpFolderWithConfig, + removeTmpFolder, + setupTemporaryFolder, +} from './utils/tmp.js'; import { tolgeeDataToDict } from './utils/data.js'; import { run } from './utils/run.js'; import './utils/toMatchContentsOf'; @@ -41,6 +46,7 @@ describe('Project 2', () => { afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); it('says projects are in sync when they do match', async () => { @@ -137,7 +143,7 @@ describe('Project 2', () => { }); }, 30e3); - it('deletes keys that no longer exist', async () => { + it('deletes keys that no longer exist via --remove-unused', async () => { const pakWithDelete = await createPak(client, [ ...DEFAULT_SCOPES, 'keys.delete', @@ -170,7 +176,36 @@ describe('Project 2', () => { expect(keys.data?.page?.totalElements).toBe(0); }, 30e3); - it('does a proper backup', async () => { + it('deletes keys that no longer exist via config', async () => { + const pakWithDelete = await createPak(client, [ + ...DEFAULT_SCOPES, + 'keys.delete', + ]); + + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pakWithDelete, + sync: { + removeUnused: true, + }, + patterns: [CODE_PROJECT_2_DELETED], + }); + + const out = await run(['-c', configFile, 'sync', '--yes'], undefined, 20e3); + + expect(out.code).toBe(0); + expect(out.stdout).toContain('- 2 strings'); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { filterKeyName: ['bird-name', 'bird-sound'] }, + }, + }); + + expect(keys.data?.page?.totalElements).toBe(0); + }, 30e3); + + it('does a proper backup (args)', async () => { const out = await run( [ 'sync', @@ -190,6 +225,20 @@ describe('Project 2', () => { await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_2_DATA); }, 30e3); + it('does a proper backup (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + sync: { + backup: TMP_FOLDER, + }, + patterns: [CODE_PROJECT_2_DELETED], + }); + const out = await run(['-c', configFile, 'sync'], undefined, 20e3); + + expect(out.code).toBe(0); + await expect(TMP_FOLDER).toMatchContentsOf(PROJECT_2_DATA); + }, 30e3); + it('logs warnings to stderr and aborts', async () => { const out = await run( ['sync', '--yes', '--api-key', pak, '--patterns', CODE_PROJECT_2_WARNING], @@ -201,7 +250,7 @@ describe('Project 2', () => { expect(out.stderr).toContain('Warnings were emitted'); }, 30e3); - it('continues when there are warnings and --continue-on-warning is set', async () => { + it('continues when there are warnings and --continue-on-warning is set (args)', async () => { const out = await run( [ 'sync', @@ -219,6 +268,20 @@ describe('Project 2', () => { expect(out.code).toBe(0); expect(out.stderr).toContain('Warnings were emitted'); }, 30e3); + + it('continues when there are warnings and --continue-on-warning is set (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + sync: { + continueOnWarning: true, + }, + patterns: [CODE_PROJECT_2_WARNING], + }); + const out = await run(['-c', configFile, 'sync'], undefined, 20e3); + + expect(out.code).toBe(0); + expect(out.stderr).toContain('Warnings were emitted'); + }, 30e3); }); describe('Project 3', () => { @@ -231,7 +294,7 @@ describe('Project 3', () => { await deleteProject(client); }); - it('handles namespaces properly', async () => { + it('handles namespaces properly (args)', async () => { const out = await run( ['sync', '--yes', '--api-key', pak, '--patterns', CODE_PROJECT_3], undefined, @@ -258,4 +321,32 @@ describe('Project 3', () => { }, }); }, 30e3); + + it('handles namespaces properly (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + apiKey: pak, + patterns: [CODE_PROJECT_3], + }); + const out = await run(['-c', configFile, 'sync', '--yes'], undefined, 20e3); + + expect(out.code).toBe(0); + expect(out.stdout).toContain('+ 1 string'); + + const keys = await client.GET('/v2/projects/{projectId}/translations', { + params: { + path: { projectId: client.getProjectId() }, + query: { filterKeyName: ['welcome'] }, + }, + }); + + expect(keys.data?.page?.totalElements).toBe(1); + + const stored = tolgeeDataToDict(keys.data); + expect(stored).toEqual({ + welcome: { + __ns: 'greeting', + en: 'Welcome!', + }, + }); + }, 30e3); }); diff --git a/test/e2e/tags.test.ts b/test/e2e/tags.test.ts index c0245ac0..c71ea29f 100644 --- a/test/e2e/tags.test.ts +++ b/test/e2e/tags.test.ts @@ -1,4 +1,5 @@ import { TolgeeClient } from '#cli/client/TolgeeClient.js'; +import { join } from 'path'; import { createPak, createProjectWithClient, @@ -8,11 +9,30 @@ import { PROJECT_1 } from './utils/api/project1.js'; import { ORIGINAL_TAGS, createTestTags, getTagsMap } from './utils/api/tags.js'; import { run } from './utils/run.js'; import { fileURLToPathSlash } from './utils/toFilePath.js'; +import { Schema } from '#cli/schema.js'; +import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js'; + +const TAGS_PROJECT_DIR = fileURLToPathSlash( + new URL('../__fixtures__/tagsProject/', import.meta.url) +); const TAGS_PROJECT_CONFIG = fileURLToPathSlash( new URL('../__fixtures__/tagsProject/config.json', import.meta.url) ); +const PROJECT_CONFIG_BASE = { + apiUrl: 'http://localhost:22222', + patterns: [join(TAGS_PROJECT_DIR, './react.tsx')], + push: { + files: [ + { + path: join(TAGS_PROJECT_DIR, './testfiles/en.json'), + language: 'en', + }, + ], + }, +} as const satisfies Schema; + let client: TolgeeClient; let pak: string; @@ -23,12 +43,14 @@ beforeEach(async () => { }); afterEach(async () => { await deleteProject(client); + await removeTmpFolder(); }); -it('updates production tags from extracted', async () => { +it('updates production tags from extracted (args)', async () => { + const { configFile } = await createTmpFolderWithConfig(PROJECT_CONFIG_BASE); const out = await run([ '-c', - TAGS_PROJECT_CONFIG, + configFile, '--api-key', pak, 'tag', @@ -47,10 +69,31 @@ it('updates production tags from extracted', async () => { }); }); -it('marks as deprecated', async () => { +it('updates production tags from extracted (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + ...PROJECT_CONFIG_BASE, + apiKey: pak, + tag: { + filterExtracted: true, + tag: ['production-v13'], + untag: ['production-*'], + }, + }); + const out = await run(['-c', configFile, 'tag']); + + expect(out.code).toBe(0); + + expect(await getTagsMap(client)).toEqual({ + ...ORIGINAL_TAGS, + controller: ['production-v13'], + }); +}); + +it('marks as deprecated (args)', async () => { + const { configFile } = await createTmpFolderWithConfig(PROJECT_CONFIG_BASE); const out = await run([ '-c', - TAGS_PROJECT_CONFIG, + configFile, '--api-key', pak, 'tag', @@ -71,10 +114,32 @@ it('marks as deprecated', async () => { }); }); -it('marks newly created keys as drafts', async () => { +it('marks as deprecated (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + ...PROJECT_CONFIG_BASE, + apiKey: pak, + tag: { + filterNotExtracted: true, + filterTag: ['production-*'], + tag: ['deprecated-v13'], + untag: ['production-*'], + }, + }); + const out = await run(['-c', configFile, 'tag']); + + expect(out.code).toBe(0); + + expect(await getTagsMap(client)).toEqual({ + ...ORIGINAL_TAGS, + desk: ['deprecated-v13'], + }); +}); + +it('marks newly created keys as drafts (args)', async () => { + const { configFile } = await createTmpFolderWithConfig(PROJECT_CONFIG_BASE); const out = await run([ '-c', - TAGS_PROJECT_CONFIG, + configFile, '--api-key', pak, 'push', @@ -88,10 +153,28 @@ it('marks newly created keys as drafts', async () => { }); }); -it('marks other keys', async () => { +it('marks newly created keys as drafts (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + ...PROJECT_CONFIG_BASE, + apiKey: pak, + push: { + ...PROJECT_CONFIG_BASE.push, + tagNewKeys: ['draft-another-branch'], + }, + }); + const out = await run(['-c', configFile, 'push']); + expect(out.code).toBe(0); + expect(await getTagsMap(client)).toEqual({ + ...ORIGINAL_TAGS, + new: ['draft-another-branch'], + }); +}); + +it('marks other keys (args)', async () => { + const { configFile } = await createTmpFolderWithConfig(PROJECT_CONFIG_BASE); const out = await run([ '-c', - TAGS_PROJECT_CONFIG, + configFile, '--api-key', pak, 'tag', @@ -112,6 +195,26 @@ it('marks other keys', async () => { }); }); +it('marks other keys (config)', async () => { + const { configFile } = await createTmpFolderWithConfig({ + ...PROJECT_CONFIG_BASE, + apiKey: pak, + tag: { + filterTag: ['production-*', 'draft-*', 'deprecated-*'], + tagOther: ['other'], + }, + }); + const out = await run(['-c', configFile, 'tag']); + + expect(out.code).toBe(0); + + expect(await getTagsMap(client)).toEqual({ + ...ORIGINAL_TAGS, + keyboard: ['other'], + remote: ['other'], + }); +}); + it('marks no key', async () => { const out = await run([ '-c', diff --git a/test/e2e/utils/tmp.ts b/test/e2e/utils/tmp.ts index f8c9dbe7..b9be4d3e 100644 --- a/test/e2e/utils/tmp.ts +++ b/test/e2e/utils/tmp.ts @@ -1,7 +1,8 @@ import { randomUUID } from 'crypto'; import { tmpdir } from 'os'; import { join } from 'path'; -import { rm } from 'fs/promises'; +import { mkdtemp, rm, writeFile } from 'fs/promises'; +import { Schema } from '#cli/schema.js'; export let TMP_FOLDER: string; @@ -20,3 +21,20 @@ export function setupTemporaryFolder() { } }); } + +export let TMP_TOLGEE_FOLDER: string | undefined; + +export async function createTmpFolderWithConfig(config: Schema) { + const tempFolder = await mkdtemp(join(tmpdir(), 'cli-project-')); + const configFile = join(tempFolder, '.tolgeerc.json'); + await writeFile(configFile, JSON.stringify(config, null, 2)); + TMP_TOLGEE_FOLDER = tempFolder; + return { tempFolder, configFile }; +} + +export async function removeTmpFolder() { + if (TMP_TOLGEE_FOLDER) { + await rm(TMP_TOLGEE_FOLDER, { recursive: true }); + TMP_TOLGEE_FOLDER = undefined; + } +}