Which features a build contains was not a decision anybody could express. The reference feature was spread directly into the route, API, schema, codec and adapter registries, so shipping without it meant editing five files by hand and hoping nothing still referred to it — and there was no way at all to take it out of service on a running deployment. The removability gate proved the editing worked; nothing made it a choice. There is now one manifest. `VITE_PRODUCT_FEATURES` narrows it at build time and `FEATURE_OVERRIDES` in the runtime document takes an installed feature out of service without a rebuild. Every registry composes from the manifest, and a test fails if a new one forgets to. Both inputs are subtractive, and the vocabulary is what enforces it rather than a check somewhere downstream: the override enum has no `ENABLED`, and a build-time selection naming something the source tree does not declare is refused instead of ignored. A configuration document that could name a feature into existence would be a configuration document choosing which code runs. Disabling is not just hiding. Withdrawing a route from navigation would leave a typed deep link that still mounts the feature, so the router refuses it too and answers with a surface that says the deployment switched it off. The platform overview now distinguishes the three states an operator actually needs: serving, switched off, and not in this build. What this is not: an env var does not shrink the bundle. A static import cannot be undone by a value, and making the import graph itself depend on a configuration string is the thing §3.5 exists to prevent — measured, `none` changes the output by 58 bytes. Physical removal remains FE-GATE-020's job, and the code comments say so rather than implying otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
323 lines
17 KiB
TypeScript
323 lines
17 KiB
TypeScript
import { INSTALLED_MESSAGE_CATALOGS } from "../../features/installed-feature-messages.ts";
|
|
|
|
const PLATFORM_KO_MESSAGES = {
|
|
"common.unavailable": "요청한 문구를 표시할 수 없습니다.",
|
|
"common.processing": "처리 중…",
|
|
"common.appName": "Frontend Skeleton",
|
|
"common.noDisplayValue": "표시 정보 없음",
|
|
"action.retry": "다시 시도",
|
|
"action.reauth": "로그인",
|
|
"action.navigateSafe": "안전한 화면으로 이동",
|
|
"action.reloadOnce": "한 번 새로고침",
|
|
"action.contactSupport": "지원 정보 확인",
|
|
"action.resolveConflict": "충돌 해결",
|
|
"action.confirmMutationApplied": "변경됨으로 확인",
|
|
"action.confirmMutationNotApplied": "변경되지 않음으로 확인",
|
|
"action.continueEditing": "계속 작성",
|
|
"action.discardAndLeave": "변경 버리고 이동",
|
|
"action.signIn": "로그인 시작",
|
|
"action.recoverSession": "세션 복구",
|
|
"action.signOut": "로그아웃",
|
|
"action.goHome": "홈으로 이동",
|
|
"action.closeNamed": "{title} 닫기",
|
|
"action.alertCloseNamed": "{title} 알림 닫기",
|
|
"shell.skipToContent": "본문으로 건너뛰기",
|
|
"shell.menu": "메뉴",
|
|
"shell.closeMenu": "메뉴 닫기",
|
|
"shell.sidebar": "사이드바",
|
|
"shell.primaryNavigation": "주요 탐색",
|
|
"shell.theme": "색상 테마",
|
|
"shell.theme.system": "시스템 테마",
|
|
"shell.theme.light": "라이트 테마",
|
|
"shell.theme.dark": "다크 테마",
|
|
"shell.locale": "언어",
|
|
"shell.locale.ko": "한국어",
|
|
"shell.locale.en": "English",
|
|
"shell.locale.pseudo": "Pseudo",
|
|
"shell.locale.rtl": "RTL smoke",
|
|
"shell.session.authenticated": "인증됨",
|
|
"shell.session.unauthenticated": "로그인 전",
|
|
"shell.session.recoveryPending": "복구 대기",
|
|
"shell.session.integrationFailed": "연동 필요",
|
|
"shell.session.actionFailed": "세션 작업을 완료하지 못했습니다.",
|
|
"route.APP_HOME.navigation": "홈",
|
|
"route.APP_HOME.title": "Clean Architecture Frontend",
|
|
"route.EXAMPLES_PLATFORM.navigation": "플랫폼 구성",
|
|
"route.EXAMPLES_PLATFORM.title": "플랫폼 구성",
|
|
"route.EXAMPLES_UI.navigation": "UI 구성요소",
|
|
"route.EXAMPLES_UI.title": "UI 구성요소",
|
|
"route.EXAMPLES_STATES.navigation": "화면 상태",
|
|
"route.EXAMPLES_STATES.title": "화면 상태",
|
|
"route.EXAMPLES_AUTH.navigation": "인증 연동",
|
|
"route.EXAMPLES_AUTH.title": "인증 연동",
|
|
"route.NOT_FOUND.navigation": "찾을 수 없음",
|
|
"route.NOT_FOUND.title": "페이지를 찾을 수 없습니다.",
|
|
"async.loading": "불러오는 중",
|
|
"async.empty": "표시할 항목이 없습니다.",
|
|
"async.refreshing": "최신 정보를 확인하고 있습니다.",
|
|
"async.staleDegraded": "기존 정보를 표시하고 있습니다.",
|
|
"async.mutationPending": "변경 사항을 저장하고 있습니다.",
|
|
"async.mutationEffectUnknown": "변경 결과를 확인할 수 없습니다.",
|
|
"async.mutationConflict": "다른 변경과 충돌했습니다.",
|
|
"access.auth.eyebrow": "401 · 인증 필요",
|
|
"access.auth.title": "로그인이 필요합니다.",
|
|
"access.auth.description":
|
|
"세션을 시작한 뒤 이전 작업을 안전하게 계속할 수 있습니다.",
|
|
"access.forbidden.eyebrow": "403 · 권한 없음",
|
|
"access.forbidden.title": "접근할 수 없습니다.",
|
|
"access.forbidden.description":
|
|
"권한을 확인하거나 접근 가능한 화면으로 이동하세요.",
|
|
"access.notFound.eyebrow": "404 · 찾을 수 없음",
|
|
"access.notFound.title": "요청한 화면이 없습니다.",
|
|
"access.notFound.description":
|
|
"주소를 확인하거나 시작 화면으로 돌아가세요.",
|
|
"route.loading": "화면을 준비하고 있습니다.",
|
|
"route.loadingNamed": "{title} 로딩 중",
|
|
"route.failure.title": "화면을 표시하지 못했습니다.",
|
|
"route.failure.description":
|
|
"잠시 후 다시 시도해 주세요. 문제가 계속되면 운영 지원 참조 정보를 확인하세요.",
|
|
"route.invalid.title": "올바르지 않은 주소입니다.",
|
|
"route.invalid.description": "주소의 경로 또는 검색 조건을 확인해 주세요.",
|
|
"route.invalid.action": "안전한 탐색 링크를 사용해 주세요.",
|
|
"route.disabledFeature.title": "현재 사용할 수 없는 기능입니다.",
|
|
"route.disabledFeature.description":
|
|
"이 기능은 배포 설정에서 중지되어 있습니다. 코드에는 포함되어 있으며 운영자가 다시 켤 수 있습니다.",
|
|
"route.disabledFeature.action": "다른 탐색 링크를 사용해 주세요.",
|
|
"route.auth.integration.title": "로그인 연동이 필요합니다.",
|
|
"route.auth.integration.description":
|
|
"외부 인증 소유자가 연결되면 이 보호 라우트를 사용할 수 있습니다.",
|
|
"route.auth.recovering.title": "세션을 복구하고 있습니다.",
|
|
"route.auth.required.title": "세션이 필요합니다.",
|
|
"route.auth.recovering.description":
|
|
"기존 세션 확인을 계속하려면 복구를 실행하세요.",
|
|
"route.auth.required.description":
|
|
"이 화면은 인증 연동 지점을 확인하기 위한 보호 라우트입니다.",
|
|
"route.documentTitle": "{title} · {appName}",
|
|
"chunk.checking": "새 릴리스 정보를 확인하고 있습니다.",
|
|
"chunk.reloadOnce": "새 버전으로 한 번만 전환합니다.",
|
|
"chunk.failure.title": "화면 자산을 복구하지 못했습니다.",
|
|
"chunk.failure.description":
|
|
"문제가 계속되면 배포 상태와 지원 참조 정보를 확인해 주세요.",
|
|
"page.notFound.title": "페이지를 찾을 수 없습니다.",
|
|
"page.notFound.description":
|
|
"주소를 확인하거나 준비된 시작 화면으로 돌아가세요.",
|
|
"template.breadcrumb": "현재 위치",
|
|
"template.relatedInformation": "관련 정보",
|
|
"template.searchAndFilter": "검색과 필터",
|
|
"template.pagination": "페이지 탐색",
|
|
"template.summary": "요약 정보",
|
|
"template.dangerActions": "위험 작업",
|
|
"template.supportReference": "지원 참조: {reference}",
|
|
"form.errorSummary": "입력 내용을 확인해 주세요.",
|
|
"form.unsaved.title": "저장하지 않은 변경이 있습니다.",
|
|
"form.unsaved.description": "이 화면을 떠나면 입력한 내용이 사라집니다.",
|
|
"form.validation.required": "필수 입력값입니다.",
|
|
"form.validation.tooSmall": "입력값이 너무 짧습니다.",
|
|
"form.validation.tooBig": "입력값이 너무 깁니다.",
|
|
"form.validation.invalidType": "입력 형식을 확인해 주세요.",
|
|
"form.validation.invalidFormat": "입력 형식을 확인해 주세요.",
|
|
"form.validation.invalidValue": "허용된 값을 선택해 주세요.",
|
|
"form.validation.unknown": "입력값을 확인해 주세요.",
|
|
"form.validation.retry": "입력값을 다시 확인해 주세요.",
|
|
"form.validation.unknownField":
|
|
"서버가 확인하지 못한 입력 항목이 있습니다.",
|
|
"form.validation.configuration": "입력 구성을 다시 확인해 주세요.",
|
|
"form.conflict":
|
|
"다른 변경과 충돌했습니다. 입력은 유지되었으니 최신 상태를 확인해 주세요.",
|
|
"form.unavailable": "저장하지 못했습니다. 잠시 후 다시 시도해 주세요.",
|
|
"form.remaining": "{count}자 남음",
|
|
"toast.region": "알림",
|
|
"error.network_unreachable": "네트워크에 연결할 수 없습니다.",
|
|
"error.request_timeout": "요청 시간이 초과되었습니다.",
|
|
"error.auth_required": "계속하려면 로그인이 필요합니다.",
|
|
"error.auth_integration_failure": "로그인 연동을 사용할 수 없습니다.",
|
|
"error.forbidden": "이 작업을 수행할 권한이 없습니다.",
|
|
"error.not_found": "요청한 항목을 찾을 수 없습니다.",
|
|
"error.rate_limited": "요청이 많습니다. 잠시 후 다시 시도해 주세요.",
|
|
"error.server_failure": "요청을 완료하지 못했습니다.",
|
|
"error.chunk_load_failure": "새 화면 파일을 불러오지 못했습니다.",
|
|
"error.build_mismatch": "현재 화면과 활성 빌드가 일치하지 않습니다.",
|
|
"error.config_mismatch": "런타임 설정 버전이 현재 화면과 일치하지 않습니다.",
|
|
"error.api_contract_mismatch": "API 계약 버전이 현재 화면과 일치하지 않습니다.",
|
|
"error.release_mismatch": "현재 화면과 활성 릴리스가 일치하지 않습니다.",
|
|
"error.asset_mismatch": "화면 자산 구성이 현재 릴리스와 일치하지 않습니다.",
|
|
"error.render_failure": "화면을 표시하지 못했습니다.",
|
|
"error.unknown_failure": "예상하지 못한 문제가 발생했습니다.",
|
|
"boot.failure.title": "애플리케이션을 시작할 수 없습니다.",
|
|
"boot.field.error": "오류",
|
|
"boot.field.code": "코드",
|
|
"boot.field.build": "빌드",
|
|
"boot.field.configSchema": "설정 스키마",
|
|
"boot.field.release": "릴리스",
|
|
"boot.supportReference": "지원 참조: {reference}",
|
|
} as const;
|
|
|
|
export const KO_MESSAGES = {
|
|
...PLATFORM_KO_MESSAGES,
|
|
...INSTALLED_MESSAGE_CATALOGS["ko-KR"],
|
|
} as const;
|
|
|
|
export type MessageKey = keyof typeof KO_MESSAGES;
|
|
|
|
const PLATFORM_EN_MESSAGES = {
|
|
"common.unavailable": "This message is unavailable.",
|
|
"common.processing": "Processing…",
|
|
"common.appName": "Frontend Skeleton",
|
|
"common.noDisplayValue": "No display value",
|
|
"action.retry": "Try again",
|
|
"action.reauth": "Sign in",
|
|
"action.navigateSafe": "Go to an available page",
|
|
"action.reloadOnce": "Reload once",
|
|
"action.contactSupport": "View support information",
|
|
"action.resolveConflict": "Resolve conflict",
|
|
"action.confirmMutationApplied": "Confirm the change was applied",
|
|
"action.confirmMutationNotApplied": "Confirm the change was not applied",
|
|
"action.continueEditing": "Continue editing",
|
|
"action.discardAndLeave": "Discard and leave",
|
|
"action.signIn": "Start sign-in",
|
|
"action.recoverSession": "Recover session",
|
|
"action.signOut": "Sign out",
|
|
"action.goHome": "Go home",
|
|
"action.closeNamed": "Close {title}",
|
|
"action.alertCloseNamed": "Dismiss {title} alert",
|
|
"shell.skipToContent": "Skip to content",
|
|
"shell.menu": "Menu",
|
|
"shell.closeMenu": "Close menu",
|
|
"shell.sidebar": "Sidebar",
|
|
"shell.primaryNavigation": "Primary navigation",
|
|
"shell.theme": "Color theme",
|
|
"shell.theme.system": "System theme",
|
|
"shell.theme.light": "Light theme",
|
|
"shell.theme.dark": "Dark theme",
|
|
"shell.locale": "Language",
|
|
"shell.locale.ko": "한국어",
|
|
"shell.locale.en": "English",
|
|
"shell.locale.pseudo": "Pseudo",
|
|
"shell.locale.rtl": "RTL smoke",
|
|
"shell.session.authenticated": "Authenticated",
|
|
"shell.session.unauthenticated": "Signed out",
|
|
"shell.session.recoveryPending": "Recovery pending",
|
|
"shell.session.integrationFailed": "Integration required",
|
|
"shell.session.actionFailed": "The session action could not be completed.",
|
|
"route.APP_HOME.navigation": "Home",
|
|
"route.APP_HOME.title": "Clean Architecture Frontend",
|
|
"route.EXAMPLES_PLATFORM.navigation": "Platform composition",
|
|
"route.EXAMPLES_PLATFORM.title": "Platform composition",
|
|
"route.EXAMPLES_UI.navigation": "UI components",
|
|
"route.EXAMPLES_UI.title": "UI components",
|
|
"route.EXAMPLES_STATES.navigation": "Screen states",
|
|
"route.EXAMPLES_STATES.title": "Screen states",
|
|
"route.EXAMPLES_AUTH.navigation": "Authentication",
|
|
"route.EXAMPLES_AUTH.title": "Authentication",
|
|
"route.NOT_FOUND.navigation": "Not found",
|
|
"route.NOT_FOUND.title": "Page not found",
|
|
"async.loading": "Loading",
|
|
"async.empty": "There are no items to display.",
|
|
"async.refreshing": "Checking for the latest information.",
|
|
"async.staleDegraded": "Showing previously loaded information.",
|
|
"async.mutationPending": "Saving changes.",
|
|
"async.mutationEffectUnknown": "The result of the change is unknown.",
|
|
"async.mutationConflict": "The change conflicts with another update.",
|
|
"access.auth.eyebrow": "401 · Authentication required",
|
|
"access.auth.title": "Sign-in is required.",
|
|
"access.auth.description":
|
|
"Start a session to safely continue the previous task.",
|
|
"access.forbidden.eyebrow": "403 · Access denied",
|
|
"access.forbidden.title": "You cannot access this page.",
|
|
"access.forbidden.description":
|
|
"Check your permissions or go to an available page.",
|
|
"access.notFound.eyebrow": "404 · Not found",
|
|
"access.notFound.title": "The requested page does not exist.",
|
|
"access.notFound.description": "Check the address or return to the start page.",
|
|
"route.loading": "Preparing the page.",
|
|
"route.loadingNamed": "Loading {title}",
|
|
"route.failure.title": "The page could not be displayed.",
|
|
"route.failure.description":
|
|
"Try again shortly. If the problem continues, check the support reference.",
|
|
"route.invalid.title": "This address is invalid.",
|
|
"route.invalid.description": "Check the path and search parameters.",
|
|
"route.invalid.action": "Use a safe navigation link.",
|
|
"route.disabledFeature.title": "This feature is not available right now.",
|
|
"route.disabledFeature.description":
|
|
"The deployment configuration has switched it off. It is still part of this build and an operator can switch it back on.",
|
|
"route.disabledFeature.action": "Use another navigation link.",
|
|
"route.auth.integration.title": "Sign-in integration is required.",
|
|
"route.auth.integration.description":
|
|
"This protected route is available after an external authentication owner is connected.",
|
|
"route.auth.recovering.title": "Recovering the session.",
|
|
"route.auth.required.title": "A session is required.",
|
|
"route.auth.recovering.description":
|
|
"Run recovery to continue checking the existing session.",
|
|
"route.auth.required.description":
|
|
"This protected route demonstrates the authentication integration seam.",
|
|
"route.documentTitle": "{title} · {appName}",
|
|
"chunk.checking": "Checking the new release information.",
|
|
"chunk.reloadOnce": "Switching to the new version once.",
|
|
"chunk.failure.title": "The page assets could not be recovered.",
|
|
"chunk.failure.description":
|
|
"If the problem continues, check deployment status and support information.",
|
|
"page.notFound.title": "Page not found.",
|
|
"page.notFound.description": "Check the address or return to the start page.",
|
|
"template.breadcrumb": "Current location",
|
|
"template.relatedInformation": "Related information",
|
|
"template.searchAndFilter": "Search and filters",
|
|
"template.pagination": "Pagination",
|
|
"template.summary": "Summary information",
|
|
"template.dangerActions": "Dangerous actions",
|
|
"template.supportReference": "Support reference: {reference}",
|
|
"form.errorSummary": "Review the entered information.",
|
|
"form.unsaved.title": "You have unsaved changes.",
|
|
"form.unsaved.description":
|
|
"The entered information will be lost if you leave this page.",
|
|
"form.validation.required": "This field is required.",
|
|
"form.validation.tooSmall": "This value is too short.",
|
|
"form.validation.tooBig": "This value is too long.",
|
|
"form.validation.invalidType": "Check the input type.",
|
|
"form.validation.invalidFormat": "Check the input format.",
|
|
"form.validation.invalidValue": "Choose an allowed value.",
|
|
"form.validation.unknown": "Check the entered value.",
|
|
"form.validation.retry": "Review the entered values.",
|
|
"form.validation.unknownField":
|
|
"The server reported an unrecognized input field.",
|
|
"form.validation.configuration": "Review the input configuration.",
|
|
"form.conflict":
|
|
"Another update conflicts with this change. Your input has been preserved.",
|
|
"form.unavailable": "The change could not be saved. Try again shortly.",
|
|
"form.remaining": "{count} characters remaining",
|
|
"toast.region": "Notifications",
|
|
"error.network_unreachable": "The network is unavailable.",
|
|
"error.request_timeout": "The request timed out.",
|
|
"error.auth_required": "Sign in to continue.",
|
|
"error.auth_integration_failure": "Sign-in integration is unavailable.",
|
|
"error.forbidden": "You are not allowed to perform this action.",
|
|
"error.not_found": "The requested item could not be found.",
|
|
"error.rate_limited": "Too many requests were made. Try again shortly.",
|
|
"error.server_failure": "The request could not be completed.",
|
|
"error.chunk_load_failure": "The new page assets could not be loaded.",
|
|
"error.build_mismatch": "The active build does not match this page.",
|
|
"error.config_mismatch": "The runtime configuration does not match this page.",
|
|
"error.api_contract_mismatch": "The API contract does not match this page.",
|
|
"error.release_mismatch": "The active release does not match this page.",
|
|
"error.asset_mismatch": "The page assets do not match this release.",
|
|
"error.render_failure": "The page could not be displayed.",
|
|
"error.unknown_failure": "An unexpected problem occurred.",
|
|
"boot.failure.title": "The application could not start.",
|
|
"boot.field.error": "Error",
|
|
"boot.field.code": "Code",
|
|
"boot.field.build": "Build",
|
|
"boot.field.configSchema": "Configuration schema",
|
|
"boot.field.release": "Release",
|
|
"boot.supportReference": "Support reference: {reference}",
|
|
} as const satisfies Record<keyof typeof PLATFORM_KO_MESSAGES, string>;
|
|
|
|
export const EN_MESSAGES = {
|
|
...PLATFORM_EN_MESSAGES,
|
|
...INSTALLED_MESSAGE_CATALOGS["en-US"],
|
|
} as const satisfies Record<MessageKey, string>;
|
|
|
|
export const MESSAGE_CATALOGS = Object.freeze({
|
|
"ko-KR": KO_MESSAGES,
|
|
"en-US": EN_MESSAGES,
|
|
"ar-EG": EN_MESSAGES,
|
|
});
|