diff --git a/src/components/common/QuotaDialog.jsx b/src/components/common/QuotaDialog.jsx index b3b0f53c..8fb7004b 100644 --- a/src/components/common/QuotaDialog.jsx +++ b/src/components/common/QuotaDialog.jsx @@ -4,66 +4,51 @@ import DialogContent from '@mui/material/DialogContent' import DialogActions from '@mui/material/DialogActions' import Typography from '@mui/material/Typography' import Button from '@mui/material/Button' -import LinearProgress from '@mui/material/LinearProgress' import OpenInNewIcon from '@mui/icons-material/OpenInNew' import Dialog from './Dialog' import DialogTitle from './DialogTitle' import CloseIconButton from './CloseIconButton' -import { isCapMeter, QUOTA_PRICING_URL, REQUEST_MORE_ACCESS_URL } from './quotaErrors' +import { isCapMeter } from './quotaErrors' +import useLimitAction from './useLimitAction' -const QuotaDialog = ({open, onClose, meter, surface, plan, usage}) => { +const QuotaDialog = ({open, onClose, errorCode, meter, surface}) => { const { t } = useTranslation() - const isCap = isCapMeter(meter) - const { used, limit, period } = usage || {} - const hasCount = Number.isFinite(used) && Number.isFinite(limit) && limit > 0 - const progress = hasCount ? Math.min(100, (used / limit) * 100) : 100 - const linkProps = {target: '_blank', rel: 'noopener noreferrer', endIcon: , sx: {textTransform: 'none', borderRadius: '100px', whiteSpace: 'nowrap', flexShrink: 0, '&:hover, &:focus, &:active': {textDecoration: 'none'}, '&:focus:not(:focus-visible)': {outline: 'none'}}} + const { action, onActionClick } = useLimitAction(open, errorCode) return ( - {t(isCap ? 'quota.title_cap' : 'quota.title_quota')} + {t(isCapMeter(meter) ? 'quota.title_cap' : 'quota.title_quota')} {t([`quota.consequence.${meter}.${surface}`, `quota.consequence.${meter}.default`, 'quota.consequence.generic'])} -
- { - plan && -
- - {t('quota.your_plan')} - - {plan} -
- } -
- {t(`quota.meter.${meter}`, {defaultValue: t('quota.meter.generic')})} - { - hasCount && - {t('quota.used_of', {used, limit})} - } -
- - { - !isCap && period === 'one_time' && - {t('quota.period_one_time')} - } -
+ { + action?.message && + {action.message} + }
- - + { + action && + + }
) diff --git a/src/components/common/__tests__/limitAction.test.js b/src/components/common/__tests__/limitAction.test.js new file mode 100644 index 00000000..814f2161 --- /dev/null +++ b/src/components/common/__tests__/limitAction.test.js @@ -0,0 +1,59 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +const { getLimitActionRequest, getLimitSubject, toLimitAction } = await import('../limitAction.js') +const { PREVIEW_LIMIT_ERROR_CODES } = await import('../../map-projects/previewLimits.js') + +const ACTION = {kind: 'example', url: 'https://online.example.org/next', label: 'Next step', message: 'A message from the service.'} + +test('no online-api URL or no token: no request', () => { + assert.equal(getLimitActionRequest({baseURL: '', token: 'personal-token', errorCode: 'mapper_access_denied', lang: 'en'}), null) + assert.equal(getLimitActionRequest({baseURL: undefined, token: 'personal-token'}), null) + assert.equal(getLimitActionRequest({baseURL: 'https://online.example.org', token: undefined}), null) +}) + +test('asks about the refused capability in the UI language, with the person\'s token', () => { + assert.deepEqual( + getLimitActionRequest({baseURL: 'https://online.example.org/', token: 'personal-token', errorCode: 'mapper_rows_per_project_not_entitled', lang: 'pt-BR'}), + { + url: 'https://online.example.org/billing/plan/', + params: {context: 'limit', capability: 'mapper.rows_per_project', lang: 'pt-BR'}, + headers: {Authorization: 'Token personal-token'}, + } + ) +}) + +test('an access check asks about its permission', () => { + assert.deepEqual( + getLimitActionRequest({baseURL: 'https://online.example.org', token: 'personal-token', errorCode: 'mapper_scispacy_denied', lang: 'en'}).params, + {context: 'limit', permission: 'users.mapper_scispacy', lang: 'en'} + ) +}) + +test('every refusal the dialogs show names one capability or permission', () => { + [ + 'mapper_match_operations_limit_reached', + 'ai_assistant_calls_limit_reached', + 'ai_assistant_change_comments_limit_reached', + 'mapper_projects_limit_reached', + 'mapper_rows_per_project_limit_reached', + ...PREVIEW_LIMIT_ERROR_CODES, + ].forEach(errorCode => assert.equal(Object.keys(getLimitSubject(errorCode)).length, 1, errorCode)) + assert.deepEqual(getLimitSubject('validation_error'), {}) +}) + +test('an action with a link and a label is shown as sent', () => { + assert.deepEqual(toLimitAction({action: ACTION}), ACTION) + assert.deepEqual(toLimitAction({action: {...ACTION, message: null}}), {...ACTION, message: null}) + assert.equal(toLimitAction({action: {...ACTION, message: ' '}}).message, null) +}) + +test('no action, or one that is not a usable link, shows nothing', () => { + assert.equal(toLimitAction({action: null}), null) + assert.equal(toLimitAction(undefined), null) + assert.equal(toLimitAction({action: {...ACTION, label: ''}}), null) + assert.equal(toLimitAction({action: {...ACTION, url: 'not a url'}}), null) + assert.equal(toLimitAction({action: {...ACTION, url: 'javascript:void(0)'}}), null) + assert.equal(toLimitAction({action: {...ACTION, url: 'data:text/html,hi'}}), null) + assert.equal(toLimitAction({action: {...ACTION, url: 42}}), null) +}) diff --git a/src/components/common/__tests__/quotaErrors.test.js b/src/components/common/__tests__/quotaErrors.test.js index 3e54c8fc..1f041c46 100644 --- a/src/components/common/__tests__/quotaErrors.test.js +++ b/src/components/common/__tests__/quotaErrors.test.js @@ -5,7 +5,7 @@ import { getQuotaError, isCapMeter, isQuotaError } from '../quotaErrors.js' test('getQuotaError: maps quota and cap error codes to a meter and kind', () => { assert.deepEqual(getQuotaError({error_code: 'mapper_match_operations_limit_reached', limit: 6, used: 6}), { - meter: 'match_operations', kind: 'quota', errorCode: 'mapper_match_operations_limit_reached', usage: {used: 6, limit: 6}, + meter: 'match_operations', kind: 'quota', errorCode: 'mapper_match_operations_limit_reached', }) assert.equal(getQuotaError({error_code: 'ai_assistant_calls_limit_reached'}).meter, 'ai_assistant_calls') assert.equal(getQuotaError({error_code: 'ai_assistant_change_comments_limit_reached'}).meter, 'ai_assistant_change_comments') @@ -15,7 +15,7 @@ test('getQuotaError: maps quota and cap error codes to a meter and kind', () => test('getQuotaError: unwraps axios error responses', () => { const error = {response: {status: 403, data: {error_code: 'mapper_match_operations_limit_reached', limit: 3, used: 3}}} - assert.deepEqual(getQuotaError(error).usage, {used: 3, limit: 3}) + assert.equal(getQuotaError(error).errorCode, 'mapper_match_operations_limit_reached') }) test('getQuotaError: ignores permission errors and unrelated payloads', () => { diff --git a/src/components/common/limitAction.js b/src/components/common/limitAction.js new file mode 100644 index 00000000..1871ba85 --- /dev/null +++ b/src/components/common/limitAction.js @@ -0,0 +1,50 @@ +// A limit refusal can come with a next step from the connected OCL Online +// service (OpenConceptLab/ocl_online#367). Open code shows only what the +// service sends; with no service configured it shows nothing. + +// What each refusal is about, as the service names it: the capability for a +// limit, the permission for an access check. +const LIMIT_SUBJECTS = { + mapper_match_operations_limit_reached: {capability: 'mapper.match_operations'}, + mapper_match_operations_not_entitled: {capability: 'mapper.match_operations'}, + mapper_rows_per_project_limit_reached: {capability: 'mapper.rows_per_project'}, + mapper_rows_per_project_not_entitled: {capability: 'mapper.rows_per_project'}, + mapper_projects_limit_reached: {capability: 'mapper.projects'}, + mapper_projects_not_entitled: {capability: 'mapper.projects'}, + ai_assistant_calls_limit_reached: {capability: 'ai_assistant.calls'}, + ai_assistant_calls_not_entitled: {capability: 'ai_assistant.calls'}, + ai_assistant_change_comments_limit_reached: {capability: 'ai_assistant.change_comments'}, + mapper_access_denied: {permission: 'users.mapper_use'}, + mapper_org_projects_denied: {permission: 'users.mapper_org_projects'}, + mapper_custom_algorithms_denied: {permission: 'users.mapper_custom_algorithms'}, + mapper_ai_assistant_denied: {permission: 'users.mapper_ai_assistant'}, + mapper_scispacy_denied: {permission: 'users.mapper_scispacy'}, +} + +export const getLimitSubject = errorCode => LIMIT_SUBJECTS[errorCode] || {} + +export const getLimitActionRequest = ({baseURL, token, errorCode, lang}) => { + if(!baseURL || !token) + return null + return { + url: `${baseURL.replace(/\/+$/, '')}/billing/plan/`, + params: {context: 'limit', ...getLimitSubject(errorCode), lang}, + headers: {Authorization: `Token ${token}`}, + } +} + +// Only an http(s) link with a label becomes a button. +export const toLimitAction = data => { + const { kind, url, label, message } = data?.action || {} + if(typeof label !== 'string' || !label.trim() || typeof url !== 'string') + return null + let href + try { + href = new URL(url) + } catch(_) { + return null + } + if(!['https:', 'http:'].includes(href.protocol)) + return null + return {kind, url: href.href, label, message: typeof message === 'string' && message.trim() ? message : null} +} diff --git a/src/components/common/quotaErrors.js b/src/components/common/quotaErrors.js index 8399554d..d588b41c 100644 --- a/src/components/common/quotaErrors.js +++ b/src/components/common/quotaErrors.js @@ -1,7 +1,3 @@ -// Community-site pages (ocl_online#170). -export const QUOTA_PRICING_URL = 'https://openconceptlab.org/pricing' -export const REQUEST_MORE_ACCESS_URL = 'https://openconceptlab.org/early-access?source=mapper-limit' - const QUOTA_ERRORS = { mapper_match_operations_limit_reached: {meter: 'match_operations', kind: 'quota'}, ai_assistant_calls_limit_reached: {meter: 'ai_assistant_calls', kind: 'quota'}, @@ -19,7 +15,7 @@ export const getQuotaError = value => { const config = QUOTA_ERRORS[data?.error_code] if(!config) return null - return {...config, errorCode: data.error_code, usage: {used: data.used, limit: data.limit}} + return {...config, errorCode: data.error_code} } export const isQuotaError = value => Boolean(getQuotaError(value)) diff --git a/src/components/common/useLimitAction.js b/src/components/common/useLimitAction.js new file mode 100644 index 00000000..bd90292c --- /dev/null +++ b/src/components/common/useLimitAction.js @@ -0,0 +1,43 @@ +/*eslint no-process-env: 0*/ +import React from 'react' +import axios from 'axios' +import { useTranslation } from 'react-i18next' + +import GAService from '../../services/GAService' +import { currentUserToken } from '../../common/utils' +import { getLimitActionRequest, getLimitSubject, toLimitAction } from './limitAction' + +/*eslint no-undef: 0*/ +const getOnlineAPIURL = () => window.OCL_ONLINE_API_URL || process.env.OCL_ONLINE_API_URL + +// Records the refusal and, when online-api is configured, asks it for the +// person's next step. Not configured, failing or nothing to offer: no action. +// axios directly, not APIService, so online-api's errors skip its sign-out and throttling handling. +const useLimitAction = (open, errorCode) => { + const { i18n } = useTranslation() + const [action, setAction] = React.useState(null) + const subject = getLimitSubject(errorCode) + + React.useEffect(() => { + setAction(null) + if(!open) + return undefined + GAService.recordActionEvent('Limit', 'limit_hit', errorCode, subject) + const request = getLimitActionRequest({baseURL: getOnlineAPIURL(), token: currentUserToken(), errorCode, lang: i18n.language}) + if(!request) + return undefined + let current = true + axios.get(request.url, {params: request.params, headers: request.headers, timeout: 10000}) + .then(response => current && setAction(toLimitAction(response.data))) + .catch(() => {}) + return () => { + current = false + } + }, [open, errorCode]) + + const onActionClick = () => GAService.recordActionEvent('Limit', 'limit_action_click', errorCode, {...subject, action_kind: action?.kind}) + + return {action, onActionClick} +} + +export default useLimitAction diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index c7f53d7b..61c8a1e8 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -289,8 +289,8 @@ const MapProject = () => { const ownNavigationRef = React.useRef(false) const refreshRowStageSnapshotRef = React.useRef({}); - const [previewLimit, setPreviewLimit] = React.useState(null) // {errorCode, limit, used} or null - const previewLimitQuota = previewLimit ? getQuotaError({error_code: previewLimit.errorCode, limit: previewLimit.limit, used: previewLimit.used}) : null + const [previewLimit, setPreviewLimit] = React.useState(null) // {errorCode} or null + const previewLimitQuota = previewLimit ? getQuotaError({error_code: previewLimit.errorCode}) : null const [row, setRow] = React.useState(false) const [loadingMatches, setLoadingMatches] = React.useState(false) @@ -809,11 +809,6 @@ const MapProject = () => { ? getNewProjectBlockReason(getMapperPreview()) : null - React.useEffect(() => { - if(newProjectBlockReason) - baseSetAlert({severity: 'error', message: t(`map_project.preview_limit_title_${newProjectBlockReason}`), duration: 8000}) - }, [newProjectBlockReason]) - React.useEffect(() => { const isDefaultApplied = isRepoDefaultFilterApplied(repoVersion) if(project?.id) @@ -1827,7 +1822,7 @@ const MapProject = () => { // The user has left the project, so no dialog can show: say so once. baseSetAlert({severity: 'error', message: t('map_project.leave_save_failed', {name: name || project?.name, detail: errorData?.detail || t('unknown_error')}), duration: 12000}) } else if(status === 403 && errorData?.error_code) { - setPreviewLimit({errorCode: errorData.error_code, limit: errorData.limit, used: errorData.used}) + setPreviewLimit({errorCode: errorData.error_code}) } else if(!isAutoSave && status !== 429 && status !== 401) { baseSetAlert({severity: 'error', message: errorData?.detail || t('unknown_error'), duration: 8000}) } @@ -2073,7 +2068,7 @@ const MapProject = () => { abortRef.current = true setLoadingMatches(false) setIsLoadingInDecisionView(false) - setPreviewLimit({errorCode: data.error_code, limit: data.limit, used: data.used}) + setPreviewLimit({errorCode: data.error_code}) return } projectLog({action: 'automatch_run_create_failed', extras: {status: status || 'no-id', ...(result.ok ? {} : {reason: result.reason, error: result.error?.message || null})}}) @@ -4003,7 +3998,7 @@ const MapProject = () => { if(isNumber(rowId)) refreshMapperQuotaCache() } - setPreviewLimit({errorCode: response.error_code, limit: response.limit, used: response.used}) + setPreviewLimit({errorCode: response.error_code}) return true } @@ -6214,10 +6209,9 @@ const MapProject = () => { const onCopyClick = event => { event.preventDefault() event.stopPropagation() - const preview = getMapperPreview() - const blockReason = getNewProjectBlockReason(preview) + const blockReason = getNewProjectBlockReason() if(blockReason) { - setPreviewLimit({errorCode: NEW_PROJECT_BLOCK_ERROR_CODES[blockReason], limit: preview.projects.limit, used: preview.projects.used}) + setPreviewLimit({errorCode: NEW_PROJECT_BLOCK_ERROR_CODES[blockReason]}) return } if(project?.url) { @@ -6234,7 +6228,7 @@ const MapProject = () => { return minFactor } - return permissionDenied ? : newProjectBlockReason ? : ( + return permissionDenied ? : newProjectBlockReason ? : (
{ (() => { @@ -6688,16 +6682,14 @@ const MapProject = () => { setPreviewLimit(null)} + errorCode={previewLimitQuota.errorCode} meter={previewLimitQuota.meter} surface={MAPPER_QUOTA_SURFACE[previewLimitQuota.meter]} - usage={{...previewLimitQuota.usage, period: 'one_time'}} /> : setPreviewLimit(null)} errorCode={previewLimit?.errorCode} - limit={previewLimit?.limit} - used={previewLimit?.used} /> } diff --git a/src/components/map-projects/MapProjects.jsx b/src/components/map-projects/MapProjects.jsx index 3eac57df..e9e92cd8 100644 --- a/src/components/map-projects/MapProjects.jsx +++ b/src/components/map-projects/MapProjects.jsx @@ -99,6 +99,15 @@ const MapProjects = () => { fetchProjects() }, []) + // MapProject sends a blocked /map-projects/new here: show the same dialog once. + React.useEffect(() => { + const reason = history.location.state?.newProjectBlock + if(reason) { + setNewProjectBlock({reason}) + history.replace({...history.location, state: undefined}) + } + }, []) + const onProjectDelete = (success) => { if(success === true) { setProjects(reject(projects, {id: deleteProject.id})) @@ -109,7 +118,7 @@ const MapProjects = () => { const startNewProject = templateURL => { const goToNewProject = () => history.push(templateURL ? `/map-projects/new?templateFrom=${encodeURIComponent(templateURL)}` : '/map-projects/new') const reasonFromCache = () => getNewProjectBlockReason(getMapperPreview()) - const showBlock = reason => setNewProjectBlock({reason, preview: getMapperPreview()}) + const showBlock = reason => setNewProjectBlock({reason}) const cachedReason = reasonFromCache() if(cachedReason) { @@ -342,9 +351,9 @@ const MapProjects = () => { setNewProjectBlock(null)} + errorCode='mapper_projects_limit_reached' meter='projects' surface='new_project' - usage={{used: newProjectBlock.preview.projects.used, limit: newProjectBlock.preview.projects.limit, period: 'one_time'}} /> : { +// Says what the account can't use. Any next step comes from the connected +// service (OpenConceptLab/ocl_online#367) and opens in a new tab. +const PreviewLimitDialog = ({open, onClose, errorCode}) => { const { t } = useTranslation() + const { action, onActionClick } = useLimitAction(open, errorCode) const kind = PREVIEW_LIMIT_COPY_KEY[errorCode] || 'generic' return ( - {t(`map_project.preview_limit_title_${kind}`)} + {t('map_project.limit_unavailable_title')} - - { - (limit || limit === 0) ? - t(`map_project.preview_limit_body_${kind}`, {limit, used}) : - t(`map_project.preview_limit_body_${kind}_no_count`) - } - - - {t('map_project.preview_limit_hardship_note')} + + {t([`map_project.limit_unavailable_body_${kind}`, 'map_project.limit_unavailable_body_generic'])} + { + action?.message && + {action.message} + } - - + { + action && + + } diff --git a/src/components/map-projects/__tests__/previewLimits.test.js b/src/components/map-projects/__tests__/previewLimits.test.js index aa362b20..15772b98 100644 --- a/src/components/map-projects/__tests__/previewLimits.test.js +++ b/src/components/map-projects/__tests__/previewLimits.test.js @@ -1,5 +1,6 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import { getPreviewLimitError, @@ -50,3 +51,15 @@ test('AI preview errors use AI Assistant dialog copy', () => { assert.equal(isAIPreviewLimitError('ai_assistant_calls_limit_reached'), true) assert.equal(isAIPreviewLimitError('mapper_match_operations_limit_reached'), false) }) + +// Limits reached go to QuotaDialog; PreviewLimitDialog shows the rest. +test('every refusal PreviewLimitDialog shows has its own message in each language', () => { + for (const locale of ['en', 'es', 'pt-BR', 'zh']) { + const catalog = JSON.parse(readFileSync(new URL(`../../../i18n/locales/${locale}/translations.json`, import.meta.url), 'utf8')).map_project + const missing = Object.entries(PREVIEW_LIMIT_COPY_KEY) + .filter(([errorCode]) => !errorCode.endsWith('_limit_reached')) + .map(([, kind]) => `limit_unavailable_body_${kind}`) + .filter(key => !catalog[key]) + assert.deepEqual(missing, [], locale) + } +}) diff --git a/src/i18n/__tests__/aiAssistantNaming.test.js b/src/i18n/__tests__/aiAssistantNaming.test.js index c714e4e9..a0a59714 100644 --- a/src/i18n/__tests__/aiAssistantNaming.test.js +++ b/src/i18n/__tests__/aiAssistantNaming.test.js @@ -28,9 +28,7 @@ const NAMES_THE_ASSISTANT = [ 'input_language_helper', 'ai_assistant', 'preview_quota_ai_calls', - 'preview_limit_title_ai_assistant', - 'preview_limit_body_ai_assistant', - 'preview_limit_body_ai_assistant_no_count', + 'limit_unavailable_body_ai_assistant', 'column_send_to_ai_assistant', 'ai_prompt_template_description', 'set_ai_assistant_output_language', diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 40e03d63..a37b20ff 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -317,40 +317,16 @@ "preview_quota_ai_calls": "OCL AI Assistant calls", "preview_quota_used_of_limit": "{{used}} of {{limit}} used", "preview_rows_activated_notice": "Preview projects hold {{limit}} rows - we activated the first {{limit}} of your {{total}}.", - "preview_limit_title_rows": "You've used this project's preview rows", - "preview_limit_title_match_operations": "You've used your preview match operations", - "preview_limit_title_projects": "The preview covers one project", - "preview_limit_title_org_projects": "Organization projects aren't in the preview", - "preview_limit_title_access": "Mapper access is limited right now", - "preview_limit_title_custom_algorithms": "Externally hosted algorithms aren't in the preview", - "preview_limit_title_ai_assistant": "The OCL AI Assistant isn't available yet", - "preview_limit_title_scispacy": "ScispaCy LOINC isn't available yet", - "preview_limit_title_rows_not_entitled": "This project's rows aren't part of your preview yet", - "preview_limit_title_match_operations_not_entitled": "Match operations aren't part of your account yet", - "preview_limit_title_projects_not_entitled": "Map projects aren't part of your account yet", - "preview_limit_title_generic": "You've reached a preview limit", - "preview_limit_body_rows": "Your free preview covers {{limit}} rows matched by AutoMatch for this project - you've used all {{limit}}. Nothing is locked: keep reviewing, editing and exporting this project as much as you like.", - "preview_limit_body_rows_no_count": "Your free preview's row limit for this project has been reached. Nothing is locked: keep reviewing, editing and exporting this project as much as you like.", - "preview_limit_body_match_operations": "Your free preview covers {{limit}} match operations - you've used all {{limit}}. Nothing is locked: keep reviewing, editing and exporting this project as much as you like.", - "preview_limit_body_match_operations_no_count": "Your free preview's match-operation limit has been reached. Nothing is locked: keep reviewing, editing and exporting this project as much as you like.", - "preview_limit_body_projects": "You already have an active Mapper project. The free preview covers one project at a time - delete the one you have, or ask about more access below.", - "preview_limit_body_projects_no_count": "You already have an active Mapper project. The free preview covers one project at a time.", - "preview_limit_body_org_projects": "Organization-owned map projects aren't part of the preview yet. Create a project under your own account instead, or ask about more access below.", - "preview_limit_body_org_projects_no_count": "Organization-owned map projects aren't part of the preview yet.", - "preview_limit_body_access": "Your account doesn't currently have Mapper access. Ask about more access below and we'll sort it out.", - "preview_limit_body_access_no_count": "Your account doesn't currently have Mapper access.", - "preview_limit_body_custom_algorithms_no_count": "Externally hosted algorithms aren't part of the preview yet. Use OCL's built-in algorithms, or ask about more access below.", - "preview_limit_body_ai_assistant": "Your free preview covers {{limit}} OCL AI Assistant calls - you've used {{used}} of {{limit}}. Matching can continue without AI analysis for the remaining rows.", - "preview_limit_body_ai_assistant_no_count": "The OCL AI Assistant isn't available for your account. Ask about more access below and we'll sort it out.", - "preview_limit_body_scispacy_no_count": "ScispaCy LOINC matching isn't available for your account. Ask about more access below and we'll sort it out.", - "preview_limit_body_rows_not_entitled_no_count": "This project doesn't have a row allowance configured yet. Ask about more access below and we'll sort it out.", - "preview_limit_body_match_operations_not_entitled_no_count": "Your account doesn't have match-operations access configured yet. Ask about more access below and we'll sort it out.", - "preview_limit_body_projects_not_entitled_no_count": "Your account doesn't have map-project access configured yet. Ask about more access below and we'll sort it out.", - "preview_limit_body_generic": "You've reached a preview limit ({{used}} of {{limit}}).", - "preview_limit_body_generic_no_count": "You've reached a preview limit for the Mapper.", - "preview_limit_hardship_note": "Working on a grant budget that doesn't stretch, or for a ministry of health? Use \"Request more access\" below - if the limit doesn't work for your situation, we'll find something that does.", - "preview_limit_notify_me": "Notify me when plans launch", - "preview_limit_request_access": "Request more access", + "limit_unavailable_title": "Not available for your account", + "limit_unavailable_body_access": "Your account doesn't have access to the Mapper.", + "limit_unavailable_body_org_projects": "Your account can't create map projects owned by an organization. You can create one under your own account instead.", + "limit_unavailable_body_custom_algorithms": "Your account can't use externally hosted algorithms. You can use OCL's built-in algorithms instead.", + "limit_unavailable_body_ai_assistant": "The OCL AI Assistant isn't available for your account.", + "limit_unavailable_body_scispacy": "ScispaCy LOINC matching isn't available for your account.", + "limit_unavailable_body_rows_not_entitled": "Your account can't run AutoMatch on this project's rows.", + "limit_unavailable_body_match_operations_not_entitled": "Your account can't run AutoMatch.", + "limit_unavailable_body_projects_not_entitled": "Your account can't create map projects.", + "limit_unavailable_body_generic": "This isn't available for your account.", "preview_limit_dismiss": "Back to my project", "preview_config_unavailable_title": "This project uses preview-restricted settings", "preview_config_unavailable_org_projects": "Organization-owned Mapper projects are not part of your preview. Choose your personal namespace as the project owner.", @@ -594,21 +570,8 @@ "quota": { "title_quota": "You've reached your quota", "title_cap": "You've reached your limit", - "your_plan": "Your plan", - "used_of": "{{used}} of {{limit}} used", - "period_one_time": "One-time preview allowance", - "request_more_access": "Request more access", - "view_pricing": "View pricing", - "meter": { - "match_operations": "Match operations", - "ai_assistant_calls": "OCL AI Assistant calls", - "ai_assistant_change_comments": "OCL AI Assistant update comments", - "projects": "Map projects", - "rows": "Rows per project", - "generic": "Usage" - }, "consequence": { - "generic": "Some features are paused until your allowance is increased.", + "generic": "Some features are paused.", "ai_assistant_calls": { "default": "Features that use the OCL AI Assistant are paused.", "auto_match": "AutoMatch will keep running without recommendations from the OCL AI Assistant." diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 3ef8477f..16fa0387 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -301,40 +301,16 @@ "preview_quota_ai_calls": "Llamadas al asistente de IA de OCL", "preview_quota_used_of_limit": "{{used}} de {{limit}} usados", "preview_rows_activated_notice": "Los proyectos de vista previa contienen {{limit}} filas: activamos las primeras {{limit}} de tus {{total}}.", - "preview_limit_title_rows": "Has usado las filas de vista previa de este proyecto", - "preview_limit_title_match_operations": "Has usado tus operaciones de coincidencia de vista previa", - "preview_limit_title_projects": "La vista previa cubre un proyecto", - "preview_limit_title_org_projects": "Los proyectos de organización no están en la vista previa", - "preview_limit_title_access": "El acceso al Mapper está limitado por ahora", - "preview_limit_title_custom_algorithms": "Los algoritmos externos no están en la vista previa", - "preview_limit_title_ai_assistant": "El asistente de IA de OCL aún no está disponible", - "preview_limit_title_scispacy": "ScispaCy LOINC aún no está disponible", - "preview_limit_title_rows_not_entitled": "Las filas de este proyecto aún no forman parte de tu vista previa", - "preview_limit_title_match_operations_not_entitled": "Las operaciones de coincidencia aún no forman parte de tu cuenta", - "preview_limit_title_projects_not_entitled": "Los proyectos de mapeo aún no forman parte de tu cuenta", - "preview_limit_title_generic": "Has alcanzado un límite de la vista previa", - "preview_limit_body_rows": "Tu vista previa gratuita cubre {{limit}} filas con AutoMatch para este proyecto: has usado las {{limit}}. Nada está bloqueado: sigue revisando, editando y exportando este proyecto tanto como quieras.", - "preview_limit_body_rows_no_count": "Se alcanzó el límite de filas de tu vista previa gratuita para este proyecto. Nada está bloqueado: sigue revisando, editando y exportando este proyecto tanto como quieras.", - "preview_limit_body_match_operations": "Tu vista previa gratuita cubre {{limit}} operaciones de coincidencia: has usado las {{limit}}. Nada está bloqueado: sigue revisando, editando y exportando este proyecto tanto como quieras.", - "preview_limit_body_match_operations_no_count": "Se alcanzó el límite de operaciones de coincidencia de tu vista previa gratuita. Nada está bloqueado: sigue revisando, editando y exportando este proyecto tanto como quieras.", - "preview_limit_body_projects": "Ya tienes un proyecto de Mapper activo. La vista previa gratuita cubre un proyecto a la vez: elimina el que tienes, o solicita más acceso abajo.", - "preview_limit_body_projects_no_count": "Ya tienes un proyecto de Mapper activo. La vista previa gratuita cubre un proyecto a la vez.", - "preview_limit_body_org_projects": "Los proyectos de mapeo propiedad de una organización aún no forman parte de la vista previa. Crea un proyecto con tu propia cuenta, o solicita más acceso abajo.", - "preview_limit_body_org_projects_no_count": "Los proyectos de mapeo propiedad de una organización aún no forman parte de la vista previa.", - "preview_limit_body_access": "Tu cuenta no tiene actualmente acceso al Mapper. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_access_no_count": "Tu cuenta no tiene actualmente acceso al Mapper.", - "preview_limit_body_custom_algorithms_no_count": "Los algoritmos externos aún no forman parte de la vista previa. Usa los algoritmos integrados de OCL, o solicita más acceso abajo.", - "preview_limit_body_ai_assistant": "Tu vista previa gratuita cubre {{limit}} llamadas al asistente de IA de OCL: has usado {{used}} de {{limit}}. La asignación puede continuar sin análisis de IA para las filas restantes.", - "preview_limit_body_ai_assistant_no_count": "El asistente de IA de OCL no está disponible para tu cuenta. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_scispacy_no_count": "La coincidencia con ScispaCy LOINC no está disponible para tu cuenta. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_rows_not_entitled_no_count": "Este proyecto aún no tiene una asignación de filas configurada. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_match_operations_not_entitled_no_count": "Tu cuenta aún no tiene acceso a operaciones de coincidencia configurado. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_projects_not_entitled_no_count": "Tu cuenta aún no tiene acceso a proyectos de mapeo configurado. Solicita más acceso abajo y lo resolveremos.", - "preview_limit_body_generic": "Has alcanzado un límite de la vista previa ({{used}} de {{limit}}).", - "preview_limit_body_generic_no_count": "Has alcanzado un límite de la vista previa del Mapper.", - "preview_limit_hardship_note": "¿Trabajas con un presupuesto de subvención ajustado, o para un ministerio de salud? Usa \"Solicitar más acceso\" abajo: si el límite no funciona para tu situación, encontraremos algo que sí.", - "preview_limit_notify_me": "Avísame cuando se lancen los planes", - "preview_limit_request_access": "Solicitar más acceso", + "limit_unavailable_title": "No disponible para tu cuenta", + "limit_unavailable_body_access": "Tu cuenta no tiene acceso al Mapper.", + "limit_unavailable_body_org_projects": "Tu cuenta no puede crear proyectos de mapeo propiedad de una organización. Puedes crear uno con tu propia cuenta.", + "limit_unavailable_body_custom_algorithms": "Tu cuenta no puede usar algoritmos alojados externamente. Puedes usar los algoritmos integrados de OCL.", + "limit_unavailable_body_ai_assistant": "El asistente de IA de OCL no está disponible para tu cuenta.", + "limit_unavailable_body_scispacy": "La coincidencia con ScispaCy LOINC no está disponible para tu cuenta.", + "limit_unavailable_body_rows_not_entitled": "Tu cuenta no puede ejecutar AutoMatch en las filas de este proyecto.", + "limit_unavailable_body_match_operations_not_entitled": "Tu cuenta no puede ejecutar AutoMatch.", + "limit_unavailable_body_projects_not_entitled": "Tu cuenta no puede crear proyectos de mapeo.", + "limit_unavailable_body_generic": "Esto no está disponible para tu cuenta.", "preview_limit_dismiss": "Volver a mi proyecto", "preview_config_unavailable_title": "Este proyecto usa configuraciones restringidas de la vista previa", "preview_config_unavailable_org_projects": "Los proyectos de Mapper propiedad de una organización no forman parte de tu vista previa. Elige tu espacio de nombres personal como propietario del proyecto.", @@ -594,21 +570,8 @@ "quota": { "title_quota": "Ha alcanzado su cuota", "title_cap": "Ha alcanzado su límite", - "your_plan": "Su plan", - "used_of": "{{used}} de {{limit}} usados", - "period_one_time": "Asignación de vista previa única", - "request_more_access": "Solicitar más acceso", - "view_pricing": "Ver precios", - "meter": { - "match_operations": "Operaciones de coincidencia", - "ai_assistant_calls": "Llamadas al asistente de IA de OCL", - "ai_assistant_change_comments": "Comentarios de actualización del asistente de IA de OCL", - "projects": "Proyectos de mapeo", - "rows": "Filas por proyecto", - "generic": "Uso" - }, "consequence": { - "generic": "Algunas funciones están en pausa hasta que se amplíe su asignación.", + "generic": "Algunas funciones están en pausa.", "ai_assistant_calls": { "default": "Las funciones que usan el asistente de IA de OCL están en pausa.", "auto_match": "AutoMatch seguirá funcionando sin recomendaciones del asistente de IA de OCL." diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index ccf40067..c2fd2657 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -317,40 +317,16 @@ "preview_quota_ai_calls": "Chamadas do Assistente de IA do OCL", "preview_quota_used_of_limit": "{{used}} de {{limit}} usado(s)", "preview_rows_activated_notice": "Projetos de prévia contêm {{limit}} linhas - ativamos as primeiras {{limit}} de suas {{total}}.", - "preview_limit_title_rows": "Você usou as linhas de prévia deste projeto", - "preview_limit_title_match_operations": "Você usou suas operações de correspondência de prévia", - "preview_limit_title_projects": "A prévia cobre um projeto", - "preview_limit_title_org_projects": "Projetos de organização não estão na prévia", - "preview_limit_title_access": "Acesso ao Mapper está limitado no momento", - "preview_limit_title_custom_algorithms": "Algoritmos hospedados externamente não estão na prévia", - "preview_limit_title_ai_assistant": "O Assistente de IA do OCL ainda não está disponível", - "preview_limit_title_scispacy": "ScispaCy LOINC ainda não está disponível", - "preview_limit_title_rows_not_entitled": "As linhas deste projeto não fazem parte de sua prévia ainda", - "preview_limit_title_match_operations_not_entitled": "Operações de correspondência não fazem parte de sua conta ainda", - "preview_limit_title_projects_not_entitled": "Projetos de mapeamento não fazem parte de sua conta ainda", - "preview_limit_title_generic": "Você atingiu um limite de prévia", - "preview_limit_body_rows": "Sua prévia gratuita cobre {{limit}} linhas com AutoMatch para este projeto - você usou todas as {{limit}}. Nada está bloqueado: continue revisando, editando e exportando este projeto quanto quiser.", - "preview_limit_body_rows_no_count": "O limite de linhas de sua prévia gratuita para este projeto foi atingido. Nada está bloqueado: continue revisando, editando e exportando este projeto quanto quiser.", - "preview_limit_body_match_operations": "Sua prévia gratuita cobre {{limit}} operações de correspondência - você usou todas as {{limit}}. Nada está bloqueado: continue revisando, editando e exportando este projeto quanto quiser.", - "preview_limit_body_match_operations_no_count": "O limite de operações de correspondência de sua prévia gratuita foi atingido. Nada está bloqueado: continue revisando, editando e exportando este projeto quanto quiser.", - "preview_limit_body_projects": "Você já tem um projeto do Mapper ativo. A prévia gratuita cobre um projeto por vez - exclua o que você tem, ou peça mais acesso abaixo.", - "preview_limit_body_projects_no_count": "Você já tem um projeto do Mapper ativo. A prévia gratuita cobre um projeto por vez.", - "preview_limit_body_org_projects": "Projetos de mapeamento pertencentes a organizações não fazem parte da prévia ainda. Crie um projeto sob sua própria conta, ou peça mais acesso abaixo.", - "preview_limit_body_org_projects_no_count": "Projetos de mapeamento pertencentes a organizações não fazem parte da prévia ainda.", - "preview_limit_body_access": "Sua conta não tem acesso ao Mapper no momento. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_access_no_count": "Sua conta não tem acesso ao Mapper no momento.", - "preview_limit_body_custom_algorithms_no_count": "Algoritmos hospedados externamente não fazem parte da prévia ainda. Use os algoritmos integrados do OCL, ou peça mais acesso abaixo.", - "preview_limit_body_ai_assistant": "Sua prévia gratuita cobre {{limit}} chamadas do Assistente de IA do OCL - você usou {{used}} de {{limit}}. A correspondência pode continuar sem análise de IA para as linhas restantes.", - "preview_limit_body_ai_assistant_no_count": "O Assistente de IA do OCL não está disponível para sua conta. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_scispacy_no_count": "A correspondência ScispaCy LOINC não está disponível para sua conta. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_rows_not_entitled_no_count": "Este projeto ainda não tem uma cota de linhas configurada. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_match_operations_not_entitled_no_count": "Sua conta não tem acesso a operações de correspondência configurado ainda. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_projects_not_entitled_no_count": "Sua conta não tem acesso a projetos de mapeamento configurado ainda. Peça mais acesso abaixo e nós resolveremos.", - "preview_limit_body_generic": "Você atingiu um limite de prévia ({{used}} de {{limit}}).", - "preview_limit_body_generic_no_count": "Você atingiu um limite de prévia para o Mapper.", - "preview_limit_hardship_note": "Trabalha com recursos de financiamento (grant) que não cobrem esse custo, ou para um ministério da saúde? Use \"Solicitar mais acesso\" abaixo - se o limite não funcionar para a sua situação, encontraremos algo que funcione.", - "preview_limit_notify_me": "Notifique-me quando os planos forem lançados", - "preview_limit_request_access": "Solicitar mais acesso", + "limit_unavailable_title": "Não disponível para sua conta", + "limit_unavailable_body_access": "Sua conta não tem acesso ao Mapper.", + "limit_unavailable_body_org_projects": "Sua conta não pode criar projetos de mapeamento pertencentes a organizações. Você pode criar um sob sua própria conta.", + "limit_unavailable_body_custom_algorithms": "Sua conta não pode usar algoritmos hospedados externamente. Você pode usar os algoritmos integrados do OCL.", + "limit_unavailable_body_ai_assistant": "O Assistente de IA do OCL não está disponível para sua conta.", + "limit_unavailable_body_scispacy": "A correspondência ScispaCy LOINC não está disponível para sua conta.", + "limit_unavailable_body_rows_not_entitled": "Sua conta não pode executar o AutoMatch nas linhas deste projeto.", + "limit_unavailable_body_match_operations_not_entitled": "Sua conta não pode executar o AutoMatch.", + "limit_unavailable_body_projects_not_entitled": "Sua conta não pode criar projetos de mapeamento.", + "limit_unavailable_body_generic": "Isto não está disponível para sua conta.", "preview_limit_dismiss": "Voltar ao meu projeto", "preview_config_unavailable_title": "Este projeto usa configurações restritas de prévia", "preview_config_unavailable_org_projects": "Projetos de Mapper pertencentes a organizações não fazem parte de sua prévia. Escolha seu namespace pessoal como proprietário do projeto.", @@ -594,21 +570,8 @@ "quota": { "title_quota": "Você atingiu sua cota", "title_cap": "Você atingiu seu limite", - "your_plan": "Seu plano", - "used_of": "{{used}} de {{limit}} usado(s)", - "period_one_time": "Cota única de prévia", - "request_more_access": "Solicitar mais acesso", - "view_pricing": "Ver preços", - "meter": { - "match_operations": "Operações de correspondência", - "ai_assistant_calls": "Chamadas do Assistente de IA do OCL", - "ai_assistant_change_comments": "Comentários de atualização do Assistente de IA do OCL", - "projects": "Projetos de Mapeamento", - "rows": "Linhas por projeto", - "generic": "Uso" - }, "consequence": { - "generic": "Alguns recursos estão pausados até que sua cota seja aumentada.", + "generic": "Alguns recursos estão pausados.", "ai_assistant_calls": { "default": "Recursos que usam o Assistente de IA do OCL estão pausados.", "auto_match": "O AutoMatch continuará rodando sem recomendações do Assistente de IA do OCL." diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index acf56c82..6358494a 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -313,40 +313,16 @@ "preview_quota_ai_calls": "OCL AI 助手调用", "preview_quota_used_of_limit": "已用 {{used}} / {{limit}}", "preview_rows_activated_notice": "预览项目最多包含 {{limit}} 行——我们已激活你 {{total}} 行中的前 {{limit}} 行。", - "preview_limit_title_rows": "本项目的预览行数已用完", - "preview_limit_title_match_operations": "预览匹配操作次数已用完", - "preview_limit_title_projects": "预览版仅支持一个项目", - "preview_limit_title_org_projects": "组织项目不在预览范围内", - "preview_limit_title_access": "当前 Mapper 访问受限", - "preview_limit_title_custom_algorithms": "外部托管算法不在预览范围内", - "preview_limit_title_ai_assistant": "OCL AI 助手暂不可用", - "preview_limit_title_scispacy": "ScispaCy LOINC 暂不可用", - "preview_limit_title_rows_not_entitled": "本项目尚未开通预览行额度", - "preview_limit_title_match_operations_not_entitled": "你的账户尚未开通匹配操作权限", - "preview_limit_title_projects_not_entitled": "你的账户尚未开通映射项目权限", - "preview_limit_title_generic": "已达到预览版限制", - "preview_limit_body_rows": "你的免费预览版覆盖本项目通过 AutoMatch 匹配的 {{limit}} 行 —— 已全部用完。项目未被锁定:你可以继续查看、编辑和导出。", - "preview_limit_body_rows_no_count": "本项目的免费预览行数限制已达到。项目未被锁定:你可以继续查看、编辑和导出。", - "preview_limit_body_match_operations": "你的免费预览版覆盖 {{limit}} 次匹配操作 —— 已全部用完。项目未被锁定:你可以继续查看、编辑和导出。", - "preview_limit_body_match_operations_no_count": "免费预览版的匹配操作限额已达到。项目未被锁定:你可以继续查看、编辑和导出。", - "preview_limit_body_projects": "你已经有一个进行中的 Mapper 项目。免费预览版一次只支持一个项目 —— 删除现有项目,或在下方申请更多访问权限。", - "preview_limit_body_projects_no_count": "你已经有一个进行中的 Mapper 项目。免费预览版一次只支持一个项目。", - "preview_limit_body_org_projects": "组织拥有的映射项目暂未开放给预览版。请改用你的个人账户创建项目,或在下方申请更多访问权限。", - "preview_limit_body_org_projects_no_count": "组织拥有的映射项目暂未开放给预览版。", - "preview_limit_body_access": "你的账户目前没有 Mapper 访问权限。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_access_no_count": "你的账户目前没有 Mapper 访问权限。", - "preview_limit_body_custom_algorithms_no_count": "外部托管算法暂未开放给预览版。请使用 OCL 内置算法,或在下方申请更多访问权限。", - "preview_limit_body_ai_assistant": "你的免费预览版包含 {{limit}} 次 OCL AI 助手调用,已使用 {{used}} / {{limit}}。剩余行仍可继续匹配,但不会进行 AI 分析。", - "preview_limit_body_ai_assistant_no_count": "你的账户暂时无法使用 OCL AI 助手。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_scispacy_no_count": "你的账户暂时无法使用 ScispaCy LOINC 匹配。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_rows_not_entitled_no_count": "本项目尚未配置行额度。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_match_operations_not_entitled_no_count": "你的账户尚未配置匹配操作权限。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_projects_not_entitled_no_count": "你的账户尚未配置映射项目权限。请在下方申请更多访问权限,我们会处理。", - "preview_limit_body_generic": "已达到预览版限制({{used}} / {{limit}})。", - "preview_limit_body_generic_no_count": "已达到 Mapper 预览版的某项限制。", - "preview_limit_hardship_note": "预算紧张,或来自卫生部?点击下方“申请更多访问权限” —— 如果现有额度不适合你的情况,我们会想办法解决。", - "preview_limit_notify_me": "计划上线时通知我", - "preview_limit_request_access": "申请更多访问权限", + "limit_unavailable_title": "你的账户无法使用此功能", + "limit_unavailable_body_access": "你的账户没有 Mapper 访问权限。", + "limit_unavailable_body_org_projects": "你的账户无法创建组织拥有的映射项目。你可以改用个人账户创建项目。", + "limit_unavailable_body_custom_algorithms": "你的账户无法使用外部托管算法。你可以改用 OCL 内置算法。", + "limit_unavailable_body_ai_assistant": "你的账户无法使用 OCL AI 助手。", + "limit_unavailable_body_scispacy": "你的账户无法使用 ScispaCy LOINC 匹配。", + "limit_unavailable_body_rows_not_entitled": "你的账户无法对此项目的行运行 AutoMatch。", + "limit_unavailable_body_match_operations_not_entitled": "你的账户无法运行 AutoMatch。", + "limit_unavailable_body_projects_not_entitled": "你的账户无法创建映射项目。", + "limit_unavailable_body_generic": "你的账户无法使用此功能。", "preview_limit_dismiss": "返回我的项目", "preview_config_unavailable_title": "此项目使用了预览版受限设置", "preview_config_unavailable_org_projects": "组织拥有的 Mapper 项目不在你的预览范围内。请选择个人命名空间作为项目所有者。", @@ -593,21 +569,8 @@ "quota": { "title_quota": "您已达到配额上限", "title_cap": "您已达到使用上限", - "your_plan": "您的套餐", - "used_of": "已使用 {{used}} / {{limit}}", - "period_one_time": "一次性预览额度", - "request_more_access": "申请更多权限", - "view_pricing": "查看价格", - "meter": { - "match_operations": "匹配操作", - "ai_assistant_calls": "OCL AI 助手调用", - "ai_assistant_change_comments": "OCL AI 助手更新备注", - "projects": "映射项目", - "rows": "每个项目的行数", - "generic": "用量" - }, "consequence": { - "generic": "在提高额度之前,部分功能已暂停。", + "generic": "部分功能已暂停。", "ai_assistant_calls": { "default": "使用 OCL AI 助手的功能已暂停。", "auto_match": "AutoMatch 将继续运行,但不再提供 OCL AI 助手的推荐。"