Seed identity locale from the URL when null
Signed-in viewers with no saved preference get the current URL locale once so later unprefixed visits have a default. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
55
apps/compliance-portal/src/lib/i18n/useEnsureViewerLocale.ts
Normal file
55
apps/compliance-portal/src/lib/i18n/useEnsureViewerLocale.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import type { useEnsureViewerLocale_identity$key } from "./__generated__/useEnsureViewerLocale_identity.graphql";
|
||||
import { useLocale } from "./useLocale";
|
||||
import { useUpdateLocale } from "./useUpdateLocale";
|
||||
|
||||
const ensureViewerLocaleFragment = graphql`
|
||||
fragment useEnsureViewerLocale_identity on Identity {
|
||||
locale
|
||||
}
|
||||
`;
|
||||
|
||||
// Seeds Identity.locale once when it is still null, using the current URL
|
||||
// locale. Never overwrites an existing preference (shared-link safe).
|
||||
export function useEnsureViewerLocale(
|
||||
identityKey: useEnsureViewerLocale_identity$key | null,
|
||||
) {
|
||||
const identity = useFragment(ensureViewerLocaleFragment, identityKey);
|
||||
const urlLocale = useLocale();
|
||||
const [updateLocale] = useUpdateLocale();
|
||||
const firedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (identity == null || identity.locale != null || firedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
firedRef.current = true;
|
||||
void updateLocale(urlLocale, { errorToast: false }).catch(() => {
|
||||
// Allow a later mount / navigation to retry if the seed failed.
|
||||
firedRef.current = false;
|
||||
});
|
||||
}, [identity, updateLocale, urlLocale]);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { MutationFeedback } from "@probo/relay";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql } from "react-relay";
|
||||
@@ -45,10 +46,16 @@ export function useUpdateLocale() {
|
||||
errorToast: t("locale.updateFailed"),
|
||||
});
|
||||
|
||||
const updateLocale = useCallback(async (locale: UrlLocale) => {
|
||||
await commit({
|
||||
variables: { input: { locale } },
|
||||
});
|
||||
const updateLocale = useCallback(async (
|
||||
locale: UrlLocale,
|
||||
feedback?: MutationFeedback,
|
||||
) => {
|
||||
await commit(
|
||||
{
|
||||
variables: { input: { locale } },
|
||||
},
|
||||
feedback,
|
||||
);
|
||||
}, [commit]);
|
||||
|
||||
return [updateLocale, isUpdating] as const;
|
||||
|
||||
@@ -27,6 +27,7 @@ import { LocaleMismatchCallout } from "#/components/LocaleMismatchCallout";
|
||||
import { PoweredBy } from "#/components/PoweredBy/PoweredBy";
|
||||
import { TopBar } from "#/components/TopBar/TopBar";
|
||||
import { useResumeAccessRequest } from "#/lib/auth/useResumeAccessRequest";
|
||||
import { useEnsureViewerLocale } from "#/lib/i18n/useEnsureViewerLocale";
|
||||
import { SubscribeDialogProvider } from "#/lib/mailingList/SubscribeDialogProvider";
|
||||
|
||||
import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql";
|
||||
@@ -36,6 +37,7 @@ export const mainLayoutQuery = graphql`
|
||||
viewer {
|
||||
__typename
|
||||
...LocaleMismatchCallout_identity
|
||||
...useEnsureViewerLocale_identity
|
||||
}
|
||||
...TopBar_query
|
||||
...SubscribeDialogProvider_query
|
||||
@@ -53,6 +55,8 @@ export function MainLayout({ queryRef }: MainLayoutProps) {
|
||||
|
||||
// Resume a deferred "request access" once the user lands back authenticated.
|
||||
useResumeAccessRequest(data.viewer != null);
|
||||
// First signed-in visit: seed Identity.locale from the URL when still null.
|
||||
useEnsureViewerLocale(data.viewer ?? null);
|
||||
|
||||
// Document viewer fills the viewport under the TopBar and scrolls its own
|
||||
// stage; every other page uses normal document flow so the footer sits after
|
||||
|
||||
Reference in New Issue
Block a user