diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml
index 77d23ab5..b42827f9 100644
--- a/.gitea/workflows/code_check.yml
+++ b/.gitea/workflows/code_check.yml
@@ -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:
diff --git a/Makefile b/Makefile
index 8466a2b2..08b09822 100644
--- a/Makefile
+++ b/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 \
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
index 964d4d55..6aeedfc0 100644
--- a/devfront/src/features/clients/ClientGeneralPage.tsx
+++ b/devfront/src/features/clients/ClientGeneralPage.tsx
@@ -305,11 +305,19 @@ function ClientGeneralPage() {
-
- {isCreate
- ? t("ui.dev.clients.general.title_create", "Create Client")
- : t("ui.dev.clients.general.title_edit", "Client Settings")}
-
+
+
+ {isCreate
+ ? t("ui.dev.clients.general.title_create", "Create Client")
+ : t("ui.dev.clients.general.title_edit", "Client Settings")}
+
+
+ {t(
+ "ui.dev.clients.general.subtitle",
+ "앱 정보, 권한 스코프, 보안 설정을 관리합니다.",
+ )}
+
+
{!isCreate && (
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml
index f6dc1fac..6b2696f2 100644
--- a/devfront/src/locales/en.toml
+++ b/devfront/src/locales/en.toml
@@ -337,8 +337,8 @@ delete_confirm = "Are you sure you want to delete this app? This action cannot b
empty = "No consents found."
load_error = "Error loading consents: {{error}}"
loading = "Loading consents..."
-showing = "Showing {{from}} to {{to}} of {{total}} users"
-subtitle = "Subtitle"
+showing = "Showing users {{from}}-{{to}} of {{total}}"
+subtitle = "Review and manage user consent grants for this OIDC relying party."
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]
@@ -354,14 +354,14 @@ rotate_error = "Rotate Error"
save_error = "Save Error"
secret_rotated = "Secret Rotated"
secret_unavailable = "SECRET_NOT_AVAILABLE"
-subtitle = "Subtitle"
+subtitle = "Manage OIDC credentials and endpoints."
[msg.dev.clients.details.redirect]
-description = "Description"
+description = "List the allowed URLs to redirect users to after successful authentication. Separate multiple values with commas (,)."
[msg.dev.clients.details.security]
-footer = "Footer"
-note = "Note"
+footer = "For secret rotation, we recommend checking admin session TTL, rate limits, and notification integrations."
+note = "Keep endpoints read-only, and tie secret rotation/copy actions to audit logs."
[msg.dev.clients.general]
load_error = "Error loading client: {{error}}"
@@ -376,15 +376,15 @@ add_subtitle = "Connect an external OIDC provider."
empty = "No IdP configurations found."
[msg.dev.clients.general.identity]
-logo_help = "Logo Help"
-subtitle = "Subtitle"
+logo_help = "PNG or SVG URL shown on the consent and authentication screens."
+subtitle = "Set the application name, description, and logo."
[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 scopes registered."
+subtitle = "Define the permission 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."
@@ -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]
@@ -1218,6 +1218,7 @@ env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
+audit_logs = "Audit Logs"
clients = "Connected Application"
logout = "Logout"
@@ -1329,6 +1330,7 @@ settings = "Settings"
[ui.dev.clients.general]
create = "Create Application"
display_new = "Add Connected Application"
+subtitle = "Manage application identity, permission scopes, and security settings."
title_create = "Create Client"
title_edit = "Client Settings"
@@ -1338,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"
@@ -1358,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"
diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml
index 64580034..1c1292e8 100644
--- a/devfront/src/locales/ko.toml
+++ b/devfront/src/locales/ko.toml
@@ -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,24 +329,24 @@ 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 = "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연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?"
@@ -364,8 +364,8 @@ footer = "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이
note = "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요."
[msg.dev.clients.general]
-load_error = "Error loading client: {{error}}"
-loading = "Loading client..."
+load_error = "클라이언트 정보를 불러오지 못했습니다: {{error}}"
+loading = "클라이언트 정보를 불러오는 중..."
save_error = "저장 실패: {{error}}"
saved = "설정이 저장되었습니다."
status_changed = "상태가 {{status}}로 변경되었습니다."
@@ -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]
@@ -1218,6 +1218,7 @@ env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
+audit_logs = "감사 로그"
clients = "연동 앱"
logout = "로그아웃"
@@ -1262,43 +1263,43 @@ type_all = "모든 유형"
type_label = "유형:"
[ui.dev.clients.consents]
-export_csv = "Export CSV"
-revoke = "Revoke"
+export_csv = "CSV 내보내기"
+revoke = "철회"
revoked_at = "철회일: "
scope_label = "권한:"
search_placeholder = "사용자 ID, 이름, 이메일로 검색"
-status_label = "Status:"
-status_revoked = "Revoked"
-subject = "Subject"
-title = "User Consent Grants"
+status_label = "상태:"
+status_revoked = "철회됨"
+subject = "사용자"
+title = "사용자 동의 내역"
[ui.dev.clients.consents.breadcrumb]
-clients = "Clients"
-current = "User Consent Grants"
-home = "Home"
+clients = "연동 앱"
+current = "동의 및 사용자"
+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]
[ui.dev.clients.details.credentials]
-client_id = "Client ID"
-client_secret = "Client Secret"
+client_id = "클라이언트 ID"
+client_secret = "클라이언트 시크릿"
title = "앱 자격 증명"
[ui.dev.clients.details.endpoints]
@@ -1307,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 설정"
@@ -1328,6 +1329,7 @@ settings = "설정"
[ui.dev.clients.general]
create = "앱 생성"
display_new = "연동 앱 추가"
+subtitle = "앱 정보, 권한 스코프, 보안 설정을 관리합니다."
title_create = "연동 앱 생성"
title_edit = "연동 앱 설정"
@@ -1337,30 +1339,30 @@ add_title = "Add Identity Provider"
add_btn = "Add Provider"
[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 추가"
+add = "스코프 추가"
description_placeholder = "권한에 대한 설명"
name_placeholder = "e.g. profile"
-title = "Scopes"
+title = "스코프"
[ui.dev.clients.general.scopes.table]
-description = "Description"
-mandatory = "Mandatory"
-name = "Scope Name"
-delete = "Delete"
+description = "설명"
+mandatory = "필수"
+name = "스코프 이름"
+delete = "삭제"
[ui.dev.clients.general.security]
private = "Server side App"
@@ -1399,7 +1401,7 @@ profile = "기본 프로필 정보 접근"
[ui.dev.clients.table]
actions = "액션"
application = "애플리케이션"
-client_id = "Client ID"
+client_id = "클라이언트 ID"
created_at = "생성일"
status = "상태"
type = "유형"
@@ -1569,7 +1571,7 @@ qr_scan = "QR 스캔"
[ui.userfront.profile]
department_empty = "소속 정보 없음"
manage = "프로필 관리"
-user_fallback = "User"
+user_fallback = "사용자"
[ui.userfront.profile.field]
affiliation = "구분"
diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml
index 00d6daa1..aa09aed6 100644
--- a/devfront/src/locales/template.toml
+++ b/devfront/src/locales/template.toml
@@ -1218,6 +1218,7 @@ env_badge = ""
scope_badge = ""
[ui.dev.nav]
+audit_logs = ""
clients = ""
logout = ""
@@ -1328,6 +1329,7 @@ settings = ""
[ui.dev.clients.general]
create = ""
display_new = ""
+subtitle = ""
title_create = ""
title_edit = ""
diff --git a/devfront/tests/clients.spec.ts b/devfront/tests/clients.spec.ts
index c1b2dc43..b304583e 100644
--- a/devfront/tests/clients.spec.ts
+++ b/devfront/tests/clients.spec.ts
@@ -1,38 +1,23 @@
import { expect, test } from "@playwright/test";
-import { seedAuth } from "./helpers/devfront-fixtures";
+import {
+ type Consent,
+ installDevApiMock,
+ makeClient,
+ seedAuth,
+} from "./helpers/devfront-fixtures";
test("clients page loads correctly", async ({ page }) => {
await seedAuth(page);
-
- await page.route("**/api/v1/dev/clients**", async (route) => {
- if (route.request().method() !== "GET") {
- await route.fulfill({
- status: 404,
- contentType: "application/json",
- body: JSON.stringify({ error: "Not found" }),
- });
- return;
- }
-
- await route.fulfill({
- status: 200,
- contentType: "application/json",
- body: JSON.stringify({
- items: [
- {
- id: "client-playwright",
- name: "Playwright Client",
- type: "private",
- status: "active",
- createdAt: new Date().toISOString(),
- redirectUris: ["http://localhost:5174/callback"],
- scopes: ["openid", "profile", "email"],
- },
- ],
- limit: 50,
- offset: 0,
+ await installDevApiMock(page, {
+ clients: [
+ makeClient("client-playwright", {
+ name: "Playwright Client",
+ createdAt: new Date().toISOString(),
+ redirectUris: ["http://localhost:5174/callback"],
}),
- });
+ ],
+ consents: [] as Consent[],
+ auditLogsByCursor: undefined,
});
await page.goto("/clients");
@@ -46,9 +31,9 @@ test("clients page loads correctly", async ({ page }) => {
// 테이블 헤더 확인
await expect(
- page.getByRole("columnheader", { name: "애플리케이션" }),
+ page.locator("th").filter({ hasText: "애플리케이션" }),
).toBeVisible();
await expect(
- page.getByRole("columnheader", { name: "Client ID" }),
+ page.locator("th").filter({ hasText: /클라이언트 ID|Client ID/i }),
).toBeVisible();
});
diff --git a/devfront/tests/devfront-audit.spec.ts b/devfront/tests/devfront-audit.spec.ts
index efc31c75..8a0890d2 100644
--- a/devfront/tests/devfront-audit.spec.ts
+++ b/devfront/tests/devfront-audit.spec.ts
@@ -7,6 +7,8 @@ import {
seedAuth,
} from "./helpers/devfront-fixtures";
+const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
+
test.describe("DevFront audit logs", () => {
test.beforeEach(async ({ page }) => {
page.on("dialog", async (dialog) => {
@@ -87,19 +89,18 @@ test.describe("DevFront audit logs", () => {
await installDevApiMock(page, state);
await page.goto("/clients/new");
- await page
- .getByPlaceholder("My Awesome Application")
- .fill("Realtime New App");
+ await page.getByPlaceholder(appNamePlaceholder).fill("Realtime New App");
await page
.getByPlaceholder(/https:\/\/app\.example\.com\/callback/i)
.fill("https://realtime.example.com/callback");
await page.getByRole("button", { name: /앱 생성|Create/i }).click();
await expect.poll(() => state.auditLogs.length).toBeGreaterThanOrEqual(1);
+ await expect.poll(() => state.clients.at(-1)?.id).toMatch(/^client-/);
+ const createdClientId = state.clients.at(-1)?.id;
+ expect(createdClientId).toBeTruthy();
- await page.goto("/clients/client-realtime/settings");
- await page
- .getByPlaceholder("My Awesome Application")
- .fill("Realtime Updated");
+ await page.goto(`/clients/${createdClientId}/settings`);
+ await page.getByPlaceholder(appNamePlaceholder).fill("Realtime Updated");
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
await expect.poll(() => state.auditLogs.length).toBeGreaterThanOrEqual(2);
diff --git a/devfront/tests/devfront-clients-lifecycle.spec.ts b/devfront/tests/devfront-clients-lifecycle.spec.ts
index 7a662395..b88c4e29 100644
--- a/devfront/tests/devfront-clients-lifecycle.spec.ts
+++ b/devfront/tests/devfront-clients-lifecycle.spec.ts
@@ -7,6 +7,8 @@ import {
seedAuth,
} from "./helpers/devfront-fixtures";
+const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
+
test.describe("DevFront clients lifecycle", () => {
test.beforeEach(async ({ page }) => {
page.on("dialog", async (dialog) => {
@@ -36,7 +38,7 @@ test.describe("DevFront clients lifecycle", () => {
await expect(page).toHaveURL(/\/clients\/new$/);
await page
- .getByPlaceholder("My Awesome Application")
+ .getByPlaceholder(appNamePlaceholder)
.fill("Playwright Created App");
await page
.getByPlaceholder(/https:\/\/app\.example\.com\/callback/i)
@@ -45,7 +47,7 @@ test.describe("DevFront clients lifecycle", () => {
.getByRole("button", { name: /앱 생성|클라이언트 생성|Create/i })
.click();
- await expect(page).toHaveURL(/\/clients\/client-2\/settings$/);
+ await expect(page).toHaveURL(/\/clients\/client-\d+\/settings$/);
await expect(
page.getByRole("heading", {
name: /연동 앱 설정|클라이언트 설정|Client Settings/i,
@@ -97,7 +99,7 @@ test.describe("DevFront clients lifecycle", () => {
await installDevApiMock(page, state);
await page.goto("/clients/client-edit/settings");
- await page.getByPlaceholder("My Awesome Application").fill("After Name");
+ await page.getByPlaceholder(appNamePlaceholder).fill("After Name");
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
await expect.poll(() => state.clients[0]?.name).toBe("After Name");
diff --git a/devfront/tests/devfront-consents.spec.ts b/devfront/tests/devfront-consents.spec.ts
index 593cab76..27518c3a 100644
--- a/devfront/tests/devfront-consents.spec.ts
+++ b/devfront/tests/devfront-consents.spec.ts
@@ -39,7 +39,7 @@ test.describe("DevFront consents", () => {
await expect(page.getByText("Alice")).toBeVisible();
await expect(page.getByText("Tenant A")).toBeVisible();
- await page.getByRole("button", { name: /권한 철회|Revoke/i }).click();
+ await page.getByRole("button", { name: /권한 철회|철회|Revoke/i }).click();
await expect(page.getByText(/Revoked|철회/i).first()).toBeVisible();
});
});
diff --git a/devfront/tests/devfront-role-switch-report.spec.ts b/devfront/tests/devfront-role-switch-report.spec.ts
index 6faedf92..e6b2af07 100644
--- a/devfront/tests/devfront-role-switch-report.spec.ts
+++ b/devfront/tests/devfront-role-switch-report.spec.ts
@@ -8,6 +8,8 @@ import {
} from "./helpers/devfront-fixtures";
import { captureEvidence } from "./helpers/evidence";
+const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
+
test.describe("DevFront role report", () => {
test.beforeEach(async ({ page }) => {
page.on("dialog", async (dialog) => {
@@ -91,7 +93,7 @@ test.describe("DevFront role report", () => {
await page.goto("/clients/tenant-a-app-1/settings");
await page
- .getByPlaceholder("My Awesome Application")
+ .getByPlaceholder(appNamePlaceholder)
.fill("Tenant A CRM Updated");
const updatePromise = page.waitForResponse(
@@ -132,7 +134,7 @@ test.describe("DevFront role report", () => {
await page.goto("/clients/new");
await page
- .getByPlaceholder("My Awesome Application")
+ .getByPlaceholder(appNamePlaceholder)
.fill("Super Admin Created App");
await page
.getByPlaceholder(/https:\/\/app\.example\.com\/callback/i)
@@ -145,6 +147,17 @@ test.describe("DevFront role report", () => {
);
await page.getByRole("button", { name: /앱 생성|Create/i }).click();
await createPromise;
+ await expect
+ .poll(() =>
+ state.auditLogs.some((item) => {
+ try {
+ return JSON.parse(item.details)?.action === "CREATE_CLIENT";
+ } catch {
+ return false;
+ }
+ }),
+ )
+ .toBe(true);
await page.goto("/audit-logs");
await expect(page.getByText("CREATE_CLIENT")).toBeVisible({
diff --git a/devfront/tests/devfront-security.spec.ts b/devfront/tests/devfront-security.spec.ts
index 7c75e36f..36e526aa 100644
--- a/devfront/tests/devfront-security.spec.ts
+++ b/devfront/tests/devfront-security.spec.ts
@@ -25,7 +25,11 @@ test.describe("DevFront security and isolation", () => {
await installDevApiMock(page, state);
await page.goto("/clients/tenant-b-client");
- await expect(page.getByText(/Error loading client|조회/i)).toBeVisible();
+ await expect(
+ page.getByText(
+ /Error loading (app|client)|앱 정보를 불러오지 못했습니다|클라이언트 정보를 불러오지 못했습니다/i,
+ ),
+ ).toBeVisible();
});
test("RBAC: non-AppManager user should not see private apps", async ({
diff --git a/locales/en.toml b/locales/en.toml
index a0a62873..2b873498 100644
--- a/locales/en.toml
+++ b/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"
@@ -1036,7 +1036,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"
@@ -1090,14 +1090,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"
@@ -1122,7 +1122,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"
@@ -1163,10 +1163,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]
@@ -1180,7 +1180,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]
@@ -1322,7 +1322,7 @@ role = "Roles & Permissions"
[ui.dev.profile.basic]
title = "User Info"
-id = "User ID"
+id = "Account ID"
name = "Name"
email = "Email"
phone = "Phone Number"
@@ -1420,6 +1420,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"
@@ -1429,7 +1430,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"
@@ -1449,7 +1450,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"
@@ -1513,7 +1514,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]
diff --git a/locales/ko.toml b/locales/ko.toml
index f8a79f60..be21c7e2 100644
--- a/locales/ko.toml
+++ b/locales/ko.toml
@@ -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 = "유형"
@@ -1536,7 +1537,7 @@ type = "타입"
type_boolean = "Boolean"
type_date = "Date"
type_number = "Number"
-type_text = "Text"
+type_text = "텍스트"
validation_placeholder = "정규표현식 (선택 사항)"
type_datetime = "일시 (DateTime)"
type_float = "실수 (Float)"
@@ -1642,25 +1643,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]
@@ -1669,7 +1670,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 설정"
@@ -1688,24 +1689,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"
@@ -1726,53 +1727,7 @@ label = "이메일 주소"
title = "이메일 인증"
[ui.dev.clients.general.scopes.table]
-description = "Description"
-mandatory = "Mandatory"
-name = "Scope Name"
-delete = "Delete"
-
-[msg.admin.users.create.form]
-login_id_help = ""
-
-[msg.admin.users.detail]
-delete_confirm = ""
-delete_error = ""
-delete_success = ""
-reset_password_confirm = ""
-
-[msg.admin.users.detail.form]
-invalid_format = ""
-
-[ui.admin.tenants.schema.field]
-is_login_id = ""
-
-[ui.admin.users.create.form]
-login_id = ""
-login_id_placeholder = ""
-
-[ui.admin.users.detail]
-contact_title = ""
-created_at = ""
-delete = ""
-go_list = ""
-password_title = ""
-reset_password = ""
-reset_password_label = ""
-save = ""
-status_title = ""
-
-[ui.admin.users.detail.form]
-job_title = ""
-job_title_placeholder = ""
-login_id = ""
-login_id_placeholder = ""
-position = ""
-position_placeholder = ""
-role_super_admin = ""
-role_tenant_admin = ""
-role_user = ""
-status_active = ""
-status_inactive = ""
-
-[ui.admin.users.list.table]
-login_id = ""
+description = "설명"
+mandatory = "필수"
+name = "스코프 이름"
+delete = "삭제"
diff --git a/locales/template.toml b/locales/template.toml
index 12aac3b9..de802eb1 100644
--- a/locales/template.toml
+++ b/locales/template.toml
@@ -1192,6 +1192,7 @@ title = ""
[ui.dev.clients.general]
create = ""
display_new = ""
+subtitle = ""
title_create = ""
title_edit = ""
diff --git a/tools/i18n-scanner/value-check.js b/tools/i18n-scanner/value-check.js
new file mode 100644
index 00000000..9da4acab
--- /dev/null
+++ b/tools/i18n-scanner/value-check.js
@@ -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();
diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml
index 538eeb5b..79d64f17 100644
--- a/userfront/assets/translations/en.toml
+++ b/userfront/assets/translations/en.toml
@@ -21,24 +21,24 @@ user_group = "User Group"
[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.userfront]
greeting = "Hello, {name}."
@@ -631,3 +631,4 @@ verify = "Verification"
[ui.userfront.signup.success]
action = "Go to sign-in"
+
diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml
index 18d2b303..e87f37ea 100644
--- a/userfront/assets/translations/ko.toml
+++ b/userfront/assets/translations/ko.toml
@@ -1,5 +1,3 @@
-[domain]
-
[domain.affiliation]
affiliate = "가족사 임직원"
general = "일반 사용자"
@@ -18,325 +16,9 @@ company_group = "COMPANY_GROUP (그룹사/지주사)"
personal = "PERSONAL (개인 워크스페이스)"
user_group = "USER_GROUP (내부 부서/팀)"
-[err.userfront]
-
-[err.userfront.auth_proxy]
-consent_accept = "동의 처리에 실패했습니다."
-consent_fetch = "동의 정보를 가져오지 못했습니다."
-consent_reject = "동의 거부에 실패했습니다."
-linked_app_revoke = "연동 해지에 실패했습니다."
-login_failed = "로그인에 실패했습니다."
-oidc_accept = "OIDC 로그인 승인에 실패했습니다."
-password_reset_complete = "비밀번호 재설정에 실패했습니다."
-password_reset_init = "비밀번호 재설정을 시작하지 못했습니다."
-
-[err.userfront.profile]
-load_failed = "프로필을 불러오지 못했습니다: {error}"
-password_change_failed = "비밀번호 변경에 실패했습니다: {error}"
-send_code_failed = "인증번호 전송 실패: {error}"
-update_failed = "프로필 업데이트에 실패했습니다: {error}"
-verify_code_failed = "인증 실패: {error}"
-
-[err.userfront.session]
-missing = "활성 세션이 없습니다."
-
[msg.userfront]
greeting = "안녕하세요, {name}님"
-[msg.userfront.audit]
-date = "접속일자: {value}"
-device = "접속환경: {value}"
-end = "더 이상 항목이 없습니다."
-ip = "접속 IP: {value}"
-load_more_error = "더 불러오지 못했습니다."
-result = "인증결과: {value}"
-session_id = "Session ID: {value}"
-status = "현황: (준비중)"
-
-[msg.userfront.dashboard]
-approved_device = "승인 기기: {device}"
-approved_ip = "승인 IP: {ip}"
-audit_empty = "최근 접속 이력이 없습니다."
-audit_load_error = "접속이력을 불러오지 못했습니다."
-auth_method = "인증수단: {method}"
-client_id = "Client ID: {id}"
-client_id_missing = "Client ID 없음"
-current_status = "현재 상태: {status}"
-last_auth = "최근 인증: {value}"
-link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
-link_open_error = "해당 링크를 열 수 없습니다."
-render_error = "대시보드 렌더링 오류: {error}"
-session_id_copied = "세션 ID가 복사되었습니다."
-
-[msg.userfront.dashboard.activities]
-empty = "연동된 앱이 없습니다."
-empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
-error = "연동 정보를 불러오지 못했습니다."
-
-[msg.userfront.dashboard.approved_session]
-copy_click = "{label}: {id}\n클릭하면 복사됩니다."
-copy_tap = "{label}: {id}\n탭하면 복사됩니다."
-none = "{label} 없음"
-
-[msg.userfront.dashboard.revoke]
-confirm = "{app} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
-error = "해지 실패: {error}"
-success = "{app} 연동이 해지되었습니다."
-
-[msg.userfront.dashboard.scopes]
-empty = "요청된 권한이 없습니다."
-
-[msg.userfront.dashboard.timeline]
-load_error = "접속이력을 불러오지 못했습니다."
-
-[msg.userfront.error]
-detail_contact = "관리자에게 문의해 주세요."
-detail_generic = "오류가 발생했습니다."
-detail_request = "요청을 처리하는 중 문제가 발생했습니다."
-id = "오류 ID: {id}"
-title = "인증 과정에서 오류가 발생했습니다"
-title_generic = "오류가 발생했습니다"
-title_with_code = "오류: {code}"
-type = "오류 종류: {type}"
-
-[msg.userfront.error.whitelist]
-"$normalizedCode" = "{error}"
-bad_request = "입력값을 확인해 주세요."
-invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
-not_found = "요청한 페이지를 찾을 수 없습니다."
-password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
-rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
-recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
-recovery_invalid = "재설정 링크가 유효하지 않습니다."
-settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
-verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
-
-[msg.userfront.error.ory]
-"$normalizedCode" = "{error}"
-access_denied = "사용자가 동의를 거부했습니다."
-consent_required = "앱 접근 동의가 필요합니다."
-interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
-invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
-invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
-invalid_request = "잘못된 요청입니다."
-invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
-login_required = "로그인이 필요합니다."
-request_forbidden = "요청이 거부되었습니다."
-server_error = "인증 서버 오류가 발생했습니다."
-temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
-unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
-unsupported_response_type = "지원하지 않는 응답 타입입니다."
-
-[msg.userfront.forgot]
-description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
-dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
-error = "전송에 실패했습니다: {error}"
-input_required = "이메일 또는 휴대폰 번호를 입력해주세요."
-sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."
-
-[msg.userfront.login]
-cookie_check_failed = "로그인 확인 실패: {error}"
-dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
-link_failed = "오류: {error}"
-link_send_failed = "전송 실패: {error}"
-link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
-link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
-link_timeout = "시간이 경과되었습니다."
-no_account = "계정이 없으신가요?"
-oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
-qr_expired = "시간이 경과되었습니다."
-qr_init_failed = "QR 초기화에 실패했습니다: {error}"
-qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
-token_missing = "로그인 토큰을 확인할 수 없습니다."
-verification_failed = "승인 처리에 실패했습니다: {error}"
-
-[msg.userfront.login.link]
-helper = "입력하신 정보로 로그인 링크를 전송합니다."
-missing_login_id = "이메일 또는 휴대폰 번호를 입력해 주세요."
-missing_phone = "휴대폰 번호를 입력해 주세요."
-resend_wait = "재발송은 {time} 후 가능합니다."
-short_code_help = "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다."
-
-[msg.userfront.login.password]
-failed = "로그인 실패: {error}"
-missing_credentials = "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요."
-
-[msg.userfront.login.qr]
-load_failed = "QR 코드를 불러오지 못했습니다."
-scan_hint = "모바일 앱으로 스캔하세요"
-
-[msg.userfront.login.short_code]
-invalid = "문자 2개와 숫자 6자리를 입력해 주세요."
-
-[msg.userfront.login.unregistered]
-body = "가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요."
-
-[msg.userfront.login.verification]
-approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
-approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
-success = "로그인 승인에 성공했습니다."
-
-[msg.userfront.login_success]
-subtitle = "성공적으로 로그인되었습니다."
-
-[msg.userfront.consent]
-accept_error = "동의 처리에 실패했습니다: {error}"
-client_id = "클라이언트 ID: {id}"
-client_unknown = "알 수 없는 앱"
-description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\n계속 진행하려면 동의 여부를 선택해 주세요."
-load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
-missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
-redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
-scope_count = "총 {count}개"
-
-[msg.userfront.consent.cancel]
-confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?"
-error = "취소 처리 중 오류가 발생했습니다: {error}"
-
-[msg.userfront.consent.scope]
-email = "이메일 주소 (계정 식별 및 알림 용도)"
-offline_access = "오프라인 접근 (로그인 유지)"
-openid = "OpenID 인증 정보 (로그인 상태 확인)"
-phone = "휴대폰 번호 (본인 인증 및 알림)"
-profile = "기본 프로필 정보 (이름, 사용자 식별자)"
-
-[msg.userfront.profile]
-department_missing = "소속 정보 없음"
-department_required = "소속을 입력해주세요."
-email_missing = "이메일 없음"
-greeting = "안녕하세요, {name}님"
-load_failed = "정보를 불러올 수 없습니다."
-name_missing = "이름 없음"
-name_required = "이름을 입력해주세요."
-phone_required = "휴대폰 번호를 입력해주세요."
-phone_verify_required = "휴대폰 번호 인증이 필요합니다."
-update_failed = "수정 실패: {error}"
-update_success = "정보가 수정되었습니다."
-
-[msg.userfront.profile.password]
-change_failed = "비밀번호 변경 실패: {error}"
-changed = "비밀번호가 변경되었습니다."
-current_required = "현재 비밀번호를 입력해 주세요."
-mismatch = "새 비밀번호가 일치하지 않습니다."
-new_required = "새 비밀번호를 입력해 주세요."
-subtitle = "현재 비밀번호 확인 후 새 비밀번호로 변경합니다."
-
-[msg.userfront.profile.phone]
-code_sent = "인증번호가 전송되었습니다."
-send_failed = "전송 실패: {error}"
-verified = "인증되었습니다."
-verify_failed = "인증 실패: {error}"
-verify_notice = "휴대폰 번호를 변경하려면 SMS 인증이 필요합니다."
-
-[msg.userfront.profile.section]
-basic = "계정 기본 정보를 관리합니다."
-organization = "소속 및 구분 정보입니다."
-security = "비밀번호를 안전하게 관리합니다."
-
-[msg.userfront.qr]
-camera_error = "카메라 오류: {error}"
-permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요."
-permission_required = "카메라 권한이 필요합니다."
-
-[msg.userfront.reset]
-invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요."
-invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)"
-invalid_title = "유효하지 않은 링크입니다."
-policy_loading = "비밀번호 정책을 불러오는 중입니다..."
-success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요."
-
-[msg.userfront.reset.error]
-empty_password = "비밀번호를 입력해주세요."
-generic = "비밀번호 변경에 실패했습니다: {error}"
-lowercase = "최소 1개 이상의 소문자를 포함해야 합니다."
-min_length = "비밀번호는 최소 {count}자 이상이어야 합니다."
-min_types = "비밀번호는 영문 대/소문자/숫자/특수문자 중 {count}가지 이상 포함해야 합니다."
-mismatch = "비밀번호가 일치하지 않습니다."
-number = "최소 1개 이상의 숫자를 포함해야 합니다."
-symbol = "최소 1개 이상의 특수문자를 포함해야 합니다."
-uppercase = "최소 1개 이상의 대문자를 포함해야 합니다."
-
-[msg.userfront.reset.policy]
-lowercase = "소문자 1개 이상"
-min_length = "최소 {count}자 이상"
-min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
-number = "숫자 1개 이상"
-symbol = "특수문자 1개 이상"
-uppercase = "대문자 1개 이상"
-
-[msg.userfront.sections]
-apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
-audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
-
-[msg.userfront.settings]
-disabled = "현재 계정 설정 화면은 준비 중입니다."
-
-[msg.userfront.signup]
-failed = "가입 실패: {error}"
-privacy_full = "개인정보 수집 및 이용 동의 전문..."
-tos_full = "서비스 이용약관 전문..."
-
-[msg.userfront.signup.agreement]
-all_hint = "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다."
-description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요."
-privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다."
-progress = "필수 약관 {total}개 중 {count}개 동의 완료"
-tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다."
-title = "서비스 이용을 위해\n약관에 동의해주세요"
-
-[msg.userfront.signup.auth]
-affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요."
-title = "본인 확인을 위해\n인증을 진행해주세요"
-
-[msg.userfront.signup.email]
-code_mismatch = "인증코드가 일치하지 않습니다."
-duplicate = "이미 가입된 이메일입니다."
-invalid = "유효한 이메일 형식이 아닙니다."
-send_failed = "발송 실패: {error}"
-verified = "✅ 이메일 인증 완료"
-verify_failed = "인증 실패: {error}"
-
-[msg.userfront.signup.password]
-length_required = "비밀번호는 최소 12자 이상이어야 합니다."
-lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다."
-mismatch = "비밀번호가 일치하지 않습니다."
-number_required = "숫자가 최소 1개 이상 포함되어야 합니다."
-symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다."
-title = "마지막으로\n비밀번호를 설정해주세요"
-uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다."
-
-[msg.userfront.signup.password.rule]
-lowercase = "소문자"
-min_length = "{count}자 이상"
-min_types = "문자 유형 {count}가지 이상"
-number = "숫자"
-symbol = "특수문자"
-uppercase = "대문자"
-
-[msg.userfront.signup.phone]
-code_mismatch = "인증코드가 일치하지 않습니다."
-send_failed = "발송 실패: {error}"
-verified = "✅ 휴대폰 인증 완료"
-verify_failed = "인증 실패: {error}"
-
-[msg.userfront.signup.policy]
-loading = "비밀번호 정책을 불러오는 중입니다..."
-lowercase = "소문자"
-min_length = "최소 {count}자 이상"
-min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
-number = "숫자"
-summary = "보안 정책: {rules}"
-symbol = "특수문자"
-uppercase = "대문자"
-
-[msg.userfront.signup.profile]
-affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다."
-title = "회원님의\n소속 정보를 알려주세요"
-
-[msg.userfront.signup.success]
-body = "성공적으로 가입되었습니다."
-title = "회원가입 완료"
-
[ui.common]
add = "추가"
all = "전체"
@@ -391,6 +73,137 @@ theme_toggle = "테마 전환"
unknown = "Unknown"
generate = "생성"
+[ui.userfront]
+app_title = "Baron SW 포탈"
+
+[err.userfront.auth_proxy]
+consent_accept = "동의 처리에 실패했습니다."
+consent_fetch = "동의 정보를 가져오지 못했습니다."
+consent_reject = "동의 거부에 실패했습니다."
+linked_app_revoke = "연동 해지에 실패했습니다."
+login_failed = "로그인에 실패했습니다."
+oidc_accept = "OIDC 로그인 승인에 실패했습니다."
+password_reset_complete = "비밀번호 재설정에 실패했습니다."
+password_reset_init = "비밀번호 재설정을 시작하지 못했습니다."
+
+[err.userfront.profile]
+load_failed = "프로필을 불러오지 못했습니다: {error}"
+password_change_failed = "비밀번호 변경에 실패했습니다: {error}"
+send_code_failed = "인증번호 전송 실패: {error}"
+update_failed = "프로필 업데이트에 실패했습니다: {error}"
+verify_code_failed = "인증 실패: {error}"
+
+[err.userfront.session]
+missing = "활성 세션이 없습니다."
+
+[msg.userfront.audit]
+date = "접속일자: {value}"
+device = "접속환경: {value}"
+end = "더 이상 항목이 없습니다."
+ip = "접속 IP: {value}"
+load_more_error = "더 불러오지 못했습니다."
+result = "인증결과: {value}"
+session_id = "Session ID: {value}"
+status = "현황: (준비중)"
+
+[msg.userfront.dashboard]
+approved_device = "승인 기기: {device}"
+approved_ip = "승인 IP: {ip}"
+audit_empty = "최근 접속 이력이 없습니다."
+audit_load_error = "접속이력을 불러오지 못했습니다."
+auth_method = "인증수단: {method}"
+client_id = "Client ID: {id}"
+client_id_missing = "Client ID 없음"
+current_status = "현재 상태: {status}"
+last_auth = "최근 인증: {value}"
+link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
+link_open_error = "해당 링크를 열 수 없습니다."
+render_error = "대시보드 렌더링 오류: {error}"
+session_id_copied = "세션 ID가 복사되었습니다."
+
+[msg.userfront.error]
+detail_contact = "관리자에게 문의해 주세요."
+detail_generic = "오류가 발생했습니다."
+detail_request = "요청을 처리하는 중 문제가 발생했습니다."
+id = "오류 ID: {id}"
+title = "인증 과정에서 오류가 발생했습니다"
+title_generic = "오류가 발생했습니다"
+title_with_code = "오류: {code}"
+type = "오류 종류: {type}"
+
+[msg.userfront.forgot]
+description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
+dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
+error = "전송에 실패했습니다: {error}"
+input_required = "이메일 또는 휴대폰 번호를 입력해주세요."
+sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."
+
+[msg.userfront.login]
+cookie_check_failed = "로그인 확인 실패: {error}"
+dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
+link_failed = "오류: {error}"
+link_send_failed = "전송 실패: {error}"
+link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
+link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
+link_timeout = "시간이 경과되었습니다."
+no_account = "계정이 없으신가요?"
+oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
+qr_expired = "시간이 경과되었습니다."
+qr_init_failed = "QR 초기화에 실패했습니다: {error}"
+qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
+token_missing = "로그인 토큰을 확인할 수 없습니다."
+verification_failed = "승인 처리에 실패했습니다: {error}"
+
+[msg.userfront.login_success]
+subtitle = "성공적으로 로그인되었습니다."
+
+[msg.userfront.consent]
+accept_error = "동의 처리에 실패했습니다: {error}"
+client_id = "클라이언트 ID: {id}"
+client_unknown = "알 수 없는 앱"
+description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\n계속 진행하려면 동의 여부를 선택해 주세요."
+load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
+missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
+redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
+scope_count = "총 {count}개"
+
+[msg.userfront.profile]
+department_missing = "소속 정보 없음"
+department_required = "소속을 입력해주세요."
+email_missing = "이메일 없음"
+greeting = "안녕하세요, {name}님"
+load_failed = "정보를 불러올 수 없습니다."
+name_missing = "이름 없음"
+name_required = "이름을 입력해주세요."
+phone_required = "휴대폰 번호를 입력해주세요."
+phone_verify_required = "휴대폰 번호 인증이 필요합니다."
+update_failed = "수정 실패: {error}"
+update_success = "정보가 수정되었습니다."
+
+[msg.userfront.qr]
+camera_error = "카메라 오류: {error}"
+permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요."
+permission_required = "카메라 권한이 필요합니다."
+
+[msg.userfront.reset]
+invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요."
+invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)"
+invalid_title = "유효하지 않은 링크입니다."
+policy_loading = "비밀번호 정책을 불러오는 중입니다..."
+success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요."
+
+[msg.userfront.sections]
+apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다."
+audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다."
+
+[msg.userfront.settings]
+disabled = "현재 계정 설정 화면은 준비 중입니다."
+
+[msg.userfront.signup]
+failed = "가입 실패: {error}"
+privacy_full = "개인정보 수집 및 이용 동의 전문..."
+tos_full = "서비스 이용약관 전문..."
+
[ui.common.badge]
admin_only = "Admin only"
command_only = "Command only"
@@ -405,27 +218,11 @@ ok = "정상"
pending = "준비 중"
success = "성공"
-[ui.userfront]
-app_title = "Baron SW 포탈"
-
[ui.userfront.app_label]
admin_console = "Admin Console"
baron = "Baron 로그인"
dev_console = "Dev Console"
-[ui.userfront.audit]
-
-[ui.userfront.audit.table]
-app = "애플리케이션"
-auth_method = "인증수단"
-date = "접속일자"
-device = "접속환경"
-ip = "IP"
-pending = "(준비중)"
-result = "인증결과"
-session_id = "Session ID"
-status = "현황"
-
[ui.userfront.auth_method]
ory = "Ory 세션"
session = "세션"
@@ -434,23 +231,6 @@ session = "세션"
last_auth_label = "최근 인증"
status_history = "상태 이력"
-[ui.userfront.dashboard.activity]
-linked = "연동됨"
-
-[ui.userfront.dashboard.approved_session]
-default = "승인한 세션 ID"
-userfront = "승인한 Userfront 세션 ID"
-
-[ui.userfront.dashboard.revoke]
-confirm_button = "해지하기"
-title = "연동 해지"
-
-[ui.userfront.dashboard.scopes]
-title = "권한 (Scopes)"
-
-[ui.userfront.dashboard.status]
-revoked = "해지됨"
-
[ui.userfront.device]
android = "Mobile(Android)"
ios = "Mobile(iOS)"
@@ -472,6 +252,258 @@ title = "비밀번호 재설정"
forgot_password = "비밀번호를 잊으셨나요?"
signup = "회원가입"
+[ui.userfront.login_success]
+later = "나중에 하기 (대시보드로 이동)"
+qr = "QR 인증 (카메라 켜기)"
+title = "로그인 완료"
+
+[ui.userfront.consent]
+accept = "동의하고 계속하기"
+requested_scopes = "요청된 권한"
+title = "접근 권한 요청"
+
+[ui.userfront.nav]
+dashboard = "대시보드"
+logout = "로그아웃"
+profile = "내 정보"
+qr_scan = "QR 스캔"
+
+[ui.userfront.profile]
+department_empty = "소속 정보 없음"
+manage = "프로필 관리"
+user_fallback = "사용자"
+
+[ui.userfront.qr]
+rescan = "다시 스캔"
+result_success = "승인 완료"
+title = "Scan QR Code"
+
+[ui.userfront.reset]
+confirm_password = "새 비밀번호 확인"
+new_password = "새 비밀번호"
+submit = "비밀번호 변경"
+subtitle = "새로운 비밀번호 설정"
+title = "새 비밀번호 설정"
+
+[ui.userfront.sections]
+apps = "나의 App 현황"
+audit = "접속이력"
+
+[ui.userfront.session]
+active = "세션 활성"
+unknown = "알 수 없음"
+
+[ui.userfront.signup]
+complete = "가입 완료"
+next_step = "다음 단계"
+title = "회원가입"
+
+[msg.userfront.dashboard.activities]
+empty = "연동된 앱이 없습니다."
+empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
+error = "연동 정보를 불러오지 못했습니다."
+
+[msg.userfront.dashboard.approved_session]
+copy_click = "{label}: {id}\n클릭하면 복사됩니다."
+copy_tap = "{label}: {id}\n탭하면 복사됩니다."
+none = "{label} 없음"
+
+[msg.userfront.dashboard.revoke]
+confirm = "{app} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
+error = "해지 실패: {error}"
+success = "{app} 연동이 해지되었습니다."
+
+[msg.userfront.dashboard.scopes]
+empty = "요청된 권한이 없습니다."
+
+[msg.userfront.dashboard.timeline]
+load_error = "접속이력을 불러오지 못했습니다."
+
+[msg.userfront.error.whitelist]
+"$normalizedCode" = "{error}"
+bad_request = "입력값을 확인해 주세요."
+invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
+not_found = "요청한 페이지를 찾을 수 없습니다."
+password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
+rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
+recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
+recovery_invalid = "재설정 링크가 유효하지 않습니다."
+settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
+verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
+
+[msg.userfront.error.ory]
+"$normalizedCode" = "{error}"
+access_denied = "사용자가 동의를 거부했습니다."
+consent_required = "앱 접근 동의가 필요합니다."
+interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
+invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
+invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
+invalid_request = "잘못된 요청입니다."
+invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
+login_required = "로그인이 필요합니다."
+request_forbidden = "요청이 거부되었습니다."
+server_error = "인증 서버 오류가 발생했습니다."
+temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
+unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
+unsupported_response_type = "지원하지 않는 응답 타입입니다."
+
+[msg.userfront.login.link]
+helper = "입력하신 정보로 로그인 링크를 전송합니다."
+missing_login_id = "이메일 또는 휴대폰 번호를 입력해 주세요."
+missing_phone = "휴대폰 번호를 입력해 주세요."
+resend_wait = "재발송은 {time} 후 가능합니다."
+short_code_help = "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다."
+
+[msg.userfront.login.password]
+failed = "로그인 실패: {error}"
+missing_credentials = "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요."
+
+[msg.userfront.login.qr]
+load_failed = "QR 코드를 불러오지 못했습니다."
+scan_hint = "모바일 앱으로 스캔하세요"
+
+[msg.userfront.login.short_code]
+invalid = "문자 2개와 숫자 6자리를 입력해 주세요."
+
+[msg.userfront.login.unregistered]
+body = "가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요."
+
+[msg.userfront.login.verification]
+approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다."
+approved_local = "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다"
+success = "로그인 승인에 성공했습니다."
+
+[msg.userfront.consent.cancel]
+confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?"
+error = "취소 처리 중 오류가 발생했습니다: {error}"
+
+[msg.userfront.consent.scope]
+email = "이메일 주소 (계정 식별 및 알림 용도)"
+offline_access = "오프라인 접근 (로그인 유지)"
+openid = "OpenID 인증 정보 (로그인 상태 확인)"
+phone = "휴대폰 번호 (본인 인증 및 알림)"
+profile = "기본 프로필 정보 (이름, 사용자 식별자)"
+
+[msg.userfront.profile.password]
+change_failed = "비밀번호 변경 실패: {error}"
+changed = "비밀번호가 변경되었습니다."
+current_required = "현재 비밀번호를 입력해 주세요."
+mismatch = "새 비밀번호가 일치하지 않습니다."
+new_required = "새 비밀번호를 입력해 주세요."
+subtitle = "현재 비밀번호 확인 후 새 비밀번호로 변경합니다."
+
+[msg.userfront.profile.phone]
+code_sent = "인증번호가 전송되었습니다."
+send_failed = "전송 실패: {error}"
+verified = "인증되었습니다."
+verify_failed = "인증 실패: {error}"
+verify_notice = "휴대폰 번호를 변경하려면 SMS 인증이 필요합니다."
+
+[msg.userfront.profile.section]
+basic = "계정 기본 정보를 관리합니다."
+organization = "소속 및 구분 정보입니다."
+security = "비밀번호를 안전하게 관리합니다."
+
+[msg.userfront.reset.error]
+empty_password = "비밀번호를 입력해주세요."
+generic = "비밀번호 변경에 실패했습니다: {error}"
+lowercase = "최소 1개 이상의 소문자를 포함해야 합니다."
+min_length = "비밀번호는 최소 {count}자 이상이어야 합니다."
+min_types = "비밀번호는 영문 대/소문자/숫자/특수문자 중 {count}가지 이상 포함해야 합니다."
+mismatch = "비밀번호가 일치하지 않습니다."
+number = "최소 1개 이상의 숫자를 포함해야 합니다."
+symbol = "최소 1개 이상의 특수문자를 포함해야 합니다."
+uppercase = "최소 1개 이상의 대문자를 포함해야 합니다."
+
+[msg.userfront.reset.policy]
+lowercase = "소문자 1개 이상"
+min_length = "최소 {count}자 이상"
+min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
+number = "숫자 1개 이상"
+symbol = "특수문자 1개 이상"
+uppercase = "대문자 1개 이상"
+
+[msg.userfront.signup.agreement]
+all_hint = "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다."
+description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요."
+privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다."
+progress = "필수 약관 {total}개 중 {count}개 동의 완료"
+tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다."
+title = "서비스 이용을 위해\n약관에 동의해주세요"
+
+[msg.userfront.signup.auth]
+affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요."
+title = "본인 확인을 위해\n인증을 진행해주세요"
+
+[msg.userfront.signup.email]
+code_mismatch = "인증코드가 일치하지 않습니다."
+duplicate = "이미 가입된 이메일입니다."
+invalid = "유효한 이메일 형식이 아닙니다."
+send_failed = "발송 실패: {error}"
+verified = "✅ 이메일 인증 완료"
+verify_failed = "인증 실패: {error}"
+
+[msg.userfront.signup.password]
+length_required = "비밀번호는 최소 12자 이상이어야 합니다."
+lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다."
+mismatch = "비밀번호가 일치하지 않습니다."
+number_required = "숫자가 최소 1개 이상 포함되어야 합니다."
+symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다."
+title = "마지막으로\n비밀번호를 설정해주세요"
+uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다."
+
+[msg.userfront.signup.phone]
+code_mismatch = "인증코드가 일치하지 않습니다."
+send_failed = "발송 실패: {error}"
+verified = "✅ 휴대폰 인증 완료"
+verify_failed = "인증 실패: {error}"
+
+[msg.userfront.signup.policy]
+loading = "비밀번호 정책을 불러오는 중입니다..."
+lowercase = "소문자"
+min_length = "최소 {count}자 이상"
+min_types = "영문 대/소문자/숫자/특수문자 중 {count}가지 이상"
+number = "숫자"
+summary = "보안 정책: {rules}"
+symbol = "특수문자"
+uppercase = "대문자"
+
+[msg.userfront.signup.profile]
+affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다."
+title = "회원님의\n소속 정보를 알려주세요"
+
+[msg.userfront.signup.success]
+body = "성공적으로 가입되었습니다."
+title = "회원가입 완료"
+
+[ui.userfront.audit.table]
+app = "애플리케이션"
+auth_method = "인증수단"
+date = "접속일자"
+device = "접속환경"
+ip = "IP"
+pending = "(준비중)"
+result = "인증결과"
+session_id = "Session ID"
+status = "현황"
+
+[ui.userfront.dashboard.activity]
+linked = "연동됨"
+
+[ui.userfront.dashboard.approved_session]
+default = "승인한 세션 ID"
+userfront = "승인한 Userfront 세션 ID"
+
+[ui.userfront.dashboard.revoke]
+confirm_button = "해지하기"
+title = "연동 해지"
+
+[ui.userfront.dashboard.scopes]
+title = "권한 (Scopes)"
+
+[ui.userfront.dashboard.status]
+revoked = "해지됨"
+
[ui.userfront.login.action]
submit = "로그인"
@@ -509,31 +541,10 @@ action_label = "확인"
page_title = "로그인 승인"
title = "승인 완료"
-[ui.userfront.login_success]
-later = "나중에 하기 (대시보드로 이동)"
-qr = "QR 인증 (카메라 켜기)"
-title = "로그인 완료"
-
-[ui.userfront.consent]
-accept = "동의하고 계속하기"
-requested_scopes = "요청된 권한"
-title = "접근 권한 요청"
-
[ui.userfront.consent.cancel]
confirm_button = "예, 취소합니다"
title = "동의 취소"
-[ui.userfront.nav]
-dashboard = "대시보드"
-logout = "로그아웃"
-profile = "내 정보"
-qr_scan = "QR 스캔"
-
-[ui.userfront.profile]
-department_empty = "소속 정보 없음"
-manage = "프로필 관리"
-user_fallback = "User"
-
[ui.userfront.profile.field]
affiliation = "구분"
company_code = "회사코드"
@@ -560,31 +571,6 @@ basic = "기본 정보"
organization = "조직 정보"
security = "보안"
-[ui.userfront.qr]
-rescan = "다시 스캔"
-result_success = "승인 완료"
-title = "Scan QR Code"
-
-[ui.userfront.reset]
-confirm_password = "새 비밀번호 확인"
-new_password = "새 비밀번호"
-submit = "비밀번호 변경"
-subtitle = "새로운 비밀번호 설정"
-title = "새 비밀번호 설정"
-
-[ui.userfront.sections]
-apps = "나의 App 현황"
-audit = "접속이력"
-
-[ui.userfront.session]
-active = "세션 활성"
-unknown = "알 수 없음"
-
-[ui.userfront.signup]
-complete = "가입 완료"
-next_step = "다음 단계"
-title = "회원가입"
-
[ui.userfront.signup.agreement]
all = "모두 동의합니다"
privacy_title = "개인정보 수집 및 이용 동의 (필수)"
@@ -595,10 +581,6 @@ tos_title = "바론 소프트웨어 이용약관 (필수)"
code_label = "인증코드 6자리"
request_code = "인증요청"
-[ui.userfront.signup.auth.email]
-label = "이메일 주소"
-title = "이메일 인증"
-
[ui.userfront.signup.password]
confirm_label = "비밀번호 확인"
label = "비밀번호"
@@ -622,3 +604,16 @@ verify = "본인인증"
[ui.userfront.signup.success]
action = "로그인하기"
+
+[msg.userfront.signup.password.rule]
+lowercase = "소문자"
+min_length = "{count}자 이상"
+min_types = "문자 유형 {count}가지 이상"
+number = "숫자"
+symbol = "특수문자"
+uppercase = "대문자"
+
+[ui.userfront.signup.auth.email]
+label = "이메일 주소"
+title = "이메일 인증"
+
diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml
index 44c85800..d954d383 100644
--- a/userfront/assets/translations/template.toml
+++ b/userfront/assets/translations/template.toml
@@ -1,5 +1,3 @@
-[domain]
-
[domain.affiliation]
affiliate = ""
general = ""
@@ -18,325 +16,9 @@ company_group = ""
personal = ""
user_group = ""
-[err.userfront]
-
-[err.userfront.auth_proxy]
-consent_accept = ""
-consent_fetch = ""
-consent_reject = ""
-linked_app_revoke = ""
-login_failed = ""
-oidc_accept = ""
-password_reset_complete = ""
-password_reset_init = ""
-
-[err.userfront.profile]
-load_failed = ""
-password_change_failed = ""
-send_code_failed = ""
-update_failed = ""
-verify_code_failed = ""
-
-[err.userfront.session]
-missing = ""
-
[msg.userfront]
greeting = ""
-[msg.userfront.audit]
-date = ""
-device = ""
-end = ""
-ip = ""
-load_more_error = ""
-result = ""
-session_id = ""
-status = ""
-
-[msg.userfront.dashboard]
-approved_device = ""
-approved_ip = ""
-audit_empty = ""
-audit_load_error = ""
-render_error = ""
-auth_method = ""
-client_id = ""
-client_id_missing = ""
-current_status = ""
-last_auth = ""
-link_missing = ""
-link_open_error = ""
-session_id_copied = ""
-
-[msg.userfront.dashboard.activities]
-empty = ""
-empty_detail = ""
-error = ""
-
-[msg.userfront.dashboard.approved_session]
-copy_click = ""
-copy_tap = ""
-none = ""
-
-[msg.userfront.dashboard.revoke]
-confirm = ""
-error = ""
-success = ""
-
-[msg.userfront.dashboard.scopes]
-empty = ""
-
-[msg.userfront.dashboard.timeline]
-load_error = ""
-
-[msg.userfront.error]
-detail_contact = ""
-detail_generic = ""
-detail_request = ""
-id = ""
-title = ""
-title_generic = ""
-title_with_code = ""
-type = ""
-
-[msg.userfront.error.whitelist]
-"$normalizedCode" = ""
-settings_disabled = ""
-invalid_session = ""
-verification_required = ""
-recovery_expired = ""
-recovery_invalid = ""
-rate_limited = ""
-not_found = ""
-bad_request = ""
-password_or_email_mismatch = ""
-
-[msg.userfront.error.ory]
-"$normalizedCode" = ""
-access_denied = ""
-consent_required = ""
-interaction_required = ""
-invalid_client = ""
-invalid_grant = ""
-invalid_request = ""
-invalid_scope = ""
-login_required = ""
-request_forbidden = ""
-server_error = ""
-temporarily_unavailable = ""
-unauthorized_client = ""
-unsupported_response_type = ""
-
-[msg.userfront.forgot]
-description = ""
-dry_send = ""
-error = ""
-input_required = ""
-sent = ""
-
-[msg.userfront.login]
-cookie_check_failed = ""
-dry_send = ""
-link_failed = ""
-link_send_failed = ""
-link_sent_email = ""
-link_sent_phone = ""
-link_timeout = ""
-no_account = ""
-oidc_failed = ""
-qr_expired = ""
-qr_init_failed = ""
-qr_login_required = ""
-token_missing = ""
-verification_failed = ""
-
-[msg.userfront.login.link]
-helper = ""
-missing_login_id = ""
-missing_phone = ""
-resend_wait = ""
-short_code_help = ""
-
-[msg.userfront.login.password]
-failed = ""
-missing_credentials = ""
-
-[msg.userfront.login.qr]
-load_failed = ""
-scan_hint = ""
-
-[msg.userfront.login.short_code]
-invalid = ""
-
-[msg.userfront.login.unregistered]
-body = ""
-
-[msg.userfront.login.verification]
-approved = ""
-approved_local = ""
-success = ""
-
-[msg.userfront.login_success]
-subtitle = ""
-
-[msg.userfront.consent]
-accept_error = ""
-client_id = ""
-client_unknown = ""
-description = ""
-load_error = ""
-missing_redirect = ""
-redirect_notice = ""
-scope_count = ""
-
-[msg.userfront.consent.cancel]
-confirm = ""
-error = ""
-
-[msg.userfront.consent.scope]
-email = ""
-offline_access = ""
-openid = ""
-phone = ""
-profile = ""
-
-[msg.userfront.profile]
-department_missing = ""
-department_required = ""
-email_missing = ""
-greeting = ""
-load_failed = ""
-name_missing = ""
-name_required = ""
-phone_required = ""
-phone_verify_required = ""
-update_failed = ""
-update_success = ""
-
-[msg.userfront.profile.password]
-change_failed = ""
-changed = ""
-current_required = ""
-mismatch = ""
-new_required = ""
-subtitle = ""
-
-[msg.userfront.profile.phone]
-code_sent = ""
-send_failed = ""
-verified = ""
-verify_failed = ""
-verify_notice = ""
-
-[msg.userfront.profile.section]
-basic = ""
-organization = ""
-security = ""
-
-[msg.userfront.qr]
-camera_error = ""
-permission_error = ""
-permission_required = ""
-
-[msg.userfront.reset]
-invalid_body = ""
-invalid_link = ""
-invalid_title = ""
-policy_loading = ""
-success = ""
-
-[msg.userfront.reset.error]
-empty_password = ""
-generic = ""
-lowercase = ""
-min_length = ""
-min_types = ""
-mismatch = ""
-number = ""
-symbol = ""
-uppercase = ""
-
-[msg.userfront.reset.policy]
-lowercase = ""
-min_length = ""
-min_types = ""
-number = ""
-symbol = ""
-uppercase = ""
-
-[msg.userfront.sections]
-apps_subtitle = ""
-audit_subtitle = ""
-
-[msg.userfront.settings]
-disabled = ""
-
-[msg.userfront.signup]
-failed = ""
-privacy_full = ""
-tos_full = ""
-
-[msg.userfront.signup.agreement]
-all_hint = ""
-description = ""
-privacy_summary = ""
-progress = ""
-tos_summary = ""
-title = ""
-
-[msg.userfront.signup.auth]
-affiliate_notice = ""
-title = ""
-
-[msg.userfront.signup.email]
-code_mismatch = ""
-duplicate = ""
-invalid = ""
-send_failed = ""
-verified = ""
-verify_failed = ""
-
-[msg.userfront.signup.password]
-length_required = ""
-lowercase_required = ""
-mismatch = ""
-number_required = ""
-symbol_required = ""
-title = ""
-uppercase_required = ""
-
-[msg.userfront.signup.password.rule]
-lowercase = ""
-min_length = ""
-min_types = ""
-number = ""
-symbol = ""
-uppercase = ""
-
-[msg.userfront.signup.phone]
-code_mismatch = ""
-send_failed = ""
-verified = ""
-verify_failed = ""
-
-[msg.userfront.signup.policy]
-loading = ""
-lowercase = ""
-min_length = ""
-min_types = ""
-number = ""
-summary = ""
-symbol = ""
-uppercase = ""
-
-[msg.userfront.signup.profile]
-affiliate_hint = ""
-title = ""
-
-[msg.userfront.signup.success]
-body = ""
-title = ""
-
[ui.common]
add = ""
all = ""
@@ -391,6 +73,137 @@ theme_toggle = ""
unknown = ""
generate = ""
+[ui.userfront]
+app_title = ""
+
+[err.userfront.auth_proxy]
+consent_accept = ""
+consent_fetch = ""
+consent_reject = ""
+linked_app_revoke = ""
+login_failed = ""
+oidc_accept = ""
+password_reset_complete = ""
+password_reset_init = ""
+
+[err.userfront.profile]
+load_failed = ""
+password_change_failed = ""
+send_code_failed = ""
+update_failed = ""
+verify_code_failed = ""
+
+[err.userfront.session]
+missing = ""
+
+[msg.userfront.audit]
+date = ""
+device = ""
+end = ""
+ip = ""
+load_more_error = ""
+result = ""
+session_id = ""
+status = ""
+
+[msg.userfront.dashboard]
+approved_device = ""
+approved_ip = ""
+audit_empty = ""
+audit_load_error = ""
+render_error = ""
+auth_method = ""
+client_id = ""
+client_id_missing = ""
+current_status = ""
+last_auth = ""
+link_missing = ""
+link_open_error = ""
+session_id_copied = ""
+
+[msg.userfront.error]
+detail_contact = ""
+detail_generic = ""
+detail_request = ""
+id = ""
+title = ""
+title_generic = ""
+title_with_code = ""
+type = ""
+
+[msg.userfront.forgot]
+description = ""
+dry_send = ""
+error = ""
+input_required = ""
+sent = ""
+
+[msg.userfront.login]
+cookie_check_failed = ""
+dry_send = ""
+link_failed = ""
+link_send_failed = ""
+link_sent_email = ""
+link_sent_phone = ""
+link_timeout = ""
+no_account = ""
+oidc_failed = ""
+qr_expired = ""
+qr_init_failed = ""
+qr_login_required = ""
+token_missing = ""
+verification_failed = ""
+
+[msg.userfront.login_success]
+subtitle = ""
+
+[msg.userfront.consent]
+accept_error = ""
+client_id = ""
+client_unknown = ""
+description = ""
+load_error = ""
+missing_redirect = ""
+redirect_notice = ""
+scope_count = ""
+
+[msg.userfront.profile]
+department_missing = ""
+department_required = ""
+email_missing = ""
+greeting = ""
+load_failed = ""
+name_missing = ""
+name_required = ""
+phone_required = ""
+phone_verify_required = ""
+update_failed = ""
+update_success = ""
+
+[msg.userfront.qr]
+camera_error = ""
+permission_error = ""
+permission_required = ""
+
+[msg.userfront.reset]
+invalid_body = ""
+invalid_link = ""
+invalid_title = ""
+policy_loading = ""
+success = ""
+
+[msg.userfront.sections]
+apps_subtitle = ""
+audit_subtitle = ""
+
+[msg.userfront.settings]
+disabled = ""
+
+[msg.userfront.signup]
+failed = ""
+privacy_full = ""
+tos_full = ""
+
[ui.common.badge]
admin_only = ""
command_only = ""
@@ -405,27 +218,11 @@ ok = ""
pending = ""
success = ""
-[ui.userfront]
-app_title = ""
-
[ui.userfront.app_label]
admin_console = ""
baron = ""
dev_console = ""
-[ui.userfront.audit]
-
-[ui.userfront.audit.table]
-app = ""
-auth_method = ""
-date = ""
-device = ""
-ip = ""
-pending = ""
-result = ""
-session_id = ""
-status = ""
-
[ui.userfront.auth_method]
ory = ""
session = ""
@@ -434,23 +231,6 @@ session = ""
last_auth_label = ""
status_history = ""
-[ui.userfront.dashboard.activity]
-linked = ""
-
-[ui.userfront.dashboard.approved_session]
-default = ""
-userfront = ""
-
-[ui.userfront.dashboard.revoke]
-confirm_button = ""
-title = ""
-
-[ui.userfront.dashboard.scopes]
-title = ""
-
-[ui.userfront.dashboard.status]
-revoked = ""
-
[ui.userfront.device]
android = ""
ios = ""
@@ -472,6 +252,258 @@ title = ""
forgot_password = ""
signup = ""
+[ui.userfront.login_success]
+later = ""
+qr = ""
+title = ""
+
+[ui.userfront.consent]
+accept = ""
+requested_scopes = ""
+title = ""
+
+[ui.userfront.nav]
+dashboard = ""
+logout = ""
+profile = ""
+qr_scan = ""
+
+[ui.userfront.profile]
+department_empty = ""
+manage = ""
+user_fallback = ""
+
+[ui.userfront.qr]
+rescan = ""
+result_success = ""
+title = ""
+
+[ui.userfront.reset]
+confirm_password = ""
+new_password = ""
+submit = ""
+subtitle = ""
+title = ""
+
+[ui.userfront.sections]
+apps = ""
+audit = ""
+
+[ui.userfront.session]
+active = ""
+unknown = ""
+
+[ui.userfront.signup]
+complete = ""
+next_step = ""
+title = ""
+
+[msg.userfront.dashboard.activities]
+empty = ""
+empty_detail = ""
+error = ""
+
+[msg.userfront.dashboard.approved_session]
+copy_click = ""
+copy_tap = ""
+none = ""
+
+[msg.userfront.dashboard.revoke]
+confirm = ""
+error = ""
+success = ""
+
+[msg.userfront.dashboard.scopes]
+empty = ""
+
+[msg.userfront.dashboard.timeline]
+load_error = ""
+
+[msg.userfront.error.whitelist]
+"$normalizedCode" = ""
+settings_disabled = ""
+invalid_session = ""
+verification_required = ""
+recovery_expired = ""
+recovery_invalid = ""
+rate_limited = ""
+not_found = ""
+bad_request = ""
+password_or_email_mismatch = ""
+
+[msg.userfront.error.ory]
+"$normalizedCode" = ""
+access_denied = ""
+consent_required = ""
+interaction_required = ""
+invalid_client = ""
+invalid_grant = ""
+invalid_request = ""
+invalid_scope = ""
+login_required = ""
+request_forbidden = ""
+server_error = ""
+temporarily_unavailable = ""
+unauthorized_client = ""
+unsupported_response_type = ""
+
+[msg.userfront.login.link]
+helper = ""
+missing_login_id = ""
+missing_phone = ""
+resend_wait = ""
+short_code_help = ""
+
+[msg.userfront.login.password]
+failed = ""
+missing_credentials = ""
+
+[msg.userfront.login.qr]
+load_failed = ""
+scan_hint = ""
+
+[msg.userfront.login.short_code]
+invalid = ""
+
+[msg.userfront.login.unregistered]
+body = ""
+
+[msg.userfront.login.verification]
+approved = ""
+approved_local = ""
+success = ""
+
+[msg.userfront.consent.cancel]
+confirm = ""
+error = ""
+
+[msg.userfront.consent.scope]
+email = ""
+offline_access = ""
+openid = ""
+phone = ""
+profile = ""
+
+[msg.userfront.profile.password]
+change_failed = ""
+changed = ""
+current_required = ""
+mismatch = ""
+new_required = ""
+subtitle = ""
+
+[msg.userfront.profile.phone]
+code_sent = ""
+send_failed = ""
+verified = ""
+verify_failed = ""
+verify_notice = ""
+
+[msg.userfront.profile.section]
+basic = ""
+organization = ""
+security = ""
+
+[msg.userfront.reset.error]
+empty_password = ""
+generic = ""
+lowercase = ""
+min_length = ""
+min_types = ""
+mismatch = ""
+number = ""
+symbol = ""
+uppercase = ""
+
+[msg.userfront.reset.policy]
+lowercase = ""
+min_length = ""
+min_types = ""
+number = ""
+symbol = ""
+uppercase = ""
+
+[msg.userfront.signup.agreement]
+all_hint = ""
+description = ""
+privacy_summary = ""
+progress = ""
+tos_summary = ""
+title = ""
+
+[msg.userfront.signup.auth]
+affiliate_notice = ""
+title = ""
+
+[msg.userfront.signup.email]
+code_mismatch = ""
+duplicate = ""
+invalid = ""
+send_failed = ""
+verified = ""
+verify_failed = ""
+
+[msg.userfront.signup.password]
+length_required = ""
+lowercase_required = ""
+mismatch = ""
+number_required = ""
+symbol_required = ""
+title = ""
+uppercase_required = ""
+
+[msg.userfront.signup.phone]
+code_mismatch = ""
+send_failed = ""
+verified = ""
+verify_failed = ""
+
+[msg.userfront.signup.policy]
+loading = ""
+lowercase = ""
+min_length = ""
+min_types = ""
+number = ""
+summary = ""
+symbol = ""
+uppercase = ""
+
+[msg.userfront.signup.profile]
+affiliate_hint = ""
+title = ""
+
+[msg.userfront.signup.success]
+body = ""
+title = ""
+
+[ui.userfront.audit.table]
+app = ""
+auth_method = ""
+date = ""
+device = ""
+ip = ""
+pending = ""
+result = ""
+session_id = ""
+status = ""
+
+[ui.userfront.dashboard.activity]
+linked = ""
+
+[ui.userfront.dashboard.approved_session]
+default = ""
+userfront = ""
+
+[ui.userfront.dashboard.revoke]
+confirm_button = ""
+title = ""
+
+[ui.userfront.dashboard.scopes]
+title = ""
+
+[ui.userfront.dashboard.status]
+revoked = ""
+
[ui.userfront.login.action]
submit = ""
@@ -509,31 +541,10 @@ action_label = ""
page_title = ""
title = ""
-[ui.userfront.login_success]
-later = ""
-qr = ""
-title = ""
-
-[ui.userfront.consent]
-accept = ""
-requested_scopes = ""
-title = ""
-
[ui.userfront.consent.cancel]
confirm_button = ""
title = ""
-[ui.userfront.nav]
-dashboard = ""
-logout = ""
-profile = ""
-qr_scan = ""
-
-[ui.userfront.profile]
-department_empty = ""
-manage = ""
-user_fallback = ""
-
[ui.userfront.profile.field]
affiliation = ""
company_code = ""
@@ -560,31 +571,6 @@ basic = ""
organization = ""
security = ""
-[ui.userfront.qr]
-rescan = ""
-result_success = ""
-title = ""
-
-[ui.userfront.reset]
-confirm_password = ""
-new_password = ""
-submit = ""
-subtitle = ""
-title = ""
-
-[ui.userfront.sections]
-apps = ""
-audit = ""
-
-[ui.userfront.session]
-active = ""
-unknown = ""
-
-[ui.userfront.signup]
-complete = ""
-next_step = ""
-title = ""
-
[ui.userfront.signup.agreement]
all = ""
privacy_title = ""
@@ -595,10 +581,6 @@ tos_title = ""
code_label = ""
request_code = ""
-[ui.userfront.signup.auth.email]
-label = ""
-title = ""
-
[ui.userfront.signup.password]
confirm_label = ""
label = ""
@@ -622,3 +604,16 @@ verify = ""
[ui.userfront.signup.success]
action = ""
+
+[msg.userfront.signup.password.rule]
+lowercase = ""
+min_length = ""
+min_types = ""
+number = ""
+symbol = ""
+uppercase = ""
+
+[ui.userfront.signup.auth.email]
+label = ""
+title = ""
+