diff --git a/src/components/map-projects/ConfigurationForm.jsx b/src/components/map-projects/ConfigurationForm.jsx
index 94de2a20..d0457a8f 100644
--- a/src/components/map-projects/ConfigurationForm.jsx
+++ b/src/components/map-projects/ConfigurationForm.jsx
@@ -27,7 +27,7 @@ import isEmpty from 'lodash/isEmpty'
import omit from 'lodash/omit'
import filter from 'lodash/filter'
-import { getCurrentUser, toV3URL } from '../../common/utils'
+import { formatDate, getCurrentUser, toV3URL } from '../../common/utils'
import { MATCHING_ALGORITHMS_DOC_LINK } from '../../common/constants'
import { getProjectConfigErrors } from './algorithms'
import NamespaceDropdown from '../common/NamespaceDropdown'
@@ -43,7 +43,7 @@ import LookupConfig from './LookupConfig'
import AdvancedSettings from './AdvancedSettings'
import RerankerConfig from './RerankerConfig'
import AIAssistantSelectorPanel from './AIAssistantSelectorPanel'
-import { hasSelectedTargetRepoVersion } from './projectTargetRepo'
+import { getTargetRepoVersionBadges, getTargetRepoVersionReleaseDate, hasSelectedTargetRepoVersion } from './projectTargetRepo'
import { getPreviewAlgorithmOptions, getPreviewConfigAccessErrors } from './previewConfig'
const VisuallyHiddenInput = styled('input')({
@@ -61,7 +61,7 @@ const VisuallyHiddenInput = styled('input')({
const deriveCanonicalUrl = relativeUrl => relativeUrl ? `https://ns.openconceptlab.org${relativeUrl}` : ''
-const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, name, setName, description, setDescription, repo, onRepoChange, repoVersion, setRepoVersion, versions, mappedSources, targetSourcesFromRows, algosSelected, setAlgosSelected, sx, algos, validColumns, columns, isValidColumnValue, updateColumn, configure, setConfigure, columnVisibilityModel, setColumnVisibilityModel, onSave, isSaving, candidatesScore, onScoreChange, includeDefaultFilter, setIncludeDefaultFilter, filters, setFilters, locales, isLoadingLocales, setAIAssistantColumns, AIAssistantColumns, inAIAssistantGroup, lookupConfig, setLookupConfig, encoderModel, setEncoderModel, isCoreUser, requestLimits, canSelectAIModel, canSetAIOutputLocale, canBridge, canScispacy, canUseOrgProjects=true, canUseCustomAlgorithms=true, promptTemplates, promptTemplate, onPromptTemplateChange, AIModels, AIModel, setAIModel, namespace, setNamespace, promptOutputLocale, setPromptOutputLocale, inputLocale, setInputLocale, oclLocales, useLexicalVariants, setUseLexicalVariants }) => {
+const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, name, setName, description, setDescription, repo, onRepoChange, repoVersion, setRepoVersion, showTargetRepoVersionError, versions, mappedSources, targetSourcesFromRows, algosSelected, setAlgosSelected, sx, algos, validColumns, columns, isValidColumnValue, updateColumn, configure, setConfigure, columnVisibilityModel, setColumnVisibilityModel, onSave, isSaving, candidatesScore, onScoreChange, includeDefaultFilter, setIncludeDefaultFilter, filters, setFilters, locales, isLoadingLocales, setAIAssistantColumns, AIAssistantColumns, inAIAssistantGroup, lookupConfig, setLookupConfig, encoderModel, setEncoderModel, isCoreUser, requestLimits, canSelectAIModel, canSetAIOutputLocale, canBridge, canScispacy, canUseOrgProjects=true, canUseCustomAlgorithms=true, promptTemplates, promptTemplate, onPromptTemplateChange, AIModels, AIModel, setAIModel, namespace, setNamespace, promptOutputLocale, setPromptOutputLocale, inputLocale, setInputLocale, oclLocales, useLexicalVariants, setUseLexicalVariants }) => {
const { t } = useTranslation();
const user = getCurrentUser()
const isLLMAlgoNotAllowed = !repoVersion?.match_algorithms?.includes('llm')
@@ -112,6 +112,7 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n
const configErrors = getProjectConfigErrors(algosSelected)
const hasConfigErrors = configErrors.length > 0
const hasTargetRepoVersion = hasSelectedTargetRepoVersion(repoVersion)
+ const isTargetRepoVersionInError = Boolean(showTargetRepoVersionError && !hasTargetRepoVersion)
const getAlgos = () => getPreviewAlgorithmOptions({
algos,
algosSelected,
@@ -242,11 +243,16 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n
onChange={(id, item) => setRepoVersion(item)}
value={repoVersion}
sx={{marginTop: '12px'}}
- error={Boolean(repo?.url && !hasTargetRepoVersion)}
- helperText={repo?.url && !hasTargetRepoVersion ? t(
+ error={isTargetRepoVersionInError}
+ helperText={isTargetRepoVersionInError ? t(
'map_project.target_repo_version_required',
'Select a target repository version before saving.'
) : ''}
+ getOptionBadges={option => getTargetRepoVersionBadges(option, versions).map(key => t(key))}
+ getOptionRightText={option => {
+ const releaseDate = getTargetRepoVersionReleaseDate(option)
+ return releaseDate ? t('map_project.version_released_on', {date: formatDate(releaseDate)}) : ''
+ }}
/>
{
effectiveTargetCanonical &&
@@ -537,7 +543,7 @@ const ConfigurationForm = ({ project, handleFileUpload, file, owner, setOwner, n
sx={{textTransform: 'none', margin: '20px 5px 5px 0px'}}
startIcon={}
onClick={onSave}
- disabled={!name || !file?.name || !owner || hasConfigErrors || hasAccessErrors || !hasTargetRepoVersion}
+ disabled={!name || !file?.name || !owner || hasConfigErrors || hasAccessErrors}
loading={isSaving}
loadingPosition="start"
>
diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx
index 7882629e..248dd197 100644
--- a/src/components/map-projects/MapProject.jsx
+++ b/src/components/map-projects/MapProject.jsx
@@ -114,7 +114,7 @@ import { getAIAssistantChoices, getModelUsed, getPromptTemplateKey, createLatest
import { DEFAULT_ENCODER_MODEL } from './rerankerModels'
import { normalizeAlgorithmInvocation, hasSuccessfulAlgorithmResponse, getAlgorithmStagesFromResponses, lookupStatusRank, buildRecommendableConceptEntry, stripConstantClassAndDatatype, buildLookupConceptUrl, serializeCandidates, toStoredRowMatchState } from './normalizers'
import { parseConceptKey } from './conceptKey'
-import { getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo'
+import { getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getSavedTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo'
import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs } from './viewBuilders.js'
import { formatClockTime, getCapacityWaitLabel, mergeCapacityWaits } from './rowProgress.js'
@@ -388,6 +388,8 @@ const MapProject = () => {
// repo state
const [repo, setRepo] = React.useState(false)
const [repoVersion, setRepoVersion] = React.useState(false)
+ // Version shows its error only after a save without one.
+ const [showTargetRepoVersionError, setShowTargetRepoVersionError] = React.useState(false)
const [mappedSources, setMappedSources] = React.useState([])
const [bridgeMappedSources, setBridgeMappedSources] = React.useState({})
const [locales, setLocales] = React.useState([])
@@ -1584,6 +1586,7 @@ const MapProject = () => {
autosaveSchedulerRef.current.cancel()
if(!repoVersion?.version_url || !selectedTargetRepoVersion) {
setConfigure(true)
+ setShowTargetRepoVersionError(true)
setAlert({
message: t(
'map_project.target_repo_version_required',
@@ -2029,6 +2032,7 @@ const MapProject = () => {
const onRepoVersionChange = version => {
setRepoVersion(version || false)
+ setShowTargetRepoVersionError(false)
if(version?.version_url) {
fetchLocaleDistribution(version.version_url)
fetchMappedSources(version.version_url, setMappedSources)
@@ -2826,16 +2830,20 @@ const MapProject = () => {
const isRunningBulkBridgeCandidates = bridgeCandidatesEndedAt ? moment().isBetween(bridgeCandidatesStartedAt, bridgeCandidatesEndedAt) : Boolean(bridgeCandidatesStartedAt)
const isRunningBulkScispacyCandidates = scispacyCandidatesStartedAt ? moment().isBetween(scispacyCandidatesStartedAt, scispacyCandidatesEndedAt) : Boolean(scispacyCandidatesStartedAt)
- const fetchVersions = (url, _selectedVersion) => {
+ // Only the latest request may set the version: a slower response is for a
+ // repository no longer chosen.
+ const versionsRequestRef = React.useRef(0)
+
+ // A newly chosen repository gets the default version. A saved project
+ // passes its own version and keeps it (OpenConceptLab/ocl_issues#2906).
+ const fetchVersions = (url, savedVersion) => {
+ const request = ++versionsRequestRef.current
APIService.new().overrideURL(dropVersion(url)).appendToUrl('versions/?verbose=true').get().then(response => {
+ if(request !== versionsRequestRef.current)
+ return
let _versions = response.data
setVersions(_versions)
- if(_selectedVersion) {
- const _version = find(_versions, {id: _selectedVersion})
- onRepoVersionChange(_version || false)
- } else {
- onRepoVersionChange(getDefaultTargetRepoVersion(_versions))
- }
+ onRepoVersionChange(savedVersion === undefined ? getDefaultTargetRepoVersion(_versions) : getSavedTargetRepoVersion(_versions, savedVersion))
})
}
@@ -2845,6 +2853,7 @@ const MapProject = () => {
if(newRepo?.url) {
fetchVersions(newRepo.url)
} else {
+ versionsRequestRef.current += 1
setVersions([])
}
}
@@ -5940,6 +5949,7 @@ const MapProject = () => {
onRepoChange={onRepoChange}
repoVersion={repoVersion}
setRepoVersion={onRepoVersionChange}
+ showTargetRepoVersionError={showTargetRepoVersionError}
mappedSources={mappedSources}
targetSourcesFromRows={targetSourcesFromRows}
versions={versions}
diff --git a/src/components/map-projects/__tests__/projectTargetRepo.test.js b/src/components/map-projects/__tests__/projectTargetRepo.test.js
index c35c99b3..20549109 100644
--- a/src/components/map-projects/__tests__/projectTargetRepo.test.js
+++ b/src/components/map-projects/__tests__/projectTargetRepo.test.js
@@ -5,12 +5,25 @@ import {
getDefaultBridgeRepoVersion,
getDefaultTargetRepoVersion,
getLatestReleasedBridgeVersion,
+ getLatestReleasedTargetRepoVersion,
getProjectTargetRepoVersion,
+ getSavedTargetRepoVersion,
+ getTargetRepoVersionBadges,
getTargetRepoVersionFromUrl,
getTargetRepoVersionId,
+ getTargetRepoVersionReleaseDate,
hasSelectedTargetRepoVersion
} from '../projectTargetRepo.js'
+// LOINC's versions as the API lists them, newest first: only 2.81 supports
+// the OCL Semantic Algorithm.
+const LOINC_VERSIONS = [
+ { id: '2.81', released: true, match_algorithms: ['es', 'llm'], revision_date: '2025-08-12T00:00:00Z', created_on: '2025-09-01T10:00:00Z' },
+ { id: '2.71.21AA', released: true, match_algorithms: ['es'], created_on: '2021-11-03T10:00:00Z' },
+ { id: '2.70.21AA', released: true, match_algorithms: ['es'], created_on: '2021-06-30T10:00:00Z' },
+ { id: 'HEAD', released: false, match_algorithms: ['es'], created_on: '2021-01-15T10:00:00Z' }
+]
+
test('getTargetRepoVersionId prefers the selected version id', () => {
assert.equal(getTargetRepoVersionId({ id: '2.81', version_url: '/orgs/OCL/sources/Test/2.81/' }), '2.81')
})
@@ -43,32 +56,88 @@ test('getProjectTargetRepoVersion returns empty when the project has no pinned v
)
})
-test('getDefaultTargetRepoVersion selects the only version transparently', () => {
- const onlyVersion = { id: '2.81', released: false }
- assert.deepEqual(getDefaultTargetRepoVersion([onlyVersion]), onlyVersion)
+test('getDefaultTargetRepoVersion skips a newer release without semantic search', () => {
+ const versions = [
+ { id: '2.82', released: true, match_algorithms: ['es'] },
+ { id: '2.81', released: true, match_algorithms: ['es', 'llm'] },
+ { id: 'HEAD', released: false, match_algorithms: ['es', 'llm'] }
+ ]
+ assert.equal(getDefaultTargetRepoVersion(versions), versions[1])
+})
+
+test('getDefaultTargetRepoVersion falls back to the latest release', () => {
+ const versions = [
+ { id: 'HEAD', released: false, match_algorithms: ['llm'] },
+ { id: '2.81', released: false, match_algorithms: ['llm'] },
+ { id: '2.80', released: true, match_algorithms: ['es'] },
+ { id: '2.79', released: true, match_algorithms: null }
+ ]
+ assert.equal(getDefaultTargetRepoVersion(versions), versions[2])
+})
+
+test('getDefaultTargetRepoVersion falls back to HEAD when nothing is released', () => {
+ const versions = [
+ { id: '2.81', released: false, match_algorithms: ['llm'] },
+ { id: 'HEAD', released: false }
+ ]
+ assert.equal(getDefaultTargetRepoVersion(versions), versions[1])
+})
+
+test('getDefaultTargetRepoVersion stays unset without a release or HEAD', () => {
+ assert.equal(getDefaultTargetRepoVersion([]), false)
+ assert.equal(getDefaultTargetRepoVersion(undefined), false)
+ assert.equal(getDefaultTargetRepoVersion({ detail: 'Not found.' }), false)
+ assert.equal(getDefaultTargetRepoVersion([{ id: '2.81', released: false }]), false)
+})
+
+test('getSavedTargetRepoVersion keeps the saved version over a newer default', () => {
+ assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, '2.70.21AA'), LOINC_VERSIONS[2])
+})
+
+test('getSavedTargetRepoVersion reads a blank saved version as HEAD', () => {
+ // HEAD's version_url carries no version, so a project saved on HEAD
+ // stores a target_repo_url without one.
+ assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, ''), LOINC_VERSIONS[3])
})
-test('getDefaultTargetRepoVersion prefers the only released version', () => {
- const releasedVersion = { id: '2.81', released: true }
+test('getSavedTargetRepoVersion stays unset when the saved version is gone', () => {
+ assert.equal(getSavedTargetRepoVersion(LOINC_VERSIONS, '2.69'), false)
+ assert.equal(getSavedTargetRepoVersion(undefined, '2.81'), false)
+})
+
+test('getLatestReleasedTargetRepoVersion is null without a versions list', () => {
+ assert.equal(getLatestReleasedTargetRepoVersion(undefined), null)
+})
+
+test('getTargetRepoVersionBadges marks the latest release and semantic search', () => {
assert.deepEqual(
- getDefaultTargetRepoVersion([
- { id: 'HEAD', released: false },
- releasedVersion
- ]),
- releasedVersion
+ LOINC_VERSIONS.map(version => getTargetRepoVersionBadges(version, LOINC_VERSIONS)),
+ [['common.latest', 'map_project.semantic_search'], [], [], []]
)
})
-test('getDefaultTargetRepoVersion stays unset when multiple choices remain', () => {
- assert.equal(
- getDefaultTargetRepoVersion([
- { id: 'HEAD', released: false },
- { id: '2.81', released: false }
- ]),
- false
+test('getTargetRepoVersionBadges marks no version latest when nothing is released', () => {
+ const versions = [
+ { id: '2.81', released: false },
+ { id: 'HEAD', released: false, match_algorithms: ['llm'] }
+ ]
+ assert.deepEqual(
+ versions.map(version => getTargetRepoVersionBadges(version, versions)),
+ [[], ['map_project.semantic_search']]
)
})
+test('getTargetRepoVersionReleaseDate dates a release by when OCL created it', () => {
+ // Not revision_date: OCL copies it from HEAD into each new version.
+ assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[0]), '2025-09-01T10:00:00Z')
+ assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[1]), '2021-11-03T10:00:00Z')
+})
+
+test('getTargetRepoVersionReleaseDate leaves HEAD and unreleased versions undated', () => {
+ assert.equal(getTargetRepoVersionReleaseDate(LOINC_VERSIONS[3]), null)
+ assert.equal(getTargetRepoVersionReleaseDate({ id: '2.81', released: false, created_on: '2025-09-01T10:00:00Z' }), null)
+})
+
test('hasSelectedTargetRepoVersion treats blank selections as invalid', () => {
assert.equal(hasSelectedTargetRepoVersion(false), false)
assert.equal(hasSelectedTargetRepoVersion({ id: 'HEAD' }), true)
diff --git a/src/components/map-projects/projectTargetRepo.js b/src/components/map-projects/projectTargetRepo.js
index d654ddb3..5ed190d2 100644
--- a/src/components/map-projects/projectTargetRepo.js
+++ b/src/components/map-projects/projectTargetRepo.js
@@ -4,21 +4,46 @@ export const getTargetRepoVersionFromUrl = url => {
return parts[4] || ''
}
+// The OCL Semantic Algorithm runs only on versions vectorized for it.
export const isLLMBridgeRepoVersion = version => version?.match_algorithms?.includes('llm')
+// The API lists versions newest first.
+export const getLatestReleasedTargetRepoVersion = versions =>
+ (Array.isArray(versions) && versions.find(version => version?.released)) || null
+
+// A newly chosen repository starts on its latest release that supports
+// semantic search, else its latest release, else HEAD.
export const getDefaultTargetRepoVersion = versions => {
- if(!Array.isArray(versions) || versions.length === 0)
+ if(!Array.isArray(versions))
return false
- if(versions.length === 1)
- return versions[0]
- const releasedVersions = versions.filter(version => version?.released)
- if(releasedVersions.length === 1)
- return releasedVersions[0]
+ return versions.find(version => version?.released && isLLMBridgeRepoVersion(version)) ||
+ getLatestReleasedTargetRepoVersion(versions) ||
+ versions.find(version => version?.id === 'HEAD') ||
+ false
+}
- return false
+// A saved project keeps its version, never the default. HEAD's version_url
+// carries no version, so a project saved on HEAD has none in its URL.
+export const getSavedTargetRepoVersion = (versions, savedVersion) =>
+ (Array.isArray(versions) && versions.find(version => version?.id === (savedVersion || 'HEAD'))) || false
+
+export const getTargetRepoVersionBadges = (version, versions) => {
+ const latestReleasedVersion = getLatestReleasedTargetRepoVersion(versions)
+ const badges = []
+ if(latestReleasedVersion && version?.id === latestReleasedVersion.id)
+ badges.push('common.latest')
+ if(isLLMBridgeRepoVersion(version))
+ badges.push('map_project.semantic_search')
+ return badges
}
+// OCL keeps no release timestamp: revision_date is copied from HEAD when a
+// version is created, and updated_on moves with later edits. created_on is
+// what OCL orders versions by, so the latest release shows the newest date.
+export const getTargetRepoVersionReleaseDate = version =>
+ (version?.released && version.created_on) || null
+
export const getDefaultBridgeRepoVersion = versions => {
if(!Array.isArray(versions) || versions.length === 0)
return false
diff --git a/src/components/repos/RepoVersionSearchAutocomplete.jsx b/src/components/repos/RepoVersionSearchAutocomplete.jsx
index e7118260..9ae471f4 100644
--- a/src/components/repos/RepoVersionSearchAutocomplete.jsx
+++ b/src/components/repos/RepoVersionSearchAutocomplete.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
+import Chip from '@mui/material/Chip';
const RepoVersionSearchAutocomplete = ({
@@ -16,7 +17,8 @@ const RepoVersionSearchAutocomplete = ({
error,
helperText,
getOptionDisabled,
- getOptionRightText
+ getOptionRightText,
+ getOptionBadges
}) => {
const [open, setOpen] = React.useState(false)
@@ -51,14 +53,20 @@ const RepoVersionSearchAutocomplete = ({
renderOption={(props, option) => {
const labelContent = getLabel(option)
const rightText = getOptionRightText ? getOptionRightText(option) : ''
+ const badges = getOptionBadges ? getOptionBadges(option) : []
return (
-
+
{labelContent}
+ {
+ badges.map(badge => (
+
+ ))
+ }
{
diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json
index 1367cf2e..534b4dcb 100644
--- a/src/i18n/locales/en/translations.json
+++ b/src/i18n/locales/en/translations.json
@@ -768,6 +768,9 @@
"bridge_version_source_required": "Set a bridge source URL before choosing a version.",
"bridge_version_reset": "Bridge repo version changed to {{version}} because the previously selected version ({{previous}}) is no longer available.",
"not_vectorised": "Not vectorised",
+ "semantic_search": "Semantic search",
+ "version_released_on": "Released {{date}}",
+ "target_repo_version_required": "Select a target repository version before saving.",
"advanced_settings": "Advanced settings",
"resolution_namespace": "Resolution Namespace",
"resolution_namespace_description": "Namespace passed to $resolveReference. When blank, defaults to {{owner}}. Drives which URL Registry entries apply when resolving canonical URLs.",
diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json
index 41180134..75f09c51 100644
--- a/src/i18n/locales/es/translations.json
+++ b/src/i18n/locales/es/translations.json
@@ -747,6 +747,9 @@
"bridge_version_source_required": "Defina una URL de fuente de Bridge antes de elegir una versión.",
"bridge_version_reset": "La versión del repositorio de Bridge se cambió a {{version}} porque la versión seleccionada anteriormente ({{previous}}) ya no está disponible.",
"not_vectorised": "No vectorizado",
+ "semantic_search": "Búsqueda semántica",
+ "version_released_on": "Publicada el {{date}}",
+ "target_repo_version_required": "Seleccione una versión del repositorio objetivo antes de guardar.",
"advanced_settings": "Configuración avanzada",
"resolution_namespace": "Espacio de nombres de resolución",
"resolution_namespace_description": "Espacio de nombres que se pasa a $resolveReference. Si se deja en blanco, usa {{owner}} por defecto. Esto determina qué entradas del Registro de URL se aplican al resolver URL canónicas.",
diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json
index 303b8574..5fc3f368 100644
--- a/src/i18n/locales/pt-BR/translations.json
+++ b/src/i18n/locales/pt-BR/translations.json
@@ -768,6 +768,9 @@
"bridge_version_source_required": "Defina uma URL de Source Bridge antes de escolher uma versão.",
"bridge_version_reset": "A versão do repositório Bridge foi alterada para {{version}} porque a versão selecionada anteriormente ({{previous}}) não está mais disponível.",
"not_vectorised": "Não vetorizado",
+ "semantic_search": "Busca semântica",
+ "version_released_on": "Publicada em {{date}}",
+ "target_repo_version_required": "Selecione uma versão do repositório de destino antes de salvar.",
"advanced_settings": "Configurações avançadas",
"resolution_namespace": "Namespace de Resolução",
"resolution_namespace_description": "Namespace passado para $resolveReference. Quando em branco, usa {{owner}} como padrão. Determina quais entradas do Registro de URL se aplicam ao resolver URLs canônicas.",
diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json
index 9953349a..5f585f68 100644
--- a/src/i18n/locales/zh/translations.json
+++ b/src/i18n/locales/zh/translations.json
@@ -772,6 +772,9 @@
"bridge_version_source_required": "请选择版本前先设置桥接源 URL。",
"bridge_version_reset": "桥接仓库版本已更改为 {{version}},因为先前选择的版本({{previous}})已不可用。",
"not_vectorised": "未向量化",
+ "semantic_search": "语义搜索",
+ "version_released_on": "发布于 {{date}}",
+ "target_repo_version_required": "保存前请选择目标存储库版本。",
"advanced_settings": "高级设置",
"resolution_namespace": "解析命名空间",
"resolution_namespace_description": "传递给 $resolveReference 的命名空间。留空时默认使用 {{owner}}。它决定解析规范 URL 时适用哪些 URL 注册表条目。",