forked from baron/baron-sso
i18n 값 품질 검사 추가 및 devfront locale placeholder 정리
This commit is contained in:
@@ -60,6 +60,12 @@ jobs:
|
||||
node tools/i18n-scanner/report.js
|
||||
cat reports/i18n-report.txt
|
||||
|
||||
- name: i18n value quality check
|
||||
run: |
|
||||
mkdir -p reports
|
||||
node tools/i18n-scanner/value-check.js
|
||||
cat reports/i18n-value-report.txt
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
|
||||
10
Makefile
10
Makefile
@@ -115,7 +115,7 @@ PLAYWRIGHT_INSTALL_ALL := npx playwright install --with-deps
|
||||
PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install --with-deps chromium
|
||||
endif
|
||||
|
||||
.PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-userfront-e2e-tests
|
||||
.PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-i18n-values code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-userfront-e2e-tests
|
||||
|
||||
CODE_CHECK_TEST_JOBS ?= 1
|
||||
PLAYWRIGHT_WORKERS ?= 1
|
||||
@@ -124,7 +124,7 @@ FLUTTER_TEST_CONCURRENCY ?= 1
|
||||
code-check: code-check-lint code-check-test-jobs
|
||||
@echo "code-check complete."
|
||||
|
||||
code-check-lint: code-check-i18n code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint
|
||||
code-check-lint: code-check-i18n code-check-i18n-values code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint
|
||||
|
||||
code-check-test-jobs:
|
||||
@echo "==> run CI-equivalent test jobs (parallel)"
|
||||
@@ -142,6 +142,12 @@ code-check-i18n:
|
||||
node tools/i18n-scanner/report.js
|
||||
@cat reports/i18n-report.txt
|
||||
|
||||
code-check-i18n-values:
|
||||
@echo "==> i18n value quality check"
|
||||
@mkdir -p reports
|
||||
node tools/i18n-scanner/value-check.js
|
||||
@cat reports/i18n-value-report.txt
|
||||
|
||||
code-check-go-lint:
|
||||
@echo "==> go lint/format check"
|
||||
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||
|
||||
@@ -987,7 +987,7 @@ type = "Type"
|
||||
type_boolean = "Boolean"
|
||||
type_date = "Date"
|
||||
type_number = "Number"
|
||||
type_text = "Text"
|
||||
type_text = "Text Value"
|
||||
validation_placeholder = "Regex Pattern (Optional)"
|
||||
|
||||
[ui.admin.tenants.sub]
|
||||
@@ -1340,7 +1340,7 @@ add_title = "Add Identity Provider"
|
||||
add_btn = "Add Provider"
|
||||
|
||||
[ui.dev.clients.general.identity]
|
||||
description = "Description"
|
||||
description = "Application Description"
|
||||
description_placeholder = "Description Placeholder"
|
||||
logo = "App Logo URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
@@ -1360,7 +1360,7 @@ name_placeholder = "e.g. profile"
|
||||
title = "Scopes"
|
||||
|
||||
[ui.dev.clients.general.scopes.table]
|
||||
description = "Description"
|
||||
description = "Scope Description"
|
||||
mandatory = "Mandatory"
|
||||
name = "Scope Name"
|
||||
delete = "Delete"
|
||||
|
||||
@@ -103,9 +103,9 @@ count = "총 {{count}}개 API 키"
|
||||
|
||||
[msg.admin.audit]
|
||||
empty = "아직 수집된 감사 로그가 없습니다."
|
||||
end = "End of audit feed"
|
||||
load_error = "Error loading logs: {{error}}"
|
||||
loading = "Loading audit logs..."
|
||||
end = "감사 로그의 마지막입니다."
|
||||
load_error = "감사 로그를 불러오지 못했습니다: {{error}}"
|
||||
loading = "감사 로그를 불러오는 중..."
|
||||
subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다."
|
||||
|
||||
[msg.admin.audit.filters]
|
||||
@@ -329,9 +329,9 @@ subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합
|
||||
deleted = "앱이 삭제되었습니다."
|
||||
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
delete_error = "삭제 실패: {{error}}"
|
||||
load_error = "Error loading clients: {{error}}"
|
||||
loading = "Loading apps..."
|
||||
showing = "Showing {{shown}} of {{total}} apps"
|
||||
load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
|
||||
loading = "앱 정보를 불러오는 중..."
|
||||
showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다."
|
||||
|
||||
[msg.dev.clients.consents]
|
||||
empty = "조회된 동의 내역이 없습니다."
|
||||
@@ -987,7 +987,7 @@ type = "타입"
|
||||
type_boolean = "Boolean"
|
||||
type_date = "Date"
|
||||
type_number = "Number"
|
||||
type_text = "Text"
|
||||
type_text = "텍스트"
|
||||
validation_placeholder = "정규표현식 (선택 사항)"
|
||||
|
||||
[ui.admin.tenants.sub]
|
||||
@@ -1298,8 +1298,8 @@ user = "사용자"
|
||||
[ui.dev.clients.details]
|
||||
|
||||
[ui.dev.clients.details.credentials]
|
||||
client_id = "Client ID"
|
||||
client_secret = "Client Secret"
|
||||
client_id = "클라이언트 ID"
|
||||
client_secret = "클라이언트 시크릿"
|
||||
title = "앱 자격 증명"
|
||||
|
||||
[ui.dev.clients.details.endpoints]
|
||||
@@ -1308,7 +1308,7 @@ title = "OIDC 엔드포인트"
|
||||
|
||||
[ui.dev.clients.details.redirect]
|
||||
callback_label = "인증 콜백 URL"
|
||||
label = "Redirect URIs"
|
||||
label = "리디렉션 URI"
|
||||
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
|
||||
save = "Redirect URIs 저장"
|
||||
title = "리디렉션 URI 설정"
|
||||
@@ -1345,11 +1345,11 @@ logo = "앱 로고 URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
logo_preview = "로고 미리보기"
|
||||
name = "앱 이름"
|
||||
name_placeholder = "My Awesome Application"
|
||||
name_placeholder = "예: 멋진 애플리케이션"
|
||||
title = "애플리케이션 정보"
|
||||
|
||||
[ui.dev.clients.general.redirect]
|
||||
label = "Redirect URIs"
|
||||
label = "리디렉션 URI"
|
||||
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
|
||||
|
||||
[ui.dev.clients.general.scopes]
|
||||
@@ -1401,7 +1401,7 @@ profile = "기본 프로필 정보 접근"
|
||||
[ui.dev.clients.table]
|
||||
actions = "액션"
|
||||
application = "애플리케이션"
|
||||
client_id = "Client ID"
|
||||
client_id = "클라이언트 ID"
|
||||
created_at = "생성일"
|
||||
status = "상태"
|
||||
type = "유형"
|
||||
@@ -1571,7 +1571,7 @@ qr_scan = "QR 스캔"
|
||||
[ui.userfront.profile]
|
||||
department_empty = "소속 정보 없음"
|
||||
manage = "프로필 관리"
|
||||
user_fallback = "User"
|
||||
user_fallback = "사용자"
|
||||
|
||||
[ui.userfront.profile.field]
|
||||
affiliation = "구분"
|
||||
|
||||
229
locales/en.toml
229
locales/en.toml
@@ -44,24 +44,24 @@ slow_down = "Requests are too frequent. Please try again shortly."
|
||||
[err.userfront]
|
||||
|
||||
[err.userfront.auth_proxy]
|
||||
consent_accept = "Consent Accept"
|
||||
consent_fetch = "Consent Fetch"
|
||||
consent_reject = "Consent Reject"
|
||||
linked_app_revoke = "Linked App Revoke"
|
||||
login_failed = "Login Failed"
|
||||
consent_accept = "Failed to accept the consent request."
|
||||
consent_fetch = "Failed to load consent details."
|
||||
consent_reject = "Failed to reject the consent request."
|
||||
linked_app_revoke = "Failed to revoke the linked application."
|
||||
login_failed = "Login failed."
|
||||
oidc_accept = "OIDC Accept"
|
||||
password_reset_complete = "Password Reset Complete"
|
||||
password_reset_init = "Password Reset Init"
|
||||
password_reset_complete = "Failed to complete the password reset."
|
||||
password_reset_init = "Failed to start the password reset."
|
||||
|
||||
[err.userfront.profile]
|
||||
load_failed = "Load Failed"
|
||||
load_failed = "Failed to load the profile."
|
||||
password_change_failed = "Password Change Failed"
|
||||
send_code_failed = "Send Code Failed"
|
||||
update_failed = "Update Failed"
|
||||
verify_code_failed = "Verify Code Failed"
|
||||
send_code_failed = "Failed to send the verification code."
|
||||
update_failed = "Failed to update the profile."
|
||||
verify_code_failed = "Verification failed."
|
||||
|
||||
[err.userfront.session]
|
||||
missing = "Missing"
|
||||
missing = "No active session was found."
|
||||
|
||||
[msg]
|
||||
|
||||
@@ -78,40 +78,40 @@ forbidden = "You do not have permission to perform this action."
|
||||
[msg.admin.api_keys]
|
||||
|
||||
[msg.admin.api_keys.create]
|
||||
error = "Error"
|
||||
name_required = "Name Required"
|
||||
scope_required = "Scope Required"
|
||||
scopes_count = "Scopes Count"
|
||||
scopes_hint = "Scopes Hint"
|
||||
subtitle = "Subtitle"
|
||||
error = "Failed to create the API key."
|
||||
name_required = "Name is required."
|
||||
scope_required = "Select at least one scope."
|
||||
scopes_count = "{{count}} scopes will be assigned."
|
||||
scopes_hint = "Choose the scopes to grant to this API key."
|
||||
subtitle = "Create and issue an API key for machine-to-machine communication."
|
||||
|
||||
[msg.admin.api_keys.create.success]
|
||||
copy_hint = "Copy Hint"
|
||||
notice = "Notice"
|
||||
notice_emphasis = "Notice Emphasis"
|
||||
notice_suffix = "Notice Suffix"
|
||||
copy_hint = "Copy the secret now. It will not be shown again."
|
||||
notice = "The generated secret is displayed only once."
|
||||
notice_emphasis = "Store it in a secure location."
|
||||
notice_suffix = "Rotate the key immediately if you think it has been exposed."
|
||||
|
||||
[msg.admin.api_keys.list]
|
||||
delete_confirm = "Delete Confirm"
|
||||
empty = "Empty"
|
||||
fetch_error = "Fetch Error"
|
||||
subtitle = "Subtitle"
|
||||
delete_confirm = "Are you sure you want to delete this API key?"
|
||||
empty = "No API keys have been issued yet."
|
||||
fetch_error = "Failed to load the API key list."
|
||||
subtitle = "View and manage the API keys issued for server-to-server communication."
|
||||
|
||||
[msg.admin.api_keys.list.registry]
|
||||
count = "Count"
|
||||
count = "{{count}} API keys loaded."
|
||||
|
||||
[msg.admin.audit]
|
||||
empty = "Empty"
|
||||
empty = "No audit logs have been collected yet."
|
||||
end = "End of audit feed"
|
||||
load_error = "Error loading logs: {{error}}"
|
||||
loading = "Loading audit logs..."
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Review command-driven ClickHouse audit logs from the admin workspace."
|
||||
|
||||
[msg.admin.audit.filters]
|
||||
empty = "Empty"
|
||||
empty = "No filters applied."
|
||||
|
||||
[msg.admin.audit.registry]
|
||||
count = "Count"
|
||||
count = "{{count}} logs loaded."
|
||||
|
||||
[msg.admin.groups]
|
||||
|
||||
@@ -120,32 +120,32 @@ description = "Adds a new organization unit such as a department or team."
|
||||
title = "Create New Organization Unit"
|
||||
|
||||
[msg.admin.groups.list]
|
||||
create_error = "Create Failed"
|
||||
create_success = "Create Success"
|
||||
delete_confirm = "Delete Confirm"
|
||||
delete_error = "Delete Error"
|
||||
delete_success = "Delete Success"
|
||||
empty = "Empty"
|
||||
create_error = "Failed to create the organization unit."
|
||||
create_success = "Organization unit created successfully."
|
||||
delete_confirm = "Are you sure you want to delete this organization unit?"
|
||||
delete_error = "Failed to delete the organization unit."
|
||||
delete_success = "Organization unit deleted successfully."
|
||||
empty = "No organization units have been registered yet."
|
||||
import_error = "Import Error"
|
||||
import_success = "Import Success"
|
||||
loading = "Loading..."
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Manage departments and teams under the current tenant."
|
||||
|
||||
[msg.admin.groups.members]
|
||||
add_success = "Add Success"
|
||||
count = "Count"
|
||||
empty = "Empty"
|
||||
remove_confirm = "Remove Confirm"
|
||||
remove_success = "Remove Success"
|
||||
title = "Title"
|
||||
add_success = "Member added successfully."
|
||||
count = "{{count}} members loaded."
|
||||
empty = "No members are assigned to this organization unit."
|
||||
remove_confirm = "Are you sure you want to remove this member?"
|
||||
remove_success = "Member removed successfully."
|
||||
title = "Member Management"
|
||||
|
||||
[msg.admin.groups.prompt]
|
||||
user_id = "User Id"
|
||||
user_id = "Enter the user's UUID to add:"
|
||||
|
||||
[msg.admin.groups.roles]
|
||||
assign_success = "Assign Success"
|
||||
description = "Description"
|
||||
empty = "Empty"
|
||||
description = "Assign or revoke roles for members of this organization unit."
|
||||
empty = "No roles have been assigned yet."
|
||||
remove_confirm = "Are you sure you want to revoke this role?"
|
||||
remove_success = "Role revoked successfully."
|
||||
|
||||
@@ -153,8 +153,8 @@ remove_success = "Role revoked successfully."
|
||||
subtitle = "Tenant isolation & least privilege by default"
|
||||
|
||||
[msg.admin.notice]
|
||||
idp_policy = "IDP Policy"
|
||||
scope = "Scope"
|
||||
idp_policy = "IDP management keys are only used through server-side wrapper APIs with audit logging and rate limits enabled."
|
||||
scope = "Administrative features are exposed only within the /admin namespace."
|
||||
|
||||
[msg.admin.org]
|
||||
hover_member_info = "Hover to see member details."
|
||||
@@ -163,19 +163,19 @@ import_error = "An error occurred during organization chart import."
|
||||
import_success = "Organization chart imported successfully."
|
||||
|
||||
[msg.admin.overview]
|
||||
description = "Description"
|
||||
description = "Review shared metrics and policy status across all tenants in one place."
|
||||
idp_fallback = "Fallback: Descope"
|
||||
idp_primary = "IDP: Ory primary"
|
||||
|
||||
[msg.admin.overview.playbook]
|
||||
description = "Description"
|
||||
idp_body = "IDP Body"
|
||||
description = "Operational guardrails and architecture decisions for the admin control plane."
|
||||
idp_body = "All IDP calls are routed through the backend only. Hydra and Kratos admin ports are never exposed publicly."
|
||||
idp_title = "Backend-only IDP access"
|
||||
tenant_body = "Tenant Body"
|
||||
tenant_body = "Tenant headers and audit logging are enabled by default and can later be extended with Keto policies."
|
||||
tenant_title = "Tenant isolation"
|
||||
|
||||
[msg.admin.overview.quick_links]
|
||||
description = "Description"
|
||||
description = "Jump to the most frequently used administrative workflows."
|
||||
|
||||
[msg.admin.overview.summary]
|
||||
audit_events_24h = "24h Audit Events"
|
||||
@@ -184,23 +184,23 @@ policy_gate = "Policy Gate Status"
|
||||
total_tenants = "Total Tenants"
|
||||
|
||||
[msg.admin.tenants]
|
||||
approve_confirm = "Approve Confirm"
|
||||
approve_success = "Approve Success"
|
||||
approve_confirm = "Do you want to approve this tenant?"
|
||||
approve_success = "Tenant approved successfully."
|
||||
delete_confirm = "Delete Tenant \"{{name}}\"?"
|
||||
delete_success = "Tenant deleted."
|
||||
empty = "Empty"
|
||||
fetch_error = "Fetch Error"
|
||||
empty = "No tenants have been registered yet."
|
||||
fetch_error = "Failed to load the tenant list."
|
||||
missing_id = "No Tenant ID."
|
||||
not_found = "Tenant not found."
|
||||
remove_sub_confirm = 'Remove tenant "{{name}}" from sub-tenants?'
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Review registered tenants and manage their current status."
|
||||
|
||||
[msg.admin.tenants.admins]
|
||||
add_success = "Add Success"
|
||||
empty = "Empty"
|
||||
remove_confirm = "Remove Confirm"
|
||||
remove_success = "Remove Success"
|
||||
subtitle = "Subtitle"
|
||||
add_success = "Tenant admin added successfully."
|
||||
empty = "No tenant admins are assigned yet."
|
||||
remove_confirm = "Are you sure you want to remove this tenant admin?"
|
||||
remove_success = "Tenant admin removed successfully."
|
||||
subtitle = "Manage the administrators assigned to this tenant."
|
||||
remove_last = "Cannot remove the last admin."
|
||||
remove_self = "Cannot remove yourself."
|
||||
|
||||
@@ -214,17 +214,17 @@ remove_last = "Cannot remove the last owner."
|
||||
remove_self = "Cannot remove yourself."
|
||||
|
||||
[msg.admin.tenants.create]
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Enter the minimum required information to create a tenant."
|
||||
|
||||
[msg.admin.tenants.create.form]
|
||||
domains_help = "Users with these email domains will be automatically assigned to this tenant."
|
||||
|
||||
[msg.admin.tenants.create.memo]
|
||||
body = "Body"
|
||||
subtitle = "Subtitle"
|
||||
body = "Leave operational notes or policy reminders for this tenant."
|
||||
subtitle = "Capture internal policy notes for administrators."
|
||||
|
||||
[msg.admin.tenants.create.profile]
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Set the basic tenant profile information."
|
||||
|
||||
[msg.admin.tenants.members]
|
||||
desc = "View the list of users belonging to this organization."
|
||||
@@ -232,7 +232,7 @@ empty = "No members found."
|
||||
limit_notice = "Showing members from the first 10 descendant organizations due to size limits."
|
||||
|
||||
[msg.admin.tenants.registry]
|
||||
count = "Count"
|
||||
count = "{{count}} tenants loaded."
|
||||
|
||||
[msg.admin.tenants.schema]
|
||||
empty = "No custom fields defined. Click \"Add Field\" to begin."
|
||||
@@ -243,8 +243,8 @@ update_success = "Schema updated successfully"
|
||||
forbidden_desc = "Only administrators can access user schema settings."
|
||||
|
||||
[msg.admin.tenants.sub]
|
||||
empty = "Empty"
|
||||
subtitle = "Subtitle"
|
||||
empty = "No child tenants are connected."
|
||||
subtitle = "Review and manage child tenants linked under this tenant."
|
||||
|
||||
[msg.admin.users]
|
||||
|
||||
@@ -266,13 +266,13 @@ password_required = "Password Required"
|
||||
success = "User created successfully."
|
||||
|
||||
[msg.admin.users.create.account]
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Fill in the account details required to create the user."
|
||||
|
||||
[msg.admin.users.create.form]
|
||||
email_required = "Email Required"
|
||||
field_invalid = "Invalid {{label}} format."
|
||||
field_required = "{{label}} is required."
|
||||
name_required = "Name Required"
|
||||
name_required = "Name is required."
|
||||
password_auto_help = "Password Auto Help"
|
||||
password_manual_help = "Password Manual Help"
|
||||
role_help = "Role Help"
|
||||
@@ -290,26 +290,26 @@ password_generated = "A secure password has been generated."
|
||||
|
||||
[msg.admin.users.detail.form]
|
||||
field_required = "Required."
|
||||
name_required = "Name Required"
|
||||
name_required = "Name is required."
|
||||
|
||||
[msg.admin.users.detail.security]
|
||||
password_hint = "Password Hint"
|
||||
|
||||
[msg.admin.users.list]
|
||||
delete_confirm = "Delete Confirm"
|
||||
empty = "Empty"
|
||||
fetch_error = "Fetch Error"
|
||||
subtitle = "Subtitle"
|
||||
delete_confirm = "Are you sure you want to delete the selected user?"
|
||||
empty = "No users match the current filters."
|
||||
fetch_error = "Failed to load the user list."
|
||||
subtitle = "Search and manage users registered in the current tenant."
|
||||
|
||||
[msg.admin.users.list.columns]
|
||||
description = "Select columns to display in the table."
|
||||
no_custom = "No custom fields defined for this tenant."
|
||||
|
||||
[msg.admin.users.list.registry]
|
||||
count = "Count"
|
||||
count = "{{count}} users loaded."
|
||||
|
||||
[msg.common]
|
||||
error = "Error"
|
||||
error = "An error occurred."
|
||||
loading = "Loading..."
|
||||
no_description = "No Description."
|
||||
parsing = "Parsing data..."
|
||||
@@ -354,7 +354,7 @@ empty = "No consents found."
|
||||
load_error = "Error loading consents: {{error}}"
|
||||
loading = "Loading consents..."
|
||||
showing = "Showing {{from}} to {{to}} of {{total}} users"
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Review consent grants and users who have approved this application."
|
||||
revoke_confirm = "Are you sure you want to revoke this user's permissions? After revocation, the user must consent again on next login."
|
||||
|
||||
[msg.dev.clients.details]
|
||||
@@ -369,15 +369,15 @@ rotate_confirm = "Rotate Confirm"
|
||||
rotate_error = "Rotate Error"
|
||||
save_error = "Save Error"
|
||||
secret_rotated = "Secret Rotated"
|
||||
secret_unavailable = "SECRET_NOT_AVAILABLE"
|
||||
subtitle = "Subtitle"
|
||||
secret_unavailable = "The client secret is not available."
|
||||
subtitle = "Inspect this application's credentials, endpoints, and security settings."
|
||||
|
||||
[msg.dev.clients.details.redirect]
|
||||
description = "Description"
|
||||
description = "List the allowed URLs that users can be redirected to after authentication. Separate multiple values with commas."
|
||||
|
||||
[msg.dev.clients.details.security]
|
||||
footer = "Footer"
|
||||
note = "Note"
|
||||
footer = "When rotating a secret, confirm the admin session TTL, rate limits, and notification flow."
|
||||
note = "Keep endpoints read-only and link secret copy or rotation actions to audit logs."
|
||||
|
||||
[msg.dev.clients.general]
|
||||
load_error = "Error loading client: {{error}}"
|
||||
@@ -393,14 +393,14 @@ empty = "No IdP configurations found."
|
||||
|
||||
[msg.dev.clients.general.identity]
|
||||
logo_help = "Logo Help"
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Manage the OIDC identity, branding, and basic metadata for this application."
|
||||
|
||||
[msg.dev.clients.general.redirect]
|
||||
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
|
||||
|
||||
[msg.dev.clients.general.scopes]
|
||||
empty = "Empty"
|
||||
subtitle = "Subtitle"
|
||||
empty = "No custom scopes have been added yet."
|
||||
subtitle = "Define the scopes this application can request."
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||
@@ -422,7 +422,7 @@ profile = "Profile"
|
||||
[msg.dev.dashboard]
|
||||
|
||||
[msg.dev.dashboard.hero]
|
||||
body = "Body"
|
||||
body = "Monitor RP readiness, consent activity, and operational status for the current developer workspace."
|
||||
title_emphasis = "Title Emphasis"
|
||||
title_prefix = "Title Prefix"
|
||||
title_suffix = "Title Suffix"
|
||||
@@ -756,16 +756,16 @@ name_placeholder = "Name Placeholder"
|
||||
section_name = "Section Name"
|
||||
section_scopes = "Section Scopes"
|
||||
submit = "Submit"
|
||||
title = "Title"
|
||||
title = "Create New API Key"
|
||||
|
||||
[ui.admin.api_keys.create.success]
|
||||
copy_secret = "Copy Secret"
|
||||
go_list = "Go List"
|
||||
title = "Title"
|
||||
title = "API Key Created"
|
||||
|
||||
[ui.admin.api_keys.list]
|
||||
add = "Add"
|
||||
title = "Title"
|
||||
title = "API Key Management"
|
||||
|
||||
[ui.admin.api_keys.list.breadcrumb]
|
||||
list = "List"
|
||||
@@ -785,7 +785,7 @@ scopes = "SCOPES"
|
||||
export_csv = "Export CSV"
|
||||
load_more = "Load more"
|
||||
target = "Target · {{target}}"
|
||||
title = "Title"
|
||||
title = "Audit Logs"
|
||||
|
||||
[ui.admin.audit.breadcrumb]
|
||||
logs = "Logs"
|
||||
@@ -831,7 +831,7 @@ import_csv = "Import Csv"
|
||||
|
||||
[ui.admin.groups.create]
|
||||
description = "Adds a new organization unit such as a department or team."
|
||||
title = "Title"
|
||||
title = "Create Organization Unit"
|
||||
|
||||
[ui.admin.groups.detail]
|
||||
breadcrumb_org = "Breadcrumb Org"
|
||||
@@ -843,7 +843,7 @@ permissions_subtitle = "Permissions Subtitle"
|
||||
permissions_title = "Permission Manage"
|
||||
|
||||
[ui.admin.groups.form]
|
||||
desc_label = "Description"
|
||||
desc_label = "Description Label"
|
||||
desc_placeholder = "Desc Placeholder"
|
||||
name_label = "Group Name"
|
||||
name_placeholder = "Name Placeholder"
|
||||
@@ -899,7 +899,7 @@ title = "Admin playbook"
|
||||
add_tenant = "Tenant Add"
|
||||
api_key_management = "API Key Management"
|
||||
user_management = "User Management"
|
||||
title = "Title"
|
||||
title = "Quick Links"
|
||||
view_audit_logs = "View Audit Logs"
|
||||
|
||||
[ui.admin.overview.summary]
|
||||
@@ -933,7 +933,7 @@ remove_title = "Remove Title"
|
||||
table_actions = "Table Actions"
|
||||
table_email = "Email"
|
||||
table_name = "Name"
|
||||
title = "Title"
|
||||
title = "Tenant Admins"
|
||||
|
||||
[ui.admin.tenants.owners]
|
||||
add_button = "Add Owner"
|
||||
@@ -958,7 +958,7 @@ action = "Create"
|
||||
section = "Tenants"
|
||||
|
||||
[ui.admin.tenants.create.form]
|
||||
description = "Description"
|
||||
description = "Tenant Description"
|
||||
domains_label = "Allowed Domains (Comma separated)"
|
||||
domains_placeholder = "example.com, example.kr"
|
||||
name = "Tenant name"
|
||||
@@ -970,7 +970,7 @@ status = "Status"
|
||||
type = "Type"
|
||||
|
||||
[ui.admin.tenants.create.memo]
|
||||
title = "Title"
|
||||
title = "Policy Memo"
|
||||
|
||||
[ui.admin.tenants.create.profile]
|
||||
title = "Tenant Profile"
|
||||
@@ -978,7 +978,7 @@ title = "Tenant Profile"
|
||||
[ui.admin.tenants.detail]
|
||||
breadcrumb_list = "Tenant List"
|
||||
header_subtitle = "Header Subtitle"
|
||||
loading = "Loading"
|
||||
loading = "Loading tenant details..."
|
||||
tab_federation = "Tab Federation"
|
||||
tab_organization = "Organization Manage"
|
||||
tab_permissions = "Permissions"
|
||||
@@ -1008,7 +1008,7 @@ status = "STATUS"
|
||||
allowed_domains = "Allowed Domains"
|
||||
allowed_domains_help = "Users with these email domains will be automatically assigned to this tenant."
|
||||
approve_button = "Approve Tenant"
|
||||
description = "Description"
|
||||
description = "Review and edit the tenant's basic profile information."
|
||||
name = "Tenant Name"
|
||||
slug = "Slug"
|
||||
status = "Status"
|
||||
@@ -1035,7 +1035,7 @@ type = "Type"
|
||||
type_boolean = "Boolean"
|
||||
type_date = "Date"
|
||||
type_number = "Number"
|
||||
type_text = "Text"
|
||||
type_text = "Text Value"
|
||||
validation_placeholder = "Regex Pattern (Optional)"
|
||||
type_datetime = "DateTime"
|
||||
type_float = "Float"
|
||||
@@ -1089,14 +1089,14 @@ submit = "User Create"
|
||||
title = "User Add"
|
||||
|
||||
[ui.admin.users.create.account]
|
||||
title = "Title"
|
||||
title = "Account Information"
|
||||
|
||||
[ui.admin.users.create.breadcrumb]
|
||||
new = "New"
|
||||
section = "Users"
|
||||
|
||||
[ui.admin.users.create.custom_fields]
|
||||
title = "Title"
|
||||
title = "Tenant Custom Fields"
|
||||
|
||||
[ui.admin.users.create.form]
|
||||
auto_password = "Auto Password"
|
||||
@@ -1119,7 +1119,7 @@ tenant = "Tenant"
|
||||
tenant_global = "Tenant Global"
|
||||
|
||||
[ui.admin.users.create.password_generated]
|
||||
title = "Title"
|
||||
title = "Initial Password Generated"
|
||||
|
||||
[ui.admin.users.detail]
|
||||
back = "Back"
|
||||
@@ -1158,10 +1158,10 @@ title = "Affiliation & Organization Info"
|
||||
[ui.admin.users.list]
|
||||
add = "User Add"
|
||||
bulk_import = "Bulk Import"
|
||||
empty = "Empty"
|
||||
fetch_error = "Fetch Error"
|
||||
empty = "No users found."
|
||||
fetch_error = "Failed to load the user list."
|
||||
search_placeholder = "Search Placeholder"
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Browse and manage registered users."
|
||||
title = "User Manage"
|
||||
|
||||
[ui.admin.users.list.breadcrumb]
|
||||
@@ -1175,7 +1175,7 @@ title = "Column Settings"
|
||||
tenant = "Tenant Filter"
|
||||
|
||||
[ui.admin.users.list.registry]
|
||||
count = "Count"
|
||||
count = "{{count}} users loaded."
|
||||
title = "User Registry"
|
||||
|
||||
[ui.admin.users.list.table]
|
||||
@@ -1316,7 +1316,7 @@ role = "Roles & Permissions"
|
||||
|
||||
[ui.dev.profile.basic]
|
||||
title = "User Info"
|
||||
id = "User ID"
|
||||
id = "Account ID"
|
||||
name = "Name"
|
||||
email = "Email"
|
||||
phone = "Phone Number"
|
||||
@@ -1414,6 +1414,7 @@ settings = "Settings"
|
||||
[ui.dev.clients.general]
|
||||
create = "Create Application"
|
||||
display_new = "Add Connected Application"
|
||||
subtitle = "Manage application settings and security configuration."
|
||||
title_create = "Create Client"
|
||||
title_edit = "Client Settings"
|
||||
|
||||
@@ -1423,7 +1424,7 @@ add_title = "Add Identity Provider"
|
||||
add_btn = "Add Provider"
|
||||
|
||||
[ui.dev.clients.general.identity]
|
||||
description = "Description"
|
||||
description = "Application Description"
|
||||
description_placeholder = "Description Placeholder"
|
||||
logo = "App Logo URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
@@ -1443,7 +1444,7 @@ name_placeholder = "e.g. profile"
|
||||
title = "Scopes"
|
||||
|
||||
[ui.dev.clients.general.scopes.table]
|
||||
description = "Description"
|
||||
description = "Scope Description"
|
||||
mandatory = "Mandatory"
|
||||
name = "Scope Name"
|
||||
delete = "Delete"
|
||||
@@ -1507,7 +1508,7 @@ subtitle = "Ship the RP controls"
|
||||
title = "Next actions"
|
||||
|
||||
[ui.dev.dashboard.ops]
|
||||
subtitle = "Subtitle"
|
||||
subtitle = "Operational indicators for the current developer workspace."
|
||||
title = "Ops board"
|
||||
|
||||
[ui.dev.dashboard.ops.card]
|
||||
|
||||
@@ -163,9 +163,9 @@ forbidden = "이 작업을 수행할 권한이 없습니다."
|
||||
|
||||
[msg.admin.audit]
|
||||
empty = "아직 수집된 감사 로그가 없습니다."
|
||||
end = "End of audit feed"
|
||||
load_error = "Error loading logs: {{error}}"
|
||||
loading = "Loading audit logs..."
|
||||
end = "감사 로그의 마지막입니다."
|
||||
load_error = "감사 로그를 불러오지 못했습니다: {{error}}"
|
||||
loading = "감사 로그를 불러오는 중..."
|
||||
subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다."
|
||||
|
||||
[msg.admin.header]
|
||||
@@ -221,9 +221,9 @@ subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합
|
||||
deleted = "앱이 삭제되었습니다."
|
||||
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
delete_error = "삭제 실패: {{error}}"
|
||||
load_error = "Error loading clients: {{error}}"
|
||||
loading = "Loading apps..."
|
||||
showing = "Showing {{shown}} of {{total}} apps"
|
||||
load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
|
||||
loading = "앱 정보를 불러오는 중..."
|
||||
showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다."
|
||||
|
||||
[msg.dev.sidebar]
|
||||
notice = "개발자 전용 콘솔입니다."
|
||||
@@ -497,7 +497,7 @@ qr_scan = "QR 스캔"
|
||||
[ui.userfront.profile]
|
||||
department_empty = "소속 정보 없음"
|
||||
manage = "프로필 관리"
|
||||
user_fallback = "User"
|
||||
user_fallback = "사용자"
|
||||
|
||||
[ui.userfront.qr]
|
||||
rescan = "다시 스캔"
|
||||
@@ -666,19 +666,19 @@ fetch_error = "사용자 목록 조회에 실패했습니다."
|
||||
subtitle = "시스템 사용자를 조회하고 관리합니다. (Local DB)"
|
||||
|
||||
[msg.dev.clients.consents]
|
||||
empty = "No consents found."
|
||||
load_error = "Error loading consents: {{error}}"
|
||||
loading = "Loading consents..."
|
||||
empty = "등록된 동의 내역이 없습니다."
|
||||
load_error = "동의 내역을 불러오지 못했습니다: {{error}}"
|
||||
loading = "동의 내역을 불러오는 중..."
|
||||
revoke_confirm = "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다."
|
||||
showing = "Showing {{from}} to {{to}} of {{total}} users"
|
||||
showing = "전체 {{total}}명 중 {{from}}번째부터 {{to}}번째 사용자를 표시합니다."
|
||||
subtitle = "OIDC Relying Party 사용자 권한을 검토·관리합니다."
|
||||
|
||||
[msg.dev.clients.details]
|
||||
copy_client_id = "Client ID가 복사되었습니다."
|
||||
copy_client_secret = "Client Secret이 복사되었습니다."
|
||||
copy_endpoint = "{{label}}가 복사되었습니다."
|
||||
load_error = "Error loading client: {{error}}"
|
||||
loading = "Loading client..."
|
||||
load_error = "앱 상세 정보를 불러오지 못했습니다: {{error}}"
|
||||
loading = "앱 상세 정보를 불러오는 중..."
|
||||
missing_id = "Client ID가 필요합니다."
|
||||
redirect_saved = "Redirect URIs가 저장되었습니다."
|
||||
rotate_confirm = "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?"
|
||||
@@ -689,8 +689,8 @@ secret_unavailable = "SECRET_NOT_AVAILABLE"
|
||||
subtitle = "OIDC 자격 증명과 엔드포인트를 관리합니다."
|
||||
|
||||
[msg.dev.clients.general]
|
||||
load_error = "Error loading client: {{error}}"
|
||||
loading = "Loading client..."
|
||||
load_error = "앱 설정을 불러오지 못했습니다: {{error}}"
|
||||
loading = "앱 설정을 불러오는 중..."
|
||||
save_error = "저장 실패: {{error}}"
|
||||
saved = "설정이 저장되었습니다."
|
||||
status_changed = "상태가 {{status}}로 변경되었습니다."
|
||||
@@ -1192,6 +1192,7 @@ title = "User Consent Grants"
|
||||
[ui.dev.clients.general]
|
||||
create = "앱 생성"
|
||||
display_new = "연동 앱 추가"
|
||||
subtitle = "앱 설정과 보안 구성을 관리합니다."
|
||||
title_create = "연동 앱 생성"
|
||||
title_edit = "연동 앱 설정"
|
||||
|
||||
@@ -1232,7 +1233,7 @@ profile = "기본 프로필 정보 접근"
|
||||
[ui.dev.clients.table]
|
||||
actions = "액션"
|
||||
application = "애플리케이션"
|
||||
client_id = "Client ID"
|
||||
client_id = "클라이언트 ID"
|
||||
created_at = "생성일"
|
||||
status = "상태"
|
||||
type = "유형"
|
||||
@@ -1535,7 +1536,7 @@ type = "타입"
|
||||
type_boolean = "Boolean"
|
||||
type_date = "Date"
|
||||
type_number = "Number"
|
||||
type_text = "Text"
|
||||
type_text = "텍스트"
|
||||
validation_placeholder = "정규표현식 (선택 사항)"
|
||||
type_datetime = "일시 (DateTime)"
|
||||
type_float = "실수 (Float)"
|
||||
@@ -1636,25 +1637,25 @@ current = "User Consent Grants"
|
||||
home = "Home"
|
||||
|
||||
[ui.dev.clients.consents.filters]
|
||||
advanced = "Advanced Filters"
|
||||
advanced = "고급 필터"
|
||||
|
||||
[ui.dev.clients.consents.stats]
|
||||
active_grants = "Active Grants"
|
||||
avg_scopes = "Avg. Scopes per User"
|
||||
total_scopes = "Total Scopes Issued"
|
||||
active_grants = "활성 동의 건수"
|
||||
avg_scopes = "사용자당 평균 스코프"
|
||||
total_scopes = "총 발급 스코프 수"
|
||||
|
||||
[ui.dev.clients.consents.table]
|
||||
action = "Action"
|
||||
first_granted = "First Granted"
|
||||
last_auth = "Last Authenticated"
|
||||
scopes = "Granted Scopes"
|
||||
status = "Status"
|
||||
tenant = "Tenant"
|
||||
user = "User"
|
||||
action = "동작"
|
||||
first_granted = "최초 동의 시각"
|
||||
last_auth = "마지막 인증 시각"
|
||||
scopes = "승인된 스코프"
|
||||
status = "상태"
|
||||
tenant = "테넌트"
|
||||
user = "사용자"
|
||||
|
||||
[ui.dev.clients.details.credentials]
|
||||
client_id = "Client ID"
|
||||
client_secret = "Client Secret"
|
||||
client_id = "클라이언트 ID"
|
||||
client_secret = "클라이언트 시크릿"
|
||||
title = "앱 자격 증명"
|
||||
|
||||
[ui.dev.clients.details.endpoints]
|
||||
@@ -1663,7 +1664,7 @@ title = "OIDC 엔드포인트"
|
||||
|
||||
[ui.dev.clients.details.redirect]
|
||||
callback_label = "인증 콜백 URL"
|
||||
label = "Redirect URIs"
|
||||
label = "리디렉션 URI"
|
||||
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
|
||||
save = "Redirect URIs 저장"
|
||||
title = "리디렉션 URI 설정"
|
||||
@@ -1682,24 +1683,24 @@ consents = "동의 및 사용자"
|
||||
settings = "설정"
|
||||
|
||||
[ui.dev.clients.general.identity]
|
||||
description = "Description"
|
||||
description = "설명"
|
||||
description_placeholder = "앱에 대한 간단한 설명을 입력하세요."
|
||||
logo = "App Logo URL"
|
||||
logo = "앱 로고 URL"
|
||||
logo_placeholder = "https://example.com/logo.png"
|
||||
logo_preview = "Logo Preview"
|
||||
logo_preview = "로고 미리보기"
|
||||
name = "앱 이름"
|
||||
name_placeholder = "My Awesome Application"
|
||||
title = "Application Identity"
|
||||
name_placeholder = "예: 멋진 애플리케이션"
|
||||
title = "애플리케이션 정보"
|
||||
|
||||
[ui.dev.clients.general.redirect]
|
||||
label = "Redirect URIs"
|
||||
label = "리디렉션 URI"
|
||||
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
|
||||
|
||||
[ui.dev.clients.general.scopes]
|
||||
add = "Scope 추가"
|
||||
description_placeholder = "권한에 대한 설명"
|
||||
name_placeholder = "e.g. profile"
|
||||
title = "Scopes"
|
||||
title = "스코프"
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = "Server side App"
|
||||
@@ -1720,7 +1721,7 @@ label = "이메일 주소"
|
||||
title = "이메일 인증"
|
||||
|
||||
[ui.dev.clients.general.scopes.table]
|
||||
description = "Description"
|
||||
mandatory = "Mandatory"
|
||||
name = "Scope Name"
|
||||
delete = "Delete"
|
||||
description = "설명"
|
||||
mandatory = "필수"
|
||||
name = "스코프 이름"
|
||||
delete = "삭제"
|
||||
|
||||
@@ -1192,6 +1192,7 @@ title = ""
|
||||
[ui.dev.clients.general]
|
||||
create = ""
|
||||
display_new = ""
|
||||
subtitle = ""
|
||||
title_create = ""
|
||||
title_edit = ""
|
||||
|
||||
|
||||
365
tools/i18n-scanner/value-check.js
Normal file
365
tools/i18n-scanner/value-check.js
Normal file
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const LOCALES_DIR = path.join(ROOT_DIR, 'locales');
|
||||
const TARGET_FILES = ['ko.toml', 'en.toml'];
|
||||
|
||||
const PLACEHOLDER_VALUES = new Set([
|
||||
'title',
|
||||
'subtitle',
|
||||
'description',
|
||||
'body',
|
||||
'note',
|
||||
'footer',
|
||||
'empty',
|
||||
'error',
|
||||
'count',
|
||||
'loading',
|
||||
'name required',
|
||||
'scope required',
|
||||
'scopes count',
|
||||
'scopes hint',
|
||||
'delete confirm',
|
||||
'fetch error',
|
||||
'create success',
|
||||
'create failed',
|
||||
'create error',
|
||||
'delete success',
|
||||
'delete error',
|
||||
'remove confirm',
|
||||
'remove success',
|
||||
'add success',
|
||||
'copy hint',
|
||||
'notice',
|
||||
'notice emphasis',
|
||||
'notice suffix',
|
||||
'idp policy',
|
||||
'idp body',
|
||||
'tenant body',
|
||||
'approve confirm',
|
||||
'approve success',
|
||||
'user id',
|
||||
'missing',
|
||||
'consent accept',
|
||||
'consent fetch',
|
||||
'consent reject',
|
||||
'linked app revoke',
|
||||
'login failed',
|
||||
'password reset complete',
|
||||
'password reset init',
|
||||
'load failed',
|
||||
'send code failed',
|
||||
'update failed',
|
||||
'verify code failed',
|
||||
'text',
|
||||
]);
|
||||
|
||||
const KO_UNTRANSLATED_VALUES = new Set([
|
||||
'End of audit feed',
|
||||
'Error loading logs: {{error}}',
|
||||
'Loading audit logs...',
|
||||
'Error loading clients: {{error}}',
|
||||
'Loading apps...',
|
||||
'Showing {{shown}} of {{total}} apps',
|
||||
'No consents found.',
|
||||
'Error loading consents: {{error}}',
|
||||
'Loading consents...',
|
||||
'Showing {{from}} to {{to}} of {{total}} users',
|
||||
'Error loading client: {{error}}',
|
||||
'Loading client...',
|
||||
'Advanced Filters',
|
||||
'Active Grants',
|
||||
'Avg. Scopes per User',
|
||||
'Total Scopes Issued',
|
||||
'Action',
|
||||
'First Granted',
|
||||
'Last Authenticated',
|
||||
'Granted Scopes',
|
||||
'Status',
|
||||
'Tenant',
|
||||
'User',
|
||||
'Client ID',
|
||||
'Client Secret',
|
||||
'Redirect URIs',
|
||||
'Application Identity',
|
||||
'App Logo URL',
|
||||
'Logo Preview',
|
||||
'My Awesome Application',
|
||||
'Scopes',
|
||||
'Mandatory',
|
||||
'Scope Name',
|
||||
'Delete',
|
||||
'Description',
|
||||
]);
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.dart_tool',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'coverage',
|
||||
'.next',
|
||||
'.cache',
|
||||
'tmp',
|
||||
'logs',
|
||||
]);
|
||||
|
||||
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.dart']);
|
||||
|
||||
const CODE_PATTERNS = [
|
||||
/\b(?:i18n\.)?t\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/\btr\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/['"]([^'"]+)['"]\s*\.tr\s*\(/g,
|
||||
];
|
||||
|
||||
function readFileRequired(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { ok: false, error: `파일이 없습니다: ${filePath}` };
|
||||
}
|
||||
return { ok: true, value: fs.readFileSync(filePath, 'utf8') };
|
||||
}
|
||||
|
||||
function walkDir(dirPath, files) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (SKIP_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
walkDir(path.join(dirPath, entry.name), files);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!CODE_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.name.includes('.test.') || entry.name.includes('.spec.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push(path.join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
function collectCodeKeys() {
|
||||
const files = [];
|
||||
walkDir(ROOT_DIR, files);
|
||||
|
||||
const keys = new Set();
|
||||
for (const filePath of files) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const pattern of CODE_PATTERNS) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
keys.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function stripInlineComment(value) {
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const ch = value[i];
|
||||
const prev = i > 0 ? value[i - 1] : '';
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '#' && !inSingle && !inDouble) {
|
||||
return value.slice(0, i).trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return value.trimEnd();
|
||||
}
|
||||
|
||||
function parseTomlStringEntries(filePath) {
|
||||
const result = readFileRequired(filePath);
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error, entries: [] };
|
||||
}
|
||||
|
||||
const entries = new Map();
|
||||
const lines = result.value.split(/\r?\n/);
|
||||
let currentSection = [];
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const rawLine = lines[index];
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[[') && line.endsWith(']]')) {
|
||||
const sectionName = line.slice(2, -2).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((part) => part.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[') && line.endsWith(']')) {
|
||||
const sectionName = line.slice(1, -1).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((part) => part.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIndex = rawLine.indexOf('=');
|
||||
if (eqIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = rawLine.slice(0, eqIndex).trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.startsWith('"') && key.endsWith('"')) {
|
||||
key = key.slice(1, -1);
|
||||
}
|
||||
|
||||
const rawValue = stripInlineComment(rawLine.slice(eqIndex + 1).trim());
|
||||
if (
|
||||
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
|
||||
(rawValue.startsWith("'") && rawValue.endsWith("'"))
|
||||
) {
|
||||
entries.set([...currentSection, key].join('.'), {
|
||||
line: index + 1,
|
||||
value: rawValue.slice(1, -1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, entries };
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
return value.replace(/\{\{[^}]+\}\}/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function formatFinding(fileName, line, key, value, reason) {
|
||||
return `${fileName}:${line} ${key} = "${value}" (${reason})`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const errors = [];
|
||||
const findings = [];
|
||||
const codeKeys = collectCodeKeys();
|
||||
const localeMaps = new Map();
|
||||
|
||||
for (const fileName of TARGET_FILES) {
|
||||
const parsed = parseTomlStringEntries(path.join(LOCALES_DIR, fileName));
|
||||
if (!parsed.ok) {
|
||||
errors.push(parsed.error);
|
||||
continue;
|
||||
}
|
||||
localeMaps.set(fileName, parsed.entries);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('i18n 값 검증 실패');
|
||||
errors.forEach((error) => console.error(`- ${error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const koEntries = localeMaps.get('ko.toml');
|
||||
const enEntries = localeMaps.get('en.toml');
|
||||
|
||||
for (const key of codeKeys) {
|
||||
for (const fileName of TARGET_FILES) {
|
||||
const entry = localeMaps.get(fileName).get(key);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PLACEHOLDER_VALUES.has(normalizeValue(entry.value))) {
|
||||
findings.push(
|
||||
formatFinding(fileName, entry.line, key, entry.value, 'placeholder value'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const koEntry = koEntries.get(key);
|
||||
const enEntry = enEntries.get(key);
|
||||
if (!koEntry || !enEntry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (koEntry.value === enEntry.value && KO_UNTRANSLATED_VALUES.has(koEntry.value)) {
|
||||
findings.push(
|
||||
formatFinding(
|
||||
'ko.toml',
|
||||
koEntry.line,
|
||||
key,
|
||||
koEntry.value,
|
||||
'untranslated english value in Korean locale',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reportsDir = path.join(ROOT_DIR, 'reports');
|
||||
if (!fs.existsSync(reportsDir)) {
|
||||
fs.mkdirSync(reportsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const generatedAt = new Date().toISOString();
|
||||
const summaryPath = path.join(reportsDir, 'i18n-value-report.txt');
|
||||
const jsonPath = path.join(reportsDir, 'i18n-value-report.json');
|
||||
|
||||
const summaryLines = [
|
||||
`generated_at: ${generatedAt}`,
|
||||
`errors: ${errors.length}`,
|
||||
`findings: ${findings.length}`,
|
||||
];
|
||||
if (findings.length > 0) {
|
||||
summaryLines.push('details:');
|
||||
findings.forEach((finding) => summaryLines.push(`- ${finding}`));
|
||||
}
|
||||
|
||||
fs.writeFileSync(summaryPath, `${summaryLines.join('\n')}\n`);
|
||||
fs.writeFileSync(
|
||||
jsonPath,
|
||||
JSON.stringify({ generated_at: generatedAt, errors, findings }, null, 2),
|
||||
);
|
||||
|
||||
if (findings.length > 0) {
|
||||
console.error('i18n 값 품질 검증 실패');
|
||||
findings.forEach((finding) => console.error(`- ${finding}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ i18n 값 품질 검증 완료');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user