Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ddd20c1
refactor(i18n): align analysis with Vite environment graphs
hyoban Sep 21, 2026
0d7f44a
fix(i18n): block fallback for unanalyzed runtime imports
hyoban Sep 21, 2026
337e307
fix(i18n): block stale dynamic import resolution after transforms
hyoban Sep 21, 2026
cb1e6e6
fix(i18n): preserve type-only export specifier resolution
hyoban Sep 21, 2026
04c269c
fix(i18n): compare import bindings before reusing resolutions
hyoban Sep 21, 2026
f930a99
fix(i18n): normalize runtime re-export binding signatures
hyoban Sep 21, 2026
0ebf8e3
feat(i18n): report incomplete namespaces and analyze built JSON
hyoban Sep 21, 2026
9b58d30
feat(i18n): trace namespace forwarding and remove unused keys
hyoban Sep 21, 2026
71c67b8
feat(i18n): validate route policy loads and forwarding boundaries
hyoban Sep 21, 2026
c2d3e04
fix(i18n): handle rest namespaces and indirect parameter mutation
hyoban Sep 21, 2026
15a2684
perf(i18n): narrow mutation checks and propagate forwarding summaries
hyoban Sep 21, 2026
079493d
fix(i18n): preserve fixed namespaces and track policy escapes
hyoban Sep 21, 2026
e78c392
fix(i18n): retain policy trust for React effect dependencies
hyoban Sep 21, 2026
c89b0d1
refactor(i18n): share local reference traversal across analyses
hyoban Sep 21, 2026
05cfeb3
fix(i18n): reject mutated namespace locals and computed policy writes
hyoban Sep 21, 2026
901cedc
refactor(i18n): limit namespace analysis to static values
hyoban Sep 21, 2026
163a324
refactor(i18n): keep finite translation keys statically visible
hyoban Sep 21, 2026
e728f8e
refactor(i18n): simplify environment analysis state
hyoban Sep 21, 2026
5349d4c
refactor(i18n): limit import diagnostics to application sources
hyoban Sep 21, 2026
3680b5b
refactor(i18n): summarize build analysis output
hyoban Sep 21, 2026
75fb596
feat(i18n): configure custom translation adapters
hyoban Sep 21, 2026
c7deb56
fix(i18n): resolve translation adapter export aliases
hyoban Sep 21, 2026
90d064a
fix(i18n): identify official APIs from resolved declarations
hyoban Sep 21, 2026
928bc73
fix(i18n): retain known namespaces in dynamic arrays
hyoban Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions oxlint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3353,9 +3353,6 @@
"web/app/components/workflow/hooks/use-checklist.ts": {
"typescript/no-empty-object-type": {
"count": 1
},
"typescript/no-explicit-any": {
"count": 4
}
},
"web/app/components/workflow/hooks/use-dynamic-test-run-options.tsx": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,20 @@ const QuotaPanel: FC<QuotaPanelProps> = ({ providers }) => {
const providerType = providerMap.get(key)
const isLoadingPlugin = loadingPluginId === providerKeyToPluginId[key]
const isConfigured = (installedProvidersMap.get(key)?.length ?? 0) > 0
const getTooltipKey = () => {
if (!providerType) return 'modelProvider.card.modelNotSupported'
if (isConfigured && providerType === PreferredProviderTypeEnum.custom)
return 'modelProvider.card.modelAPI'
return 'modelProvider.card.modelSupported'
}
const tooltipText = t(($) => $[getTooltipKey()], {
modelName: modelNameMap[key],
ns: 'common',
})
const tooltipText = !providerType
? t(($) => $['modelProvider.card.modelNotSupported'], {
modelName: modelNameMap[key],
ns: 'common',
})
: isConfigured && providerType === PreferredProviderTypeEnum.custom
? t(($) => $['modelProvider.card.modelAPI'], {
modelName: modelNameMap[key],
ns: 'common',
})
: t(($) => $['modelProvider.card.modelSupported'], {
modelName: modelNameMap[key],
ns: 'common',
})
return (
<Tooltip key={key}>
<TooltipTrigger
Expand Down
14 changes: 5 additions & 9 deletions web/app/components/main-nav/components/workspace-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { WorkspaceMenuItemContent } from './workspace-menu-content'
const workspaceSwitchActionIconWrapClassName = 'flex size-5 shrink-0 items-center justify-center'
const workspaceSwitchActionIconClassName = 'size-3.5 shrink-0'
const workspaceSwitchListClassName = 'max-h-[240px] overflow-y-auto overscroll-contain scroll-py-1'
const workspaceSwitchI18nKey = (key: string) => key as 'mainNav.workspace.settings'
type WorkspaceSort = 'lastOpened' | 'createdAt'

const getWorkspaceName = (workspace: TenantListItemResponse) => workspace.name || workspace.id
Expand All @@ -46,19 +45,19 @@ function WorkspaceSwitchControls({
}) {
const { t } = useTranslation()
const [sortMenuOpen, setSortMenuOpen] = useState(false)
const sortMenuLabel = t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.sort.openMenu')], {
const sortMenuLabel = t(($) => $['mainNav.workspace.sort.openMenu'], {
ns: 'common',
})
const sortOptions: Array<{ value: WorkspaceSort; label: string }> = [
{
value: 'lastOpened',
label: t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.sort.lastOpened')], {
label: t(($) => $['mainNav.workspace.sort.lastOpened'], {
ns: 'common',
}),
},
{
value: 'createdAt',
label: t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.sort.createdTime')], {
label: t(($) => $['mainNav.workspace.sort.createdTime'], {
ns: 'common',
}),
},
Expand Down Expand Up @@ -133,10 +132,7 @@ function WorkspaceSwitchControls({
<SearchInput
value={searchText}
onValueChange={onSearchTextChange}
placeholder={t(
($) => $[workspaceSwitchI18nKey('mainNav.workspace.searchPlaceholder')],
{ ns: 'common' },
)}
placeholder={t(($) => $['mainNav.workspace.searchPlaceholder'], { ns: 'common' })}
autoFocus
/>
</div>
Expand Down Expand Up @@ -208,7 +204,7 @@ export function WorkspaceSwitcher({
role="status"
className="flex h-8 items-center px-3 system-xs-regular text-text-tertiary"
>
{t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.noResults')], {
{t(($) => $['mainNav.workspace.noResults'], {
ns: 'common',
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ export const CreateSubscriptionButton = ({
}
}, [t])

const methodDescriptionMap = {
[SupportedCreationMethods.OAUTH]: t(
($) => $['subscription.addType.options.oauth.description'],
{
ns: 'pluginTrigger',
},
),
[SupportedCreationMethods.APIKEY]: t(
($) => $['subscription.addType.options.apikey.description'],
{
ns: 'pluginTrigger',
},
),
[SupportedCreationMethods.MANUAL]: t(
($) => $['subscription.addType.options.manual.description'],
{
ns: 'pluginTrigger',
},
),
[DEFAULT_METHOD]: '',
}

const onClickClientSettings = useCallback(
(e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
e.stopPropagation()
Expand Down Expand Up @@ -336,13 +358,7 @@ export const CreateSubscriptionButton = ({
<TooltipContent>
{subscriptionCount >= MAX_COUNT
? t(($) => $['subscription.maxCount'], { ns: 'pluginTrigger', num: MAX_COUNT })
: t(
($) =>
$[
`subscription.addType.options.${methodType!.toLowerCase() as Lowercase<SupportedCreationMethods>}.description`
],
{ ns: 'pluginTrigger' },
)}
: methodDescriptionMap[methodType!]}
</TooltipContent>
</Tooltip>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
Expand Down Expand Up @@ -47,11 +46,7 @@ export const useAvailableNodesMetaData = () => {
mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(($) => $[`blocks.${metaData.type}`], { ns: 'workflow' })
const description = t(
($) =>
$[`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>],
{ ns: 'workflow' },
)
const description = t(($) => $[`blocksAbout.${metaData.type}`], { ns: 'workflow' })
return {
...node,
metaData: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
Expand Down Expand Up @@ -65,11 +64,7 @@ export const useAvailableNodesMetaData = () => {
mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(($) => $[`blocks.${metaData.type}`], { ns: 'workflow' })
const description = t(
($) =>
$[`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>],
{ ns: 'workflow' },
)
const description = t(($) => $[`blocksAbout.${metaData.type}`], { ns: 'workflow' })
const helpLinkPath = getNodeHelpLinkPath(metaData.helpLinkUri)
return {
...node,
Expand Down
25 changes: 11 additions & 14 deletions web/app/components/workflow/hooks/use-checklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type { ModelItem } from '@/app/components/header/account-setting/model-pr
import type { Emoji } from '@/app/components/tools/types'
import type { AgentToolPublishIssue } from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog'
import type { DataSet } from '@/models/datasets'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'
import isDeepEqual from 'fast-deep-equal'
import { useCallback, useEffect, useMemo, useRef } from 'react'
Expand Down Expand Up @@ -557,22 +556,22 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
}
}

const isRequiredNodesType = Object.keys(nodesExtraData!).filter(
(key: any) => (nodesExtraData as any)[key].metaData.isRequired,
)
const isRequiredNodesType = Object.entries(nodesExtraData!)
.filter(([, node]) => node.metaData.isRequired)
.map(([type]) => type as BlockEnum)

isRequiredNodesType.forEach((type: string) => {
isRequiredNodesType.forEach((type) => {
if (!filteredNodes.some((node) => node.data.type === type)) {
list.push({
id: `${type}-need-added`,
type,
title: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], {
title: t(($) => $[`blocks.${type}`], {
ns: 'workflow',
}),
errorMessages: [
t(($) => $['common.needAdd'], {
ns: 'workflow',
node: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], {
node: t(($) => $[`blocks.${type}`], {
ns: 'workflow',
}),
}),
Expand Down Expand Up @@ -896,18 +895,16 @@ export const useChecklistBeforePublish = () => {
}
}

const isRequiredNodesType = Object.keys(nodesExtraData!).filter(
(key: any) => (nodesExtraData as any)[key].metaData.isRequired,
)

for (let i = 0; i < isRequiredNodesType.length; i++) {
const type = isRequiredNodesType[i]
const isRequiredNodesType = Object.entries(nodesExtraData!)
.filter(([, node]) => node.metaData.isRequired)
.map(([type]) => type as BlockEnum)

for (const type of isRequiredNodesType) {
if (!filteredNodes.some((node) => node.data.type === type)) {
toast.error(
t(($) => $['common.needAdd'], {
ns: 'workflow',
node: t(($) => $[`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>], {
node: t(($) => $[`blocks.${type}`], {
ns: 'workflow',
}),
}),
Expand Down
55 changes: 55 additions & 0 deletions web/i18n/__tests__/translation-adapters.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { SelectorParam } from 'i18next'

const mocks = vi.hoisted(() => {
const t = vi.fn(() => 'Translated')
const translations = { t }
return {
translations,
t,
client: vi.fn(() => translations),
locale: vi.fn(async () => 'en-US'),
server: vi.fn(async () => translations),
use: vi.fn((promise: Promise<unknown>) => promise),
}
})

vi.mock('server-only', () => ({}))
vi.mock('react-i18next', () => ({ useTranslation: mocks.client }))
vi.mock('react', () => ({ use: mocks.use }))
vi.mock('@/i18n/server', () => ({
getLocaleOnServer: mocks.locale,
getTranslation: mocks.server,
}))

beforeEach(() => vi.clearAllMocks())

describe('translation adapter contracts', () => {
it.each(['common', undefined] as const)(
'forwards the client namespace %s unchanged',
async (ns) => {
const { useTranslation } = await import('../lib.client')
expect(useTranslation(ns)).toBe(mocks.translations)
expect(mocks.client).toHaveBeenCalledExactlyOnceWith(ns)
},
)

it.each(['common', undefined] as const)(
'loads the server namespace %s with the current locale',
async (ns) => {
const { useTranslation } = await import('../lib.server')
expect(await useTranslation(ns)).toBe(mocks.translations)
expect(mocks.locale).toHaveBeenCalledTimes(1)
expect(mocks.server).toHaveBeenCalledExactlyOnceWith('en-US', ns)
expect(mocks.use).toHaveBeenCalledTimes(1)
},
)

it('loads metadata from exactly the requested namespace and selector', async () => {
const { getRouteMetadata } = await import('@/app/route-metadata')
const selector: SelectorParam<'common'> = ($) => $['operation.save']
expect(await getRouteMetadata('common', selector)).toEqual({ title: 'Translated' })
expect(mocks.locale).toHaveBeenCalledTimes(1)
expect(mocks.server).toHaveBeenCalledExactlyOnceWith('en-US', 'common')
expect(mocks.t).toHaveBeenCalledExactlyOnceWith(selector, { ns: 'common' })
})
})
5 changes: 0 additions & 5 deletions web/i18n/locales/ar-TN/explore.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
"appCard.addToWorkspace": "إضافة إلى مساحة العمل",
"appCard.try": "التفاصيل",
"appCustomize.nameRequired": "اسم التطبيق مطلوب",
"appCustomize.subTitle": "أيقونة التطبيق واسمه",
"appCustomize.title": "إنشاء تطبيق من {{name}}",
"apps.allCategories": "موصى به",
"apps.description": "قوالب جاهزة للاستخدام من المجتمع وفريق Dify.",
"apps.resetFilter": "مسح الفلتر",
"apps.resultNum": "{{num}} نتائج",
"apps.title": "قوالب",
"apps.viewMore": "عرض المزيد",
Expand All @@ -30,17 +28,14 @@
"continueWork.title": "الأخيرة",
"learnDify.description": "اتبع المسار — أو انتقل مباشرة إلى الخطوة التي تشعر بأنك مستعد لها.",
"learnDify.hide": "إخفاء",
"learnDify.moreTemplates": "المزيد من قوالب التعلّم →",
"learnDify.title": "تعلّم Dify",
"sidebar.action.delete": "حذف",
"sidebar.action.pin": "تثبيت",
"sidebar.action.rename": "إعادة تسمية",
"sidebar.action.unpin": "إلغاء التثبيت",
"sidebar.chat": "دردشة",
"sidebar.delete.content": "هل أنت متأكد أنك تريد حذف هذا التطبيق؟",
"sidebar.delete.title": "حذف التطبيق",
"sidebar.webApps": "تطبيقات الويب",
"title": "استكشاف",
"tryApp.category": "الفئة",
"tryApp.createFromSampleApp": "إنشاء من هذا التطبيق النموذجي",
"tryApp.requirements": "المتطلبات",
Expand Down
5 changes: 0 additions & 5 deletions web/i18n/locales/az-AZ/explore.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
"appCard.addToWorkspace": "Şablondan istifadə et",
"appCard.try": "Təfərrüatlar",
"appCustomize.nameRequired": "Tətbiqin adı tələb olunur",
"appCustomize.subTitle": "Tətbiqin nişanı və adı",
"appCustomize.title": "{{name}} əsasında tətbiqin yaradılması",
"apps.allCategories": "Hamısı",
"apps.description": "İcma və Dify komandası tərəfindən hazırlanmış istifadəyə hazır şablonlar.",
"apps.resetFilter": "Süzgəci təmizlə",
"apps.resultNum": "{{num}} nəticə",
"apps.title": "Şablonlar",
"apps.viewMore": "Daha çox göstər",
Expand All @@ -30,17 +28,14 @@
"continueWork.title": "Son istifadə edilənlər",
"learnDify.description": "Ardıcıllıqla irəliləyin və ya hazır olduğunuz mərhələyə keçin.",
"learnDify.hide": "Gizlət",
"learnDify.moreTemplates": "Daha çox öyrənmə şablonu →",
"learnDify.title": "Dify ilə işin öyrənilməsi",
"sidebar.action.delete": "Sil",
"sidebar.action.pin": "Sancaqla",
"sidebar.action.rename": "Adını dəyiş",
"sidebar.action.unpin": "Sancaqdan çıxar",
"sidebar.chat": "Söhbət",
"sidebar.delete.content": "Bu tətbiqi silmək istədiyinizə əminsiniz?",
"sidebar.delete.title": "Tətbiqin silinməsi",
"sidebar.webApps": "Veb tətbiqləri",
"title": "Kəşf",
"tryApp.category": "Kateqoriya",
"tryApp.createFromSampleApp": "Bu nümunə tətbiqdən yarat",
"tryApp.requirements": "Tələblər",
Expand Down
5 changes: 0 additions & 5 deletions web/i18n/locales/de-DE/explore.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
"appCard.addToWorkspace": "Vorlage verwenden",
"appCard.try": "Details",
"appCustomize.nameRequired": "App-Name ist erforderlich",
"appCustomize.subTitle": "App-Symbol & Name",
"appCustomize.title": "App aus {{name}} erstellen",
"apps.allCategories": "Alle",
"apps.description": "Sofort nutzbare Vorlagen aus der Community und vom Dify-Team.",
"apps.resetFilter": "Filter löschen",
"apps.resultNum": "{{num}} Ergebnisse",
"apps.title": "Vorlagen",
"apps.viewMore": "Mehr anzeigen",
Expand All @@ -30,17 +28,14 @@
"continueWork.title": "Zuletzt verwendet",
"learnDify.description": "Folge dem Pfad – oder springe direkt zu dem Schritt, für den du bereit bist.",
"learnDify.hide": "Ausblenden",
"learnDify.moreTemplates": "Weitere Lernvorlagen →",
"learnDify.title": "Dify kennenlernen",
"sidebar.action.delete": "Löschen",
"sidebar.action.pin": "Anheften",
"sidebar.action.rename": "Umbenennen",
"sidebar.action.unpin": "Lösen",
"sidebar.chat": "Chat",
"sidebar.delete.content": "Sind Sie sicher, dass Sie diese App löschen möchten?",
"sidebar.delete.title": "App löschen",
"sidebar.webApps": "Web-Apps",
"title": "Entdecken",
"tryApp.category": "Kategorie",
"tryApp.createFromSampleApp": "Aus dieser Beispiel-App erstellen",
"tryApp.requirements": "Anforderungen",
Expand Down
Loading
Loading