diff --git a/README.md b/README.md index 82172db..0418471 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,21 @@ Searchs for usage of `$t` and writes keys to JSON files. Input from source code ```javascript $t('some_key') + +// with namespace to split into separate files +$t('common:some_key') + +// nested key +$t('messages.success') ``` Output to translation file ```json { - "some_key": "__MISSING_TRANSLATION__" + "some_key": "__MISSING_TRANSLATION__", + "messages": { + "success": "__MISSING_TRANSLATION__" + } } ``` @@ -61,7 +70,12 @@ module.exports = { // Optional: Default value for new translations defaultValue: '__MISSING_TRANSLATION__', // Optional: Regex to extract transations from source code + // The translation key must be the first match parseRegex: /\B\$t\s*\(\s*['"]([\w/: ._-]+)['"]/g, + // Optional: Parser function, receives file content as string and should return list of found translation keys + // Type: (filePath: string, content: string) => string[] + parser: (filePath, content) => + [...content.matchAll(/\B\$t\s*\(\s*['"]([\w/: ._-]+)['"]/g)].map((matches) => matches[1]), // Optional: Keep missing translations and not delete them keepMissing: false } diff --git a/src/parse.ts b/src/parse.ts index f2c1abe..e4d9b4f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -44,21 +44,22 @@ export const getFileList = async ( */ export const parseFile = async (options: I18nExtractOptions, filePath: string): Promise => { const content = readFileSync(filePath, { encoding: 'utf8' }) - return parseContent(options, content) + return parseContent(options, filePath, content) } /** * Parse content and return found translation keys */ -export const parseContent = async (options: I18nExtractOptions, content: string): Promise => { - const regex = options.parseRegex ?? /\B\$t\s*\(\s*['"]([\w/: ._-]+)['"]/g - const matches: TranslationKeyList = new Set() - let match - do { - match = regex.exec(content) - if (match) { +export const parseContent = async (options: I18nExtractOptions, filePath: string, content: string): Promise => { + if (options.parser) { + const results = options.parser(filePath, content) + return results instanceof Set ? results : new Set(results) + } else { + const regex = options.parseRegex ?? /\B\$t\s*\(\s*['"]([\w/: ._-]+)['"]/g + const matches: TranslationKeyList = new Set() + for (const match of content.matchAll(regex)) { matches.add(match[1]) } - } while (match != null) - return matches + return matches + } } diff --git a/src/types.ts b/src/types.ts index 54b6634..202c0d0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,8 @@ export type TranslationMap = R export type TranslationMapLoad = TranslationMap export type TranslationMapWrite = TranslationMap +export type CustomParser = (filePath: string, content: string) => (string[] | Set) + export interface I18nExtractOptions { input: string[] output: string @@ -28,5 +30,6 @@ export interface I18nExtractOptions { namespaces?: Namespace[] defaultValue?: string parseRegex?: RegExp + parser?: CustomParser keepMissing?: boolean } diff --git a/tests/parse.spec.ts b/tests/parse.spec.ts index 5c40fc1..439614a 100644 --- a/tests/parse.spec.ts +++ b/tests/parse.spec.ts @@ -1,7 +1,8 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import path from 'node:path' import { getFileList, parseContent, parseFile, parseFiles } from '@/parse' -import type { I18nExtractOptions } from '@/types' +import { CustomParser, I18nExtractOptions } from '@/types' +import { readFileSync } from 'fs' const options: I18nExtractOptions = { input: [ @@ -9,10 +10,11 @@ const options: I18nExtractOptions = { 'examples/namespaces/src/**/*.ts', '!**/__tests__/**' ], - output: 'examples/default/locales/{{lng}}.json', + output: 'examples/namespaces/locales/{{lng}}.json', languages: ['de', 'en-GB'], defaultNamespace: 'common' } +const getFileContent = (filePath: string) => readFileSync(filePath, { encoding: 'utf8' }) /** * parseFiles @@ -61,6 +63,43 @@ describe('parseFiles', () => { 'other_key' ]) }) + + it('should parse all files with custom parser', async () => { + const parser = vi.fn((_, content) => + [...content.matchAll(/\B\$t\s*\('([\w/: _-]+)'/g)].map((matches) => matches[1])) + const results = await parseFiles({ + input: [ + 'examples/default/src/**/*.vue', + 'examples/default/src/**/*.ts', + '!**/__tests__/**' + ], + output: 'examples/default/locales/{{lng}}.json', + languages: ['de', 'en-GB'], + parser + }) + + expect(Object.keys(results).sort()).toStrictEqual(['default']) + expect(results.default).toStrictEqual([ + 'key_1', + 'key_2', + 'key_4', + 'new_key' + ]) + + expect(parser).toHaveBeenCalledTimes(3) + expect(parser).toHaveBeenCalledWith( + 'examples/default/src/i18n.ts', + getFileContent('examples/default/src/i18n.ts') + ) + expect(parser).toHaveBeenCalledWith( + 'examples/default/src/typescript-file.ts', + getFileContent('examples/default/src/typescript-file.ts') + ) + expect(parser).toHaveBeenCalledWith( + 'examples/default/src/vue-file.vue', + getFileContent('examples/default/src/vue-file.vue') + ) + }) }) /** @@ -140,7 +179,7 @@ describe('parseFile', () => { * parseContent */ describe('parseContent', () => { - const parseToArray = async (content: string) => Array.from(await parseContent(options, content)) + const parseToArray = async (content: string) => Array.from(await parseContent(options, 'some-file.txt', content)) it('should find translations from content', async () => { expect(await parseToArray('$t("key_1")')).toStrictEqual(['key_1']) @@ -185,7 +224,19 @@ describe('parseContent', () => { expect(Array.from(await parseContent({ ...options, parseRegex: /\B\$tc\s*\(\s*['"]([\w/: ._-]+)['"]/g - }, '$tc("key_1")'))).toStrictEqual(['key_1']) + }, 'some-file.txt', '$tc("key_1")'))).toStrictEqual(['key_1']) }) -}) + it('should parse with parser function', async () => { + const parser = vi.fn((_, content) => + [...content.matchAll(/\B\$tx\s*\(\s*['"]([\w/: ._-]+)['"]/g)].map((matches) => matches[1])) + + expect(Array.from(await parseContent({ + ...options, + parser + }, 'some-file.txt', '$tx("key_1")'))).toStrictEqual(['key_1']) + + expect(parser).toHaveBeenCalledTimes(1) + expect(parser).toHaveBeenCalledWith('some-file.txt', '$tx("key_1")') + }) +})