Browse Source

Feat/i18n restructure (#2529)

crazywoola 1 year ago
parent
commit
9574730050
100 changed files with 385 additions and 596 deletions
  1. 1 0
      web/.eslintrc.json
  2. 1 2
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
  3. 1 2
      web/app/(commonLayout)/apps/page.tsx
  4. 4 5
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx
  5. 3 10
      web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
  6. 2 3
      web/app/(commonLayout)/datasets/Doc.tsx
  7. 4 4
      web/app/activate/activateForm.tsx
  8. 1 1
      web/app/components/app-sidebar/basic.tsx
  9. 3 4
      web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
  10. 4 5
      web/app/components/app/annotation/header-opts/index.tsx
  11. 2 3
      web/app/components/app/configuration/config-prompt/conversation-histroy/history-panel.tsx
  12. 1 1
      web/app/components/app/configuration/config-voice/param-config-content.tsx
  13. 2 3
      web/app/components/app/configuration/config/agent/agent-tools/choose-tool/index.tsx
  14. 2 2
      web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx
  15. 1 1
      web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx
  16. 2 3
      web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx
  17. 1 3
      web/app/components/app/configuration/toolbox/moderation/index.tsx
  18. 4 5
      web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx
  19. 3 4
      web/app/components/app/configuration/tools/external-data-tool-modal.tsx
  20. 2 3
      web/app/components/app/overview/customize/index.tsx
  21. 2 2
      web/app/components/app/overview/settings/index.tsx
  22. 3 3
      web/app/components/billing/pricing/plan-item.tsx
  23. 2 3
      web/app/components/datasets/create/file-uploader/index.tsx
  24. 2 3
      web/app/components/datasets/create/step-two/index.tsx
  25. 2 3
      web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx
  26. 1 1
      web/app/components/datasets/documents/detail/embedding/index.tsx
  27. 1 1
      web/app/components/datasets/documents/index.tsx
  28. 4 4
      web/app/components/develop/doc.tsx
  29. 2 3
      web/app/components/develop/secret-key/secret-key-modal.tsx
  30. 1 1
      web/app/components/explore/category.tsx
  31. 3 4
      web/app/components/header/account-about/index.tsx
  32. 2 3
      web/app/components/header/account-dropdown/index.tsx
  33. 2 2
      web/app/components/header/account-setting/language-page/index.tsx
  34. 4 4
      web/app/components/header/account-setting/members-page/index.tsx
  35. 1 3
      web/app/components/header/account-setting/members-page/invite-modal/index.tsx
  36. 8 8
      web/app/components/header/account-setting/model-provider-page/declarations.ts
  37. 1 2
      web/app/components/header/account-setting/model-provider-page/hooks.ts
  38. 4 5
      web/app/components/header/maintenance-notice.tsx
  39. 1 1
      web/app/components/i18n.tsx
  40. 0 23
      web/app/components/locale-switcher.tsx
  41. 2 2
      web/app/components/tools/edit-custom-collection-modal/test-api.tsx
  42. 2 3
      web/app/components/tools/tool-list/header.tsx
  43. 3 3
      web/app/components/tools/tool-list/item.tsx
  44. 2 2
      web/app/components/tools/tool-nav-list/item.tsx
  45. 0 2
      web/app/install/installForm.tsx
  46. 2 2
      web/app/signin/_header.tsx
  47. 3 4
      web/app/signin/normalForm.tsx
  48. 2 2
      web/app/signin/oneMoreStep.tsx
  49. 1 3
      web/context/i18n.ts
  50. 175 0
      web/i18n/README.md
  51. 0 82
      web/i18n/README_CN.md
  52. 0 81
      web/i18n/README_EN.md
  53. 0 16
      web/i18n/client.ts
  54. 0 0
      web/i18n/en-US/app-annotation.ts
  55. 0 0
      web/i18n/en-US/app-api.ts
  56. 0 0
      web/i18n/en-US/app-debug.ts
  57. 0 0
      web/i18n/en-US/app-log.ts
  58. 0 0
      web/i18n/en-US/app-overview.ts
  59. 0 0
      web/i18n/en-US/app.ts
  60. 0 0
      web/i18n/en-US/billing.ts
  61. 0 0
      web/i18n/en-US/common.ts
  62. 0 0
      web/i18n/en-US/custom.ts
  63. 0 0
      web/i18n/en-US/dataset-creation.ts
  64. 0 0
      web/i18n/en-US/dataset-documents.ts
  65. 0 0
      web/i18n/en-US/dataset-hit-testing.ts
  66. 0 0
      web/i18n/en-US/dataset-settings.ts
  67. 0 0
      web/i18n/en-US/dataset.ts
  68. 0 0
      web/i18n/en-US/explore.ts
  69. 0 0
      web/i18n/en-US/layout.ts
  70. 0 0
      web/i18n/en-US/login.ts
  71. 0 0
      web/i18n/en-US/register.ts
  72. 0 0
      web/i18n/en-US/share-app.ts
  73. 0 0
      web/i18n/en-US/tools.ts
  74. 31 189
      web/i18n/i18next-config.ts
  75. 0 26
      web/i18n/i18next-serverside-config.ts
  76. 16 2
      web/i18n/index.ts
  77. 29 30
      web/utils/language.ts
  78. 0 0
      web/i18n/pt-BR/app-annotation.ts
  79. 0 0
      web/i18n/pt-BR/app-api.ts
  80. 0 0
      web/i18n/pt-BR/app-debug.ts
  81. 0 0
      web/i18n/pt-BR/app-log.ts
  82. 0 0
      web/i18n/pt-BR/app-overview.ts
  83. 0 0
      web/i18n/pt-BR/app.ts
  84. 0 0
      web/i18n/pt-BR/billing.ts
  85. 0 0
      web/i18n/pt-BR/common.ts
  86. 0 0
      web/i18n/pt-BR/custom.ts
  87. 0 0
      web/i18n/pt-BR/dataset-creation.ts
  88. 0 0
      web/i18n/pt-BR/dataset-documents.ts
  89. 0 0
      web/i18n/pt-BR/dataset-hit-testing.ts
  90. 0 0
      web/i18n/pt-BR/dataset-settings.ts
  91. 0 0
      web/i18n/pt-BR/dataset.ts
  92. 0 0
      web/i18n/pt-BR/explore.ts
  93. 0 0
      web/i18n/pt-BR/layout.ts
  94. 0 0
      web/i18n/pt-BR/login.ts
  95. 0 0
      web/i18n/pt-BR/register.ts
  96. 0 0
      web/i18n/pt-BR/share-app.ts
  97. 0 0
      web/i18n/pt-BR/tools.ts
  98. 27 4
      web/i18n/server.ts
  99. 0 0
      web/i18n/uk-UA/app-annotation.ts
  100. 0 0
      web/i18n/lang/app-api.uk.ts

+ 1 - 0
web/.eslintrc.json

@@ -8,6 +8,7 @@
       "error",
       "type"
     ],
+    "@typescript-eslint/no-var-requires": "off",
     "no-console": "off",
     "indent": "off",
     "@typescript-eslint/indent": [

+ 1 - 2
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx

@@ -1,8 +1,7 @@
 import React from 'react'
 import ChartView from './chartView'
 import CardView from './cardView'
-import { getLocaleOnServer } from '@/i18n/server'
-import { useTranslation as translate } from '@/i18n/i18next-serverside-config'
+import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
 import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
 
 export type IDevelopProps = {

+ 1 - 2
web/app/(commonLayout)/apps/page.tsx

@@ -1,8 +1,7 @@
 import classNames from 'classnames'
 import style from '../list.module.css'
 import Apps from './Apps'
-import { getLocaleOnServer } from '@/i18n/server'
-import { useTranslation as translate } from '@/i18n/i18next-serverside-config'
+import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
 
 const AppList = async () => {
   const locale = getLocaleOnServer()

File diff suppressed because it is too large
+ 4 - 5
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx


+ 3 - 10
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx

@@ -1,15 +1,8 @@
 import React from 'react'
-import { getLocaleOnServer } from '@/i18n/server'
-import { useTranslation as translate } from '@/i18n/i18next-serverside-config'
+import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
 import Form from '@/app/components/datasets/settings/form'
 
-type Props = {
-  params: { datasetId: string }
-}
-
-const Settings = async ({
-  params: { datasetId },
-}: Props) => {
+const Settings = async () => {
   const locale = getLocaleOnServer()
   const { t } = await translate(locale, 'dataset-settings')
 
@@ -19,7 +12,7 @@ const Settings = async ({
         <div className='mb-1 text-lg font-semibold text-gray-900'>{t('title')}</div>
         <div className='text-sm text-gray-500'>{t('desc')}</div>
       </div>
-      <Form datasetId={datasetId} />
+      <Form />
     </div>
   )
 }

+ 2 - 3
web/app/(commonLayout)/datasets/Doc.tsx

@@ -5,7 +5,7 @@ import { useContext } from 'use-context-selector'
 import TemplateEn from './template/template.en.mdx'
 import TemplateZh from './template/template.zh.mdx'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 type DocProps = {
   apiBaseUrl: string
@@ -14,11 +14,10 @@ const Doc: FC<DocProps> = ({
   apiBaseUrl,
 }) => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   return (
     <article className='mx-1 px-4 sm:mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
       {
-        language !== LanguagesSupportedUnderscore[1]
+        locale !== LanguagesSupported[1]
           ? <TemplateEn apiBaseUrl={apiBaseUrl} />
           : <TemplateZh apiBaseUrl={apiBaseUrl} />
       }

+ 4 - 4
web/app/activate/activateForm.tsx

@@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
 
 import { SimpleSelect } from '@/app/components/base/select'
 import { timezones } from '@/utils/timezone'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported, languages } from '@/utils/language'
+import { LanguagesSupported, languages } from '@/i18n/language'
 import { activateMember, invitationCheck } from '@/service/common'
 import Toast from '@/app/components/base/toast'
 import Loading from '@/app/components/base/loading'
@@ -42,9 +42,9 @@ const ActivateForm = () => {
   const [name, setName] = useState('')
   const [password, setPassword] = useState('')
   const [timezone, setTimezone] = useState('Asia/Shanghai')
-  const [language, setLanguage] = useState(getModelRuntimeSupported(locale))
+  const [language, setLanguage] = useState(locale)
   const [showSuccess, setShowSuccess] = useState(false)
-  const defaultLanguage = useCallback(() => (window.navigator.language.startsWith('zh') ? LanguagesSupportedUnderscore[1] : LanguagesSupportedUnderscore[0]) || LanguagesSupportedUnderscore[0], [])
+  const defaultLanguage = useCallback(() => (window.navigator.language.startsWith('zh') ? LanguagesSupported[1] : LanguagesSupported[0]) || LanguagesSupported[0], [])
 
   const showErrorMessage = useCallback((message: string) => {
     Toast.notify({
@@ -207,7 +207,7 @@ const ActivateForm = () => {
                 <Link
                   className='text-primary-600'
                   target='_blank' rel='noopener noreferrer'
-                  href={`https://docs.dify.ai/${language !== LanguagesSupportedUnderscore[1] ? 'user-agreement' : `v/${locale.toLowerCase()}/policies`}/open-source`}
+                  href={`https://docs.dify.ai/${language !== LanguagesSupported[1] ? 'user-agreement' : `v/${locale.toLowerCase()}/policies`}/open-source`}
                 >{t('login.license.link')}</Link>
               </div>
             </div>

File diff suppressed because it is too large
+ 1 - 1
web/app/components/app-sidebar/basic.tsx


+ 3 - 4
web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx

@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
 import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 const CSV_TEMPLATE_QA_EN = [
   ['question', 'answer'],
@@ -25,11 +25,10 @@ const CSVDownload: FC = () => {
   const { t } = useTranslation()
 
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const { CSVDownloader, Type } = useCSVDownloader()
 
   const getTemplate = () => {
-    return language !== LanguagesSupportedUnderscore[1] ? CSV_TEMPLATE_QA_EN : CSV_TEMPLATE_QA_CN
+    return locale !== LanguagesSupported[1] ? CSV_TEMPLATE_QA_EN : CSV_TEMPLATE_QA_CN
   }
 
   return (
@@ -58,7 +57,7 @@ const CSVDownload: FC = () => {
       <CSVDownloader
         className="block mt-2 cursor-pointer"
         type={Type.Link}
-        filename={`template-${language}`}
+        filename={`template-${locale}`}
         bom={true}
         data={getTemplate()}
       >

+ 4 - 5
web/app/components/app/annotation/header-opts/index.tsx

@@ -20,7 +20,7 @@ import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows
 
 import I18n from '@/context/i18n'
 import { fetchExportAnnotationList } from '@/service/annotation'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 const CSV_HEADER_QA_EN = ['Question', 'Answer']
 const CSV_HEADER_QA_CN = ['问题', '答案']
@@ -40,7 +40,6 @@ const HeaderOptions: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const { CSVDownloader, Type } = useCSVDownloader()
   const [list, setList] = useState<AnnotationItemBasic[]>([])
 
@@ -56,7 +55,7 @@ const HeaderOptions: FC<Props> = ({
     const content = listTransformer(list).join('\n')
     const file = new Blob([content], { type: 'application/jsonl' })
     a.href = URL.createObjectURL(file)
-    a.download = `annotations-${language}.jsonl`
+    a.download = `annotations-${locale}.jsonl`
     a.click()
   }
 
@@ -110,10 +109,10 @@ const HeaderOptions: FC<Props> = ({
             >
               <CSVDownloader
                 type={Type.Link}
-                filename={`annotations-${language}`}
+                filename={`annotations-${locale}`}
                 bom={true}
                 data={[
-                  language !== LanguagesSupportedUnderscore[1] ? CSV_HEADER_QA_EN : CSV_HEADER_QA_CN,
+                  locale !== LanguagesSupported[1] ? CSV_HEADER_QA_EN : CSV_HEADER_QA_CN,
                   ...list.map(item => [item.question, item.answer]),
                 ]}
               >

+ 2 - 3
web/app/components/app/configuration/config-prompt/conversation-histroy/history-panel.tsx

@@ -7,7 +7,7 @@ import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
 import Panel from '@/app/components/app/configuration/base/feature-panel'
 import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
 import I18n from '@/context/i18n'
-import { LanguagesSupported, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 type Props = {
   showWarning: boolean
@@ -20,7 +20,6 @@ const HistoryPanel: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
 
   return (
     <Panel
@@ -46,7 +45,7 @@ const HistoryPanel: FC<Props> = ({
       {showWarning && (
         <div className='flex justify-between py-2 px-3 rounded-b-xl bg-[#FFFAEB] text-xs text-gray-700'>
           <div>{t('appDebug.feature.conversationHistory.tip')}
-            <a href={`${language === LanguagesSupported[1]
+            <a href={`${locale === LanguagesSupported[1]
               ? 'https://docs.dify.ai/v/zh-hans/guides/application-design/prompt-engineering'
               : 'https://docs.dify.ai/features/prompt-engineering'}`}
             target='_blank' rel='noopener noreferrer'

+ 1 - 1
web/app/components/app/configuration/config-voice/param-config-content.tsx

@@ -13,7 +13,7 @@ import ConfigContext from '@/context/debug-configuration'
 import { fetchAppVoices } from '@/service/apps'
 import Tooltip from '@/app/components/base/tooltip'
 import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
-import { languages } from '@/utils/language'
+import { languages } from '@/i18n/language'
 const VoiceParamConfig: FC = () => {
   const { t } = useTranslation()
   const pathname = usePathname()

+ 2 - 3
web/app/components/app/configuration/config/agent/agent-tools/choose-tool/index.tsx

@@ -10,7 +10,7 @@ import Drawer from '@/app/components/base/drawer-plus'
 import ConfigContext from '@/context/debug-configuration'
 import type { ModelConfig } from '@/models/debug'
 import I18n from '@/context/i18n'
-import { getModelRuntimeSupported } from '@/utils/language'
+
 type Props = {
   show: boolean
   onHide: () => void
@@ -24,7 +24,6 @@ const ChooseTool: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const {
     modelConfig,
     setModelConfig,
@@ -60,7 +59,7 @@ const ChooseTool: FC<Props> = ({
                 provider_type: collection.type,
                 provider_name: collection.name,
                 tool_name: tool.name,
-                tool_label: tool.label[language],
+                tool_label: tool.label[locale],
                 tool_parameters: parameters,
                 enabled: true,
               })

+ 2 - 2
web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx

@@ -13,7 +13,7 @@ import I18n from '@/context/i18n'
 import Button from '@/app/components/base/button'
 import Loading from '@/app/components/base/loading'
 import { DiagonalDividingLine } from '@/app/components/base/icons/src/public/common'
-import { getModelRuntimeSupported } from '@/utils/language'
+import { getLanguage } from '@/i18n/language'
 type Props = {
   collection: Collection
   toolName: string
@@ -32,7 +32,7 @@ const SettingBuiltInTool: FC<Props> = ({
   onSave,
 }) => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+  const language = getLanguage(locale)
   const { t } = useTranslation()
 
   const [isLoading, setIsLoading] = useState(true)

+ 1 - 1
web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx

@@ -7,7 +7,7 @@ import { usePathname } from 'next/navigation'
 import Panel from '@/app/components/app/configuration/base/feature-panel'
 import { Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
 import ConfigContext from '@/context/debug-configuration'
-import { languages } from '@/utils/language'
+import { languages } from '@/i18n/language'
 import { fetchAppVoices } from '@/service/apps'
 import AudioBtn from '@/app/components/base/audio-btn'
 

+ 2 - 3
web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
 import I18n from '@/context/i18n'
 import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
-import { LanguagesSupported, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 type Props = {
   onReturnToSimpleMode: () => void
 }
@@ -15,7 +15,6 @@ const AdvancedModeWarning: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const [show, setShow] = React.useState(true)
   if (!show)
     return null
@@ -27,7 +26,7 @@ const AdvancedModeWarning: FC<Props> = ({
           <span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span>
           <a
             className='font-medium text-[#155EEF]'
-            href={`https://docs.dify.ai/${language === LanguagesSupported[1] ? 'v/zh-hans/guides/application-design/prompt-engineering' : 'features/prompt-engineering'}`}
+            href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/guides/application-design/prompt-engineering' : 'features/prompt-engineering'}`}
             target='_blank' rel='noopener noreferrer'
           >
             {t('appDebug.promptMode.advancedWarning.learnMore')}

+ 1 - 3
web/app/components/app/configuration/toolbox/moderation/index.tsx

@@ -7,12 +7,10 @@ import { useModalContext } from '@/context/modal-context'
 import ConfigContext from '@/context/debug-configuration'
 import { fetchCodeBasedExtensionList } from '@/service/common'
 import I18n from '@/context/i18n'
-import { getModelRuntimeSupported } from '@/utils/language'
 const Moderation = () => {
   const { t } = useTranslation()
   const { setShowModerationSettingModal } = useModalContext()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const {
     moderationConfig,
     setModerationConfig,
@@ -39,7 +37,7 @@ const Moderation = () => {
     else if (moderationConfig.type === 'api')
       prefix = t('common.apiBasedExtension.selector.title')
     else
-      prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[language] || ''
+      prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale] || ''
 
     if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
       suffix = t('appDebug.feature.moderation.allEnabled')

+ 4 - 5
web/app/components/app/configuration/toolbox/moderation/moderation-setting-modal.tsx

@@ -17,7 +17,7 @@ import {
 } from '@/service/common'
 import type { CodeBasedExtensionItem } from '@/models/common'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
 import { useModalContext } from '@/context/modal-context'
 import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@@ -44,7 +44,6 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
   const { t } = useTranslation()
   const { notify } = useToastContext()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
   const [localeData, setLocaleData] = useState<ModerationConfig>(data)
   const { setShowAccountSettingModal } = useModalContext()
@@ -200,12 +199,12 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
     }
 
     if (localeData.type === 'keywords' && !localeData.config.keywords) {
-      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: language !== LanguagesSupportedUnderscore[1] ? 'keywords' : '关键词' }) })
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }) })
       return
     }
 
     if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
-      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: language !== LanguagesSupportedUnderscore[1] ? 'API Extension' : 'API 扩展' }) })
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
       return
     }
 
@@ -214,7 +213,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
         if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
           notify({
             type: 'error',
-            message: t('appDebug.errorMessage.valueOfVarRequired', { key: language !== LanguagesSupportedUnderscore[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
+            message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
           })
           return
         }

+ 3 - 4
web/app/components/app/configuration/tools/external-data-tool-modal.tsx

@@ -12,7 +12,7 @@ import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/educatio
 import { fetchCodeBasedExtensionList } from '@/service/common'
 import { SimpleSelect } from '@/app/components/base/select'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import type {
   CodeBasedExtensionItem,
   ExternalDataTool,
@@ -41,7 +41,6 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
   const { t } = useTranslation()
   const { notify } = useToastContext()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
   const [showEmojiPicker, setShowEmojiPicker] = useState(false)
   const { data: codeBasedExtensionList } = useSWR(
@@ -157,7 +156,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
     }
 
     if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) {
-      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: language !== LanguagesSupportedUnderscore[1] ? 'API Extension' : 'API 扩展' }) })
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
       return
     }
 
@@ -166,7 +165,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
         if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
           notify({
             type: 'error',
-            message: t('appDebug.errorMessage.valueOfVarRequired', { key: language !== LanguagesSupportedUnderscore[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
+            message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
           })
           return
         }

+ 2 - 3
web/app/components/app/overview/customize/index.tsx

@@ -9,7 +9,7 @@ import I18n from '@/context/i18n'
 import Button from '@/app/components/base/button'
 import Modal from '@/app/components/base/modal'
 import Tag from '@/app/components/base/tag'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 type IShareLinkProps = {
   isShow: boolean
@@ -44,7 +44,6 @@ const CustomizeModal: FC<IShareLinkProps> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const isChatApp = mode === 'chat'
 
   return <Modal
@@ -102,7 +101,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
         className='w-36 mt-2'
         onClick={() =>
           window.open(
-            `https://docs.dify.ai/${language !== LanguagesSupportedUnderscore[1]
+            `https://docs.dify.ai/${locale !== LanguagesSupported[1]
               ? 'user-guide/launching-dify-apps/developing-with-apis'
               : `v/${locale.toLowerCase()}/guides/application-publishing/developing-with-apis`
             }`,

+ 2 - 2
web/app/components/app/overview/settings/index.tsx

@@ -13,7 +13,7 @@ import type { AppDetailResponse } from '@/models/app'
 import type { Language } from '@/types/app'
 import EmojiPicker from '@/app/components/base/emoji-picker'
 
-import { languages } from '@/utils/language'
+import { languages } from '@/i18n/language'
 
 export type ISettingsModalProps = {
   appInfo: AppDetailResponse
@@ -122,7 +122,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
         />
         <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
         <SimpleSelect
-          items={languages}
+          items={languages.filter(item => item.supported)}
           defaultValue={language}
           onSelect={item => setLanguage(item.value as Language)}
         />

+ 3 - 3
web/app/components/billing/pricing/plan-item.tsx

@@ -12,7 +12,7 @@ import { PlanRange } from './select-plan-range'
 import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
 import { useAppContext } from '@/context/app-context'
 import { fetchSubscriptionUrls } from '@/service/billing'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import I18n from '@/context/i18n'
 
 type Props = {
@@ -73,8 +73,8 @@ const PlanItem: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
-  const isZh = language === LanguagesSupportedUnderscore[1]
+
+  const isZh = locale === LanguagesSupported[1]
   const [loading, setLoading] = React.useState(false)
   const i18nPrefix = `billing.plans.${plan}`
   const isFreePlan = plan === Plan.sandbox

+ 2 - 3
web/app/components/datasets/create/file-uploader/index.tsx

@@ -12,7 +12,7 @@ import { upload } from '@/service/base'
 import { fetchFileUploadConfig } from '@/service/common'
 import { fetchSupportFileTypes } from '@/service/datasets'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 const FILES_NUMBER_LIMIT = 20
 
@@ -36,7 +36,6 @@ const FileUploader = ({
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const [dragging, setDragging] = useState(false)
   const dropRef = useRef<HTMLDivElement>(null)
   const dragRef = useRef<HTMLDivElement>(null)
@@ -77,7 +76,7 @@ const FileUploader = ({
     res = res.map(item => item.toLowerCase())
     res = res.filter((item, index, self) => self.indexOf(item) === index)
 
-    return res.map(item => item.toUpperCase()).join(language !== LanguagesSupportedUnderscore[1] ? ', ' : '、 ')
+    return res.map(item => item.toUpperCase()).join(locale !== LanguagesSupported[1] ? ', ' : '、 ')
   })()
   const ACCEPTS = supportTypes.map((ext: string) => `.${ext}`)
   const fileUploadConfig = useMemo(() => fileUploadConfigResponse ?? {

+ 2 - 3
web/app/components/datasets/create/step-two/index.tsx

@@ -42,7 +42,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import Tooltip from '@/app/components/base/tooltip'
 import TooltipPlus from '@/app/components/base/tooltip-plus'
 import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 type ValueOf<T> = T[keyof T]
 type StepTwoProps = {
@@ -89,7 +89,6 @@ const StepTwo = ({
 }: StepTwoProps) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
 
@@ -114,7 +113,7 @@ const StepTwo = ({
   const [docForm, setDocForm] = useState<DocForm | string>(
     (datasetId && documentDetail) ? documentDetail.doc_form : DocForm.TEXT,
   )
-  const [docLanguage, setDocLanguage] = useState<string>(language !== LanguagesSupportedUnderscore[1] ? 'English' : 'Chinese')
+  const [docLanguage, setDocLanguage] = useState<string>(locale !== LanguagesSupported[1] ? 'English' : 'Chinese')
   const [QATipHide, setQATipHide] = useState(false)
   const [previewSwitched, setPreviewSwitched] = useState(false)
   const [showPreview, { setTrue: setShowPreview, setFalse: hidePreview }] = useBoolean()

+ 2 - 3
web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx

@@ -9,7 +9,7 @@ import { useContext } from 'use-context-selector'
 import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
 import { DocForm } from '@/models/datasets'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 const CSV_TEMPLATE_QA_EN = [
   ['question', 'answer'],
@@ -35,11 +35,10 @@ const CSV_TEMPLATE_CN = [
 const CSVDownload: FC<{ docForm: DocForm }> = ({ docForm }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const { CSVDownloader, Type } = useCSVDownloader()
 
   const getTemplate = () => {
-    if (language === LanguagesSupportedUnderscore[1]) {
+    if (locale === LanguagesSupported[1]) {
       if (docForm === DocForm.QA)
         return CSV_TEMPLATE_QA_CN
       return CSV_TEMPLATE_CN

File diff suppressed because it is too large
+ 1 - 1
web/app/components/datasets/documents/detail/embedding/index.tsx


File diff suppressed because it is too large
+ 1 - 1
web/app/components/datasets/documents/index.tsx


+ 4 - 4
web/app/components/develop/doc.tsx

@@ -5,7 +5,7 @@ import TemplateZh from './template/template.zh.mdx'
 import TemplateChatEn from './template/template_chat.en.mdx'
 import TemplateChatZh from './template/template_chat.zh.mdx'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 
 type IDocProps = {
   appDetail: any
@@ -13,7 +13,7 @@ type IDocProps = {
 
 const Doc = ({ appDetail }: IDocProps) => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+
   const variables = appDetail?.model_config?.configs?.prompt_variables || []
   const inputs = variables.reduce((res: any, variable: any) => {
     res[variable.key] = variable.name || ''
@@ -24,10 +24,10 @@ const Doc = ({ appDetail }: IDocProps) => {
     <article className="prose prose-xl" >
       {appDetail?.mode === 'completion'
         ? (
-          language !== LanguagesSupportedUnderscore[1] ? <TemplateEn appDetail={appDetail} variables={variables} inputs={inputs} /> : <TemplateZh appDetail={appDetail} variables={variables} inputs={inputs} />
+          locale !== LanguagesSupported[1] ? <TemplateEn appDetail={appDetail} variables={variables} inputs={inputs} /> : <TemplateZh appDetail={appDetail} variables={variables} inputs={inputs} />
         )
         : (
-          language !== LanguagesSupportedUnderscore[1] ? <TemplateChatEn appDetail={appDetail} variables={variables} inputs={inputs} /> : <TemplateChatZh appDetail={appDetail} variables={variables} inputs={inputs} />
+          locale !== LanguagesSupported[1] ? <TemplateChatEn appDetail={appDetail} variables={variables} inputs={inputs} /> : <TemplateChatZh appDetail={appDetail} variables={variables} inputs={inputs} />
         )}
     </article>
   )

+ 2 - 3
web/app/components/develop/secret-key/secret-key-modal.tsx

@@ -27,7 +27,7 @@ import Tooltip from '@/app/components/base/tooltip'
 import Loading from '@/app/components/base/loading'
 import Confirm from '@/app/components/base/confirm'
 import I18n from '@/context/i18n'
-import { LanguagesSupported, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import { useAppContext } from '@/context/app-context'
 
 type ISecretKeyModalProps = {
@@ -56,7 +56,6 @@ const SecretKeyModal = ({
   const [delKeyID, setDelKeyId] = useState('')
 
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
 
   // const [isCopied, setIsCopied] = useState(false)
   const [copyValue, setCopyValue] = useState('')
@@ -102,7 +101,7 @@ const SecretKeyModal = ({
   }
 
   const formatDate = (timestamp: string) => {
-    if (language === LanguagesSupported[0])
+    if (locale === LanguagesSupported[0])
       return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format((+timestamp) * 1000)
     else
       return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)

+ 1 - 1
web/app/components/explore/category.tsx

@@ -3,7 +3,7 @@ import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
-import exploreI18n from '@/i18n/lang/explore.en'
+import exploreI18n from '@/i18n/en-US/explore'
 import type { AppCategory } from '@/models/explore'
 
 const categoryI18n = exploreI18n.category

+ 3 - 4
web/app/components/header/account-about/index.tsx

@@ -9,7 +9,7 @@ import { XClose } from '@/app/components/base/icons/src/vender/line/general'
 import type { LangGeniusVersionResponse } from '@/models/common'
 import { IS_CE_EDITION } from '@/config'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import LogoSite from '@/app/components/base/logo/logo-site'
 
 type IAccountSettingProps = {
@@ -26,7 +26,6 @@ export default function AccountAbout({
 }: IAccountSettingProps) {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version
 
   return (
@@ -49,8 +48,8 @@ export default function AccountAbout({
                 IS_CE_EDITION
                   ? <Link href={'https://github.com/langgenius/dify/blob/main/LICENSE'} target='_blank' rel='noopener noreferrer'>Open Source License</Link>
                   : <>
-                    <Link href={language !== LanguagesSupportedUnderscore[1] ? 'https://docs.dify.ai/user-agreement/privacy-policy' : 'https://docs.dify.ai/v/zh-hans/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer'>Privacy Policy</Link>,
-                    <Link href={language !== LanguagesSupportedUnderscore[1] ? 'https://docs.dify.ai/user-agreement/terms-of-service' : 'https://docs.dify.ai/v/zh-hans/user-agreement/terms-of-service'} target='_blank' rel='noopener noreferrer'>Terms of Service</Link>
+                    <Link href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/user-agreement/privacy-policy' : 'https://docs.dify.ai/v/zh-hans/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer'>Privacy Policy</Link>,
+                    <Link href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/user-agreement/terms-of-service' : 'https://docs.dify.ai/v/zh-hans/user-agreement/terms-of-service'} target='_blank' rel='noopener noreferrer'>Terms of Service</Link>
                   </>
               }
             </div>

+ 2 - 3
web/app/components/header/account-dropdown/index.tsx

@@ -16,7 +16,7 @@ import { useAppContext } from '@/context/app-context'
 import { ArrowUpRight, ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
 import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
 import { useModalContext } from '@/context/modal-context'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 export type IAppSelecotr = {
   isMobile: boolean
 }
@@ -30,7 +30,6 @@ export default function AppSelector({ isMobile }: IAppSelecotr) {
   const [aboutVisible, setAboutVisible] = useState(false)
 
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
   const { t } = useTranslation()
   const { userProfile, langeniusVersionInfo } = useAppContext()
   const { setShowAccountSettingModal } = useModalContext()
@@ -123,7 +122,7 @@ export default function AppSelector({ isMobile }: IAppSelecotr) {
                       <Link
                         className={classNames(itemClassName, 'group justify-between')}
                         href={
-                          language !== LanguagesSupportedUnderscore[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
+                          locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
                         }
                         target='_blank' rel='noopener noreferrer'>
                         <div>{t('common.userProfile.helpCenter')}</div>

+ 2 - 2
web/app/components/header/account-setting/language-page/index.tsx

@@ -10,7 +10,7 @@ import { updateUserProfile } from '@/service/common'
 import { ToastContext } from '@/app/components/base/toast'
 import I18n from '@/context/i18n'
 import { timezones } from '@/utils/timezone'
-import { languages } from '@/utils/language'
+import { languages } from '@/i18n/language'
 
 const titleClassName = `
   mb-2 text-sm font-medium text-gray-900
@@ -53,7 +53,7 @@ export default function LanguagePage() {
         <div className={titleClassName}>{t('common.language.displayLanguage')}</div>
         <SimpleSelect
           defaultValue={locale || userProfile.interface_language}
-          items={languages}
+          items={languages.filter(item => item.supported)}
           onSelect={item => handleSelect('language', item)}
           disabled={editing}
         />

+ 4 - 4
web/app/components/header/account-setting/members-page/index.tsx

@@ -20,7 +20,7 @@ import { useProviderContext } from '@/context/provider-context'
 import { Plan } from '@/app/components/billing/type'
 import UpgradeBtn from '@/app/components/billing/upgrade-btn'
 import { NUM_INFINITE } from '@/app/components/billing/config'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 dayjs.extend(relativeTime)
 
 const MembersPage = () => {
@@ -31,7 +31,7 @@ const MembersPage = () => {
     normal: t('common.members.normal'),
   }
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+
   const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
   const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
   const [inviteModalVisible, setInviteModalVisible] = useState(false)
@@ -55,7 +55,7 @@ const MembersPage = () => {
                 {isNotUnlimitedMemberPlan
                   ? (
                     <div className='flex space-x-1'>
-                      <div>{t('billing.plansCommon.member')}{language !== LanguagesSupportedUnderscore[1] && accounts.length > 1 && 's'}</div>
+                      <div>{t('billing.plansCommon.member')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div>
                       <div className='text-gray-700'>{accounts.length}</div>
                       <div>/</div>
                       <div>{plan.total.teamMembers === NUM_INFINITE ? t('billing.plansCommon.unlimited') : plan.total.teamMembers}</div>
@@ -64,7 +64,7 @@ const MembersPage = () => {
                   : (
                     <div className='flex space-x-1'>
                       <div>{accounts.length}</div>
-                      <div>{t('billing.plansCommon.memberAfter')}{language !== LanguagesSupportedUnderscore[1] && accounts.length > 1 && 's'}</div>
+                      <div>{t('billing.plansCommon.memberAfter')}{locale !== LanguagesSupported[1] && accounts.length > 1 && 's'}</div>
                     </div>
                   )}
               </div>

+ 1 - 3
web/app/components/header/account-setting/members-page/invite-modal/index.tsx

@@ -15,7 +15,6 @@ import { emailRegex } from '@/config'
 import { ToastContext } from '@/app/components/base/toast'
 import type { InvitationResult } from '@/models/common'
 import I18n from '@/context/i18n'
-import { getModelRuntimeSupported } from '@/utils/language'
 
 import 'react-multi-email/dist/style.css'
 type IInviteModalProps = {
@@ -32,7 +31,6 @@ const InviteModal = ({
   const { notify } = useContext(ToastContext)
 
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
 
   const InvitingRoles = useMemo(() => [
     {
@@ -51,7 +49,7 @@ const InviteModal = ({
       try {
         const { result, invitation_results } = await inviteMember({
           url: '/workspaces/current/members/invite-email',
-          body: { emails, role: role.name, language },
+          body: { emails, role: role.name, language: locale },
         })
 
         if (result === 'success') {

+ 8 - 8
web/app/components/header/account-setting/model-provider-page/declarations.ts

@@ -1,8 +1,8 @@
 export type FormValue = Record<string, any>
 
 export type TypeWithI18N<T = string> = {
-  'en_US': T
-  'zh_Hans': T
+  'en-US': T
+  'zh-Hans': T
   [key: string]: T
 }
 
@@ -67,16 +67,16 @@ export enum ModelStatusEnum {
 
 export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
   'no-configure': {
-    en_US: 'No Configure',
-    zh_Hans: '未配置凭据',
+    'en-US': 'No Configure',
+    'zh-Hans': '未配置凭据',
   },
   'quota-exceeded': {
-    en_US: 'Quota Exceeded',
-    zh_Hans: '额度不足',
+    'en-US': 'Quota Exceeded',
+    'zh-Hans': '额度不足',
   },
   'no-permission': {
-    en_US: 'No Permission',
-    zh_Hans: '无使用权限',
+    'en-US': 'No Permission',
+    'zh-Hans': '无使用权限',
   },
 }
 

+ 1 - 2
web/app/components/header/account-setting/model-provider-page/hooks.ts

@@ -16,7 +16,6 @@ import {
   ConfigurateMethodEnum,
   ModelTypeEnum,
 } from './declarations'
-import { getModelRuntimeSupported } from '@/utils/language'
 import I18n from '@/context/i18n'
 import {
   fetchDefaultModal,
@@ -59,7 +58,7 @@ export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
 
 export const useLanguage = () => {
   const { locale } = useContext(I18n)
-  return getModelRuntimeSupported(locale)
+  return locale.replace('-', '_')
 }
 
 export const useProviderCrenditialsFormSchemasValue = (

+ 4 - 5
web/app/components/header/maintenance-notice.tsx

@@ -2,11 +2,10 @@ import { useState } from 'react'
 import { useContext } from 'use-context-selector'
 import I18n from '@/context/i18n'
 import { X } from '@/app/components/base/icons/src/vender/line/general'
-import { NOTICE_I18N, getModelRuntimeSupported } from '@/utils/language'
+import { NOTICE_I18N } from '@/i18n/language'
 
 const MaintenanceNotice = () => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
 
   const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1')
   const handleJumpNotice = () => {
@@ -26,11 +25,11 @@ const MaintenanceNotice = () => {
 
   return (
     <div className='shrink-0 flex items-center px-4 h-[38px] bg-[#FFFAEB] border-b border-[0.5px] border-b-[#FEF0C7] z-20'>
-      <div className='shrink-0 flex items-center mr-2 px-2 h-[22px] bg-[#F79009] text-white text-[11px] font-medium rounded-xl'>{titleByLocale[language]}</div>
+      <div className='shrink-0 flex items-center mr-2 px-2 h-[22px] bg-[#F79009] text-white text-[11px] font-medium rounded-xl'>{titleByLocale[locale]}</div>
       {
         (NOTICE_I18N.href && NOTICE_I18N.href !== '#')
-          ? <div className='grow text-xs font-medium text-gray-700 cursor-pointer' onClick={handleJumpNotice}>{descByLocale[language]}</div>
-          : <div className='grow text-xs font-medium text-gray-700'>{descByLocale[language]}</div>
+          ? <div className='grow text-xs font-medium text-gray-700 cursor-pointer' onClick={handleJumpNotice}>{descByLocale[locale]}</div>
+          : <div className='grow text-xs font-medium text-gray-700'>{descByLocale[locale]}</div>
       }
       <X className='shrink-0 w-4 h-4 text-gray-500 cursor-pointer' onClick={handleCloseNotice} />
     </div>

+ 1 - 1
web/app/components/i18n.tsx

@@ -5,7 +5,7 @@ import React, { useEffect } from 'react'
 import { changeLanguage } from '@/i18n/i18next-config'
 import I18NContext from '@/context/i18n'
 import type { Locale } from '@/i18n'
-import { setLocaleOnClient } from '@/i18n/client'
+import { setLocaleOnClient } from '@/i18n'
 
 export type II18nProps = {
   locale: Locale

+ 0 - 23
web/app/components/locale-switcher.tsx

@@ -1,23 +0,0 @@
-'use client'
-
-import { i18n } from '@/i18n'
-import { setLocaleOnClient } from '@/i18n/client'
-
-const LocaleSwitcher = () => {
-  return (
-    <div className="mt-4">
-      <p>Locale switcher:</p>
-      <ul>
-        {i18n.locales.map((locale) => {
-          return (
-            <li key={locale}>
-              <div className='cursor-pointer ' onClick={() => setLocaleOnClient(locale)}>{locale}</div>
-            </li>
-          )
-        })}
-      </ul>
-    </div>
-  )
-}
-
-export default LocaleSwitcher

+ 2 - 2
web/app/components/tools/edit-custom-collection-modal/test-api.tsx

@@ -10,7 +10,7 @@ import Button from '@/app/components/base/button'
 import Drawer from '@/app/components/base/drawer-plus'
 import I18n from '@/context/i18n'
 import { testAPIAvailable } from '@/service/tools'
-import { getModelRuntimeSupported } from '@/utils/language'
+import { getLanguage } from '@/i18n/language'
 
 type Props = {
   customCollection: CustomCollectionBackend
@@ -27,7 +27,7 @@ const TestApi: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+  const language = getLanguage(locale)
   const [credentialsModalShow, setCredentialsModalShow] = useState(false)
   const [tempCredential, setTempCredential] = React.useState<Credential>(customCollection.credentials)
   const [result, setResult] = useState<string>('')

+ 2 - 3
web/app/components/tools/tool-list/header.tsx

@@ -8,8 +8,7 @@ import type { Collection } from '../types'
 import { CollectionType, LOC } from '../types'
 import { Settings01 } from '../../base/icons/src/vender/line/general'
 import I18n from '@/context/i18n'
-import { getModelRuntimeSupported } from '@/utils/language'
-
+import { getLanguage } from '@/i18n/language'
 type Props = {
   icon: JSX.Element
   collection: Collection
@@ -26,7 +25,7 @@ const Header: FC<Props> = ({
   onShowEditCustomCollection,
 }) => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+  const language = getLanguage(locale)
   const { t } = useTranslation()
   const isInToolsPage = loc === LOC.tools
   const isInDebugPage = !isInToolsPage

+ 3 - 3
web/app/components/tools/tool-list/item.tsx

@@ -10,8 +10,7 @@ import { CollectionType } from '../types'
 import TooltipPlus from '../../base/tooltip-plus'
 import I18n from '@/context/i18n'
 import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
-import { getModelRuntimeSupported } from '@/utils/language'
-
+import { getLanguage } from '@/i18n/language'
 type Props = {
   collection: Collection
   icon: JSX.Element
@@ -33,7 +32,8 @@ const Item: FC<Props> = ({
 }) => {
   const { t } = useTranslation()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+  const language = getLanguage(locale)
+
   const isBuiltIn = collection.type === CollectionType.builtIn
   const canShowDetail = !isBuiltIn || (isBuiltIn && isInToolsPage)
   const [showDetail, setShowDetail] = useState(false)

+ 2 - 2
web/app/components/tools/tool-nav-list/item.tsx

@@ -6,7 +6,7 @@ import cn from 'classnames'
 import AppIcon from '../../base/app-icon'
 import type { Collection } from '@/app/components/tools/types'
 import I18n from '@/context/i18n'
-import { getModelRuntimeSupported } from '@/utils/language'
+import { getLanguage } from '@/i18n/language'
 
 type Props = {
   isCurrent: boolean
@@ -20,7 +20,7 @@ const Item: FC<Props> = ({
   onClick,
 }) => {
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
+  const language = getLanguage(locale)
   return (
     <div
       className={cn(isCurrent && 'bg-white shadow-xs rounded-lg', 'mt-1 flex h-9 items-center px-2 space-x-2 cursor-pointer')}

+ 0 - 2
web/app/install/installForm.tsx

@@ -17,8 +17,6 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
 
 const InstallForm = () => {
   const { t } = useTranslation()
-  // const { locale } = useContext(I18n)
-  // const language = getModelRuntimeSupported(locale)
   const router = useRouter()
 
   const [email, setEmail] = React.useState('')

+ 2 - 2
web/app/signin/_header.tsx

@@ -2,7 +2,7 @@
 import React from 'react'
 import { useContext } from 'use-context-selector'
 import Select from '@/app/components/base/select/locale'
-import { languages } from '@/utils/language'
+import { languages } from '@/i18n/language'
 import { type Locale } from '@/i18n'
 import I18n from '@/context/i18n'
 import LogoSite from '@/app/components/base/logo/logo-site'
@@ -17,7 +17,7 @@ const Header = () => {
     <LogoSite />
     <Select
       value={locale}
-      items={languages}
+      items={languages.filter(item => item.supported)}
       onChange={(value) => {
         setLocaleOnClient(value as Locale)
       }}

+ 3 - 4
web/app/signin/normalForm.tsx

@@ -12,7 +12,7 @@ import { IS_CE_EDITION, apiPrefix } from '@/config'
 import Button from '@/app/components/base/button'
 import { login, oauth } from '@/service/common'
 import I18n from '@/context/i18n'
-import { LanguagesSupportedUnderscore, getModelRuntimeSupported } from '@/utils/language'
+import { LanguagesSupported } from '@/i18n/language'
 import { getPurifyHref } from '@/utils'
 const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/
 
@@ -67,7 +67,6 @@ const NormalForm = () => {
   const { t } = useTranslation()
   const router = useRouter()
   const { locale } = useContext(I18n)
-  const language = getModelRuntimeSupported(locale)
 
   const [state, dispatch] = useReducer(reducer, {
     formValid: false,
@@ -283,13 +282,13 @@ const NormalForm = () => {
             <Link
               className='text-primary-600'
               target='_blank' rel='noopener noreferrer'
-              href={language !== LanguagesSupportedUnderscore[1] ? 'https://docs.dify.ai/user-agreement/terms-of-service' : 'https://docs.dify.ai/v/zh-hans/user-agreement/terms-of-service'}
+              href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/user-agreement/terms-of-service' : 'https://docs.dify.ai/v/zh-hans/user-agreement/terms-of-service'}
             >{t('login.tos')}</Link>
             &nbsp;&&nbsp;
             <Link
               className='text-primary-600'
               target='_blank' rel='noopener noreferrer'
-              href={language !== LanguagesSupportedUnderscore[1] ? 'https://docs.dify.ai/user-agreement/privacy-policy' : 'https://docs.dify.ai/v/zh-hans/user-agreement/privacy-policy'}
+              href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/user-agreement/privacy-policy' : 'https://docs.dify.ai/v/zh-hans/user-agreement/privacy-policy'}
             >{t('login.pp')}</Link>
           </div>
 

+ 2 - 2
web/app/signin/oneMoreStep.tsx

@@ -10,7 +10,7 @@ import Tooltip from '@/app/components/base/tooltip/index'
 
 import { SimpleSelect } from '@/app/components/base/select'
 import { timezones } from '@/utils/timezone'
-import { LanguagesSupported, languages } from '@/utils/language'
+import { LanguagesSupported, languages } from '@/i18n/language'
 import { oneMoreStep } from '@/service/common'
 import Toast from '@/app/components/base/toast'
 // import I18n from '@/context/i18n'
@@ -122,7 +122,7 @@ const OneMoreStep = () => {
             <div className="relative mt-1 rounded-md shadow-sm">
               <SimpleSelect
                 defaultValue={LanguagesSupported[0]}
-                items={languages}
+                items={languages.filter(item => item.supported)}
                 onSelect={(item) => {
                   dispatch({ type: 'interface_language', value: item.value })
                 }}

+ 1 - 3
web/context/i18n.ts

@@ -5,14 +5,12 @@ type II18NContext = {
   locale: Locale
   i18n: Record<string, any>
   setLocaleOnClient: (locale: Locale, reloadPage?: boolean) => void
-  //   setI8N: (i18n: Record<string, string>) => void,
 }
 
 const I18NContext = createContext<II18NContext>({
-  locale: 'en',
+  locale: 'en-US',
   i18n: {},
   setLocaleOnClient: (lang: Locale, reloadPage?: boolean) => { },
-  //   setI8N: () => {},
 })
 
 export default I18NContext

+ 175 - 0
web/i18n/README.md

@@ -0,0 +1,175 @@
+# Internationalization (i18n)
+
+## Introduction
+
+This directory contains the internationalization (i18n) files for this project.
+
+## File Structure
+
+```
+├── [  24]  README.md
+├── [   0]  README_CN.md
+├── [ 704]  en-US
+│   ├── [2.4K]  app-annotation.ts
+│   ├── [5.2K]  app-api.ts
+│   ├── [ 16K]  app-debug.ts
+│   ├── [2.1K]  app-log.ts
+│   ├── [5.3K]  app-overview.ts
+│   ├── [1.9K]  app.ts
+│   ├── [4.1K]  billing.ts
+│   ├── [ 17K]  common.ts
+│   ├── [ 859]  custom.ts
+│   ├── [5.7K]  dataset-creation.ts
+│   ├── [ 10K]  dataset-documents.ts
+│   ├── [ 761]  dataset-hit-testing.ts
+│   ├── [1.7K]  dataset-settings.ts
+│   ├── [2.0K]  dataset.ts
+│   ├── [ 941]  explore.ts
+│   ├── [  52]  layout.ts
+│   ├── [2.3K]  login.ts
+│   ├── [  52]  register.ts
+│   ├── [2.5K]  share-app.ts
+│   └── [2.8K]  tools.ts
+├── [1.6K]  i18next-config.ts
+├── [ 634]  index.ts
+├── [4.4K]  language.ts
+```
+
+We use English as the default language. The i18n files are organized by language and then by module. For example, the English translation for the `app` module is in `en-US/app.ts`.
+
+If you want to add a new language or modify an existing translation, you can create a new file for the language or modify the existing file. The file name should be the language code (e.g., `zh-CN` for Chinese) and the file extension should be `.ts`.
+
+For example, if you want to add french translation, you can create a new folder `fr-FR` and add the translation files in it.
+
+By default we will use `LanguagesSupported` to determine which languages are supported. For example, in login page and settings page, we will use `LanguagesSupported` to determine which languages are supported and display them in the language selection dropdown.
+
+## Example
+
+1. Create a new folder for the new language.
+
+```
+cp -r en-US fr-FR
+```
+
+2. Modify the translation files in the new folder.
+
+3. Add type to new language in the `language.ts` file.
+
+```typescript
+export type I18nText = {
+  'en-US': string
+  'zh-Hans': string
+  'pt-BR': string
+  'es-ES': string
+  'fr-FR': string
+  'de-DE': string
+  'ja-JP': string
+  'ko-KR': string
+  'ru-RU': string
+  'it-IT': string
+  'uk-UA': string
+  'YOUR_LANGUAGE_CODE': string
+}
+```
+
+4. Add the new language to the `language.ts` file.
+
+```typescript
+
+export const languages = [
+  {
+    value: 'en-US',
+    name: 'English(United States)',
+    example: 'Hello, Dify!',
+    supported: true,
+  },
+  {
+    value: 'zh-Hans',
+    name: '简体中文',
+    example: '你好,Dify!',
+    supported: true,
+  },
+  {
+    value: 'pt-BR',
+    name: 'Português(Brasil)',
+    example: 'Olá, Dify!',
+    supported: true,
+  },
+  {
+    value: 'es-ES',
+    name: 'Español(España)',
+    example: 'Saluton, Dify!',
+    supported: false,
+  },
+  {
+    value: 'fr-FR',
+    name: 'Français(France)',
+    example: 'Bonjour, Dify!',
+    supported: false,
+  },
+  {
+    value: 'de-DE',
+    name: 'Deutsch(Deutschland)',
+    example: 'Hallo, Dify!',
+    supported: false,
+  },
+  {
+    value: 'ja-JP',
+    name: '日本語(日本)',
+    example: 'こんにちは、Dify!',
+    supported: false,
+  },
+  {
+    value: 'ko-KR',
+    name: '한국어(대한민국)',
+    example: '안녕, Dify!',
+    supported: false,
+  },
+  {
+    value: 'ru-RU',
+    name: 'Русский(Россия)',
+    example: ' Привет, Dify!',
+    supported: false,
+  },
+  {
+    value: 'it-IT',
+    name: 'Italiano(Italia)',
+    example: 'Ciao, Dify!',
+    supported: false,
+  },
+  {
+    value: 'th-TH',
+    name: 'ไทย(ประเทศไทย)',
+    example: 'สวัสดี Dify!',
+    supported: false,
+  },
+  {
+    value: 'id-ID',
+    name: 'Bahasa Indonesia',
+    example: 'Saluto, Dify!',
+    supported: false,
+  },
+  {
+    value: 'uk-UA',
+    name: 'Українська(Україна)',
+    example: 'Привет, Dify!',
+    supported: true,
+  },
+  // Add your language here 👇
+  ...
+  // Add your language here 👆
+]
+```
+
+5. Don't forget to mark the supported field as `true` if the language is supported.
+
+6. Sometime you might need to do some changes in the server side. Please change this file as well. 👇
+https://github.com/langgenius/dify/blob/61e4bbabaf2758354db4073cbea09fdd21a5bec1/api/constants/languages.py#L5
+
+
+
+## Clean Up
+
+That's it! You have successfully added a new language to the project. If you want to remove a language, you can simply delete the folder and remove the language from the `language.ts` file.
+
+We have a list of languages that we support in the `language.ts` file. But some of them are not supported yet. So, they are marked as `false`. If you want to support a language, you can follow the steps above and mark the supported field as `true`.

+ 0 - 82
web/i18n/README_CN.md

@@ -1,82 +0,0 @@
-# 前端 i18n 修改
-
-## 后端多语言支持
-
-`api/libs/helper.py:117` 中添加对应的语言支持。如:
-```python
-def supported_language(lang):
-    if lang in ['en-US', 'zh-Hans', 'de', 'de-AT']:
-        return lang
-```
-
-
-## 添加多语言文件
-
-在 `web/i18n/lang` 下添加不同模块的多语言文件。文件命令为 模块名.{LANG}.ts。详细参考[LANG](https://www.venea.net/web/culture_code) 
-
-## 引入新添加的多语言文件
-在 `web/i18n/i18next-config.ts` 中 resources 对象中中引入新添加的多语言文件。如:
-
-```javascript
-const resources = {
-    'en': {...},
-    'zh-Hans': {...},
-    // 引入新添加的语言
-    'new LANG': {
-      translation: {
-        common: commonNewLan,
-        layout: layoutNewLan,
-        ...
-      }
-    }
-}
-```
-
-## 翻译过程中的改动
-
-### 日期格式化的多语言处理
-
-目前日期做多语言格式化的文件涉及到如下 2 个: 
-
-```javascript
-1. web/app/components/header/account-setting/members-page/index.tsx
-// Line: 78 
-{dayjs(Number((account.last_login_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}
-2. web/app/components/develop/secret-key/secret-key-modal.tsx
-// Line:82
-const formatDate = (timestamp: any) => {
-    if (locale === 'en') {
-      return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format((+timestamp) * 1000)
-    } else {
-      return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)
-    }
-  }
-```
-看需求做对应的改动。
-
-### 翻译中带变量的内容的处理
-
-翻译中会存在带变量的情况,变量的值会在运行时被替换。翻译中的变量会用{{ 和 }} 包裹。
-翻译带变量的内容时:
-  1. 不能改变量的名称。即:变量的名称不需要做翻译。
-  2. 确保变量填充后,语句仍保持通顺。
-
-查找所有翻译中带变量的方式:在 ./web/i18n/lang 下搜索:{{。
-
-### 翻译内容太长破坏 UI
-
-如果某个翻译的内容比其他语言的长很多,检查下是否会破坏 UI。
-
-## 帮助文档
-
-目前的帮助文档的调整逻辑是:中文跳转中文,其他语言跳英文。如果帮助文档也做了多语言。需要做这块的改动。
-
-## 验证
-
-新增语言包建议通过本地部署最新代码来验证,可参考:https://docs.dify.ai/getting-started/install-self-hosted/local-source-code
-验证点:
-1. 首次初始化安装是否存在新语言下拉选项,以及是否可以用新语言进行初始化
-2. 个人设置中是否存在新语言下拉选项,以及是否可以选择并保存新语言
-3. 界面各处文案是否使用新语言来展示,以及文案是否破坏 UI
-4. 从模板创建应用内容是否均为新语言
-5. (CLOUD 版)通过 OAuth 授权登录后,是否直接设置当前浏览器语言为界面语言

+ 0 - 81
web/i18n/README_EN.md

@@ -1,81 +0,0 @@
-# Frontend i18n modification
-
-## Backend i18n modification
-
-`api/libs/helper.py:117` Add corresponding language support. Such as:
-```python
-def supported_language(lang):
-    if lang in ['en-US', 'zh-Hans', 'de', 'de-AT']:
-        return lang
-```
-
-## Adding multiple language files
-
-Add multilingual files for different modules under web/i18n/lang. The file name is Module name.{LANG}.ts. Please refer [LANG](https://www.venea.net/web/culture_code) for details.
-
-## Introducing a newly added multilingual file 
-
-Introduce the newly added multilingual file in the resources object in web/i18n/i18next-config.ts. For example:
-
-```javascript
-const resources = {
-    'en': {...},
-    'zh-Hans': {...},  
-    _// Introduce the newly added language_
-    'new LANG': {  
-      translation: {  
-        common: commonNewLan,  
-        layout: layoutNewLan,  
-        ...  
-      }  
-    }
-}
-```
-## Changes in the translation process
-
-### Multi-language processing of date formatting
-
-Currently, two files are involved in date formatting in multiple languages:
-
-```javascript
-1. web/app/components/header/account-setting/members-page/index.tsx
-_// Line 78_   
-{dayjs(Number((account.last_login_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}  
-2. web/app/components/develop/secret-key/secret-key-modal.tsx  
-_// Line 82_
-const formatDate = (timestamp: any) => {
-    if (locale === 'en') {  
-      return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format((+timestamp) * 1000)  
-    } else {  
-      return new Intl.DateTimeFormat('fr-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }).format((+timestamp) * 1000)  
-    }  
-  }
-```
-
-Make corresponding changes based on requirements.
-
-### Handling translation content with variables
-
-There will be variables in the translation, and the value of the variables will be replaced at runtime. Variables in translation will be wrapped in {{ and }}.
-When translating content with variables:
-  1. Do not change the variable name. That is: the variable name does not need to be translated. 
-  2. Ensure that the statement remains smooth after the variable is filled.
-Find all translations with variables: search for {{ under ./web/i18n/lang. 
-
-### Translation content is too long to destroy UI
-
-If a certain translation content is much longer than other languages, check if it will destroy the UI.
-
-## Help documentation
-
-The current logic for adjusting the help documentation is: Chinese jumps to Chinese, other languages jump to English. If the help documentation is also multilingual, changes need to be made in this area. 
-
-## Verification
-
-It is recommended to verify the newly added language pack through local deployment of the latest code. For reference: https://docs.dify.ai/getting-started/install-self-hosted/local-source-code
-Verification points:
-1. Whether the initial installation has new language drop-down options, and whether the new language can be used for initialization
-2. Whether there is a new language drop-down option in personal settings, and whether the new language can be selected and saved 
-3. Whether the text in the interface is displayed in the new language, and whether the text destroys the UI
-4. Whether the content created from the template is all in the new language
-5. (CLOUD version) After logging in through OAuth authorization, whether the current browser language is set directly as the interface language

+ 0 - 16
web/i18n/client.ts

@@ -1,16 +0,0 @@
-import Cookies from 'js-cookie'
-import type { Locale } from '.'
-import { i18n } from '.'
-import { LOCALE_COOKIE_NAME } from '@/config'
-import { changeLanguage } from '@/i18n/i18next-config'
-
-// same logic as server
-export const getLocaleOnClient = (): Locale => {
-  return Cookies.get(LOCALE_COOKIE_NAME) as Locale || i18n.defaultLocale
-}
-
-export const setLocaleOnClient = (locale: Locale, reloadPage = true) => {
-  Cookies.set(LOCALE_COOKIE_NAME, locale)
-  changeLanguage(locale)
-  reloadPage && location.reload()
-}

web/i18n/lang/app-annotation.en.ts → web/i18n/en-US/app-annotation.ts


web/i18n/lang/app-api.en.ts → web/i18n/en-US/app-api.ts


web/i18n/lang/app-debug.en.ts → web/i18n/en-US/app-debug.ts


web/i18n/lang/app-log.en.ts → web/i18n/en-US/app-log.ts


web/i18n/lang/app-overview.en.ts → web/i18n/en-US/app-overview.ts


web/i18n/lang/app.en.ts → web/i18n/en-US/app.ts


web/i18n/lang/billing.en.ts → web/i18n/en-US/billing.ts


web/i18n/lang/common.en.ts → web/i18n/en-US/common.ts


web/i18n/lang/custom.en.ts → web/i18n/en-US/custom.ts


web/i18n/lang/dataset-creation.en.ts → web/i18n/en-US/dataset-creation.ts


web/i18n/lang/dataset-documents.en.ts → web/i18n/en-US/dataset-documents.ts


web/i18n/lang/dataset-hit-testing.en.ts → web/i18n/en-US/dataset-hit-testing.ts


web/i18n/lang/dataset-settings.en.ts → web/i18n/en-US/dataset-settings.ts


web/i18n/lang/dataset.en.ts → web/i18n/en-US/dataset.ts


web/i18n/lang/explore.en.ts → web/i18n/en-US/explore.ts


web/i18n/lang/layout.en.ts → web/i18n/en-US/layout.ts


web/i18n/lang/login.en.ts → web/i18n/en-US/login.ts


web/i18n/lang/layout.pt.ts → web/i18n/en-US/register.ts


web/i18n/lang/share-app.en.ts → web/i18n/en-US/share-app.ts


web/i18n/lang/tools.en.ts → web/i18n/en-US/tools.ts


+ 31 - 189
web/i18n/i18next-config.ts

@@ -1,202 +1,44 @@
 'use client'
 import i18n from 'i18next'
 import { initReactI18next } from 'react-i18next'
-import commonEn from './lang/common.en'
-import commonZh from './lang/common.zh'
-import commonUk from './lang/common.uk' // Ukrainian import
-import commonPt from './lang/common.pt' // Portuguese import
-import loginEn from './lang/login.en'
-import loginZh from './lang/login.zh'
-import loginPt from './lang/login.pt' // Portuguese import
-import loginUk from './lang/login.uk' // Ukrainian import
-import registerEn from './lang/register.en'
-import registerZh from './lang/register.zh'
-import registerPt from './lang/register.pt' // Portuguese import
-import registerUk from './lang/register.uk' // Ukrainian import
-import layoutEn from './lang/layout.en'
-import layoutZh from './lang/layout.zh'
-import layoutPt from './lang/layout.pt' // Portuguese import
-import layoutUk from './lang/layout.uk' // Ukrainian import
-import appEn from './lang/app.en'
-import appZh from './lang/app.zh'
-import appPt from './lang/app.pt' // Portuguese import
-import appUk from './lang/app.uk' // Ukrainian import
-import appOverviewEn from './lang/app-overview.en'
-import appOverviewZh from './lang/app-overview.zh'
-import appOverviewPt from './lang/app-overview.pt' // Portuguese import
-import appOverviewUk from './lang/app-overview.uk' // Ukrainian import
-import appDebugEn from './lang/app-debug.en'
-import appDebugZh from './lang/app-debug.zh'
-import appDebugPt from './lang/app-debug.pt' // Portuguese import
-import appDebugUk from './lang/app-debug.uk' // Ukrainian import
-import appApiEn from './lang/app-api.en'
-import appApiZh from './lang/app-api.zh'
-import appApiPt from './lang/app-api.pt' // Portuguese import
-import appApiUk from './lang/app-api.uk' // Ukrainian import
-import appLogEn from './lang/app-log.en'
-import appLogZh from './lang/app-log.zh'
-import appLogPt from './lang/app-log.pt' // Portuguese import
-import appLogUk from './lang/app-log.uk' // Ukrainian import
-import appAnnotationEn from './lang/app-annotation.en'
-import appAnnotationZh from './lang/app-annotation.zh'
-import appAnnotationPt from './lang/app-annotation.pt' // Portuguese import
-import appAnnotationUk from './lang/app-annotation.uk' // Ukrainian import
-import shareEn from './lang/share-app.en'
-import shareZh from './lang/share-app.zh'
-import sharePt from './lang/share-app.pt' // Portuguese import
-import shareUk from './lang/share-app.uk' // Ukrainian import
-import datasetEn from './lang/dataset.en'
-import datasetZh from './lang/dataset.zh'
-import datasetPt from './lang/dataset.pt' // Portuguese import
-import datasetUk from './lang/dataset.uk' // Ukrainian import
-import datasetDocumentsEn from './lang/dataset-documents.en'
-import datasetDocumentsZh from './lang/dataset-documents.zh'
-import datasetDocumentsPt from './lang/dataset-documents.pt' // Portuguese import
-import datasetDocumentsUk from './lang/dataset-documents.uk' // Ukrainian import
-import datasetHitTestingEn from './lang/dataset-hit-testing.en'
-import datasetHitTestingZh from './lang/dataset-hit-testing.zh'
-import datasetHitTestingPt from './lang/dataset-hit-testing.pt' // Portuguese import
-import datasetHitTestingUk from './lang/dataset-hit-testing.uk' // Ukrainian import
-import datasetSettingsEn from './lang/dataset-settings.en'
-import datasetSettingsZh from './lang/dataset-settings.zh'
-import datasetSettingsPt from './lang/dataset-settings.pt' // Portuguese import
-import datasetSettingsUk from './lang/dataset-settings.uk' // Ukrainian import
-import datasetCreationEn from './lang/dataset-creation.en'
-import datasetCreationZh from './lang/dataset-creation.zh'
-import datasetCreationPt from './lang/dataset-creation.pt' // Portuguese import
-import datasetCreationUk from './lang/dataset-creation.uk' // Ukrainian import
-import exploreEn from './lang/explore.en'
-import exploreZh from './lang/explore.zh'
-import explorePt from './lang/explore.pt' // Portuguese import
-import exploreUk from './lang/explore.uk' // Ukrainian import
-import billingEn from './lang/billing.en'
-import billingZh from './lang/billing.zh'
-import billingPt from './lang/billing.pt' // Portuguese import
-import billingUk from './lang/billing.uk' // Ukrainian import
-import customEn from './lang/custom.en'
-import customZh from './lang/custom.zh'
-import customPt from './lang/custom.pt' // Portuguese import
-import customUk from './lang/custom.uk' // Ukrainian import
-import toolsEn from './lang/tools.en'
-import toolsZh from './lang/tools.zh'
-import toolsPt from './lang/tools.pt' // Portuguese import
-import toolsUk from './lang/tools.uk' // Ukrainian import
 
-const resources = {
-  'en-US': {
-    translation: {
-      common: commonEn,
-      layout: layoutEn, // page layout
-      login: loginEn,
-      register: registerEn,
-      // app
-      app: appEn,
-      appOverview: appOverviewEn,
-      appDebug: appDebugEn,
-      appApi: appApiEn,
-      appLog: appLogEn,
-      appAnnotation: appAnnotationEn,
-      // share
-      share: shareEn,
-      dataset: datasetEn,
-      datasetDocuments: datasetDocumentsEn,
-      datasetHitTesting: datasetHitTestingEn,
-      datasetSettings: datasetSettingsEn,
-      datasetCreation: datasetCreationEn,
-      explore: exploreEn,
-      // billing
-      billing: billingEn,
-      custom: customEn,
-      // tools
-      tools: toolsEn,
-    },
-  },
-  'zh-Hans': {
-    translation: {
-      common: commonZh,
-      layout: layoutZh,
-      login: loginZh,
-      register: registerZh,
-      // app
-      app: appZh,
-      appOverview: appOverviewZh,
-      appDebug: appDebugZh,
-      appApi: appApiZh,
-      appLog: appLogZh,
-      appAnnotation: appAnnotationZh,
-      // share
-      share: shareZh,
-      dataset: datasetZh,
-      datasetDocuments: datasetDocumentsZh,
-      datasetHitTesting: datasetHitTestingZh,
-      datasetSettings: datasetSettingsZh,
-      datasetCreation: datasetCreationZh,
-      explore: exploreZh,
-      billing: billingZh,
-      custom: customZh,
-      // tools
-      tools: toolsZh,
-    },
-  },
-  'pt-BR': {
-    translation: {
-      common: commonPt,
-      layout: layoutPt,
-      login: loginPt,
-      register: registerPt,
-      // app
-      app: appPt,
-      appOverview: appOverviewPt,
-      appDebug: appDebugPt,
-      appApi: appApiPt,
-      appLog: appLogPt,
-      appAnnotation: appAnnotationPt,
-      // share
-      share: sharePt,
-      dataset: datasetPt,
-      datasetDocuments: datasetDocumentsPt,
-      datasetHitTesting: datasetHitTestingPt,
-      datasetSettings: datasetSettingsPt,
-      datasetCreation: datasetCreationPt,
-      explore: explorePt,
-      billing: billingPt,
-      custom: customPt,
-      tools: toolsPt,
-    },
-  },
-  'uk-UA': {
-    translation: {
-      common: commonUk,
-      layout: layoutUk,
-      login: loginUk,
-      register: registerUk,
-      app: appUk,
-      appOverview: appOverviewUk,
-      appDebug: appDebugUk,
-      appApi: appApiUk,
-      appLog: appLogUk,
-      appAnnotation: appAnnotationUk,
-      share: shareUk,
-      dataset: datasetUk,
-      datasetDocuments: datasetDocumentsUk,
-      datasetHitTesting: datasetHitTestingUk,
-      datasetSettings: datasetSettingsUk,
-      datasetCreation: datasetCreationUk,
-      explore: exploreUk,
-      billing: billingUk,
-      custom: customUk,
-      tools: toolsUk,
-    },
+import { LanguagesSupported } from '@/i18n/language'
+
+const loadLangResources = (lang: string) => ({
+  translation: {
+    common: require(`./${lang}/common`).default,
+    layout: require(`./${lang}/layout`).default,
+    login: require(`./${lang}/login`).default,
+    register: require(`./${lang}/register`).default,
+    app: require(`./${lang}/app`).default,
+    appOverview: require(`./${lang}/app-overview`).default,
+    appDebug: require(`./${lang}/app-debug`).default,
+    appApi: require(`./${lang}/app-api`).default,
+    appLog: require(`./${lang}/app-log`).default,
+    appAnnotation: require(`./${lang}/app-annotation`).default,
+    share: require(`./${lang}/share-app`).default,
+    dataset: require(`./${lang}/dataset`).default,
+    datasetDocuments: require(`./${lang}/dataset-documents`).default,
+    datasetHitTesting: require(`./${lang}/dataset-hit-testing`).default,
+    datasetSettings: require(`./${lang}/dataset-settings`).default,
+    datasetCreation: require(`./${lang}/dataset-creation`).default,
+    explore: require(`./${lang}/explore`).default,
+    billing: require(`./${lang}/billing`).default,
+    custom: require(`./${lang}/custom`).default,
+    tools: require(`./${lang}/tools`).default,
   },
-}
+})
+
+// Automatically generate the resources object
+const resources = LanguagesSupported.reduce((acc: any, lang: string) => {
+  acc[lang] = loadLangResources(lang)
+  return acc
+}, {})
 
 i18n.use(initReactI18next)
-  // init i18next
-  // for all options read: https://www.i18next.com/overview/configuration-options
   .init({
     lng: undefined,
     fallbackLng: 'en-US',
-    // debug: true,
     resources,
   })
 

+ 0 - 26
web/i18n/i18next-serverside-config.ts

@@ -1,26 +0,0 @@
-import { createInstance } from 'i18next'
-import resourcesToBackend from 'i18next-resources-to-backend'
-import { initReactI18next } from 'react-i18next/initReactI18next'
-import type { Locale } from '.'
-
-// https://locize.com/blog/next-13-app-dir-i18n/
-const initI18next = async (lng: Locale, ns: string) => {
-  const i18nInstance = createInstance()
-  await i18nInstance
-    .use(initReactI18next)
-    .use(resourcesToBackend((language: string, namespace: string) => import(`./lang/${namespace}.${language}.ts`)))
-    .init({
-      lng: lng === 'zh-Hans' ? 'zh' : lng,
-      ns,
-      fallbackLng: 'en',
-    })
-  return i18nInstance
-}
-
-export async function useTranslation(lng: Locale, ns = '', options: Record<string, any> = {}) {
-  const i18nextInstance = await initI18next(lng, ns)
-  return {
-    t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
-    i18n: i18nextInstance,
-  }
-}

+ 16 - 2
web/i18n/index.ts

@@ -1,8 +1,22 @@
-import { LanguagesSupported } from '@/utils/language'
+import Cookies from 'js-cookie'
+
+import { changeLanguage } from '@/i18n/i18next-config'
+import { LOCALE_COOKIE_NAME } from '@/config'
+import { LanguagesSupported } from '@/i18n/language'
 
 export const i18n = {
-  defaultLocale: 'en',
+  defaultLocale: 'en-US',
   locales: LanguagesSupported,
 } as const
 
 export type Locale = typeof i18n['locales'][number]
+
+export const getLocaleOnClient = (): Locale => {
+  return Cookies.get(LOCALE_COOKIE_NAME) as Locale || i18n.defaultLocale
+}
+
+export const setLocaleOnClient = (locale: Locale, reloadPage = true) => {
+  Cookies.set(LOCALE_COOKIE_NAME, locale)
+  changeLanguage(locale)
+  reloadPage && location.reload()
+}

+ 29 - 30
web/utils/language.ts

@@ -4,110 +4,109 @@ export type Item = {
   example: string
 }
 
-export const LanguagesSupported = ['en-US', 'zh-Hans', 'pt-BR', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'ko-KR', 'ru-RU', 'it-IT', 'th-TH', 'id-ID', 'uk-UA']
-export const LanguagesSupportedUnderscore = ['en_US', 'zh_Hans', 'pt_BR', 'es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'ko_KR', 'ru_RU', 'it_IT', 'th_TH', 'id_ID', 'uk_UA']
+export type I18nText = {
+  'en-US': string
+  'zh-Hans': string
+  'pt-BR': string
+  'es-ES': string
+  'fr-FR': string
+  'de-DE': string
+  'ja-JP': string
+  'ko-KR': string
+  'ru-RU': string
+  'it-IT': string
+  'uk-UA': string
+}
 
 export const languages = [
   {
     value: 'en-US',
     name: 'English(United States)',
     example: 'Hello, Dify!',
+    supported: true,
   },
   {
     value: 'zh-Hans',
     name: '简体中文',
     example: '你好,Dify!',
+    supported: true,
   },
   {
     value: 'pt-BR',
     name: 'Português(Brasil)',
     example: 'Olá, Dify!',
+    supported: true,
   },
   {
     value: 'es-ES',
     name: 'Español(España)',
     example: 'Saluton, Dify!',
+    supported: false,
   },
   {
     value: 'fr-FR',
     name: 'Français(France)',
     example: 'Bonjour, Dify!',
+    supported: false,
   },
   {
     value: 'de-DE',
     name: 'Deutsch(Deutschland)',
     example: 'Hallo, Dify!',
+    supported: false,
   },
   {
     value: 'ja-JP',
     name: '日本語(日本)',
     example: 'こんにちは、Dify!',
+    supported: false,
   },
   {
     value: 'ko-KR',
     name: '한국어(대한민국)',
     example: '안녕, Dify!',
+    supported: false,
   },
   {
     value: 'ru-RU',
     name: 'Русский(Россия)',
     example: ' Привет, Dify!',
+    supported: false,
   },
   {
     value: 'it-IT',
     name: 'Italiano(Italia)',
     example: 'Ciao, Dify!',
+    supported: false,
   },
   {
     value: 'th-TH',
     name: 'ไทย(ประเทศไทย)',
     example: 'สวัสดี Dify!',
+    supported: false,
   },
   {
     value: 'id-ID',
     name: 'Bahasa Indonesia',
     example: 'Saluto, Dify!',
+    supported: false,
   },
   {
     value: 'uk-UA',
     name: 'Українська(Україна)',
     example: 'Привет, Dify!',
+    supported: true,
   },
 ]
 
-export const getModelRuntimeSupported = (locale: string) => {
+export const LanguagesSupported = languages.filter(item => item.supported).map(item => item.value)
+
+export const getLanguage = (locale: string) => {
   if (locale === 'zh-Hans')
     return locale.replace('-', '_')
 
   return LanguagesSupported[0].replace('-', '_')
 }
-export const languageMaps = {
-  'en-US': 'en-US',
-  'zh-Hans': 'zh-Hans',
-  'pt-BR': 'pt-BR',
-  'es-ES': 'es-ES',
-  'fr-FR': 'fr-FR',
-  'de-DE': 'de-DE',
-  'ja-JP': 'ja-JP',
-  'ko-KR': 'ko-KR',
-  'ru-RU': 'ru-RU',
-  'it-IT': 'it-IT',
-  'uk-UA': 'uk-UA',
-}
-
-export type I18nText = {
-  'en-US': string
-  'zh-Hans': string
-  'pt-BR': string
-  'es-ES': string
-  'fr-FR': string
-  'de-DE': string
-  'ja-JP': string
-  'ko-KR': string
-  'ru-RU': string
-  'it-IT': string
-  'uk-UA': string
-}
 
 export const NOTICE_I18N = {
   title: {

web/i18n/lang/app-annotation.pt.ts → web/i18n/pt-BR/app-annotation.ts


web/i18n/lang/app-api.pt.ts → web/i18n/pt-BR/app-api.ts


web/i18n/lang/app-debug.pt.ts → web/i18n/pt-BR/app-debug.ts


web/i18n/lang/app-log.pt.ts → web/i18n/pt-BR/app-log.ts


web/i18n/lang/app-overview.pt.ts → web/i18n/pt-BR/app-overview.ts


web/i18n/lang/app.pt.ts → web/i18n/pt-BR/app.ts


web/i18n/lang/billing.pt.ts → web/i18n/pt-BR/billing.ts


web/i18n/lang/common.pt.ts → web/i18n/pt-BR/common.ts


web/i18n/lang/custom.pt.ts → web/i18n/pt-BR/custom.ts


web/i18n/lang/dataset-creation.pt.ts → web/i18n/pt-BR/dataset-creation.ts


web/i18n/lang/dataset-documents.pt.ts → web/i18n/pt-BR/dataset-documents.ts


web/i18n/lang/dataset-hit-testing.pt.ts → web/i18n/pt-BR/dataset-hit-testing.ts


web/i18n/lang/dataset-settings.pt.ts → web/i18n/pt-BR/dataset-settings.ts


web/i18n/lang/dataset.pt.ts → web/i18n/pt-BR/dataset.ts


web/i18n/lang/explore.pt.ts → web/i18n/pt-BR/explore.ts


web/i18n/lang/layout.uk.ts → web/i18n/pt-BR/layout.ts


web/i18n/lang/login.pt.ts → web/i18n/pt-BR/login.ts


web/i18n/lang/layout.zh.ts → web/i18n/pt-BR/register.ts


web/i18n/lang/share-app.pt.ts → web/i18n/pt-BR/share-app.ts


web/i18n/lang/tools.pt.ts → web/i18n/pt-BR/tools.ts


+ 27 - 4
web/i18n/server.ts

@@ -1,13 +1,36 @@
-import 'server-only'
-
 import { cookies, headers } from 'next/headers'
 import Negotiator from 'negotiator'
 import { match } from '@formatjs/intl-localematcher'
-import type { Locale } from '.'
+
+import { createInstance } from 'i18next'
+import resourcesToBackend from 'i18next-resources-to-backend'
+import { initReactI18next } from 'react-i18next/initReactI18next'
 import { i18n } from '.'
+import type { Locale } from '.'
+
+// https://locize.com/blog/next-13-app-dir-i18n/
+const initI18next = async (lng: Locale, ns: string) => {
+  const i18nInstance = createInstance()
+  await i18nInstance
+    .use(initReactI18next)
+    .use(resourcesToBackend((language: string, namespace: string) => import(`./${language}/${namespace}.ts`)))
+    .init({
+      lng: lng === 'zh-Hans' ? 'zh-Hans' : lng,
+      ns,
+      fallbackLng: 'en-US',
+    })
+  return i18nInstance
+}
+
+export async function useTranslation(lng: Locale, ns = '', options: Record<string, any> = {}) {
+  const i18nextInstance = await initI18next(lng, ns)
+  return {
+    t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
+    i18n: i18nextInstance,
+  }
+}
 
 export const getLocaleOnServer = (): Locale => {
-  // @ts-expect-error locales are readonly
   const locales: string[] = i18n.locales
 
   let languages: string[] | undefined

web/i18n/lang/app-annotation.uk.ts → web/i18n/uk-UA/app-annotation.ts


+ 0 - 0
web/i18n/lang/app-api.uk.ts


Some files were not shown because too many files changed in this diff