Potential bug with 'locale' in Shopify Admin Extension

Hi! I’m pretty new to Shopify extension development, actually I’m working on my first Shopify app, but i might have found a bug.

What I want to do is to get the ‘locale’ containing the region as well, so I can display dates, currencies and so on like the user is expecting them.

In the general account settings I set the language to ‘English’ and the regional format to ‘American English’.

Now I try to get the locale in the loader method in my `app._index.tsx`:

export const loader = async ({request}: LoaderFunctionArgs) => {
  const { admin } = await authenticate.admin(request);

  const url = new URL(request.url);

  const locale = url.searchParams.get("locale");
  console.log('locale from URL', locale); // output: 'en' instead of 'en-US'
};

Following the documentation I should be able to get the locale from the GET request.
Get access to the user’s locale

‘For example, apps rendered in the Shopify admin receive the app user’s chosen locale in the locale request parameter in Shopify’s GET requests to the app.’

Now the bug I am talking about is, that the locale is ‘en’ instead of ‘en-US’.

Now the reason, why I think it is a bug:

When I inspect the page, I see the following HTML code:

<!-- Simplified code -->

<!-- Here the locale is correct -->
<html lang="en-US">
  <body>
  <!-- The iFrame of the Shopify extension - locale=en instead of locale=en-US -->
  <iframe 
    title="extension-title" 
    src="https://some-url.trycloudflare.com/app?otherProperties&locale=en&otherParameters">
  </iframe>
</body>

In the iFrame’s src the locale GET parameter has the value ‘en’ instead of ‘en-US’.
And using the app bridge I also get ‘en’ as the locale, so I think the cause is, that the wrong locale is passed to the iFrame here.

Does anyone else experience the same issue or am I doing something wrong?

If you have any questions on how to reproduce this, feel free to ask.

Hi @lunerasol

From digging into this, it does look like this is a platform limitation. Right now, Shopify’s locale query parameter and App Bridge’s shopify.config.locale are only guaranteed to give you a valid BCP 47 language tag (for example en or en-US), not necessarily the “language + regional format” combination you see in the admin shell’s <html lang="en-US">.

So even if the admin UI is rendered as en-US based on “English” + “American English” in settings, it’s still expected (though not ideal) that your embedded app iframe receives locale=en and App Bridge reports locale='en'.

There’s currently no documented way for embedded apps to read the admin user’s “Regional format” as structured data, which means you can’t strictly rely on Shopify alone to get en-US versus just en.

Your practical next step would be to implement your own locale detection that combines browser signals and Shopify’s locale—for example, in your Remix loader read Accept-Language from request.headers and use its first entry (often en-US), with the locale query param as a fallback; and on the client, prefer navigator.language / navigator.languages[0] and then fall back to shopify.config.locale.

Hi @Liam-Shopify ,

thanks for your reply! When implementing your suggested solutions, I actually found the solution or let’s say I managed to let the dev docs AI Assistant pointing it out to me.

On the client, there is the window.polaris object, which has the PolarisGlobal type. It is actually not mentioned in the docs, but one can find it in the @shopify/polaris-types/dist/polaris.d.ts file.

interface PolarisGlobal {
    /**
     * The shop's currency code, which may differ from the user's currency.
     */
    currencyCode: string;
    /**
     * The user's locale, which may differ from the shop's locale.
     */
    locale: string;
    translations: {
        // translations
    };
}
declare global {
    var polaris: PolarisGlobal;
}

This is the output, which has the exact locale, i was hoping for, the one the admin user set in the account settings.

So here is my final implementation:

const defaultLocale = 'en';

interface LoaderData {
  loaderLocale: string;
}

export const loader = async ({
  request,
}: LoaderFunctionArgs): Promise<LoaderData> => {
  const { admin } = await authenticate.admin(request);

  const url = new URL(request.url);

  // Read locale from the URL query
  const loaderLocale = url.searchParams.get('locale') || defaultLocale;
  
  // Alternatively get the locale from the request header
  // const loaderLocale = request.headers.get('Accept-Language');

  return { loaderLocale };
}

export default function Index() {
  const { loaderLocale } = useLoaderData<LoaderData>();

  /** Locale to be used for translation and formatting throughout the app */
  let appLocale = adminUserLocale; // fallback from Remix loader for Server-Side Rendering
  if (typeof window !== 'undefined') {
    const {navigator, polaris } = window;
    // get locale from browser settings
    // appLocale = navigator.language;
    // The solution - get locale from admin user account settings
    appLocale = polaris.locale
    console.log('PolarisGlobal', polaris)
  }
}