mirror of
https://github.com/langgenius/dify.git
synced 2026-09-19 10:11:30 +08:00
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:
co-authored by
autofix-ci[bot]
parent
fde08d24fe
commit
a84c2d36a3
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+514
-421
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 : ''
|
||||
|
||||
Reference in New Issue
Block a user