From 554cb788de13698d6005b5e5859a2a562fd86846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B3=BD=E6=B1=9F?= Date: Tue, 14 May 2024 22:56:03 +0800 Subject: [PATCH] feat(bin): add support for extracting keys and values from expressions like t.t(key, text) --- src/bin/extra-text.ts | 185 +++++++++++++++++++++++++++++------- src/bin/index.ts | 10 +- test/bin/extra-text.test.ts | 26 +++-- 3 files changed, 173 insertions(+), 48 deletions(-) diff --git a/src/bin/extra-text.ts b/src/bin/extra-text.ts index 34b312f..a861c32 100644 --- a/src/bin/extra-text.ts +++ b/src/bin/extra-text.ts @@ -4,17 +4,39 @@ import chalk from './chalk' const fs = require('fs') /** - * 基于文本解析出tr函数包裹的内容 + * 验证文案是否有效 + * @param label 包含 `'" 符号的完整整文本 + * @param content 符号内的文本内容 + * @returns + */ +function validateLabelAndContent(label: string, content: string) { + if ( + (label.match(/^`.*`$/) && label.includes('${')) || + content.startsWith(' ') || + content.endsWith(' ') || + content.includes('\n') || + content.includes('\\n') || + content.includes('\t') || + content.includes('\\t') + ) + return false + + return true +} + +/** + * 基于文本解析出 t 函数包裹的内容 * @param fileContent * @param funcName 获取国际化文本的函数名 + * @param success 正确的列表 + * @param error 错误的列表 */ -export function extraText( +export function extraTextFromT( fileContent: string, - funcName, -): { - success: string[] // 正确的列表 - error: string[] // 错误的列表 -} { + funcName: string, + success: string[], + error: string[], +) { const regexp = new RegExp( /\WfuncName\(\n*[ ]*((['"`])(.+?)\2)(,|\))/.source.replace( 'funcName', @@ -22,30 +44,92 @@ export function extraText( ), 'g', ) - const success: string[] = [] - const error: string[] = [] let temp: string[] | null + while ((temp = regexp.exec(fileContent))) { const label = temp[1] const content = temp[3] - if ( - (label.match(/^`.*`$/) && label.includes('${')) || - content.startsWith(' ') || - content.endsWith(' ') || - content.includes('\n') || - content.includes('\\n') || - content.includes('\t') || - content.includes('\\t') - ) { + if (!validateLabelAndContent(label, content)) { error.push(content) } else { success.push(content) } } +} - return { - success, - error, +/** + * 基于文本解析出 t.t 函数包裹的内容 + * @param fileContent + * @param funcName 获取国际化文本的函数名 + * @param keyTextMap 自定义key与文案的映射(需要为一对一) + * @param textKeyMap 文案与自定义key与的映射(可以是一对多) + * @param textSuccess 正确的文案列表 + * @param textError 错误的文案列表 + * @param keySuccess 正确的自定义key列表 + * @param keyError 错误的自定义key列表 + */ +export function extraTextFromTDotT( + fileContent: string, + funcName, + keyTextMap: Record, + textKeyMap: Record, + textSuccess: string[], + textError: string[], + keySuccess: string[], + keyError: string[], +) { + const regexp = new RegExp( + /\WfuncName\.t\(\n*[ ]*((['"`])(.+?)\2),\n*[ ]*((['"`])(.+?)\5)(,|\))/.source.replace( + 'funcName', + funcName, + ), + 'g', + ) + + let temp: string[] | null + + while ((temp = regexp.exec(fileContent))) { + const keyLabel = temp[1] + const keyContent = temp[3] + const textLabel = temp[4] + const textContent = temp[6] + + const keyValidate = validateLabelAndContent(keyLabel, keyContent) + const textValidate = validateLabelAndContent(textLabel, textContent) + + if (keyValidate) { + keySuccess.push(keyContent) + } else { + keyError.push(keyContent) + } + if (textValidate) { + textSuccess.push(textContent) + } else { + textError.push(textLabel) + } + + // 自定义key和文案都有效 + if (keyValidate && textValidate) { + const text = keyTextMap[keyContent] + if (text && text != text) { + textError.push( + t( + '当前自定义key={0},配置了不一样的文案({1}和{2}),应该满足key与文案一对一的关系', + keyContent, + text, + textContent, + ), + ) + } else if (!text) { + keyTextMap[keyContent] = textContent + } + + const keys = textKeyMap[textContent] || [] + if (!keys.includes(keyContent)) { + keys.push(keyContent) + textKeyMap[textContent] = keys + } + } } } @@ -59,28 +143,61 @@ export default function extraTexts( filepaths: string[], funcName = 't', ): { - success: string[] // 正确的列表 - error: string[] // 错误的列表 + textSuccess: string[] // 正确的列表 + textError: string[] // 错误的列表 + keySuccess: string[] // 正确的列表 + keyError: string[] // 错误的列表 } { - let success: string[] = [] - let error: string[] = [] + let textSuccess: string[] = [] + let textError: string[] = [] + let keySuccess: string[] = [] + let keyError: string[] = [] + const keyTextMap: Record = {} + const textKeyMap: Record = {} + filepaths.forEach((filepath) => { const fileContent = fs.readFileSync(filepath, { encoding: 'utf-8', }) - const trTextRes = extraText(fileContent, funcName) - success.push(...trTextRes.success) - error.push(...trTextRes.error) + extraTextFromT(fileContent, funcName, textSuccess, textError) + extraTextFromTDotT( + fileContent, + funcName, + keyTextMap, + textKeyMap, + textSuccess, + textError, + keySuccess, + keyError, + ) }) - success = Array.from(new Set(success)) - error = Array.from(new Set(error)) + textSuccess = Array.from(new Set(textSuccess)) + textError = Array.from(new Set(textError)) + keySuccess = Array.from(new Set(keySuccess)) + keyError = Array.from(new Set(keyError)) - logSuccess(chalk.greenBright(t('解析符合要求的翻译文案数:')), success.length) - logSuccess(chalk.greenBright(t('解析不符合要求的翻译文案数:')), error.length) + logSuccess( + chalk.greenBright(t('解析符合要求的翻译文案数:')), + textSuccess.length, + ) + logSuccess( + chalk.greenBright(t('解析不符合要求的翻译文案数:')), + textError.length, + ) + logSuccess( + chalk.greenBright(t('解析符合要求的自定义key数:')), + keySuccess.length, + ) + logSuccess( + chalk.greenBright(t('解析不符合要求的自定义key数:')), + keyError.length, + ) return { - success, - error, + textSuccess, + textError, + keySuccess, + keyError, } } diff --git a/src/bin/index.ts b/src/bin/index.ts index e9b7673..73928d3 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -82,7 +82,7 @@ async function translateController({ }) const translateRes = await translateTextsToLangsImpl( - trTextRes.success, + trTextRes.textSuccess, sourceLangs, incrementalMode, ) @@ -101,20 +101,20 @@ async function translateController({ writeFilesSync({ filepath: path.join(outputPath, logDirname, 'texts.json'), - fileContent: trTextRes.success, + fileContent: trTextRes.textSuccess, showName: t( '提取的翻译文案({0})', - chalk.greenBright(trTextRes.success.length), + chalk.greenBright(trTextRes.textSuccess.length), ), indentSize, }) writeFilesSync({ filepath: path.join(outputPath, logDirname, 'texts-error.json'), - fileContent: trTextRes.error, + fileContent: trTextRes.textError, showName: t( '提取的编写不规范的翻译文案({0})', - chalk.redBright(trTextRes.error.length), + chalk.redBright(trTextRes.textError.length), ), indentSize, }) diff --git a/test/bin/extra-text.test.ts b/test/bin/extra-text.test.ts index e2b7625..e8840b4 100644 --- a/test/bin/extra-text.test.ts +++ b/test/bin/extra-text.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'fs' import { join } from 'path' import { binExtraText } from '../utils' -const { extraText } = binExtraText +const { extraTextFromT } = binExtraText describe('验证翻译文本提取功能', () => { const content = readFileSync(join(__dirname, '../i18n/text.ts'), { @@ -28,25 +28,33 @@ describe('验证翻译文本提取功能', () => { describe('验证规范文本提取', () => { it('默认提取', () => { - const texts = extraText(content, 't') - expect(texts.success).toEqual(successTexts) + const success = [] + const error = [] + extraTextFromT(content, 't', success, error) + expect(success).toEqual(successTexts) }) it('自定义函数名(t -> i18n)', () => { - const texts = extraText(content.replaceAll('t(', 'i18n('), 'i18n') - expect(texts.success).toEqual(successTexts) + const success = [] + const error = [] + extraTextFromT(content.replaceAll('t(', 'i18n('), 'i18n', success, error) + expect(success).toEqual(successTexts) }) }) describe('验证不规范文本提取', () => { it('默认提取', () => { - const texts = extraText(content, 't') - expect(texts.error[1]).toBe(errrorTexts[1]) + const success = [] + const error = [] + const texts = extraTextFromT(content, 't', success, error) + expect(error[1]).toBe(errrorTexts[1]) }) it('未知匹配函数名i18n', () => { - const texts = extraText(content, 'i18n') - expect(texts.success).toEqual([]) + const success = [] + const error = [] + const texts = extraTextFromT(content, 'i18n', success, error) + expect(success).toEqual([]) }) }) })