Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
aeccd50
refactor: use json for translation
hyoban Dec 24, 2025
9819698
Merge remote-tracking branch 'origin/main' into 12-24-json-for-transl…
lyzno1 Dec 24, 2025
1c8d7ae
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
bb66174
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
d9a3e42
update
hyoban Dec 25, 2025
469bcd1
migrate to flatted keys
hyoban Dec 25, 2025
6cd33fc
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
3e9c427
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
2559d75
fix test
hyoban Dec 25, 2025
fd5d932
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
177bd5b
update script
hyoban Dec 25, 2025
e6dbd1f
clean deps
hyoban Dec 25, 2025
c07f38b
remove eslint rule
hyoban Dec 25, 2025
2007d5d
clean deps
hyoban Dec 25, 2025
07873f9
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
1dfbe13
sort keys
hyoban Dec 25, 2025
9183831
clean
hyoban Dec 25, 2025
76dc2ea
Revert "migrate to flatted keys"
hyoban Dec 25, 2025
c98ca4d
readd base change
hyoban Dec 25, 2025
4de1de2
save eslint rules
hyoban Dec 25, 2025
2585c8a
save res
hyoban Dec 25, 2025
8389f82
Revert "save res"
hyoban Dec 25, 2025
731d158
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 25, 2025
fcd25d3
load on server
hyoban Dec 25, 2025
3736e2d
type for namespace
hyoban Dec 25, 2025
c6d367a
lint update
hyoban Dec 26, 2025
d5418ba
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 26, 2025
ea99537
lint update
hyoban Dec 26, 2025
06a082b
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 26, 2025
a5ee766
align translations
lyzno1 Dec 25, 2025
2ffe51d
auto generate
lyzno1 Dec 25, 2025
017274f
Update i18n configuration to use JSON files for translations and impr…
lyzno1 Dec 25, 2025
aad4947
lint
lyzno1 Dec 25, 2025
e258503
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 26, 2025
01d6a6c
migrate with lint
hyoban Dec 26, 2025
6f2663d
manual change
hyoban Dec 26, 2025
40cb58c
fix test
hyoban Dec 26, 2025
caf3e58
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 26, 2025
d9a439d
type safe
hyoban Dec 26, 2025
b6e140f
type safe
hyoban Dec 26, 2025
9276f57
type safe
hyoban Dec 26, 2025
f9db9a6
type safe
hyoban Dec 26, 2025
89a1d15
type safe
hyoban Dec 26, 2025
8698d4f
type safe
hyoban Dec 26, 2025
5058339
type safe
hyoban Dec 26, 2025
815389d
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 29, 2025
74107a8
new trans
hyoban Dec 29, 2025
de2a4fe
type check
hyoban Dec 29, 2025
2195676
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 29, 2025
9cd5712
type check
hyoban Dec 29, 2025
bc0495d
update test
hyoban Dec 29, 2025
6c78157
add script to for translations
hyoban Dec 29, 2025
ca9c77f
lint as
hyoban Dec 29, 2025
0e87b3b
Merge branch 'main' into 12-24-json-for-translation
hyoban Dec 29, 2025
92aa56a
check namespace files
hyoban Dec 29, 2025
5390889
fix script
hyoban Dec 29, 2025
3f80857
Merge remote-tracking branch 'origin/main' into 12-24-json-for-transl…
lyzno1 Dec 29, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 3 additions & 3 deletions .github/workflows/translate-i18n-base-on-english.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
paths:
- 'web/i18n/en-US/*.ts'
- 'web/i18n/en-US/*.json'

permissions:
contents: write
Expand All @@ -28,13 +28,13 @@ jobs:
run: |
git fetch origin "${{ github.event.before }}" || true
git fetch origin "${{ github.sha }}" || true
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.ts')
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.json')
echo "Changed files: $changed_files"
if [ -n "$changed_files" ]; then
echo "FILES_CHANGED=true" >> $GITHUB_ENV
file_args=""
for file in $changed_files; do
filename=$(basename "$file" .ts)
filename=$(basename "$file" .json)
file_args="$file_args --file $filename"
done
echo "FILE_ARGS=$file_args" >> $GITHUB_ENV
Expand Down
51 changes: 24 additions & 27 deletions web/__tests__/i18n-upload-features.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ const getSupportedLocales = (): string[] => {

// Helper function to load translation file content
const loadTranslationContent = (locale: string): string => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')

if (!fs.existsSync(filePath))
throw new Error(`Translation file not found: ${filePath}`)

return fs.readFileSync(filePath, 'utf-8')
}

// Helper function to check if upload features exist
// Helper function to check if upload features exist (supports flattened JSON)
const hasUploadFeatures = (content: string): { [key: string]: boolean } => {
return {
fileUpload: /fileUpload\s*:\s*\{/.test(content),
imageUpload: /imageUpload\s*:\s*\{/.test(content),
documentUpload: /documentUpload\s*:\s*\{/.test(content),
audioUpload: /audioUpload\s*:\s*\{/.test(content),
featureBar: /bar\s*:\s*\{/.test(content),
fileUpload: /"feature\.fileUpload\.title"/.test(content),
imageUpload: /"feature\.imageUpload\.title"/.test(content),
documentUpload: /"feature\.documentUpload\.title"/.test(content),
audioUpload: /"feature\.audioUpload\.title"/.test(content),
featureBar: /"feature\.bar\.empty"/.test(content),
}
}

Expand All @@ -45,7 +45,7 @@ describe('Upload Features i18n Translations - Issue #23062', () => {

it('all locales should have translation files', () => {
supportedLocales.forEach((locale) => {
const filePath = path.join(I18N_DIR, locale, 'app-debug.ts')
const filePath = path.join(I18N_DIR, locale, 'app-debug.json')
expect(fs.existsSync(filePath)).toBe(true)
})
})
Expand Down Expand Up @@ -76,12 +76,9 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
previouslyMissingLocales.forEach((locale) => {
const content = loadTranslationContent(locale)

// Verify audioUpload exists
expect(/audioUpload\s*:\s*\{/.test(content)).toBe(true)

// Verify it has title and description
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
// Verify audioUpload exists with title and description (flattened JSON format)
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)

console.log(`✅ ${locale} - Issue #23062 resolved: audioUpload feature present`)
})
Expand All @@ -91,28 +88,28 @@ describe('Upload Features i18n Translations - Issue #23062', () => {
supportedLocales.forEach((locale) => {
const content = loadTranslationContent(locale)

// Check fileUpload has required properties
if (/fileUpload\s*:\s*\{/.test(content)) {
expect(/fileUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/fileUpload[^}]*description\s*:/.test(content)).toBe(true)
// Check fileUpload has required properties (flattened JSON format)
if (/"feature\.fileUpload\.title"/.test(content)) {
expect(/"feature\.fileUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.fileUpload\.description"/.test(content)).toBe(true)
}

// Check imageUpload has required properties
if (/imageUpload\s*:\s*\{/.test(content)) {
expect(/imageUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/imageUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.imageUpload\.title"/.test(content)) {
expect(/"feature\.imageUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.imageUpload\.description"/.test(content)).toBe(true)
}

// Check documentUpload has required properties
if (/documentUpload\s*:\s*\{/.test(content)) {
expect(/documentUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/documentUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.documentUpload\.title"/.test(content)) {
expect(/"feature\.documentUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.documentUpload\.description"/.test(content)).toBe(true)
}

// Check audioUpload has required properties
if (/audioUpload\s*:\s*\{/.test(content)) {
expect(/audioUpload[^}]*title\s*:/.test(content)).toBe(true)
expect(/audioUpload[^}]*description\s*:/.test(content)).toBe(true)
if (/"feature\.audioUpload\.title"/.test(content)) {
expect(/"feature\.audioUpload\.title"/.test(content)).toBe(true)
expect(/"feature\.audioUpload\.description"/.test(content)).toBe(true)
}
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,32 +70,32 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navConfig = [
...(isCurrentWorkspaceEditor
? [{
name: t('common.appMenus.promptEng'),
name: t('appMenus.promptEng', { ns: 'common' }),
href: `/app/${appId}/${(mode === AppModeEnum.WORKFLOW || mode === AppModeEnum.ADVANCED_CHAT) ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
}]
: []
),
{
name: t('common.appMenus.apiAccess'),
name: t('appMenus.apiAccess', { ns: 'common' }),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
},
...(isCurrentWorkspaceEditor
? [{
name: mode !== AppModeEnum.WORKFLOW
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
? t('appMenus.logAndAnn', { ns: 'common' })
: t('appMenus.logs', { ns: 'common' }),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
}]
: []
),
{
name: t('common.appMenus.overview'),
name: t('appMenus.overview', { ns: 'common' }),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
Expand All @@ -104,7 +104,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return navConfig
}, [t])

useDocumentTitle(appDetail?.name || t('common.menus.appDetail'))
useDocumentTitle(appDetail?.name || t('menus.appDetail', { ns: 'common' }))

useEffect(() => {
if (appDetail) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { IAppCardProps } from '@/app/components/app/overview/app-card'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import type { App } from '@/types/app'
import type { I18nKeysByPrefix } from '@/types/i18n'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
Expand Down Expand Up @@ -62,7 +63,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className="flex flex-col gap-1">
<div className="text-xs text-text-secondary">
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
</div>
<div
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
Expand All @@ -71,19 +72,19 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
window.open(triggerDocUrl, '_blank')
}}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</div>
</div>
), [t, triggerDocUrl])

const disableWebAppTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' }))
: null
const disableApiTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
? buildTriggerModeMessage(t('overview.apiInfo.title', { ns: 'appOverview' }))
: null
const disableMcpTooltip = disableAppCards
? buildTriggerModeMessage(t('tools.mcp.server.title'))
? buildTriggerModeMessage(t('mcp.server.title', { ns: 'tools' }))
: null

const updateAppDetail = async () => {
Expand All @@ -94,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
catch (error) { console.error(error) }
}

const handleCallbackResult = (err: Error | null, message?: string) => {
const handleCallbackResult = (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => {
const type = err ? 'error' : 'success'

message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
Expand All @@ -104,7 +105,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {

notify({
type,
message: t(`common.actionMsg.${message}` as any) as string,
message: t(`actionMsg.${message}`, { ns: 'common' }) as string,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import * as React from 'react'
Expand All @@ -16,7 +17,9 @@ dayjs.extend(quarterOfYear)

const today = dayjs()

const TIME_PERIOD_MAPPING = [
type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>

const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
{ value: 0, name: 'today' },
{ value: 7, name: 'last7days' },
{ value: 30, name: 'last30days' },
Expand All @@ -35,8 +38,8 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow'
const [period, setPeriod] = useState<PeriodParams>(IS_CLOUD_EDITION
? { name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
: { name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
? { name: t('filter.period.today', { ns: 'appLog' }), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }
: { name: t('filter.period.last7days', { ns: 'appLog' }), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } },
)

if (!appDetail)
Expand All @@ -45,7 +48,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
return (
<div>
<div className="mb-4">
<div className="system-xl-semibold mb-2 text-text-primary">{t('common.appMenus.overview')}</div>
<div className="system-xl-semibold mb-2 text-text-primary">{t('appMenus.overview', { ns: 'common' })}</div>
<div className="flex items-center justify-between">
{IS_CLOUD_EDITION
? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
import type { FC } from 'react'
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { SimpleSelect } from '@/app/components/base/select'

type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>

type Props = {
periodMapping: { [key: string]: { value: number, name: string } }
periodMapping: { [key: string]: { value: number, name: TimePeriodName } }
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
Expand All @@ -25,9 +28,9 @@ const LongTimeRangePicker: FC<Props> = ({
const handleSelect = React.useCallback((item: Item) => {
const id = item.value
const value = periodMapping[id]?.value ?? '-1'
const name = item.name || t('appLog.filter.period.allTime')
const name = item.name || t('filter.period.allTime', { ns: 'appLog' })
if (value === -1) {
onSelect({ name: t('appLog.filter.period.allTime'), query: undefined })
onSelect({ name: t('filter.period.allTime', { ns: 'appLog' }), query: undefined })
}
else if (value === 0) {
const startOfToday = today.startOf('day').format(queryDateFormat)
Expand All @@ -53,7 +56,7 @@ const LongTimeRangePicker: FC<Props> = ({

return (
<SimpleSelect
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelect}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { Dayjs } from 'dayjs'
import type { FC } from 'react'
import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart'
import type { I18nKeysByPrefix } from '@/types/i18n'
import dayjs from 'dayjs'
import * as React from 'react'
import { useCallback, useState } from 'react'
Expand All @@ -13,8 +14,10 @@ import RangeSelector from './range-selector'

const today = dayjs()

type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>

type Props = {
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParams) => void
queryDateFormat: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart'
import type { Item } from '@/app/components/base/select'
import type { I18nKeysByPrefix } from '@/types/i18n'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import dayjs from 'dayjs'
import * as React from 'react'
Expand All @@ -12,9 +13,11 @@ import { cn } from '@/utils/classnames'

const today = dayjs()

type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'>

type Props = {
isCustomRange: boolean
ranges: { value: number, name: string }[]
ranges: { value: number, name: TimePeriodName }[]
onSelect: (payload: PeriodParamsWithTimeRange) => void
}

Expand Down Expand Up @@ -42,7 +45,7 @@ const RangeSelector: FC<Props> = ({
const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => {
return (
<div className={cn('flex h-8 cursor-pointer items-center space-x-1.5 rounded-lg bg-components-input-bg-normal pl-3 pr-2', isOpen && 'bg-state-base-hover-alt')}>
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('appLog.filter.period.custom') : item?.name}</div>
<div className="system-sm-regular text-components-input-text-filled">{isCustomRange ? t('filter.period.custom', { ns: 'appLog' }) : item?.name}</div>
<RiArrowDownSLine className={cn('size-4 text-text-quaternary', isOpen && 'text-text-secondary')} />
</div>
)
Expand All @@ -66,7 +69,7 @@ const RangeSelector: FC<Props> = ({
}, [])
return (
<SimpleSelect
items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}` as any) as string }))}
items={ranges.map(v => ({ ...v, name: t(`filter.period.${v.name}`, { ns: 'appLog' }) }))}
className="mt-0 !w-40"
notClearable={true}
onSelect={handleSelectRange}
Expand Down
Loading
Loading