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 node tools/i18n-scanner/report.js
cat reports/i18n-report.txt 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 - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:

View File

@@ -115,7 +115,7 @@ PLAYWRIGHT_INSTALL_ALL := npx playwright install --with-deps
PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install --with-deps chromium PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install --with-deps chromium
endif 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 CODE_CHECK_TEST_JOBS ?= 1
PLAYWRIGHT_WORKERS ?= 1 PLAYWRIGHT_WORKERS ?= 1
@@ -124,7 +124,7 @@ FLUTTER_TEST_CONCURRENCY ?= 1
code-check: code-check-lint code-check-test-jobs code-check: code-check-lint code-check-test-jobs
@echo "code-check complete." @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: code-check-test-jobs:
@echo "==> run CI-equivalent test jobs (parallel)" @echo "==> run CI-equivalent test jobs (parallel)"
@@ -142,6 +142,12 @@ code-check-i18n:
node tools/i18n-scanner/report.js node tools/i18n-scanner/report.js
@cat reports/i18n-report.txt @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: code-check-go-lint:
@echo "==> go lint/format check" @echo "==> go lint/format check"
@if command -v golangci-lint >/dev/null 2>&1; then \ @if command -v golangci-lint >/dev/null 2>&1; then \

View File

@@ -987,7 +987,7 @@ type = "Type"
type_boolean = "Boolean" type_boolean = "Boolean"
type_date = "Date" type_date = "Date"
type_number = "Number" type_number = "Number"
type_text = "Text" type_text = "Text Value"
validation_placeholder = "Regex Pattern (Optional)" validation_placeholder = "Regex Pattern (Optional)"
[ui.admin.tenants.sub] [ui.admin.tenants.sub]
@@ -1340,7 +1340,7 @@ add_title = "Add Identity Provider"
add_btn = "Add Provider" add_btn = "Add Provider"
[ui.dev.clients.general.identity] [ui.dev.clients.general.identity]
description = "Description" description = "Application Description"
description_placeholder = "Description Placeholder" description_placeholder = "Description Placeholder"
logo = "App Logo URL" logo = "App Logo URL"
logo_placeholder = "https://example.com/logo.png" logo_placeholder = "https://example.com/logo.png"
@@ -1360,7 +1360,7 @@ name_placeholder = "e.g. profile"
title = "Scopes" title = "Scopes"
[ui.dev.clients.general.scopes.table] [ui.dev.clients.general.scopes.table]
description = "Description" description = "Scope Description"
mandatory = "Mandatory" mandatory = "Mandatory"
name = "Scope Name" name = "Scope Name"
delete = "Delete" delete = "Delete"

View File

@@ -103,9 +103,9 @@ count = "총 {{count}}개 API 키"
[msg.admin.audit] [msg.admin.audit]
empty = "아직 수집된 감사 로그가 없습니다." empty = "아직 수집된 감사 로그가 없습니다."
end = "End of audit feed" end = "감사 로그의 마지막입니다."
load_error = "Error loading logs: {{error}}" load_error = "감사 로그를 불러오지 못했습니다: {{error}}"
loading = "Loading audit logs..." loading = "감사 로그를 불러오는 중..."
subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다." subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다."
[msg.admin.audit.filters] [msg.admin.audit.filters]
@@ -329,9 +329,9 @@ subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합
deleted = "앱이 삭제되었습니다." deleted = "앱이 삭제되었습니다."
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
delete_error = "삭제 실패: {{error}}" delete_error = "삭제 실패: {{error}}"
load_error = "Error loading clients: {{error}}" load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
loading = "Loading apps..." loading = "앱 정보를 불러오는 중..."
showing = "Showing {{shown}} of {{total}} apps" showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다."
[msg.dev.clients.consents] [msg.dev.clients.consents]
empty = "조회된 동의 내역이 없습니다." empty = "조회된 동의 내역이 없습니다."
@@ -987,7 +987,7 @@ type = "타입"
type_boolean = "Boolean" type_boolean = "Boolean"
type_date = "Date" type_date = "Date"
type_number = "Number" type_number = "Number"
type_text = "Text" type_text = "텍스트"
validation_placeholder = "정규표현식 (선택 사항)" validation_placeholder = "정규표현식 (선택 사항)"
[ui.admin.tenants.sub] [ui.admin.tenants.sub]
@@ -1298,8 +1298,8 @@ user = "사용자"
[ui.dev.clients.details] [ui.dev.clients.details]
[ui.dev.clients.details.credentials] [ui.dev.clients.details.credentials]
client_id = "Client ID" client_id = "클라이언트 ID"
client_secret = "Client Secret" client_secret = "클라이언트 시크릿"
title = "앱 자격 증명" title = "앱 자격 증명"
[ui.dev.clients.details.endpoints] [ui.dev.clients.details.endpoints]
@@ -1308,7 +1308,7 @@ title = "OIDC 엔드포인트"
[ui.dev.clients.details.redirect] [ui.dev.clients.details.redirect]
callback_label = "인증 콜백 URL" callback_label = "인증 콜백 URL"
label = "Redirect URIs" label = "리디렉션 URI"
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback" placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
save = "Redirect URIs 저장" save = "Redirect URIs 저장"
title = "리디렉션 URI 설정" title = "리디렉션 URI 설정"
@@ -1345,11 +1345,11 @@ logo = "앱 로고 URL"
logo_placeholder = "https://example.com/logo.png" logo_placeholder = "https://example.com/logo.png"
logo_preview = "로고 미리보기" logo_preview = "로고 미리보기"
name = "앱 이름" name = "앱 이름"
name_placeholder = "My Awesome Application" name_placeholder = "예: 멋진 애플리케이션"
title = "애플리케이션 정보" title = "애플리케이션 정보"
[ui.dev.clients.general.redirect] [ui.dev.clients.general.redirect]
label = "Redirect URIs" label = "리디렉션 URI"
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)" placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
[ui.dev.clients.general.scopes] [ui.dev.clients.general.scopes]
@@ -1401,7 +1401,7 @@ profile = "기본 프로필 정보 접근"
[ui.dev.clients.table] [ui.dev.clients.table]
actions = "액션" actions = "액션"
application = "애플리케이션" application = "애플리케이션"
client_id = "Client ID" client_id = "클라이언트 ID"
created_at = "생성일" created_at = "생성일"
status = "상태" status = "상태"
type = "유형" type = "유형"
@@ -1571,7 +1571,7 @@ qr_scan = "QR 스캔"
[ui.userfront.profile] [ui.userfront.profile]
department_empty = "소속 정보 없음" department_empty = "소속 정보 없음"
manage = "프로필 관리" manage = "프로필 관리"
user_fallback = "User" user_fallback = "사용자"
[ui.userfront.profile.field] [ui.userfront.profile.field]
affiliation = "구분" affiliation = "구분"

View File

@@ -44,24 +44,24 @@ slow_down = "Requests are too frequent. Please try again shortly."
[err.userfront] [err.userfront]
[err.userfront.auth_proxy] [err.userfront.auth_proxy]
consent_accept = "Consent Accept" consent_accept = "Failed to accept the consent request."
consent_fetch = "Consent Fetch" consent_fetch = "Failed to load consent details."
consent_reject = "Consent Reject" consent_reject = "Failed to reject the consent request."
linked_app_revoke = "Linked App Revoke" linked_app_revoke = "Failed to revoke the linked application."
login_failed = "Login Failed" login_failed = "Login failed."
oidc_accept = "OIDC Accept" oidc_accept = "OIDC Accept"
password_reset_complete = "Password Reset Complete" password_reset_complete = "Failed to complete the password reset."
password_reset_init = "Password Reset Init" password_reset_init = "Failed to start the password reset."
[err.userfront.profile] [err.userfront.profile]
load_failed = "Load Failed" load_failed = "Failed to load the profile."
password_change_failed = "Password Change Failed" password_change_failed = "Password Change Failed"
send_code_failed = "Send Code Failed" send_code_failed = "Failed to send the verification code."
update_failed = "Update Failed" update_failed = "Failed to update the profile."
verify_code_failed = "Verify Code Failed" verify_code_failed = "Verification failed."
[err.userfront.session] [err.userfront.session]
missing = "Missing" missing = "No active session was found."
[msg] [msg]
@@ -78,40 +78,40 @@ forbidden = "You do not have permission to perform this action."
[msg.admin.api_keys] [msg.admin.api_keys]
[msg.admin.api_keys.create] [msg.admin.api_keys.create]
error = "Error" error = "Failed to create the API key."
name_required = "Name Required" name_required = "Name is required."
scope_required = "Scope Required" scope_required = "Select at least one scope."
scopes_count = "Scopes Count" scopes_count = "{{count}} scopes will be assigned."
scopes_hint = "Scopes Hint" scopes_hint = "Choose the scopes to grant to this API key."
subtitle = "Subtitle" subtitle = "Create and issue an API key for machine-to-machine communication."
[msg.admin.api_keys.create.success] [msg.admin.api_keys.create.success]
copy_hint = "Copy Hint" copy_hint = "Copy the secret now. It will not be shown again."
notice = "Notice" notice = "The generated secret is displayed only once."
notice_emphasis = "Notice Emphasis" notice_emphasis = "Store it in a secure location."
notice_suffix = "Notice Suffix" notice_suffix = "Rotate the key immediately if you think it has been exposed."
[msg.admin.api_keys.list] [msg.admin.api_keys.list]
delete_confirm = "Delete Confirm" delete_confirm = "Are you sure you want to delete this API key?"
empty = "Empty" empty = "No API keys have been issued yet."
fetch_error = "Fetch Error" fetch_error = "Failed to load the API key list."
subtitle = "Subtitle" subtitle = "View and manage the API keys issued for server-to-server communication."
[msg.admin.api_keys.list.registry] [msg.admin.api_keys.list.registry]
count = "Count" count = "{{count}} API keys loaded."
[msg.admin.audit] [msg.admin.audit]
empty = "Empty" empty = "No audit logs have been collected yet."
end = "End of audit feed" end = "End of audit feed"
load_error = "Error loading logs: {{error}}" load_error = "Error loading logs: {{error}}"
loading = "Loading audit logs..." loading = "Loading audit logs..."
subtitle = "Subtitle" subtitle = "Review command-driven ClickHouse audit logs from the admin workspace."
[msg.admin.audit.filters] [msg.admin.audit.filters]
empty = "Empty" empty = "No filters applied."
[msg.admin.audit.registry] [msg.admin.audit.registry]
count = "Count" count = "{{count}} logs loaded."
[msg.admin.groups] [msg.admin.groups]
@@ -120,32 +120,32 @@ description = "Adds a new organization unit such as a department or team."
title = "Create New Organization Unit" title = "Create New Organization Unit"
[msg.admin.groups.list] [msg.admin.groups.list]
create_error = "Create Failed" create_error = "Failed to create the organization unit."
create_success = "Create Success" create_success = "Organization unit created successfully."
delete_confirm = "Delete Confirm" delete_confirm = "Are you sure you want to delete this organization unit?"
delete_error = "Delete Error" delete_error = "Failed to delete the organization unit."
delete_success = "Delete Success" delete_success = "Organization unit deleted successfully."
empty = "Empty" empty = "No organization units have been registered yet."
import_error = "Import Error" import_error = "Import Error"
import_success = "Import Success" import_success = "Import Success"
loading = "Loading..." loading = "Loading..."
subtitle = "Subtitle" subtitle = "Manage departments and teams under the current tenant."
[msg.admin.groups.members] [msg.admin.groups.members]
add_success = "Add Success" add_success = "Member added successfully."
count = "Count" count = "{{count}} members loaded."
empty = "Empty" empty = "No members are assigned to this organization unit."
remove_confirm = "Remove Confirm" remove_confirm = "Are you sure you want to remove this member?"
remove_success = "Remove Success" remove_success = "Member removed successfully."
title = "Title" title = "Member Management"
[msg.admin.groups.prompt] [msg.admin.groups.prompt]
user_id = "User Id" user_id = "Enter the user's UUID to add:"
[msg.admin.groups.roles] [msg.admin.groups.roles]
assign_success = "Assign Success" assign_success = "Assign Success"
description = "Description" description = "Assign or revoke roles for members of this organization unit."
empty = "Empty" empty = "No roles have been assigned yet."
remove_confirm = "Are you sure you want to revoke this role?" remove_confirm = "Are you sure you want to revoke this role?"
remove_success = "Role revoked successfully." remove_success = "Role revoked successfully."
@@ -153,8 +153,8 @@ remove_success = "Role revoked successfully."
subtitle = "Tenant isolation & least privilege by default" subtitle = "Tenant isolation & least privilege by default"
[msg.admin.notice] [msg.admin.notice]
idp_policy = "IDP Policy" idp_policy = "IDP management keys are only used through server-side wrapper APIs with audit logging and rate limits enabled."
scope = "Scope" scope = "Administrative features are exposed only within the /admin namespace."
[msg.admin.org] [msg.admin.org]
hover_member_info = "Hover to see member details." 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." import_success = "Organization chart imported successfully."
[msg.admin.overview] [msg.admin.overview]
description = "Description" description = "Review shared metrics and policy status across all tenants in one place."
idp_fallback = "Fallback: Descope" idp_fallback = "Fallback: Descope"
idp_primary = "IDP: Ory primary" idp_primary = "IDP: Ory primary"
[msg.admin.overview.playbook] [msg.admin.overview.playbook]
description = "Description" description = "Operational guardrails and architecture decisions for the admin control plane."
idp_body = "IDP Body" 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" 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" tenant_title = "Tenant isolation"
[msg.admin.overview.quick_links] [msg.admin.overview.quick_links]
description = "Description" description = "Jump to the most frequently used administrative workflows."
[msg.admin.overview.summary] [msg.admin.overview.summary]
audit_events_24h = "24h Audit Events" audit_events_24h = "24h Audit Events"
@@ -184,23 +184,23 @@ policy_gate = "Policy Gate Status"
total_tenants = "Total Tenants" total_tenants = "Total Tenants"
[msg.admin.tenants] [msg.admin.tenants]
approve_confirm = "Approve Confirm" approve_confirm = "Do you want to approve this tenant?"
approve_success = "Approve Success" approve_success = "Tenant approved successfully."
delete_confirm = "Delete Tenant \"{{name}}\"?" delete_confirm = "Delete Tenant \"{{name}}\"?"
delete_success = "Tenant deleted." delete_success = "Tenant deleted."
empty = "Empty" empty = "No tenants have been registered yet."
fetch_error = "Fetch Error" fetch_error = "Failed to load the tenant list."
missing_id = "No Tenant ID." missing_id = "No Tenant ID."
not_found = "Tenant not found." not_found = "Tenant not found."
remove_sub_confirm = 'Remove tenant "{{name}}" from sub-tenants?' remove_sub_confirm = 'Remove tenant "{{name}}" from sub-tenants?'
subtitle = "Subtitle" subtitle = "Review registered tenants and manage their current status."
[msg.admin.tenants.admins] [msg.admin.tenants.admins]
add_success = "Add Success" add_success = "Tenant admin added successfully."
empty = "Empty" empty = "No tenant admins are assigned yet."
remove_confirm = "Remove Confirm" remove_confirm = "Are you sure you want to remove this tenant admin?"
remove_success = "Remove Success" remove_success = "Tenant admin removed successfully."
subtitle = "Subtitle" subtitle = "Manage the administrators assigned to this tenant."
remove_last = "Cannot remove the last admin." remove_last = "Cannot remove the last admin."
remove_self = "Cannot remove yourself." remove_self = "Cannot remove yourself."
@@ -214,17 +214,17 @@ remove_last = "Cannot remove the last owner."
remove_self = "Cannot remove yourself." remove_self = "Cannot remove yourself."
[msg.admin.tenants.create] [msg.admin.tenants.create]
subtitle = "Subtitle" subtitle = "Enter the minimum required information to create a tenant."
[msg.admin.tenants.create.form] [msg.admin.tenants.create.form]
domains_help = "Users with these email domains will be automatically assigned to this tenant." domains_help = "Users with these email domains will be automatically assigned to this tenant."
[msg.admin.tenants.create.memo] [msg.admin.tenants.create.memo]
body = "Body" body = "Leave operational notes or policy reminders for this tenant."
subtitle = "Subtitle" subtitle = "Capture internal policy notes for administrators."
[msg.admin.tenants.create.profile] [msg.admin.tenants.create.profile]
subtitle = "Subtitle" subtitle = "Set the basic tenant profile information."
[msg.admin.tenants.members] [msg.admin.tenants.members]
desc = "View the list of users belonging to this organization." 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." limit_notice = "Showing members from the first 10 descendant organizations due to size limits."
[msg.admin.tenants.registry] [msg.admin.tenants.registry]
count = "Count" count = "{{count}} tenants loaded."
[msg.admin.tenants.schema] [msg.admin.tenants.schema]
empty = "No custom fields defined. Click \"Add Field\" to begin." 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." forbidden_desc = "Only administrators can access user schema settings."
[msg.admin.tenants.sub] [msg.admin.tenants.sub]
empty = "Empty" empty = "No child tenants are connected."
subtitle = "Subtitle" subtitle = "Review and manage child tenants linked under this tenant."
[msg.admin.users] [msg.admin.users]
@@ -266,13 +266,13 @@ password_required = "Password Required"
success = "User created successfully." success = "User created successfully."
[msg.admin.users.create.account] [msg.admin.users.create.account]
subtitle = "Subtitle" subtitle = "Fill in the account details required to create the user."
[msg.admin.users.create.form] [msg.admin.users.create.form]
email_required = "Email Required" email_required = "Email Required"
field_invalid = "Invalid {{label}} format." field_invalid = "Invalid {{label}} format."
field_required = "{{label}} is required." field_required = "{{label}} is required."
name_required = "Name Required" name_required = "Name is required."
password_auto_help = "Password Auto Help" password_auto_help = "Password Auto Help"
password_manual_help = "Password Manual Help" password_manual_help = "Password Manual Help"
role_help = "Role Help" role_help = "Role Help"
@@ -290,26 +290,26 @@ password_generated = "A secure password has been generated."
[msg.admin.users.detail.form] [msg.admin.users.detail.form]
field_required = "Required." field_required = "Required."
name_required = "Name Required" name_required = "Name is required."
[msg.admin.users.detail.security] [msg.admin.users.detail.security]
password_hint = "Password Hint" password_hint = "Password Hint"
[msg.admin.users.list] [msg.admin.users.list]
delete_confirm = "Delete Confirm" delete_confirm = "Are you sure you want to delete the selected user?"
empty = "Empty" empty = "No users match the current filters."
fetch_error = "Fetch Error" fetch_error = "Failed to load the user list."
subtitle = "Subtitle" subtitle = "Search and manage users registered in the current tenant."
[msg.admin.users.list.columns] [msg.admin.users.list.columns]
description = "Select columns to display in the table." description = "Select columns to display in the table."
no_custom = "No custom fields defined for this tenant." no_custom = "No custom fields defined for this tenant."
[msg.admin.users.list.registry] [msg.admin.users.list.registry]
count = "Count" count = "{{count}} users loaded."
[msg.common] [msg.common]
error = "Error" error = "An error occurred."
loading = "Loading..." loading = "Loading..."
no_description = "No Description." no_description = "No Description."
parsing = "Parsing data..." parsing = "Parsing data..."
@@ -354,7 +354,7 @@ empty = "No consents found."
load_error = "Error loading consents: {{error}}" load_error = "Error loading consents: {{error}}"
loading = "Loading consents..." loading = "Loading consents..."
showing = "Showing {{from}} to {{to}} of {{total}} users" 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." 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] [msg.dev.clients.details]
@@ -369,15 +369,15 @@ rotate_confirm = "Rotate Confirm"
rotate_error = "Rotate Error" rotate_error = "Rotate Error"
save_error = "Save Error" save_error = "Save Error"
secret_rotated = "Secret Rotated" secret_rotated = "Secret Rotated"
secret_unavailable = "SECRET_NOT_AVAILABLE" secret_unavailable = "The client secret is not available."
subtitle = "Subtitle" subtitle = "Inspect this application's credentials, endpoints, and security settings."
[msg.dev.clients.details.redirect] [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] [msg.dev.clients.details.security]
footer = "Footer" footer = "When rotating a secret, confirm the admin session TTL, rate limits, and notification flow."
note = "Note" note = "Keep endpoints read-only and link secret copy or rotation actions to audit logs."
[msg.dev.clients.general] [msg.dev.clients.general]
load_error = "Error loading client: {{error}}" load_error = "Error loading client: {{error}}"
@@ -393,14 +393,14 @@ empty = "No IdP configurations found."
[msg.dev.clients.general.identity] [msg.dev.clients.general.identity]
logo_help = "Logo Help" logo_help = "Logo Help"
subtitle = "Subtitle" subtitle = "Manage the OIDC identity, branding, and basic metadata for this application."
[msg.dev.clients.general.redirect] [msg.dev.clients.general.redirect]
help = "Enter the redirect URIs. You can modify them in the Federation tab after creation." help = "Enter the redirect URIs. You can modify them in the Federation tab after creation."
[msg.dev.clients.general.scopes] [msg.dev.clients.general.scopes]
empty = "Empty" empty = "No custom scopes have been added yet."
subtitle = "Subtitle" subtitle = "Define the scopes this application can request."
[msg.dev.clients.general.security] [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." 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]
[msg.dev.dashboard.hero] [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_emphasis = "Title Emphasis"
title_prefix = "Title Prefix" title_prefix = "Title Prefix"
title_suffix = "Title Suffix" title_suffix = "Title Suffix"
@@ -756,16 +756,16 @@ name_placeholder = "Name Placeholder"
section_name = "Section Name" section_name = "Section Name"
section_scopes = "Section Scopes" section_scopes = "Section Scopes"
submit = "Submit" submit = "Submit"
title = "Title" title = "Create New API Key"
[ui.admin.api_keys.create.success] [ui.admin.api_keys.create.success]
copy_secret = "Copy Secret" copy_secret = "Copy Secret"
go_list = "Go List" go_list = "Go List"
title = "Title" title = "API Key Created"
[ui.admin.api_keys.list] [ui.admin.api_keys.list]
add = "Add" add = "Add"
title = "Title" title = "API Key Management"
[ui.admin.api_keys.list.breadcrumb] [ui.admin.api_keys.list.breadcrumb]
list = "List" list = "List"
@@ -785,7 +785,7 @@ scopes = "SCOPES"
export_csv = "Export CSV" export_csv = "Export CSV"
load_more = "Load more" load_more = "Load more"
target = "Target · {{target}}" target = "Target · {{target}}"
title = "Title" title = "Audit Logs"
[ui.admin.audit.breadcrumb] [ui.admin.audit.breadcrumb]
logs = "Logs" logs = "Logs"
@@ -831,7 +831,7 @@ import_csv = "Import Csv"
[ui.admin.groups.create] [ui.admin.groups.create]
description = "Adds a new organization unit such as a department or team." description = "Adds a new organization unit such as a department or team."
title = "Title" title = "Create Organization Unit"
[ui.admin.groups.detail] [ui.admin.groups.detail]
breadcrumb_org = "Breadcrumb Org" breadcrumb_org = "Breadcrumb Org"
@@ -843,7 +843,7 @@ permissions_subtitle = "Permissions Subtitle"
permissions_title = "Permission Manage" permissions_title = "Permission Manage"
[ui.admin.groups.form] [ui.admin.groups.form]
desc_label = "Description" desc_label = "Description Label"
desc_placeholder = "Desc Placeholder" desc_placeholder = "Desc Placeholder"
name_label = "Group Name" name_label = "Group Name"
name_placeholder = "Name Placeholder" name_placeholder = "Name Placeholder"
@@ -899,7 +899,7 @@ title = "Admin playbook"
add_tenant = "Tenant Add" add_tenant = "Tenant Add"
api_key_management = "API Key Management" api_key_management = "API Key Management"
user_management = "User Management" user_management = "User Management"
title = "Title" title = "Quick Links"
view_audit_logs = "View Audit Logs" view_audit_logs = "View Audit Logs"
[ui.admin.overview.summary] [ui.admin.overview.summary]
@@ -933,7 +933,7 @@ remove_title = "Remove Title"
table_actions = "Table Actions" table_actions = "Table Actions"
table_email = "Email" table_email = "Email"
table_name = "Name" table_name = "Name"
title = "Title" title = "Tenant Admins"
[ui.admin.tenants.owners] [ui.admin.tenants.owners]
add_button = "Add Owner" add_button = "Add Owner"
@@ -958,7 +958,7 @@ action = "Create"
section = "Tenants" section = "Tenants"
[ui.admin.tenants.create.form] [ui.admin.tenants.create.form]
description = "Description" description = "Tenant Description"
domains_label = "Allowed Domains (Comma separated)" domains_label = "Allowed Domains (Comma separated)"
domains_placeholder = "example.com, example.kr" domains_placeholder = "example.com, example.kr"
name = "Tenant name" name = "Tenant name"
@@ -970,7 +970,7 @@ status = "Status"
type = "Type" type = "Type"
[ui.admin.tenants.create.memo] [ui.admin.tenants.create.memo]
title = "Title" title = "Policy Memo"
[ui.admin.tenants.create.profile] [ui.admin.tenants.create.profile]
title = "Tenant Profile" title = "Tenant Profile"
@@ -978,7 +978,7 @@ title = "Tenant Profile"
[ui.admin.tenants.detail] [ui.admin.tenants.detail]
breadcrumb_list = "Tenant List" breadcrumb_list = "Tenant List"
header_subtitle = "Header Subtitle" header_subtitle = "Header Subtitle"
loading = "Loading" loading = "Loading tenant details..."
tab_federation = "Tab Federation" tab_federation = "Tab Federation"
tab_organization = "Organization Manage" tab_organization = "Organization Manage"
tab_permissions = "Permissions" tab_permissions = "Permissions"
@@ -1008,7 +1008,7 @@ status = "STATUS"
allowed_domains = "Allowed Domains" allowed_domains = "Allowed Domains"
allowed_domains_help = "Users with these email domains will be automatically assigned to this tenant." allowed_domains_help = "Users with these email domains will be automatically assigned to this tenant."
approve_button = "Approve Tenant" approve_button = "Approve Tenant"
description = "Description" description = "Review and edit the tenant's basic profile information."
name = "Tenant Name" name = "Tenant Name"
slug = "Slug" slug = "Slug"
status = "Status" status = "Status"
@@ -1035,7 +1035,7 @@ type = "Type"
type_boolean = "Boolean" type_boolean = "Boolean"
type_date = "Date" type_date = "Date"
type_number = "Number" type_number = "Number"
type_text = "Text" type_text = "Text Value"
validation_placeholder = "Regex Pattern (Optional)" validation_placeholder = "Regex Pattern (Optional)"
type_datetime = "DateTime" type_datetime = "DateTime"
type_float = "Float" type_float = "Float"
@@ -1089,14 +1089,14 @@ submit = "User Create"
title = "User Add" title = "User Add"
[ui.admin.users.create.account] [ui.admin.users.create.account]
title = "Title" title = "Account Information"
[ui.admin.users.create.breadcrumb] [ui.admin.users.create.breadcrumb]
new = "New" new = "New"
section = "Users" section = "Users"
[ui.admin.users.create.custom_fields] [ui.admin.users.create.custom_fields]
title = "Title" title = "Tenant Custom Fields"
[ui.admin.users.create.form] [ui.admin.users.create.form]
auto_password = "Auto Password" auto_password = "Auto Password"
@@ -1119,7 +1119,7 @@ tenant = "Tenant"
tenant_global = "Tenant Global" tenant_global = "Tenant Global"
[ui.admin.users.create.password_generated] [ui.admin.users.create.password_generated]
title = "Title" title = "Initial Password Generated"
[ui.admin.users.detail] [ui.admin.users.detail]
back = "Back" back = "Back"
@@ -1158,10 +1158,10 @@ title = "Affiliation & Organization Info"
[ui.admin.users.list] [ui.admin.users.list]
add = "User Add" add = "User Add"
bulk_import = "Bulk Import" bulk_import = "Bulk Import"
empty = "Empty" empty = "No users found."
fetch_error = "Fetch Error" fetch_error = "Failed to load the user list."
search_placeholder = "Search Placeholder" search_placeholder = "Search Placeholder"
subtitle = "Subtitle" subtitle = "Browse and manage registered users."
title = "User Manage" title = "User Manage"
[ui.admin.users.list.breadcrumb] [ui.admin.users.list.breadcrumb]
@@ -1175,7 +1175,7 @@ title = "Column Settings"
tenant = "Tenant Filter" tenant = "Tenant Filter"
[ui.admin.users.list.registry] [ui.admin.users.list.registry]
count = "Count" count = "{{count}} users loaded."
title = "User Registry" title = "User Registry"
[ui.admin.users.list.table] [ui.admin.users.list.table]
@@ -1316,7 +1316,7 @@ role = "Roles & Permissions"
[ui.dev.profile.basic] [ui.dev.profile.basic]
title = "User Info" title = "User Info"
id = "User ID" id = "Account ID"
name = "Name" name = "Name"
email = "Email" email = "Email"
phone = "Phone Number" phone = "Phone Number"
@@ -1414,6 +1414,7 @@ settings = "Settings"
[ui.dev.clients.general] [ui.dev.clients.general]
create = "Create Application" create = "Create Application"
display_new = "Add Connected Application" display_new = "Add Connected Application"
subtitle = "Manage application settings and security configuration."
title_create = "Create Client" title_create = "Create Client"
title_edit = "Client Settings" title_edit = "Client Settings"
@@ -1423,7 +1424,7 @@ add_title = "Add Identity Provider"
add_btn = "Add Provider" add_btn = "Add Provider"
[ui.dev.clients.general.identity] [ui.dev.clients.general.identity]
description = "Description" description = "Application Description"
description_placeholder = "Description Placeholder" description_placeholder = "Description Placeholder"
logo = "App Logo URL" logo = "App Logo URL"
logo_placeholder = "https://example.com/logo.png" logo_placeholder = "https://example.com/logo.png"
@@ -1443,7 +1444,7 @@ name_placeholder = "e.g. profile"
title = "Scopes" title = "Scopes"
[ui.dev.clients.general.scopes.table] [ui.dev.clients.general.scopes.table]
description = "Description" description = "Scope Description"
mandatory = "Mandatory" mandatory = "Mandatory"
name = "Scope Name" name = "Scope Name"
delete = "Delete" delete = "Delete"
@@ -1507,7 +1508,7 @@ subtitle = "Ship the RP controls"
title = "Next actions" title = "Next actions"
[ui.dev.dashboard.ops] [ui.dev.dashboard.ops]
subtitle = "Subtitle" subtitle = "Operational indicators for the current developer workspace."
title = "Ops board" title = "Ops board"
[ui.dev.dashboard.ops.card] [ui.dev.dashboard.ops.card]

View File

@@ -163,9 +163,9 @@ forbidden = "이 작업을 수행할 권한이 없습니다."
[msg.admin.audit] [msg.admin.audit]
empty = "아직 수집된 감사 로그가 없습니다." empty = "아직 수집된 감사 로그가 없습니다."
end = "End of audit feed" end = "감사 로그의 마지막입니다."
load_error = "Error loading logs: {{error}}" load_error = "감사 로그를 불러오지 못했습니다: {{error}}"
loading = "Loading audit logs..." loading = "감사 로그를 불러오는 중..."
subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다." subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다."
[msg.admin.header] [msg.admin.header]
@@ -221,9 +221,9 @@ subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합
deleted = "앱이 삭제되었습니다." deleted = "앱이 삭제되었습니다."
delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
delete_error = "삭제 실패: {{error}}" delete_error = "삭제 실패: {{error}}"
load_error = "Error loading clients: {{error}}" load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
loading = "Loading apps..." loading = "앱 정보를 불러오는 중..."
showing = "Showing {{shown}} of {{total}} apps" showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다."
[msg.dev.sidebar] [msg.dev.sidebar]
notice = "개발자 전용 콘솔입니다." notice = "개발자 전용 콘솔입니다."
@@ -497,7 +497,7 @@ qr_scan = "QR 스캔"
[ui.userfront.profile] [ui.userfront.profile]
department_empty = "소속 정보 없음" department_empty = "소속 정보 없음"
manage = "프로필 관리" manage = "프로필 관리"
user_fallback = "User" user_fallback = "사용자"
[ui.userfront.qr] [ui.userfront.qr]
rescan = "다시 스캔" rescan = "다시 스캔"
@@ -666,19 +666,19 @@ fetch_error = "사용자 목록 조회에 실패했습니다."
subtitle = "시스템 사용자를 조회하고 관리합니다. (Local DB)" subtitle = "시스템 사용자를 조회하고 관리합니다. (Local DB)"
[msg.dev.clients.consents] [msg.dev.clients.consents]
empty = "No consents found." empty = "등록된 동의 내역이 없습니다."
load_error = "Error loading consents: {{error}}" load_error = "동의 내역을 불러오지 못했습니다: {{error}}"
loading = "Loading consents..." loading = "동의 내역을 불러오는 중..."
revoke_confirm = "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다." revoke_confirm = "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다."
showing = "Showing {{from}} to {{to}} of {{total}} users" showing = "전체 {{total}}명 중 {{from}}번째부터 {{to}}번째 사용자를 표시합니다."
subtitle = "OIDC Relying Party 사용자 권한을 검토·관리합니다." subtitle = "OIDC Relying Party 사용자 권한을 검토·관리합니다."
[msg.dev.clients.details] [msg.dev.clients.details]
copy_client_id = "Client ID가 복사되었습니다." copy_client_id = "Client ID가 복사되었습니다."
copy_client_secret = "Client Secret이 복사되었습니다." copy_client_secret = "Client Secret이 복사되었습니다."
copy_endpoint = "{{label}}가 복사되었습니다." copy_endpoint = "{{label}}가 복사되었습니다."
load_error = "Error loading client: {{error}}" load_error = "앱 상세 정보를 불러오지 못했습니다: {{error}}"
loading = "Loading client..." loading = "앱 상세 정보를 불러오는 중..."
missing_id = "Client ID가 필요합니다." missing_id = "Client ID가 필요합니다."
redirect_saved = "Redirect URIs가 저장되었습니다." redirect_saved = "Redirect URIs가 저장되었습니다."
rotate_confirm = "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?" rotate_confirm = "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?"
@@ -689,8 +689,8 @@ secret_unavailable = "SECRET_NOT_AVAILABLE"
subtitle = "OIDC 자격 증명과 엔드포인트를 관리합니다." subtitle = "OIDC 자격 증명과 엔드포인트를 관리합니다."
[msg.dev.clients.general] [msg.dev.clients.general]
load_error = "Error loading client: {{error}}" load_error = "앱 설정을 불러오지 못했습니다: {{error}}"
loading = "Loading client..." loading = "앱 설정을 불러오는 중..."
save_error = "저장 실패: {{error}}" save_error = "저장 실패: {{error}}"
saved = "설정이 저장되었습니다." saved = "설정이 저장되었습니다."
status_changed = "상태가 {{status}}로 변경되었습니다." status_changed = "상태가 {{status}}로 변경되었습니다."
@@ -1192,6 +1192,7 @@ title = "User Consent Grants"
[ui.dev.clients.general] [ui.dev.clients.general]
create = "앱 생성" create = "앱 생성"
display_new = "연동 앱 추가" display_new = "연동 앱 추가"
subtitle = "앱 설정과 보안 구성을 관리합니다."
title_create = "연동 앱 생성" title_create = "연동 앱 생성"
title_edit = "연동 앱 설정" title_edit = "연동 앱 설정"
@@ -1232,7 +1233,7 @@ profile = "기본 프로필 정보 접근"
[ui.dev.clients.table] [ui.dev.clients.table]
actions = "액션" actions = "액션"
application = "애플리케이션" application = "애플리케이션"
client_id = "Client ID" client_id = "클라이언트 ID"
created_at = "생성일" created_at = "생성일"
status = "상태" status = "상태"
type = "유형" type = "유형"
@@ -1535,7 +1536,7 @@ type = "타입"
type_boolean = "Boolean" type_boolean = "Boolean"
type_date = "Date" type_date = "Date"
type_number = "Number" type_number = "Number"
type_text = "Text" type_text = "텍스트"
validation_placeholder = "정규표현식 (선택 사항)" validation_placeholder = "정규표현식 (선택 사항)"
type_datetime = "일시 (DateTime)" type_datetime = "일시 (DateTime)"
type_float = "실수 (Float)" type_float = "실수 (Float)"
@@ -1636,25 +1637,25 @@ current = "User Consent Grants"
home = "Home" home = "Home"
[ui.dev.clients.consents.filters] [ui.dev.clients.consents.filters]
advanced = "Advanced Filters" advanced = "고급 필터"
[ui.dev.clients.consents.stats] [ui.dev.clients.consents.stats]
active_grants = "Active Grants" active_grants = "활성 동의 건수"
avg_scopes = "Avg. Scopes per User" avg_scopes = "사용자당 평균 스코프"
total_scopes = "Total Scopes Issued" total_scopes = "총 발급 스코프 수"
[ui.dev.clients.consents.table] [ui.dev.clients.consents.table]
action = "Action" action = "동작"
first_granted = "First Granted" first_granted = "최초 동의 시각"
last_auth = "Last Authenticated" last_auth = "마지막 인증 시각"
scopes = "Granted Scopes" scopes = "승인된 스코프"
status = "Status" status = "상태"
tenant = "Tenant" tenant = "테넌트"
user = "User" user = "사용자"
[ui.dev.clients.details.credentials] [ui.dev.clients.details.credentials]
client_id = "Client ID" client_id = "클라이언트 ID"
client_secret = "Client Secret" client_secret = "클라이언트 시크릿"
title = "앱 자격 증명" title = "앱 자격 증명"
[ui.dev.clients.details.endpoints] [ui.dev.clients.details.endpoints]
@@ -1663,7 +1664,7 @@ title = "OIDC 엔드포인트"
[ui.dev.clients.details.redirect] [ui.dev.clients.details.redirect]
callback_label = "인증 콜백 URL" callback_label = "인증 콜백 URL"
label = "Redirect URIs" label = "리디렉션 URI"
placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback" placeholder = "https://your-app.com/callback, http://localhost:3000/auth/callback"
save = "Redirect URIs 저장" save = "Redirect URIs 저장"
title = "리디렉션 URI 설정" title = "리디렉션 URI 설정"
@@ -1682,24 +1683,24 @@ consents = "동의 및 사용자"
settings = "설정" settings = "설정"
[ui.dev.clients.general.identity] [ui.dev.clients.general.identity]
description = "Description" description = "설명"
description_placeholder = "앱에 대한 간단한 설명을 입력하세요." description_placeholder = "앱에 대한 간단한 설명을 입력하세요."
logo = "App Logo URL" logo = "앱 로고 URL"
logo_placeholder = "https://example.com/logo.png" logo_placeholder = "https://example.com/logo.png"
logo_preview = "Logo Preview" logo_preview = "로고 미리보기"
name = "앱 이름" name = "앱 이름"
name_placeholder = "My Awesome Application" name_placeholder = "예: 멋진 애플리케이션"
title = "Application Identity" title = "애플리케이션 정보"
[ui.dev.clients.general.redirect] [ui.dev.clients.general.redirect]
label = "Redirect URIs" label = "리디렉션 URI"
placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)" placeholder = "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)"
[ui.dev.clients.general.scopes] [ui.dev.clients.general.scopes]
add = "Scope 추가" add = "Scope 추가"
description_placeholder = "권한에 대한 설명" description_placeholder = "권한에 대한 설명"
name_placeholder = "e.g. profile" name_placeholder = "e.g. profile"
title = "Scopes" title = "스코프"
[ui.dev.clients.general.security] [ui.dev.clients.general.security]
private = "Server side App" private = "Server side App"
@@ -1720,7 +1721,7 @@ label = "이메일 주소"
title = "이메일 인증" title = "이메일 인증"
[ui.dev.clients.general.scopes.table] [ui.dev.clients.general.scopes.table]
description = "Description" description = "설명"
mandatory = "Mandatory" mandatory = "필수"
name = "Scope Name" name = "스코프 이름"
delete = "Delete" delete = "삭제"

View File

@@ -1192,6 +1192,7 @@ title = ""
[ui.dev.clients.general] [ui.dev.clients.general]
create = "" create = ""
display_new = "" display_new = ""
subtitle = ""
title_create = "" title_create = ""
title_edit = "" 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();