Skip to content

Commit

Permalink
feat: add parser option (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
freakzlike authored Sep 29, 2024
1 parent db8d3cc commit 08b5b1d
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 17 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__"
}
}
```

Expand Down Expand Up @@ -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
}
Expand Down
21 changes: 11 additions & 10 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,22 @@ export const getFileList = async (
*/
export const parseFile = async (options: I18nExtractOptions, filePath: string): Promise<TranslationKeyList> => {
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<TranslationKeyList> => {
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<TranslationKeyList> => {
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
}
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type TranslationMap <T extends TranslationResult = TranslationResult> = R
export type TranslationMapLoad = TranslationMap
export type TranslationMapWrite = TranslationMap<TranslationResultWrite>

export type CustomParser = (filePath: string, content: string) => (string[] | Set<string>)

export interface I18nExtractOptions {
input: string[]
output: string
Expand All @@ -28,5 +30,6 @@ export interface I18nExtractOptions {
namespaces?: Namespace[]
defaultValue?: string
parseRegex?: RegExp
parser?: CustomParser
keepMissing?: boolean
}
63 changes: 57 additions & 6 deletions tests/parse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
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: [
'examples/namespaces/src/**/*.vue',
'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
Expand Down Expand Up @@ -61,6 +63,43 @@ describe('parseFiles', () => {
'other_key'
])
})

it('should parse all files with custom parser', async () => {
const parser = vi.fn<CustomParser>((_, 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')
)
})
})

/**
Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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<CustomParser>((_, 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")')
})
})

0 comments on commit 08b5b1d

Please sign in to comment.