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:
Émile Ré
2026-07-21 15:51:17 +02:00
parent 5b57333994
commit acd41b72f3
3 changed files with 70 additions and 4 deletions

View 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]);
}

View File

@@ -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;

View File

@@ -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