diff --git a/apps/login/src/app/(login)/layout.tsx b/apps/login/src/app/(login)/layout.tsx
index 3e1955268..1aa9e3a82 100644
--- a/apps/login/src/app/(login)/layout.tsx
+++ b/apps/login/src/app/(login)/layout.tsx
@@ -11,6 +11,10 @@ import React, { Suspense } from "react";
import ThemeSwitch from "@/components/theme-switch";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
+import { headers } from "next/headers";
+import { getServiceConfig } from "@/lib/service-url";
+import { getAllowedLanguages } from "@/lib/zitadel";
+import { LANGS, getLanguage } from "@/lib/i18n";
const lato = Lato({
weight: ["400", "700", "900"],
@@ -23,6 +27,21 @@ export async function generateMetadata(): Promise {
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const _headers = await headers();
+ const { serviceConfig } = getServiceConfig(_headers);
+
+ let languages = LANGS;
+ try {
+ const settings = await getAllowedLanguages({ serviceConfig });
+ if (settings.allowedLanguages?.length) {
+ languages = settings.allowedLanguages
+ .filter((code) => LANGS.find((l) => l.code === code))
+ .map((code) => getLanguage(code));
+ }
+ } catch (e) {
+ console.error("Failed to load supported languages", e);
+ }
+
return (
@@ -52,7 +71,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
diff --git a/apps/login/src/components/language-switcher.tsx b/apps/login/src/components/language-switcher.tsx
index b1f1549a3..882a39532 100644
--- a/apps/login/src/components/language-switcher.tsx
+++ b/apps/login/src/components/language-switcher.tsx
@@ -1,7 +1,7 @@
"use client";
import { setLanguageCookie } from "@/lib/cookies";
-import { Lang, LANGS } from "@/lib/i18n";
+import { Lang } from "@/lib/i18n";
import { getThemeConfig, getComponentRoundness, APPEARANCE_STYLES } from "@/lib/theme";
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
@@ -22,12 +22,12 @@ function getLanguageSwitcherCardAppearance(): string {
return appearance?.card || "bg-black/5 dark:bg-white/5"; // Fallback to current styling
}
-export function LanguageSwitcher() {
+export function LanguageSwitcher({ languages }: { languages: Lang[] }) {
const currentLocale = useLocale();
const switcherRoundness = getLanguageSwitcherRoundness();
const cardAppearance = getLanguageSwitcherCardAppearance();
- const [selected, setSelected] = useState(LANGS.find((l) => l.code === currentLocale) || LANGS[0]);
+ const [selected, setSelected] = useState(languages.find((l) => l.code === currentLocale) || languages[0]);
const router = useRouter();
@@ -61,7 +61,7 @@ export function LanguageSwitcher() {
"transition duration-100 ease-in data-[leave]:data-[closed]:opacity-0",
)}
>
- {LANGS.map((lang) => (
+ {languages.map((lang) => (
{
const fallback = "en";
const cookiesList = await cookies();
- let locale: string = fallback;
-
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
+ let allowedLanguages = LANGS.map((l) => l.code);
+ let defaultLanguage = fallback;
+
+ try {
+ const settings = await getAllowedLanguages({ serviceConfig });
+ if (settings.allowedLanguages?.length) {
+ const localLanguageCodes = LANGS.map((l) => l.code);
+ allowedLanguages = settings.allowedLanguages.filter((l) => localLanguageCodes.includes(l));
+ }
+ if (settings.defaultLanguage) {
+ defaultLanguage = settings.defaultLanguage;
+ }
+ } catch (e) {
+ console.warn("Failed to load global settings", e);
+ }
+
+ let locale: string = defaultLanguage;
+
const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
if (languageHeader) {
- const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code
- if (LANGS.map((l) => l.code).includes(headerLocale)) {
+ // splits "en-US,en;q=0.9" to ["en", "US"] or ["en"]
+ const headerLocale = languageHeader.split(",")[0].split("-")[0];
+ if (allowedLanguages.includes(headerLocale)) {
locale = headerLocale;
}
}
const languageCookie = cookiesList?.get(LANGUAGE_COOKIE_NAME);
if (languageCookie && languageCookie.value) {
- if (LANGS.map((l) => l.code).includes(languageCookie.value)) {
+ if (allowedLanguages.includes(languageCookie.value)) {
locale = languageCookie.value;
+ } else {
+ // If the cookie tells a language that is other than the supported ones, fall back to the default.
+ locale = defaultLanguage;
}
}
@@ -34,7 +54,9 @@ export default getRequestConfig(async () => {
let translations: JsonObject | {} = {};
try {
- const i18nJSON = await getHostedLoginTranslation({ serviceConfig, locale,
+ const i18nJSON = await getHostedLoginTranslation({
+ serviceConfig,
+ locale,
organization: i18nOrganization,
});
@@ -46,16 +68,23 @@ export default getRequestConfig(async () => {
}
const customMessages = translations;
- const localeMessages = (await import(`../../locales/${locale}.json`)).default;
- const fallbackMessages = (await import(`../../locales/${fallback}.json`))
- .default;
+
+ // Load locale messages, fall back to default language messages if locale not found
+ let localeMessages;
+ try {
+ localeMessages = (await import(`../../locales/${locale}.json`)).default;
+ } catch {
+ try {
+ localeMessages = (await import(`../../locales/${defaultLanguage}.json`)).default;
+ } catch {
+ localeMessages = (await import(`../../locales/${fallback}.json`)).default;
+ }
+ }
+
+ const fallbackMessages = (await import(`../../locales/${fallback}.json`)).default;
return {
locale,
- messages: deepmerge.all([
- fallbackMessages,
- localeMessages,
- customMessages,
- ]) as Record,
+ messages: deepmerge.all([fallbackMessages, localeMessages, customMessages]) as Record,
};
});
diff --git a/apps/login/src/lib/i18n.ts b/apps/login/src/lib/i18n.ts
index fd00c2b03..d54d133d1 100644
--- a/apps/login/src/lib/i18n.ts
+++ b/apps/login/src/lib/i18n.ts
@@ -39,7 +39,7 @@ export const LANGS: Lang[] = [
{
name: "Русский",
code: "ru",
- },
+ },
{
name: "Türkçe",
code: "tr",
@@ -60,3 +60,15 @@ export const LANGS: Lang[] = [
export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE";
export const LANGUAGE_HEADER_NAME = "accept-language";
+
+export function getLanguage(code: string): Lang {
+ const lang = LANGS.find((l) => l.code === code);
+ if (lang) {
+ return lang;
+ }
+
+ return {
+ code,
+ name: new Intl.DisplayNames([code], { type: "language" }).of(code) || code,
+ };
+}
diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts
index e5077db65..a8c93119a 100644
--- a/apps/login/src/lib/zitadel.ts
+++ b/apps/login/src/lib/zitadel.ts
@@ -29,8 +29,8 @@ import {
VerifyPasskeyRegistrationRequest,
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
-import { unstable_cacheLife as cacheLife } from "next/cache";
import { getTranslations } from "next-intl/server";
+import { unstable_cacheLife as cacheLife } from "next/cache";
import { getUserAgent } from "./fingerprint";
import { createServiceForHost } from "./service";
@@ -159,10 +159,15 @@ export async function registerTOTP({ serviceConfig, userId }: WithServiceConfig<
return userService.registerTOTP({ userId }, {});
}
-export async function getGeneralSettings({ serviceConfig }: WithServiceConfig) {
+export async function getAllowedLanguages({ serviceConfig }: WithServiceConfig) {
const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
- const callback = settingsService.getGeneralSettings({}, {}).then((resp) => resp.supportedLanguages);
+ const callback = settingsService.getGeneralSettings({}, {}).then((resp) => {
+ return {
+ allowedLanguages: resp.allowedLanguages,
+ defaultLanguage: resp.defaultLanguage,
+ };
+ });
return useCache ? cacheWrapper(callback) : callback;
}