1
0
forked from baron/baron-sso

i18n 값 품질 검사 추가 및 devfront locale placeholder 정리

This commit is contained in:
2026-03-25 16:48:55 +09:00
parent cab204281b
commit ced369cdbc
8 changed files with 555 additions and 175 deletions

View File

@@ -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:

View File

@@ -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 \

View File

@@ -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"

View File

@@ -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 = "구분"

View File

@@ -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]

View File

@@ -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 = "삭제"

View File

@@ -1192,6 +1192,7 @@ title = ""
[ui.dev.clients.general]
create = ""
display_new = ""
subtitle = ""
title_create = ""
title_edit = ""

View 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();