mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-02-06 19:11:09 +08:00
Some checks failed
Auto I18N Weekly / Auto I18N (push) Has been cancelled
* feat(i18n): clean hardcoded ui string add ci * fix(i18n): update snap * fix: test * fix(i18n): update plurals * chore: revert other branch change * refactor: use ast detect hardcoded string * chore: typo error * feat(i18n): add webview app * chore(i18n): update i18n * fix: pr comment * chore: distinguish label and labelKey * refactor: align v2 desgin
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
import * as path from 'path'
|
|
import { Node, Project } from 'ts-morph'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
HardcodedStringDetector,
|
|
hasCJK,
|
|
hasEnglishUIText,
|
|
isInCodeContext,
|
|
isNonUIString,
|
|
shouldSkipNode
|
|
} from '../check-hardcoded-strings'
|
|
|
|
function createTestProject() {
|
|
return new Project({
|
|
skipAddingFilesFromTsConfig: true,
|
|
skipFileDependencyResolution: true,
|
|
compilerOptions: { jsx: 2 } // React JSX
|
|
})
|
|
}
|
|
|
|
function findStringLiteral(project: Project, code: string, targetString: string): Node | undefined {
|
|
const sourceFile = project.createSourceFile('test.tsx', code, { overwrite: true })
|
|
let found: Node | undefined
|
|
sourceFile.forEachDescendant((node) => {
|
|
if (Node.isStringLiteral(node) && node.getLiteralValue() === targetString) {
|
|
found = node
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
function findTemplateLiteral(project: Project, code: string): Node | undefined {
|
|
const sourceFile = project.createSourceFile('test.tsx', code, { overwrite: true })
|
|
let found: Node | undefined
|
|
sourceFile.forEachDescendant((node) => {
|
|
if (Node.isNoSubstitutionTemplateLiteral(node) || Node.isTemplateExpression(node)) {
|
|
found = node
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
// Mock fs module
|
|
vi.mock('fs')
|
|
|
|
describe('check-hardcoded-strings', () => {
|
|
const mockSrcDir = '/mock/src/renderer/src'
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('hasCJK', () => {
|
|
it('should detect Chinese characters', () => {
|
|
expect(hasCJK('测试文本')).toBe(true)
|
|
expect(hasCJK('Hello 世界')).toBe(true)
|
|
expect(hasCJK('中文')).toBe(true)
|
|
})
|
|
|
|
it('should detect Japanese characters', () => {
|
|
expect(hasCJK('こんにちは')).toBe(true) // Hiragana
|
|
expect(hasCJK('カタカナ')).toBe(true) // Katakana
|
|
expect(hasCJK('日本語')).toBe(true) // Kanji
|
|
})
|
|
|
|
it('should detect Korean characters', () => {
|
|
expect(hasCJK('한국어')).toBe(true) // Hangul
|
|
expect(hasCJK('안녕하세요')).toBe(true)
|
|
})
|
|
|
|
it('should return false for non-CJK text', () => {
|
|
expect(hasCJK('Hello World')).toBe(false)
|
|
expect(hasCJK('12345')).toBe(false)
|
|
expect(hasCJK('')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('hasEnglishUIText', () => {
|
|
it('should detect English UI text patterns', () => {
|
|
expect(hasEnglishUIText('Create New File')).toBe(true)
|
|
expect(hasEnglishUIText('Save As')).toBe(true)
|
|
expect(hasEnglishUIText('Open Project')).toBe(true)
|
|
})
|
|
|
|
it('should reject single words', () => {
|
|
expect(hasEnglishUIText('Save')).toBe(false)
|
|
expect(hasEnglishUIText('Cancel')).toBe(false)
|
|
})
|
|
|
|
it('should reject lowercase text', () => {
|
|
expect(hasEnglishUIText('create new file')).toBe(false)
|
|
expect(hasEnglishUIText('save as')).toBe(false)
|
|
})
|
|
|
|
it('should reject too long phrases', () => {
|
|
expect(hasEnglishUIText('This Is A Very Long Phrase With Many Words')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isNonUIString', () => {
|
|
it('should identify empty strings', () => {
|
|
expect(isNonUIString('')).toBe(true)
|
|
})
|
|
|
|
it('should identify pure numbers', () => {
|
|
expect(isNonUIString('123')).toBe(true)
|
|
expect(isNonUIString('0')).toBe(true)
|
|
expect(isNonUIString('999')).toBe(true)
|
|
})
|
|
|
|
it('should not mark regular UI text as non-UI', () => {
|
|
expect(isNonUIString('Hello World')).toBe(false)
|
|
expect(isNonUIString('Save')).toBe(false)
|
|
expect(isNonUIString('确认')).toBe(false)
|
|
expect(isNonUIString('请输入内容')).toBe(false)
|
|
expect(isNonUIString('-')).toBe(false) // Even short strings may be UI in specific contexts
|
|
})
|
|
|
|
it('should not filter technical strings (now handled by AST context)', () => {
|
|
// With AST-based detection, these are no longer filtered
|
|
// because we only check specific UI contexts where they rarely appear
|
|
expect(isNonUIString('./path/to/file')).toBe(false)
|
|
expect(isNonUIString('https://example.com')).toBe(false)
|
|
expect(isNonUIString('#fff')).toBe(false)
|
|
expect(isNonUIString('snake_case_id')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('File filtering', () => {
|
|
const IGNORED_DIRS = ['__tests__', 'node_modules', 'i18n', 'locales', 'types', 'assets']
|
|
const IGNORED_FILES = ['*.test.ts', '*.test.tsx', '*.d.ts']
|
|
|
|
const mockShouldSkipFile = (filePath: string): boolean => {
|
|
const relativePath = filePath.replace(mockSrcDir + '/', '')
|
|
|
|
if (IGNORED_DIRS.some((dir) => relativePath.includes(dir))) {
|
|
return true
|
|
}
|
|
|
|
const fileName = path.basename(filePath)
|
|
if (
|
|
IGNORED_FILES.some((pattern) => {
|
|
const regex = new RegExp(pattern.replace('*', '.*'))
|
|
return regex.test(fileName)
|
|
})
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
it('should skip test files', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/components/Button.test.tsx`)).toBe(true)
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/utils/helper.test.ts`)).toBe(true)
|
|
})
|
|
|
|
it('should skip type definition files', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/types/index.d.ts`)).toBe(true)
|
|
})
|
|
|
|
it('should skip i18n/locales directories', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/i18n/locales/en-us.json`)).toBe(true)
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/locales/zh-cn.json`)).toBe(true)
|
|
})
|
|
|
|
it('should skip __tests__ directories', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/components/__tests__/Button.test.tsx`)).toBe(true)
|
|
})
|
|
|
|
it('should NOT skip regular component files', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/components/Button.tsx`)).toBe(false)
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/pages/Home.tsx`)).toBe(false)
|
|
})
|
|
|
|
it('should NOT skip regular TypeScript files', () => {
|
|
expect(mockShouldSkipFile(`${mockSrcDir}/utils/helper.ts`)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('HardcodedStringDetector', () => {
|
|
// These are integration tests that would require actual files
|
|
// For unit testing, we test the exported utility functions instead
|
|
|
|
it('should be instantiable', () => {
|
|
const detector = new HardcodedStringDetector()
|
|
expect(detector).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('Legacy pattern compatibility (regex patterns for reference)', () => {
|
|
// Keep legacy pattern tests for backward compatibility reference
|
|
const CHINESE_PATTERNS = [
|
|
{ regex: />([^<]*[\u4e00-\u9fff][^<]*)</g, name: 'JSX text content' },
|
|
{
|
|
regex: /(?:placeholder|title|label|message|description|tooltip)=["']([^"']*[\u4e00-\u9fff][^"']*)["']/g,
|
|
name: 'attribute'
|
|
}
|
|
]
|
|
|
|
it('should detect Chinese characters in JSX text content (regex)', () => {
|
|
const testLine = '<span>测试文本</span>'
|
|
const matches = testLine.match(CHINESE_PATTERNS[0].regex)
|
|
expect(matches).not.toBeNull()
|
|
})
|
|
|
|
it('should detect Chinese characters in placeholder attribute (regex)', () => {
|
|
const testLine = 'placeholder="请输入内容"'
|
|
const matches = testLine.match(CHINESE_PATTERNS[1].regex)
|
|
expect(matches).not.toBeNull()
|
|
})
|
|
|
|
it('should detect Chinese characters in title attribute (regex)', () => {
|
|
const testLine = 'title="提示信息"'
|
|
const matches = testLine.match(CHINESE_PATTERNS[1].regex)
|
|
expect(matches).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('shouldSkipNode', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = createTestProject()
|
|
})
|
|
|
|
it('should skip import declarations', () => {
|
|
const node = findStringLiteral(project, `import { foo } from 'some-module'`, 'some-module')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip export declarations', () => {
|
|
const node = findStringLiteral(project, `export { foo } from 'some-module'`, 'some-module')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip logger calls', () => {
|
|
const node = findStringLiteral(project, `logger.info('测试日志')`, '测试日志')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip console calls', () => {
|
|
const node = findStringLiteral(project, `console.log('测试日志')`, '测试日志')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip t() translation function calls', () => {
|
|
const node = findStringLiteral(project, `t('common.save')`, 'common.save')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip type alias declarations', () => {
|
|
const node = findStringLiteral(project, `type Status = '成功' | '失败'`, '成功')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip interface declarations', () => {
|
|
const node = findStringLiteral(project, `interface Foo { status: '成功' }`, '成功')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip enum members', () => {
|
|
const node = findStringLiteral(project, `enum Status { Success = '成功' }`, '成功')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should skip language/locale variable declarations', () => {
|
|
const node = findStringLiteral(project, `const languageOptions = ['中文', 'English']`, '中文')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(true)
|
|
})
|
|
|
|
it('should NOT skip regular string literals', () => {
|
|
const node = findStringLiteral(project, `const message = '测试消息'`, '测试消息')
|
|
expect(node).toBeDefined()
|
|
expect(shouldSkipNode(node!)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isInCodeContext', () => {
|
|
let project: Project
|
|
|
|
beforeEach(() => {
|
|
project = createTestProject()
|
|
})
|
|
|
|
it('should detect tagged template expressions with css tag', () => {
|
|
const node = findTemplateLiteral(project, 'const style = css`color: red;`')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect tagged template expressions with styled tag', () => {
|
|
const node = findTemplateLiteral(project, 'const Button = styled.button`padding: 10px;`')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect CSS variable names', () => {
|
|
const node = findStringLiteral(project, `const customStyle = 'color: blue'`, 'color: blue')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect code variable names', () => {
|
|
const node = findStringLiteral(project, `const pythonCode = 'print("hello")'`, 'print("hello")')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect CSS property assignments', () => {
|
|
const node = findStringLiteral(project, `const obj = { style: 'color: red' }`, 'color: red')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect code property assignments', () => {
|
|
const node = findStringLiteral(project, `const obj = { script: 'console.log(1)' }`, 'console.log(1)')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect JSX style attributes', () => {
|
|
const node = findStringLiteral(project, `<div style={'color: red'} />`, 'color: red')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect executeJavaScript calls', () => {
|
|
const node = findStringLiteral(project, `webview.executeJavaScript('document.title')`, 'document.title')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should detect executeJavaScript with string concatenation', () => {
|
|
const node = findStringLiteral(project, `webview.executeJavaScript('var x = ' + value + ';')`, 'var x = ')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(true)
|
|
})
|
|
|
|
it('should NOT detect regular strings', () => {
|
|
const node = findStringLiteral(project, `const message = '普通消息'`, '普通消息')
|
|
expect(node).toBeDefined()
|
|
expect(isInCodeContext(node!)).toBe(false)
|
|
})
|
|
})
|
|
})
|