handleFormChange("confirm_password", e.target.value)}
- placeholder="Confirm password"
- className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
+ placeholder={t("auth.common.password.confirm_password.placeholder")}
+ className="w-full border border-subtle bg-surface-1! pr-12 placeholder:text-placeholder"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
autoComplete="new-password"
@@ -317,7 +319,7 @@ export function InstanceSetupForm() {
{showPassword.retypePassword ? (
@@ -366,7 +368,7 @@ export function InstanceSetupForm() {
- {isSubmitting ? : "Continue"}
+ {isSubmitting ? : t("common.continue")}
diff --git a/apps/admin/package.json b/apps/admin/package.json
index f76cd3c15a2..ba4a24d92e2 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -10,6 +10,7 @@
"build": "react-router build",
"preview": "react-router build && serve -s build/client -l 3001",
"start": "serve -s build/client -l 3001",
+ "test:i18n": "node --experimental-strip-types --test tests/i18n-wiring.test.ts",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:lint": "oxlint --max-warnings=759 .",
"check:types": "react-router typegen && tsc --noEmit",
@@ -25,6 +26,7 @@
"@headlessui/react": "catalog:",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
+ "@plane/i18n": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/services": "workspace:*",
"@plane/types": "workspace:*",
diff --git a/apps/admin/providers/core.tsx b/apps/admin/providers/core.tsx
index 3b22c70878a..6b012878ef0 100644
--- a/apps/admin/providers/core.tsx
+++ b/apps/admin/providers/core.tsx
@@ -11,6 +11,7 @@ import { AppProgressBar } from "@/lib/b-progress";
import { ToastWithTheme } from "./toast";
import { StoreProvider } from "./store.provider";
import { InstanceProvider } from "./instance.provider";
+import { AdminTranslationProvider } from "./translation-provider";
import { UserProvider } from "./user.provider";
const DEFAULT_SWR_CONFIG = {
@@ -24,16 +25,18 @@ const DEFAULT_SWR_CONFIG = {
export function CoreProviders({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
-
-
- {children}
-
-
-
-
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
);
}
diff --git a/apps/admin/providers/translation-provider.tsx b/apps/admin/providers/translation-provider.tsx
new file mode 100644
index 00000000000..47ef98fd972
--- /dev/null
+++ b/apps/admin/providers/translation-provider.tsx
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { useEffect } from "react";
+import { TranslationProvider, useTranslation } from "@plane/i18n";
+
+type TAdminTranslationProviderProps = {
+ children: React.ReactNode;
+};
+
+function DocumentLanguageSync({ children }: TAdminTranslationProviderProps) {
+ const { currentLocale } = useTranslation();
+
+ useEffect(() => {
+ document.documentElement.lang = currentLocale;
+ }, [currentLocale]);
+
+ return children;
+}
+
+export function AdminTranslationProvider({ children }: TAdminTranslationProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/admin/tests/i18n-wiring.test.ts b/apps/admin/tests/i18n-wiring.test.ts
new file mode 100644
index 00000000000..848d5c36646
--- /dev/null
+++ b/apps/admin/tests/i18n-wiring.test.ts
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import test from "node:test";
+
+const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
+
+function read(relativePath: string): string {
+ return fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8");
+}
+
+test("God Mode uses the shared Plane translation runtime", () => {
+ const packageJson = JSON.parse(read("apps/admin/package.json")) as {
+ dependencies?: Record
;
+ };
+ const coreProviders = read("apps/admin/providers/core.tsx");
+ const translationProvider = read("apps/admin/providers/translation-provider.tsx");
+
+ assert.equal(packageJson.dependencies?.["@plane/i18n"], "workspace:*");
+ assert.match(coreProviders, /AdminTranslationProvider/);
+ assert.match(translationProvider, /TranslationProvider/);
+ assert.match(translationProvider, /useTranslation/);
+});
+
+test("God Mode synchronizes the document language after translations initialize", () => {
+ const translationProvider = read("apps/admin/providers/translation-provider.tsx");
+
+ assert.match(translationProvider, /document\.documentElement\.lang\s*=\s*currentLocale/);
+});
+
+test("God Mode breadcrumb segments resolve existing shared translation keys", () => {
+ const labels = read("apps/admin/components/common/header/core.ts");
+ const header = read("apps/admin/components/common/header/index.tsx");
+
+ assert.match(labels, /general:\s*"common\.general"/);
+ assert.match(labels, /authentication:\s*"workspace_settings\.settings\.members\.details\.authentication"/);
+ assert.match(labels, /workspace:\s*"common\.workspace"/);
+ assert.match(labels, /create:\s*"common\.create"/);
+ assert.match(header, /useTranslation/);
+ assert.match(header, /t\(translationKey\)/);
+ assert.match(header, /t\("settings"\)/);
+});
+
+test("God Mode does not add an English-placeholder Admin namespace", () => {
+ const localesRoot = path.join(REPO_ROOT, "packages/i18n/src/locales");
+ const unexpected = fs
+ .readdirSync(localesRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => path.join(localesRoot, entry.name, "admin.json"))
+ .filter((localeFile) => fs.existsSync(localeFile));
+
+ assert.deepEqual(unexpected, []);
+});
+
+test("God Mode reuses shared translations for common fields and actions", () => {
+ const signIn = read("apps/admin/app/(all)/(home)/sign-in-form.tsx");
+ const setup = read("apps/admin/components/instance/setup-form.tsx");
+ const general = read("apps/admin/app/(all)/(dashboard)/general/form.tsx");
+ const newUserPopup = read("apps/admin/components/common/new-user-popup.tsx");
+
+ assert.match(signIn, /t\("auth\.common\.email\.label"\)/);
+ assert.match(signIn, /t\("auth\.common\.password\.label"\)/);
+ assert.match(setup, /t\("first_name"\)/);
+ assert.match(setup, /t\("last_name"\)/);
+ assert.match(setup, /t\("common\.continue"\)/);
+ assert.match(general, /t\("save_changes"\)/);
+ assert.match(general, /t\("saving"\)/);
+ assert.match(newUserPopup, /t\("create_workspace"\)/);
+ assert.match(newUserPopup, /t\("close"\)/);
+
+ const importantSurfaceClassCount = [signIn, setup].reduce(
+ (count, source) => count + (source.match(/\bbg-surface-1!/g)?.length ?? 0),
+ 0
+ );
+ assert.equal(importantSurfaceClassCount, 8);
+ assert.doesNotMatch(signIn, /!bg-surface-1/);
+ assert.doesNotMatch(setup, /!bg-surface-1/);
+ assert.match(newUserPopup, /Start your journey by creating your first workspace\./);
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bbf11e23d18..d5093420653 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -625,6 +625,9 @@ importers:
'@plane/hooks':
specifier: workspace:*
version: link:../../packages/hooks
+ '@plane/i18n':
+ specifier: workspace:*
+ version: link:../../packages/i18n
'@plane/propel':
specifier: workspace:*
version: link:../../packages/propel