style: format with vp fmt (#38803)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou
2026-07-12 15:57:46 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent fde08d24fe
commit a84c2d36a3
6213 changed files with 227959 additions and 187183 deletions
@@ -334,7 +334,9 @@ vi.mock('#i18n', () => ({
// Assert
expect(result.output).toContain(`vi.mock('#i18n', async () => {`)
expect(result.output).toContain(`const { withSelectorKey } = await import('@/test/i18n-mock')`)
expect(result.output).toContain(
`const { withSelectorKey } = await import('@/test/i18n-mock')`,
)
expect(result.output).toContain('t: withSelectorKey((key: string) => key)')
expect(transformSource(result.output, 'example.spec.ts')).toEqual({
changes: 0,
@@ -359,7 +361,9 @@ vi.mock('#i18n', () => ({
const result = transformSource(source, 'example.spec.ts')
// Assert
expect(result.output).toContain('t: withSelectorKey((...args: Parameters<typeof mockTranslation>) => mockTranslation(...args))')
expect(result.output).toContain(
't: withSelectorKey((...args: Parameters<typeof mockTranslation>) => mockTranslation(...args))',
)
expect(transformSource(result.output, 'example.spec.ts')).toEqual({
changes: 0,
output: result.output,
@@ -418,14 +422,17 @@ vi.mock('react-i18next', () => ({
// Act
const result = transformSource(source, 'example.spec.tsx')
const diagnostics = ts.transpileModule(result.output, {
compilerOptions: { jsx: ts.JsxEmit.ReactJSX },
fileName: 'example.spec.tsx',
reportDiagnostics: true,
}).diagnostics ?? []
const diagnostics =
ts.transpileModule(result.output, {
compilerOptions: { jsx: ts.JsxEmit.ReactJSX },
fileName: 'example.spec.tsx',
reportDiagnostics: true,
}).diagnostics ?? []
// Assert
expect(diagnostics.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error)).toEqual([])
expect(
diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error),
).toEqual([])
expect(result.output).toContain('{components?.Key}')
expect(result.output).not.toMatch(/[ \t]+$/m)
expect(transformSource(result.output, 'example.spec.tsx')).toEqual({
+185 -83
View File
@@ -8,11 +8,7 @@ let webRoot: string
function writeJson(relativePath: string, value: Record<string, string>) {
mkdirSync(path.dirname(path.join(webRoot, relativePath)), { recursive: true })
writeFileSync(
path.join(webRoot, relativePath),
`${JSON.stringify(value, null, 2)}\n`,
'utf8',
)
writeFileSync(path.join(webRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`, 'utf8')
}
function writeSource(relativePath: string, content: string) {
@@ -20,10 +16,14 @@ function writeSource(relativePath: string, content: string) {
writeFileSync(path.join(webRoot, relativePath), content, 'utf8')
}
function sortedUnusedKeysByNamespace(result: Awaited<ReturnType<typeof analyzeUnusedTranslations>>) {
function sortedUnusedKeysByNamespace(
result: Awaited<ReturnType<typeof analyzeUnusedTranslations>>,
) {
return Object.fromEntries(
Object.entries(result.unusedKeysByNamespace)
.map(([namespace, keys]) => [namespace, [...keys].sort()]),
Object.entries(result.unusedKeysByNamespace).map(([namespace, keys]) => [
namespace,
[...keys].sort(),
]),
)
}
@@ -45,7 +45,9 @@ describe('prune-unused-i18n', () => {
'account.changeEmail.description': 'Description',
'account.changeEmail.unused': 'Unused',
})
writeSource('src/selectors.tsx', `
writeSource(
'src/selectors.tsx',
`
import { Trans, useTranslation } from 'react-i18next'
export function SelectorExample() {
@@ -61,7 +63,8 @@ describe('prune-unused-i18n', () => {
</Trans>
)
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -80,14 +83,17 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/common.json', {
'unused.common': 'Unused common key',
})
writeSource('src/default-namespace.tsx', `
writeSource(
'src/default-namespace.tsx',
`
import { useTranslation } from 'react-i18next'
export function DefaultNamespaceExample(key: string) {
const { t } = useTranslation()
return t($ => $[key])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -106,14 +112,17 @@ describe('prune-unused-i18n', () => {
members_other: '{{count}} members',
unused: 'Unused',
})
writeSource('src/selector-variable.ts', `
writeSource(
'src/selector-variable.ts',
`
import type { SelectorParam } from 'i18next'
import { createInstance } from 'i18next'
const instance = createInstance()
const memberKey: SelectorParam<'app'> = $ => $['members']
instance.t(memberKey, { count: 2 })
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -132,7 +141,9 @@ describe('prune-unused-i18n', () => {
second: 'Second',
unused: 'Unused',
})
writeSource('src/selector-map.ts', `
writeSource(
'src/selector-map.ts',
`
import type { SelectorParam } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -151,7 +162,8 @@ describe('prune-unused-i18n', () => {
const selector = enabled ? selectors[mode] : undefined
return selector ? t(selector) : null
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -169,7 +181,9 @@ describe('prune-unused-i18n', () => {
hidden: 'Potentially used by the dynamic selector',
used: 'Used',
})
writeSource('src/mixed-selector-map.ts', `
writeSource(
'src/mixed-selector-map.ts',
`
import type { SelectorParam } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -181,7 +195,8 @@ describe('prune-unused-i18n', () => {
}
return t(selectors[kind as keyof typeof selectors])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -197,7 +212,9 @@ describe('prune-unused-i18n', () => {
hidden: 'Potentially used by the dynamic selector',
used: 'Used',
})
writeSource('src/overridden-selector-map.ts', `
writeSource(
'src/overridden-selector-map.ts',
`
import type { SelectorParam } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -218,7 +235,8 @@ describe('prune-unused-i18n', () => {
}
return t(selectors.known)
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -234,7 +252,9 @@ describe('prune-unused-i18n', () => {
unused: 'Unused',
used: 'Used',
})
writeSource('src/computed-selector-map.ts', `
writeSource(
'src/computed-selector-map.ts',
`
import { useTranslation } from 'react-i18next'
const property = 'label'
@@ -246,7 +266,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation()
return t(selectors[property])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -264,7 +285,9 @@ describe('prune-unused-i18n', () => {
used: 'Used',
unused: 'Unused',
})
writeSource('src/unrelated-generic.ts', `
writeSource(
'src/unrelated-generic.ts',
`
import { useTranslation } from 'react-i18next'
const identity = <Value>(value: Value) => value
@@ -274,7 +297,8 @@ describe('prune-unused-i18n', () => {
identity('not.a.translation.key')
return t($ => $['used'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -292,7 +316,9 @@ describe('prune-unused-i18n', () => {
used: 'Used',
unused: 'Unused',
})
writeSource('src/translation-adapter.ts', `
writeSource(
'src/translation-adapter.ts',
`
import type { SelectorParam } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -307,7 +333,8 @@ describe('prune-unused-i18n', () => {
const translate: Translate = selector => t(selector)
return renderLabel(translate)
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -325,10 +352,12 @@ describe('prune-unused-i18n', () => {
'unused.app': 'Unused app key',
})
writeJson('i18n/en-US/deployments.json', {
'unused': 'Unused deployment key',
unused: 'Unused deployment key',
'versions.deployTo': 'Deploy to {{name}}',
})
writeSource('src/destructured-translation.ts', `
writeSource(
'src/destructured-translation.ts',
`
import type { SelectorParam } from 'i18next'
type DeploymentTranslate = <Selector extends SelectorParam<'deployments'>>(
@@ -339,7 +368,8 @@ describe('prune-unused-i18n', () => {
export function buildLabel({ t }: { t: DeploymentTranslate }) {
return t($ => $['versions.deployTo'], { name: 'Production' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -362,7 +392,9 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/app.json', {
'unused.app': 'Unused app key',
})
writeSource('src/named-translation-parameter.ts', `
writeSource(
'src/named-translation-parameter.ts',
`
import type { SelectorParam } from 'i18next'
type AgentTranslate = <Selector extends SelectorParam<'agentV2'>>(
@@ -372,7 +404,8 @@ describe('prune-unused-i18n', () => {
export function buildLabel(t: AgentTranslate) {
return t($ => $['agentDetail.used'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -396,7 +429,9 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/app.json', {
'unused.app': 'Unused app key',
})
writeSource('src/branded-translation-parameter.ts', `
writeSource(
'src/branded-translation-parameter.ts',
`
type AgentTranslate = {
readonly $TFunctionBrand: 'agentV2'
(selector: (source: Record<string, string>) => string): string
@@ -409,7 +444,8 @@ describe('prune-unused-i18n', () => {
export function destructuredLabel({ t }: { t: AgentTranslate }) {
return t($ => $['agentDetail.destructured'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -432,7 +468,9 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/common.json', {
'unused.common': 'Unused common key',
})
writeSource('src/block-body-adapter.ts', `
writeSource(
'src/block-body-adapter.ts',
`
import type { SelectorParam } from 'i18next'
type Translate = <Selector extends SelectorParam<'app'>>(
@@ -452,7 +490,8 @@ describe('prune-unused-i18n', () => {
export function renderLabel(translate: Translate) {
return getStringTranslate(translate)($ => $['used'], { ns: 'app' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -471,7 +510,9 @@ describe('prune-unused-i18n', () => {
unused: 'Unused',
used: 'Used',
})
writeSource('src/named-selector-adapter.ts', `
writeSource(
'src/named-selector-adapter.ts',
`
import type { SelectorParam } from 'i18next'
type Translate = <Selector extends SelectorParam<'workflow'>>(
@@ -492,7 +533,8 @@ describe('prune-unused-i18n', () => {
export function renderLabel(translate: Translate) {
return translateString(translate, $ => $['used'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -509,9 +551,11 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/plugin.json', {
'source.first': 'First source',
'source.second': 'Second source',
'unused': 'Unused',
unused: 'Unused',
})
writeSource('src/nested-selector-map.ts', `
writeSource(
'src/nested-selector-map.ts',
`
import type { SelectorParam } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -543,7 +587,8 @@ describe('prune-unused-i18n', () => {
const config = sourceConfigs[source]
return t(config.tipSelector, { ns: 'plugin' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -561,7 +606,9 @@ describe('prune-unused-i18n', () => {
hidden: 'Potentially used',
used: 'Used',
})
writeSource('src/untyped-adapter.js', `
writeSource(
'src/untyped-adapter.js',
`
import { useTranslation } from 'react-i18next'
export function UntypedAdapter() {
@@ -569,7 +616,8 @@ describe('prune-unused-i18n', () => {
const translate = selector => t(selector)
return translate($ => $['used'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -587,7 +635,9 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/permission-keys.json', {
'server.permission': 'Server permission',
})
writeSource('src/open-key-adapter.ts', `
writeSource(
'src/open-key-adapter.ts',
`
import type { SelectorKey } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -595,7 +645,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation()
return (key: string) => t(key as SelectorKey, { ns: 'permissionKeys' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -612,16 +663,19 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/plugin.json', {
'voice.language.enUS': 'English',
'voice.language.zhCN': 'Chinese',
'unrelated': 'Unrelated',
unrelated: 'Unrelated',
})
writeSource('src/dynamic-selector.tsx', `
writeSource(
'src/dynamic-selector.tsx',
`
import { useTranslation } from 'react-i18next'
export function DynamicSelectorExample(language: string) {
const { t } = useTranslation('plugin')
return t($ => $[\`voice.language.\${language}\`])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -642,7 +696,9 @@ describe('prune-unused-i18n', () => {
'status.failed': 'Failed',
'status.unused': 'Unused',
})
writeSource('src/typed-selector.tsx', `
writeSource(
'src/typed-selector.tsx',
`
import { useTranslation } from 'react-i18next'
type StatusKey = 'status.ready' | 'status.failed'
@@ -651,7 +707,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation('common')
return t($ => $[statusKey])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -671,14 +728,17 @@ describe('prune-unused-i18n', () => {
'operation.close': 'Close',
'unused.common': 'Unused common',
})
writeSource('src/multi-namespace-selector.tsx', `
writeSource(
'src/multi-namespace-selector.tsx',
`
import { useTranslation } from 'react-i18next'
export function MultiNamespaceSelectorExample() {
const { t } = useTranslation(['app', 'common'])
return t($ => $.common['operation.close'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -700,7 +760,9 @@ describe('prune-unused-i18n', () => {
confirm: 'Confirm',
unused: 'Unused',
})
writeSource('src/secondary-selector-access.tsx', `
writeSource(
'src/secondary-selector-access.tsx',
`
import { useTranslation } from 'react-i18next'
export function SecondarySelectorAccessExample() {
@@ -708,7 +770,8 @@ describe('prune-unused-i18n', () => {
t($ => $.common.close)
return t($ => $['common']['confirm'])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -729,14 +792,17 @@ describe('prune-unused-i18n', () => {
writeJson('i18n/en-US/common.json', {
'maybe.used': 'Maybe used',
})
writeSource('src/unresolved-namespace-selector.tsx', `
writeSource(
'src/unresolved-namespace-selector.tsx',
`
import { useTranslation } from 'react-i18next'
export function UnresolvedNamespaceSelectorExample(keyFromServer: string) {
const { t } = useTranslation(['app', 'common'])
return t($ => $.common[keyFromServer])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -752,7 +818,7 @@ describe('prune-unused-i18n', () => {
// Arrange
writeJson('i18n/en-US/app.json', {
'literal.title': 'Title',
'withDefault': 'With default',
withDefault: 'With default',
'trans.shared': 'Shared app',
'unused.app': 'Unused app',
})
@@ -761,7 +827,9 @@ describe('prune-unused-i18n', () => {
'trans.shared': 'Shared common',
'unused.common': 'Unused common',
})
writeSource('src/example.tsx', `
writeSource(
'src/example.tsx',
`
import { Trans, useTranslation } from 'react-i18next'
export function Example() {
@@ -780,7 +848,8 @@ describe('prune-unused-i18n', () => {
</>
)
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -801,9 +870,11 @@ describe('prune-unused-i18n', () => {
'voice.language.enUS': 'English',
'voice.language.zhCN': 'Chinese',
'voice.language.unused': 'Fallback language',
'unrelated': 'Unrelated',
unrelated: 'Unrelated',
})
writeSource('src/dynamic.tsx', `
writeSource(
'src/dynamic.tsx',
`
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'notice'
@@ -815,7 +886,8 @@ describe('prune-unused-i18n', () => {
t(\`\${i18nPrefix}.reason.\${deprecatedReasonKey}\`)
t(\`voice.language.\${language}\`, 'Fallback', { ns: 'plugin' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -837,14 +909,17 @@ describe('prune-unused-i18n', () => {
'maybe.used': 'Maybe used',
'otherwise.unused': 'Otherwise unused',
})
writeSource('src/unresolved.tsx', `
writeSource(
'src/unresolved.tsx',
`
import { useTranslation } from 'react-i18next'
export function UnresolvedExample(keyFromServer: string) {
const { t } = useTranslation('app')
return t(keyFromServer)
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -861,7 +936,9 @@ describe('prune-unused-i18n', () => {
'duplicateError.value': 'Value',
'outside.unused': 'Outside',
})
writeSource('src/typed-prefix.tsx', `
writeSource(
'src/typed-prefix.tsx',
`
import type { I18nKeysByPrefix } from '@/types/i18n'
import { useTranslation } from 'react-i18next'
@@ -869,7 +946,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation()
return t(errorKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -893,7 +971,9 @@ describe('prune-unused-i18n', () => {
'status.failed': 'Failed',
'status.unused': 'Unused',
})
writeSource('src/object-map.tsx', `
writeSource(
'src/object-map.tsx',
`
import { useTranslation } from 'react-i18next'
const statusI18nKey = {
@@ -905,7 +985,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation('common')
return t(statusI18nKey[status])
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -922,7 +1003,9 @@ describe('prune-unused-i18n', () => {
'mainNav.workspace.searchPlaceholder': 'Search',
'mainNav.workspace.unused': 'Unused',
})
writeSource('src/identity-helper.tsx', `
writeSource(
'src/identity-helper.tsx',
`
import { useTranslation } from 'react-i18next'
const workspaceSwitchI18nKey = (key: string) => key as 'mainNav.workspace.settings'
@@ -931,7 +1014,8 @@ describe('prune-unused-i18n', () => {
const { t } = useTranslation()
return t(workspaceSwitchI18nKey('mainNav.workspace.searchPlaceholder'), { ns: 'common' })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -950,14 +1034,17 @@ describe('prune-unused-i18n', () => {
'overview.unused_one': '1 unused',
'overview.unused_other': '{{count}} unused',
})
writeSource('src/plural.tsx', `
writeSource(
'src/plural.tsx',
`
import { useTranslation } from 'react-i18next'
export function PluralExample(count: number) {
const { t } = useTranslation('deployments')
return t('overview.environments', { count })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -976,13 +1063,16 @@ describe('prune-unused-i18n', () => {
'overview.chip.unused_one': '1 unused',
'overview.chip.unused_other': '{{count}} unused',
})
writeSource('src/typed-t-function.ts', `
writeSource(
'src/typed-t-function.ts',
`
import type { TFunction } from 'i18next'
export function renderStatus(t: TFunction<'deployments'>) {
return t('overview.chip.behind', { count: 2 })
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -999,7 +1089,9 @@ describe('prune-unused-i18n', () => {
'agentDetail.configure.tools.credential.authOne': 'Auth 1',
'agentDetail.configure.tools.unused': 'Unused',
})
writeSource('src/typed-key-field.ts', `
writeSource(
'src/typed-key-field.ts',
`
type I18nKeysWithPrefix<Namespace extends string, Prefix extends string> =
'agentDetail.configure.tools.credential.authOne' | 'agentDetail.configure.tools.unused'
@@ -1010,7 +1102,8 @@ describe('prune-unused-i18n', () => {
export const tool: Tool = {
credentialKey: 'agentDetail.configure.tools.credential.authOne',
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -1028,7 +1121,9 @@ describe('prune-unused-i18n', () => {
'gotoAnything.actions.createChatflowDesc': 'Create a chatflow',
'gotoAnything.actions.unused': 'Unused',
})
writeSource('src/i18next-instance.tsx', `
writeSource(
'src/i18next-instance.tsx',
`
import { getI18n } from 'react-i18next'
const i18n = getI18n()
@@ -1038,7 +1133,8 @@ describe('prune-unused-i18n', () => {
i18n.t('gotoAnything.actions.createChatflow', { ns: 'app' })
return tr('gotoAnything.actions.createChatflowDesc')
}
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -1052,19 +1148,21 @@ describe('prune-unused-i18n', () => {
it('should collect keys from imported and parameterized t functions', async () => {
// Arrange
writeJson('i18n/en-US/app.json', {
'noAccessPermission': 'No access',
noAccessPermission: 'No access',
'typeSelector.chatbot': 'Chatbot',
'unused.app': 'Unused app',
})
writeJson('i18n/en-US/app-api.json', {
'pause': 'Pause',
pause: 'Pause',
'unused.api': 'Unused API',
})
writeJson('i18n/en-US/tools.json', {
'mcp.server.publishTip': 'Publish first',
'unused.tools': 'Unused tools',
})
writeSource('src/parameterized.tsx', `
writeSource(
'src/parameterized.tsx',
`
import type { TFunction } from 'i18next'
import { t as globalT } from 'i18next'
import { useTranslation } from 'react-i18next'
@@ -1082,7 +1180,8 @@ describe('prune-unused-i18n', () => {
}
globalT('pause', { ns: 'appApi' })
`)
`,
)
// Act
const result = await analyzeUnusedTranslations({ webRoot })
@@ -1107,14 +1206,17 @@ describe('prune-unused-i18n', () => {
kept: '保留',
unused: '未使用',
})
writeSource('src/example.tsx', `
writeSource(
'src/example.tsx',
`
import { useTranslation } from 'react-i18next'
export function Example() {
const { t } = useTranslation('app')
return t('kept')
}
`)
`,
)
const result = await analyzeUnusedTranslations({ webRoot })
// Act
+49 -46
View File
@@ -35,8 +35,8 @@ type NestedTranslation = {
type AnalysisResult = {
file: string
missingKeys: string[]
changedValues: { key: string, oldValue: TranslationValue, newValue: TranslationValue }[]
newKeys: { key: string, value: TranslationValue }[]
changedValues: { key: string; oldValue: TranslationValue; newValue: TranslationValue }[]
newKeys: { key: string; value: TranslationValue }[]
}
/**
@@ -51,12 +51,10 @@ function flattenObject(obj: NestedTranslation, prefix = ''): FlatTranslation {
if (typeof value === 'string') {
result[newKey] = value
}
else if (Array.isArray(value)) {
} else if (Array.isArray(value)) {
// Preserve arrays as-is
result[newKey] = value as string[]
}
else if (typeof value === 'object' && value !== null) {
} else if (typeof value === 'object' && value !== null) {
Object.assign(result, flattenObject(value as NestedTranslation, newKey))
}
}
@@ -72,8 +70,7 @@ function valuesEqual(a: TranslationValue, b: TranslationValue): boolean {
return a === b
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false
if (a.length !== b.length) return false
return a.every((item, index) => item === b[index])
}
return false
@@ -84,7 +81,7 @@ function valuesEqual(a: TranslationValue, b: TranslationValue): boolean {
*/
function formatValue(value: TranslationValue): string {
if (Array.isArray(value)) {
return `[${value.map(v => `"${v}"`).join(', ')}]`
return `[${value.map((v) => `"${v}"`).join(', ')}]`
}
return `"${value}"`
}
@@ -100,8 +97,7 @@ function parseTsContent(content: string): NestedTranslation {
.trim()
// Remove trailing semicolon if present
if (cleaned.endsWith(';'))
cleaned = cleaned.slice(0, -1)
if (cleaned.endsWith(';')) cleaned = cleaned.slice(0, -1)
// Use Function constructor to safely evaluate the object literal
// This handles JS object syntax like unquoted keys, template literals, etc.
@@ -109,8 +105,7 @@ function parseTsContent(content: string): NestedTranslation {
// eslint-disable-next-line no-new-func
const fn = new Function(`return (${cleaned})`)
return fn() as NestedTranslation
}
catch (e) {
} catch (e) {
console.error('Failed to parse TS content:', e)
console.error('Content preview:', cleaned.slice(0, 200))
return {}
@@ -128,8 +123,7 @@ function getMainBranchFile(filePath: string): string | null {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
})
}
catch {
} catch {
return null
}
}
@@ -139,7 +133,7 @@ function getMainBranchFile(filePath: string): string | null {
*/
function getTranslationFiles(): string[] {
const files = fs.readdirSync(I18N_DIR)
return files.filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''))
return files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''))
}
/**
@@ -157,10 +151,9 @@ function getMainBranchNamespaces(): string[] {
return output
.trim()
.split('\n')
.filter(f => f.endsWith('.ts'))
.map(f => path.basename(f, '.ts'))
}
catch {
.filter((f) => f.endsWith('.ts'))
.map((f) => path.basename(f, '.ts'))
} catch {
return []
}
}
@@ -181,15 +174,15 @@ function checkNamespaceFiles(): NamespaceCheckResult {
const currentFiles = fs.readdirSync(I18N_DIR)
const currentJsonFiles = currentFiles
.filter(f => f.endsWith('.json'))
.map(f => f.replace('.json', ''))
.filter((f) => f.endsWith('.json'))
.map((f) => f.replace('.json', ''))
const currentTsFiles = currentFiles
.filter(f => f.endsWith('.ts'))
.map(f => f.replace('.ts', ''))
.filter((f) => f.endsWith('.ts'))
.map((f) => f.replace('.ts', ''))
// Check which namespaces from main are missing json files
const missingJsonFiles = mainNamespaces.filter(ns => !currentJsonFiles.includes(ns))
const missingJsonFiles = mainNamespaces.filter((ns) => !currentJsonFiles.includes(ns))
// ts files should not exist in current branch
const unexpectedTsFiles = currentTsFiles
@@ -216,7 +209,10 @@ function analyzeFile(baseName: string): AnalysisResult {
// Read current branch JSON file
const jsonPath = path.join(I18N_DIR, `${baseName}.json`)
const currentContent = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as Record<string, TranslationValue>
const currentContent = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as Record<
string,
TranslationValue
>
// Read main branch TS file
const tsContent = getMainBranchFile(`${baseName}.ts`)
@@ -264,7 +260,9 @@ function analyzeFile(baseName: string): AnalysisResult {
* Main analysis function
*/
function main() {
console.log('🔍 Analyzing i18n differences between current branch (flat JSON) and main branch (nested TS)...\n')
console.log(
'🔍 Analyzing i18n differences between current branch (flat JSON) and main branch (nested TS)...\n',
)
// Check namespace file consistency first
console.log('📂 Checking namespace files...')
@@ -283,8 +281,7 @@ function main() {
console.log(` - ${ns}.json (was ${ns}.ts in main)`)
}
hasNamespaceError = true
}
else {
} else {
console.log('\n✅ All namespaces from main branch have corresponding JSON files')
}
@@ -294,8 +291,7 @@ function main() {
console.log(` - ${ns}.ts`)
}
hasNamespaceError = true
}
else {
} else {
console.log('✅ No TS files in current branch (all converted to JSON)')
}
@@ -371,21 +367,28 @@ function main() {
// Write detailed report to JSON file
const reportPath = path.join(__dirname, '../i18n-analysis-report.json')
fs.writeFileSync(reportPath, JSON.stringify({
summary: {
totalFiles: files.length,
missingKeys: totalMissing,
changedValues: totalChanged,
newKeys: totalNew,
},
namespaceCheck: {
mainNamespaces: nsCheck.mainNamespaces,
currentJsonFiles: nsCheck.currentJsonFiles,
missingJsonFiles: nsCheck.missingJsonFiles,
unexpectedTsFiles: nsCheck.unexpectedTsFiles,
},
details: allResults,
}, null, 2))
fs.writeFileSync(
reportPath,
JSON.stringify(
{
summary: {
totalFiles: files.length,
missingKeys: totalMissing,
changedValues: totalChanged,
newKeys: totalNew,
},
namespaceCheck: {
mainNamespaces: nsCheck.mainNamespaces,
currentJsonFiles: nsCheck.currentJsonFiles,
missingJsonFiles: nsCheck.missingJsonFiles,
unexpectedTsFiles: nsCheck.unexpectedTsFiles,
},
details: allResults,
},
null,
2,
),
)
console.log(`\n📄 Detailed report written to: i18n-analysis-report.json`)
+43 -49
View File
@@ -8,7 +8,9 @@ const __dirname = path.dirname(__filename)
const targetLanguage = 'en-US'
const languages = data.languages.filter(language => language.supported).map(language => language.value)
const languages = data.languages
.filter((language) => language.supported)
.map((language) => language.value)
function parseArgs(argv) {
const args = {
@@ -24,8 +26,7 @@ function parseArgs(argv) {
let cursor = startIndex + 1
while (cursor < argv.length && !argv[cursor].startsWith('--')) {
const value = argv[cursor].trim()
if (value)
values.push(value)
if (value) values.push(value)
cursor++
}
return { values, nextIndex: cursor - 1 }
@@ -37,7 +38,7 @@ function parseArgs(argv) {
return false
}
const invalid = values.find(value => value.includes(','))
const invalid = values.find((value) => value.includes(','))
if (invalid) {
args.errors.push(`${flag} expects space-separated values. Example: ${flag} app billing`)
return false
@@ -66,8 +67,7 @@ function parseArgs(argv) {
if (arg === '--file') {
const { values, nextIndex } = collectValues(index)
if (validateList(values, '--file'))
args.files.push(...values)
if (validateList(values, '--file')) args.files.push(...values)
index = nextIndex
continue
}
@@ -79,8 +79,7 @@ function parseArgs(argv) {
if (arg === '--lang') {
const { values, nextIndex } = collectValues(index)
if (validateList(values, '--lang'))
args.languages.push(...values)
if (validateList(values, '--lang')) args.languages.push(...values)
index = nextIndex
continue
}
@@ -116,13 +115,12 @@ async function getKeysFromLanguage(language) {
}
// Filter only .json files
const translationFiles = files.filter(file => file.endsWith('.json'))
const translationFiles = files.filter((file) => file.endsWith('.json'))
translationFiles.forEach((file) => {
const filePath = path.join(folderPath, file)
const fileName = file.replace(/\.json$/, '') // Remove file extension
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) =>
c.toUpperCase()) // Convert to camel case
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase()) // Convert to camel case
try {
const content = fs.readFileSync(filePath, 'utf8')
@@ -135,10 +133,9 @@ async function getKeysFromLanguage(language) {
}
// Flat structure: just get all keys directly
const fileKeys = Object.keys(translationObj).map(key => `${camelCaseFileName}.${key}`)
const fileKeys = Object.keys(translationObj).map((key) => `${camelCaseFileName}.${key}`)
allKeys.push(...fileKeys)
}
catch (error) {
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message)
reject(error)
}
@@ -160,11 +157,10 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) {
// Filter keys that belong to this file
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
const fileSpecificKeys = extraKeys
.filter(key => key.startsWith(`${camelCaseFileName}.`))
.map(key => key.substring(camelCaseFileName.length + 1)) // Remove file prefix
.filter((key) => key.startsWith(`${camelCaseFileName}.`))
.map((key) => key.substring(camelCaseFileName.length + 1)) // Remove file prefix
if (fileSpecificKeys.length === 0)
return false
if (fileSpecificKeys.length === 0) return false
console.log(`🔄 Processing file: ${filePath}`)
@@ -180,8 +176,7 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) {
delete translationObj[keyToRemove]
console.log(`🗑️ Removed key: ${keyToRemove}`)
modified = true
}
else {
} else {
console.log(`⚠️ Could not find key: ${keyToRemove}`)
}
}
@@ -195,8 +190,7 @@ async function removeExtraKeysFromFile(language, fileName, extraKeys) {
}
return false
}
catch (error) {
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message)
return false
}
@@ -214,22 +208,28 @@ async function main() {
const allTargetKeys = await getKeysFromLanguage(targetLanguage)
// Filter target keys by file if specified
const camelTargetFiles = targetFiles.map(file => file.replace(/[-_](.)/g, (_, c) => c.toUpperCase()))
const camelTargetFiles = targetFiles.map((file) =>
file.replace(/[-_](.)/g, (_, c) => c.toUpperCase()),
)
const targetKeys = targetFiles.length
? allTargetKeys.filter(key => camelTargetFiles.some(file => key.startsWith(`${file}.`)))
? allTargetKeys.filter((key) => camelTargetFiles.some((file) => key.startsWith(`${file}.`)))
: allTargetKeys
// Filter languages by target language if specified
const languagesToProcess = targetLangs.length ? targetLangs : languages
const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language)))
const allLanguagesKeys = await Promise.all(
languagesToProcess.map((language) => getKeysFromLanguage(language)),
)
// Filter language keys by file if specified
const languagesKeys = targetFiles.length
? allLanguagesKeys.map(keys => keys.filter(key => camelTargetFiles.some(file => key.startsWith(`${file}.`))))
? allLanguagesKeys.map((keys) =>
keys.filter((key) => camelTargetFiles.some((file) => key.startsWith(`${file}.`))),
)
: allLanguagesKeys
const keysCount = languagesKeys.map(keys => keys.length)
const keysCount = languagesKeys.map((keys) => keys.length)
const targetKeysCount = targetKeys.length
const comparison = languagesToProcess.reduce((result, language, index) => {
@@ -245,12 +245,11 @@ async function main() {
for (let index = 0; index < languagesToProcess.length; index++) {
const language = languagesToProcess[index]
const languageKeys = languagesKeys[index]
const missingKeys = targetKeys.filter(key => !languageKeys.includes(key))
const extraKeys = languageKeys.filter(key => !targetKeys.includes(key))
const missingKeys = targetKeys.filter((key) => !languageKeys.includes(key))
const extraKeys = languageKeys.filter((key) => !targetKeys.includes(key))
console.log(`Missing keys in ${language}:`, missingKeys)
if (missingKeys.length > 0)
hasDiff = true
if (missingKeys.length > 0) hasDiff = true
// Show extra keys only when there are extra keys (negative difference)
if (extraKeys.length > 0) {
@@ -262,21 +261,20 @@ async function main() {
// Get all translation files
const i18nFolder = path.resolve(__dirname, '../i18n', language)
const files = fs.readdirSync(i18nFolder)
.filter(file => file.endsWith('.json'))
.map(file => file.replace(/\.json$/, ''))
.filter(f => targetFiles.length === 0 || targetFiles.includes(f))
const files = fs
.readdirSync(i18nFolder)
.filter((file) => file.endsWith('.json'))
.map((file) => file.replace(/\.json$/, ''))
.filter((f) => targetFiles.length === 0 || targetFiles.includes(f))
let totalRemoved = 0
for (const fileName of files) {
const removed = await removeExtraKeysFromFile(language, fileName, extraKeys)
if (removed)
totalRemoved++
if (removed) totalRemoved++
}
console.log(`✅ Auto-removal completed for ${language}. Modified ${totalRemoved} files.`)
}
else {
} else {
hasDiff = true
}
}
@@ -286,21 +284,17 @@ async function main() {
}
console.log('🚀 Starting i18n:check script...')
if (targetFiles.length)
console.log(`📁 Checking files: ${targetFiles.join(', ')}`)
if (targetFiles.length) console.log(`📁 Checking files: ${targetFiles.join(', ')}`)
if (targetLangs.length)
console.log(`🌍 Checking languages: ${targetLangs.join(', ')}`)
if (targetLangs.length) console.log(`🌍 Checking languages: ${targetLangs.join(', ')}`)
if (autoRemove)
console.log('🤖 Auto-remove mode: ENABLED')
if (autoRemove) console.log('🤖 Auto-remove mode: ENABLED')
const hasDiff = await compareKeysCount()
if (hasDiff) {
console.error('\n❌ i18n keys are not aligned. Fix issues above.')
process.exitCode = 1
}
else {
} else {
console.log('\n✅ All i18n files are in sync')
}
}
@@ -312,13 +306,13 @@ async function bootstrap() {
}
if (args.errors.length) {
args.errors.forEach(message => console.error(`${message}`))
args.errors.forEach((message) => console.error(`${message}`))
printHelp()
process.exit(1)
return
}
const unknownLangs = targetLangs.filter(lang => !languages.includes(lang))
const unknownLangs = targetLangs.filter((lang) => !languages.includes(lang))
if (unknownLangs.length) {
console.error(`❌ Unsupported languages: ${unknownLangs.join(', ')}`)
process.exit(1)
@@ -20,17 +20,16 @@ function run(command, args, options = {}) {
let stdout = ''
let stderr = ''
child.stdout.on('data', data => stdout += data)
child.stderr.on('data', data => stderr += data)
child.stdout.on('data', (data) => (stdout += data))
child.stderr.on('data', (data) => (stderr += data))
child.on('error', reject)
child.on('close', status => resolve({ status, stdout, stderr }))
child.on('close', (status) => resolve({ status, stdout, stderr }))
})
}
function parseVpLintJson(stdout) {
const jsonStart = stdout.indexOf('{')
if (jsonStart === -1)
return { diagnostics: [] }
if (jsonStart === -1) return { diagnostics: [] }
return JSON.parse(stdout.slice(jsonStart))
}
@@ -39,7 +38,10 @@ function relativeDiagnostic(diagnostic) {
const label = diagnostic.labels?.[0]
const diagnosticFile = label?.file ?? diagnostic.filename
const filePath = diagnosticFile
? path.relative(webDir, path.isAbsolute(diagnosticFile) ? diagnosticFile : path.join(webDir, diagnosticFile))
? path.relative(
webDir,
path.isAbsolute(diagnosticFile) ? diagnosticFile : path.join(webDir, diagnosticFile),
)
: '<unknown>'
const span = label?.span ? `:${label.span.line}:${label.span.column}` : ''
@@ -53,8 +55,7 @@ async function ensureCleanWorktree() {
throw new Error('Failed to check git status.')
}
if (!result.stdout.trim())
return
if (!result.stdout.trim()) return
console.error('This check runs knip --fix and must start from a clean worktree.')
console.error('Commit or stash your changes first, then run it again.')
@@ -63,14 +64,18 @@ async function ensureCleanWorktree() {
}
async function restoreWorktree() {
const restoreResult = await run('git', ['restore', '--staged', '--worktree', '.'], { cwd: repoRoot })
const restoreResult = await run('git', ['restore', '--staged', '--worktree', '.'], {
cwd: repoRoot,
})
if (restoreResult.status !== 0) {
process.stdout.write(restoreResult.stdout)
process.stderr.write(restoreResult.stderr)
throw new Error('Failed to restore tracked files after knip --fix.')
}
const cleanResult = await run('git', ['clean', '-fd', '--', 'web', '.eslintcache'], { cwd: repoRoot })
const cleanResult = await run('git', ['clean', '-fd', '--', 'web', '.eslintcache'], {
cwd: repoRoot,
})
if (cleanResult.status !== 0) {
process.stdout.write(cleanResult.stdout)
process.stderr.write(cleanResult.stderr)
@@ -96,38 +101,41 @@ try {
}
console.log('Running Vite+ unused checks after knip --fix...')
const lintResult = await run(vp, [
'lint',
'-A',
'all',
'-D',
'no-unused-vars',
'--format',
'json',
'--ignore-pattern',
'public/**',
'--ignore-pattern',
'coverage/**',
'--ignore-pattern',
'.next/**',
'--ignore-pattern',
'**/__tests__/**',
'--ignore-pattern',
'**/*.spec.ts',
'--ignore-pattern',
'**/*.spec.tsx',
'--ignore-pattern',
'**/*.test.ts',
'--ignore-pattern',
'**/*.test.tsx',
'.',
], { cwd: webDir })
const lintResult = await run(
vp,
[
'lint',
'-A',
'all',
'-D',
'no-unused-vars',
'--format',
'json',
'--ignore-pattern',
'public/**',
'--ignore-pattern',
'coverage/**',
'--ignore-pattern',
'.next/**',
'--ignore-pattern',
'**/__tests__/**',
'--ignore-pattern',
'**/*.spec.ts',
'--ignore-pattern',
'**/*.spec.tsx',
'--ignore-pattern',
'**/*.test.ts',
'--ignore-pattern',
'**/*.test.tsx',
'.',
],
{ cwd: webDir },
)
let lintOutput
try {
lintOutput = parseVpLintJson(lintResult.stdout)
}
catch {
} catch {
process.stdout.write(lintResult.stdout)
process.stderr.write(lintResult.stderr)
throw new Error('Failed to parse Vite+ lint JSON output.')
@@ -138,20 +146,18 @@ try {
if (unusedMessages.length > 0) {
hasUnusedMessages = true
console.error('Unused declarations remain after applying knip --production --fix.')
console.error('Remove these declarations; if they are only referenced by tests, remove the matching tests too.')
for (const message of unusedMessages)
console.error(message)
}
else {
console.error(
'Remove these declarations; if they are only referenced by tests, remove the matching tests too.',
)
for (const message of unusedMessages) console.error(message)
} else {
console.log('No Vite+ unused declarations remain after knip --production --fix.')
}
}
finally {
} finally {
if (shouldRestore) {
console.log('Restoring checkout after knip --fix...')
await restoreWorktree()
}
}
if (hasUnusedMessages)
process.exit(1)
if (hasUnusedMessages) process.exit(1)
+11 -19
View File
@@ -15,8 +15,7 @@ const pathExists = async (path) => {
await stat(path)
console.debug(`Path exists: ${path}`)
return true
}
catch (err) {
} catch (err) {
if (err.code === 'ENOENT') {
console.warn(`Path does not exist: ${path}`)
return false
@@ -33,8 +32,7 @@ const STANDALONE_ROOT_CANDIDATES = [
const getStandaloneRoot = async () => {
for (const standaloneRoot of STANDALONE_ROOT_CANDIDATES) {
const serverScriptPath = path.join(standaloneRoot, 'server.js')
if (await pathExists(serverScriptPath))
return standaloneRoot
if (await pathExists(serverScriptPath)) return standaloneRoot
}
throw new Error(
@@ -71,13 +69,11 @@ const copyAllDirs = async (standaloneRoot) => {
await mkdir(destParent, { recursive: true })
if (await pathExists(src)) {
await copyDir(src, dest)
}
else {
} else {
console.error(`Error: ${src} directory does not exist. This is a required build artifact.`)
process.exit(1)
}
}
catch (err) {
} catch (err) {
console.error(`Error processing ${src}:`, err.message)
process.exit(1)
}
@@ -101,18 +97,14 @@ const main = async () => {
console.debug(`Server script path: ${serverScriptPath}`)
console.debug(`Environment variables - PORT: ${port}, HOSTNAME: ${host}`)
const server = spawn(
process.execPath,
[serverScriptPath],
{
env: {
...process.env,
PORT: port,
HOSTNAME: host,
},
stdio: 'inherit',
const server = spawn(process.execPath, [serverScriptPath], {
env: {
...process.env,
PORT: port,
HOSTNAME: host,
},
)
stdio: 'inherit',
})
server.on('error', (err) => {
console.error('Failed to start server:', err)
+65 -89
View File
@@ -11,7 +11,8 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const DEFAULT_DOCS_JSON_URL = 'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json'
const DEFAULT_DOCS_JSON_URL =
'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json'
const DOCS_JSON_URL = process.env.DOCS_JSON_URL || DEFAULT_DOCS_JSON_URL
const OUTPUT_PATH = path.resolve(__dirname, '../types/doc-paths.ts')
@@ -57,10 +58,12 @@ type DocsJson = {
[key: string]: unknown
}
const OPENAPI_BASE_URL = (process.env.DOCS_OPENAPI_BASE_URL || new URL('.', DOCS_JSON_URL).toString()).replace(/\/?$/, '/')
const OPENAPI_BASE_URL = (
process.env.DOCS_OPENAPI_BASE_URL || new URL('.', DOCS_JSON_URL).toString()
).replace(/\/?$/, '/')
const DOCS_PRODUCTS = ['cloud', 'self-host'] as const
type DocsProduct = typeof DOCS_PRODUCTS[number]
type DocsProduct = (typeof DOCS_PRODUCTS)[number]
type ProductAvailability = Record<string, Set<DocsProduct>>
function isDocsProduct(segment: string): segment is DocsProduct {
@@ -73,11 +76,7 @@ function isDocsProduct(segment: string): segment is DocsProduct {
* e.g., "获取知识库列表" -> "获取知识库列表"
*/
function summaryToSlug(summary: string): string {
return summary
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
return summary.toLowerCase().replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
}
/**
@@ -92,44 +91,36 @@ function getFirstPathSegment(apiPath: string): string {
/**
* Recursively extract OpenAPI file paths from navigation structure
*/
function extractOpenAPIPaths(item: NavItem | undefined, paths: Set<string> = new Set()): Set<string> {
if (!item)
return paths
function extractOpenAPIPaths(
item: NavItem | undefined,
paths: Set<string> = new Set(),
): Set<string> {
if (!item) return paths
if (Array.isArray(item)) {
for (const el of item)
extractOpenAPIPaths(el, paths)
for (const el of item) extractOpenAPIPaths(el, paths)
return paths
}
if (typeof item === 'object') {
if (item.openapi && typeof item.openapi === 'string')
paths.add(item.openapi)
if (item.openapi && typeof item.openapi === 'string') paths.add(item.openapi)
if (item.pages)
extractOpenAPIPaths(item.pages, paths)
if (item.pages) extractOpenAPIPaths(item.pages, paths)
if (item.groups)
extractOpenAPIPaths(item.groups, paths)
if (item.groups) extractOpenAPIPaths(item.groups, paths)
if (item.dropdowns)
extractOpenAPIPaths(item.dropdowns, paths)
if (item.dropdowns) extractOpenAPIPaths(item.dropdowns, paths)
if (item.languages)
extractOpenAPIPaths(item.languages, paths)
if (item.languages) extractOpenAPIPaths(item.languages, paths)
if (item.products)
extractOpenAPIPaths(item.products, paths)
if (item.products) extractOpenAPIPaths(item.products, paths)
if (item.tabs)
extractOpenAPIPaths(item.tabs, paths)
if (item.tabs) extractOpenAPIPaths(item.tabs, paths)
if (item.menu)
extractOpenAPIPaths(item.menu, paths)
if (item.menu) extractOpenAPIPaths(item.menu, paths)
if (item.versions)
extractOpenAPIPaths(item.versions, paths)
if (item.versions) extractOpenAPIPaths(item.versions, paths)
}
return paths
@@ -148,11 +139,10 @@ async function fetchOpenAPIAndExtractPaths(openapiPath: string): Promise<Endpoin
return new Map()
}
const spec = await response.json() as OpenAPISpec
const spec = (await response.json()) as OpenAPISpec
const pathMap: EndpointPathMap = new Map()
if (!spec.paths)
return pathMap
if (!spec.paths) return pathMap
const httpMethods = ['get', 'post', 'put', 'patch', 'delete'] as const
@@ -163,8 +153,7 @@ async function fetchOpenAPIAndExtractPaths(openapiPath: string): Promise<Endpoin
// Try to get tag from operation, fallback to path segment
const tag = operation.tags?.[0]
const segment = tag ? summaryToSlug(tag) : getFirstPathSegment(apiPath)
if (!segment)
continue
if (!segment) continue
const slug = summaryToSlug(operation.summary)
// Skip empty slugs
@@ -183,12 +172,10 @@ async function fetchOpenAPIAndExtractPaths(openapiPath: string): Promise<Endpoin
* Recursively extract all page paths from navigation structure
*/
function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set()): Set<string> {
if (!item)
return paths
if (!item) return paths
if (Array.isArray(item)) {
for (const el of item)
extractPaths(el, paths)
for (const el of item) extractPaths(el, paths)
return paths
}
@@ -199,37 +186,28 @@ function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set())
}
if (typeof item === 'object') {
if (item.root)
paths.add(item.root)
if (item.root) paths.add(item.root)
// Handle pages array
if (item.pages)
extractPaths(item.pages, paths)
if (item.pages) extractPaths(item.pages, paths)
// Handle groups array
if (item.groups)
extractPaths(item.groups, paths)
if (item.groups) extractPaths(item.groups, paths)
// Handle dropdowns
if (item.dropdowns)
extractPaths(item.dropdowns, paths)
if (item.dropdowns) extractPaths(item.dropdowns, paths)
// Handle languages
if (item.languages)
extractPaths(item.languages, paths)
if (item.languages) extractPaths(item.languages, paths)
if (item.products)
extractPaths(item.products, paths)
if (item.products) extractPaths(item.products, paths)
if (item.tabs)
extractPaths(item.tabs, paths)
if (item.tabs) extractPaths(item.tabs, paths)
if (item.menu)
extractPaths(item.menu, paths)
if (item.menu) extractPaths(item.menu, paths)
// Handle versions in navigation
if (item.versions)
extractPaths(item.versions, paths)
if (item.versions) extractPaths(item.versions, paths)
}
return paths
@@ -238,21 +216,20 @@ function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set())
function addPathToGroup(groups: Record<string, Set<string>>, pathWithoutLang: string): void {
const parts = pathWithoutLang.split('/')
const section = parts[0]
if (!section)
return
if (!section) return
if (!groups[section])
groups[section] = new Set()
if (!groups[section]) groups[section] = new Set()
groups[section]!.add(pathWithoutLang)
}
function getProductPathInfo(pathWithoutLang: string): { product: DocsProduct, pathWithoutProduct: string } | undefined {
function getProductPathInfo(
pathWithoutLang: string,
): { product: DocsProduct; pathWithoutProduct: string } | undefined {
const parts = pathWithoutLang.split('/')
const [product, ...rest] = parts
if (!product || !isDocsProduct(product) || rest.length === 0)
return undefined
if (!product || !isDocsProduct(product) || rest.length === 0) return undefined
return {
product,
@@ -263,19 +240,20 @@ function getProductPathInfo(pathWithoutLang: string): { product: DocsProduct, pa
/**
* Group paths by their prefix structure
*/
function groupPathsBySection(paths: Set<string>): { groups: Record<string, Set<string>>, productAvailability: ProductAvailability } {
function groupPathsBySection(paths: Set<string>): {
groups: Record<string, Set<string>>
productAvailability: ProductAvailability
} {
const groups: Record<string, Set<string>> = {}
const productAvailability: ProductAvailability = {}
for (const fullPath of paths) {
// Remove language prefix (en/, zh/, ja/)
const withoutLang = fullPath.replace(/^(en|zh|ja)\//, '')
if (!withoutLang || withoutLang === fullPath)
continue
if (!withoutLang || withoutLang === fullPath) continue
// Skip non-doc paths (like .json files for OpenAPI)
if (withoutLang.endsWith('.json') || withoutLang === 'None')
continue
if (withoutLang.endsWith('.json') || withoutLang === 'None') continue
addPathToGroup(groups, withoutLang)
@@ -286,8 +264,7 @@ function groupPathsBySection(paths: Set<string>): { groups: Record<string, Set<s
addPathToGroup(groups, productlessPath)
if (!productAvailability[normalizedPath])
productAvailability[normalizedPath] = new Set()
if (!productAvailability[normalizedPath]) productAvailability[normalizedPath] = new Set()
productAvailability[normalizedPath]!.add(productPathInfo.product)
}
@@ -305,7 +282,7 @@ function groupPathsBySection(paths: Set<string>): { groups: Record<string, Set<s
function sectionToTypeName(section: string): string {
return section
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
}
@@ -325,8 +302,8 @@ function generateTypeDefinitions(
`// Generated at: ${new Date().toISOString()}`,
'',
'// Language prefixes',
'export type DocLanguage = \'en\' | \'zh\' | \'ja\'',
'export type DocsProduct = \'cloud\' | \'self-host\'',
"export type DocLanguage = 'en' | 'zh' | 'ja'",
"export type DocsProduct = 'cloud' | 'self-host'",
'',
]
@@ -350,8 +327,11 @@ function generateTypeDefinitions(
// Add UseDifyNodesPath helper type after UseDifyPath
if (section === 'use-dify') {
lines.push('// UseDify node paths (without prefix)')
// eslint-disable-next-line no-template-curly-in-string
lines.push('type ExtractNodesPath<T> = T extends `/use-dify/nodes/${infer Path}` ? Path : never')
/* eslint-disable no-template-curly-in-string */
lines.push(
'type ExtractNodesPath<T> = T extends `/use-dify/nodes/${infer Path}` ? Path : never',
)
/* eslint-enable no-template-curly-in-string */
lines.push('export type UseDifyNodesPath = ExtractNodesPath<UseDifyPath>')
lines.push('')
}
@@ -389,8 +369,10 @@ function generateTypeDefinitions(
lines.push('// Product availability for productless docs paths')
lines.push('export const docPathProductAvailability: Record<string, readonly DocsProduct[]> = {')
for (const path of Object.keys(productAvailability).sort()) {
const products = [...productAvailability[path]!].sort((a, b) => DOCS_PRODUCTS.indexOf(a) - DOCS_PRODUCTS.indexOf(b))
lines.push(` '${path}': [${products.map(product => `'${product}'`).join(', ')}],`)
const products = [...productAvailability[path]!].sort(
(a, b) => DOCS_PRODUCTS.indexOf(a) - DOCS_PRODUCTS.indexOf(b),
)
lines.push(` '${path}': [${products.map((product) => `'${product}'`).join(', ')}],`)
}
lines.push('}')
lines.push('')
@@ -405,7 +387,7 @@ async function main(): Promise<void> {
if (!response.ok)
throw new Error(`Failed to fetch docs.json: ${response.status} ${response.statusText}`)
const docsJson = await response.json() as DocsJson
const docsJson = (await response.json()) as DocsJson
console.log('Successfully fetched docs.json')
// Extract paths from navigation
@@ -424,13 +406,11 @@ async function main(): Promise<void> {
for (const openapiPath of openApiPaths) {
const langMatch = /^(en|zh|ja)\//.exec(openapiPath)
if (langMatch?.[1] !== 'en')
continue
if (langMatch?.[1] !== 'en') continue
console.log(`Fetching OpenAPI spec: ${openapiPath}`)
const pathMap = await fetchOpenAPIAndExtractPaths(openapiPath)
for (const enPath of pathMap.values())
enApiPaths.push(enPath)
for (const enPath of pathMap.values()) enApiPaths.push(enPath)
}
// Deduplicate English API paths
@@ -445,11 +425,7 @@ async function main(): Promise<void> {
console.log(`Found ${Object.keys(productAvailability).length} product-aware paths`)
// Generate TypeScript
const tsContent = generateTypeDefinitions(
groups,
productAvailability,
uniqueEnApiPaths,
)
const tsContent = generateTypeDefinitions(groups, productAvailability, uniqueEnApiPaths)
// Write to file
await writeFile(OUTPUT_PATH, tsContent, 'utf-8')
+55 -32
View File
@@ -11,35 +11,35 @@ const svgAssetsDir = path.resolve(__dirname, '../../packages/iconify-collections
const generateDir = async (currentPath) => {
try {
await mkdir(currentPath, { recursive: true })
}
catch (err) {
} catch (err) {
console.error(err.message)
}
}
const processSvgStructure = (svgStructure, replaceFillOrStrokeColor) => {
if (svgStructure?.children.length) {
svgStructure.children = svgStructure.children.filter(c => c.type !== 'text')
svgStructure.children = svgStructure.children.filter((c) => c.type !== 'text')
svgStructure.children.forEach((child) => {
if (child?.name === 'path' && replaceFillOrStrokeColor) {
if (child?.attributes?.stroke)
child.attributes.stroke = 'currentColor'
if (child?.attributes?.stroke) child.attributes.stroke = 'currentColor'
if (child?.attributes.fill)
child.attributes.fill = 'currentColor'
if (child?.attributes.fill) child.attributes.fill = 'currentColor'
}
if (child?.children.length)
processSvgStructure(child, replaceFillOrStrokeColor)
if (child?.children.length) processSvgStructure(child, replaceFillOrStrokeColor)
})
}
}
const generateSvgComponent = async (fileHandle, entry, relativeSegments, replaceFillOrStrokeColor) => {
const generateSvgComponent = async (
fileHandle,
entry,
relativeSegments,
replaceFillOrStrokeColor,
) => {
const currentPath = path.resolve(iconsDir, 'src', ...relativeSegments)
try {
await access(currentPath)
}
catch {
} catch {
await generateDir(currentPath)
}
@@ -54,7 +54,8 @@ const generateSvgComponent = async (fileHandle, entry, relativeSegments, replace
name: fileName,
}
const componentRender = template(`
const componentRender = template(
`
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
@@ -75,16 +76,28 @@ const Icon = (
Icon.displayName = '<%= svgName %>'
export default Icon
`.trim())
`.trim(),
)
await writeFile(path.resolve(currentPath, `${fileName}.json`), `${JSON.stringify(svgData, '', '\t')}\n`)
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`)
await writeFile(
path.resolve(currentPath, `${fileName}.json`),
`${JSON.stringify(svgData, '', '\t')}\n`,
)
await writeFile(
path.resolve(currentPath, `${fileName}.tsx`),
`${componentRender({ svgName: fileName })}\n`,
)
const indexingRender = template(`
const indexingRender = template(
`
export { default as <%= svgName %> } from './<%= svgName %>'
`.trim())
`.trim(),
)
await appendFile(path.resolve(currentPath, 'index.ts'), `${indexingRender({ svgName: fileName })}\n`)
await appendFile(
path.resolve(currentPath, 'index.ts'),
`${indexingRender({ svgName: fileName })}\n`,
)
}
const generateImageComponent = async (entry, relativeSegments) => {
@@ -92,25 +105,30 @@ const generateImageComponent = async (entry, relativeSegments) => {
try {
await access(currentPath)
}
catch {
} catch {
await generateDir(currentPath)
}
const prefixFileName = camelCase(entry.split('.')[0])
const fileName = prefixFileName.charAt(0).toUpperCase() + prefixFileName.slice(1)
const componentCSSRender = template(`
const componentCSSRender = template(
`
.wrapper {
display: inline-flex;
background: url(<%= assetPath %>) center center no-repeat;
background-size: contain;
}
`.trim())
`.trim(),
)
await writeFile(path.resolve(currentPath, `${fileName}.module.css`), `${componentCSSRender({ assetPath: path.posix.join('~@/app/components/base/icons/assets', ...relativeSegments, entry) })}\n`)
await writeFile(
path.resolve(currentPath, `${fileName}.module.css`),
`${componentCSSRender({ assetPath: path.posix.join('~@/app/components/base/icons/assets', ...relativeSegments, entry) })}\n`,
)
const componentRender = template(`
const componentRender = template(
`
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
@@ -131,13 +149,19 @@ const Icon = (
Icon.displayName = '<%= fileName %>'
export default Icon
`.trim())
`.trim(),
)
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ fileName })}\n`)
await writeFile(
path.resolve(currentPath, `${fileName}.tsx`),
`${componentRender({ fileName })}\n`,
)
const indexingRender = template(`
const indexingRender = template(
`
export { default as <%= fileName %> } from './<%= fileName %>'
`.trim())
`.trim(),
)
await appendFile(path.resolve(currentPath, 'index.ts'), `${indexingRender({ fileName })}\n`)
}
@@ -162,13 +186,12 @@ const walk = async (basePath, entry, relativeSegments, replaceFillOrStrokeColor)
if (stat.isFile() && /.+\.png$/.test(entry))
await generateImageComponent(entry, relativeSegments)
}
finally {
} finally {
fileHandle?.close()
}
}
(async () => {
;(async () => {
await rm(path.resolve(iconsDir, 'src'), { recursive: true, force: true })
await walk(svgAssetsDir, 'public', [], false)
await walk(svgAssetsDir, 'vender', [], true)
+2 -6
View File
@@ -28,10 +28,7 @@ async function generateIcons() {
for (const { size, name } of sizes) {
const outputPath = path.join(outputDir, name)
await sharp(inputPath)
.resize(size, size)
.png()
.toFile(outputPath)
await sharp(inputPath).resize(size, size).png().toFile(outputPath)
console.log(`✓ Generated ${name} (${size}x${size})`)
}
@@ -45,8 +42,7 @@ async function generateIcons() {
console.log('✓ Generated apple-touch-icon.png (180x180)')
console.log('\n✅ All icons generated successfully!')
}
catch (error) {
} catch (error) {
console.error('Error generating icons:', error)
process.exit(1)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -23
View File
@@ -4,10 +4,7 @@ import type {
} from './i18n-prune/core'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import {
analyzeUnusedTranslations,
removeUnusedTranslations,
} from './i18n-prune/core'
import { analyzeUnusedTranslations, removeUnusedTranslations } from './i18n-prune/core'
type CliArgs = {
write: boolean
@@ -54,16 +51,14 @@ function parseArgs(argv: string[]): CliArgs {
}
if (arg === '--file') {
const { values, nextIndex } = collectValues(argv, index)
if (!values.length)
args.errors.push('--file requires at least one value')
if (!values.length) args.errors.push('--file requires at least one value')
args.files.push(...values)
index = nextIndex
continue
}
if (arg === '--lang') {
const { values, nextIndex } = collectValues(argv, index)
if (!values.length)
args.errors.push('--lang requires at least one value')
if (!values.length) args.errors.push('--lang requires at least one value')
args.locales.push(...values)
index = nextIndex
continue
@@ -95,19 +90,25 @@ function countUnusedKeys(result: AnalyzeUnusedTranslationsResult) {
return Object.values(result.unusedKeysByNamespace).reduce((total, keys) => total + keys.length, 0)
}
function printHumanSummary(result: AnalyzeUnusedTranslationsResult, removed?: RemoveUnusedTranslationsResult) {
function printHumanSummary(
result: AnalyzeUnusedTranslationsResult,
removed?: RemoveUnusedTranslationsResult,
) {
const totalUnused = countUnusedKeys(result)
console.log(`Found ${totalUnused} unused i18n keys.`)
for (const [namespace, keys] of Object.entries(result.unusedKeysByNamespace)) {
console.log(`\n${namespace} (${keys.length})`)
for (const key of keys)
console.log(` - ${key}`)
for (const key of keys) console.log(` - ${key}`)
}
if (result.protectedNamespaces.length) {
console.log(`\nProtected namespaces with unresolved dynamic keys: ${result.protectedNamespaces.join(', ')}`)
console.log('These namespaces were not pruned because at least one key could not be statically resolved.')
console.log(
`\nProtected namespaces with unresolved dynamic keys: ${result.protectedNamespaces.join(', ')}`,
)
console.log(
'These namespaces were not pruned because at least one key could not be statically resolved.',
)
}
if (result.dynamicKeyPatterns.length) {
@@ -120,10 +121,8 @@ function printHumanSummary(result: AnalyzeUnusedTranslationsResult, removed?: Re
console.log(` ... ${result.dynamicKeyPatterns.length - 20} more`)
}
if (removed)
console.log(`\nRemoved ${removed.removedKeys.length} keys across locale files.`)
else if (totalUnused)
console.log('\nRun again with --write to remove these keys.')
if (removed) console.log(`\nRemoved ${removed.removedKeys.length} keys across locale files.`)
else if (totalUnused) console.log('\nRun again with --write to remove these keys.')
}
async function runCli() {
@@ -134,8 +133,7 @@ async function runCli() {
}
if (args.errors.length) {
for (const error of args.errors)
console.error(error)
for (const error of args.errors) console.error(error)
printHelp()
process.exitCode = 1
return
@@ -148,13 +146,11 @@ async function runCli() {
if (args.json) {
console.log(JSON.stringify({ analysis: result, removal: removed }, null, 2))
}
else {
} else {
printHumanSummary(result, removed)
}
if (!args.write && countUnusedKeys(result))
process.exitCode = 1
if (!args.write && countUnusedKeys(result)) process.exitCode = 1
}
const entryPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ''