vendure-ecommerce / storefront-qwik-starter

An e-commerce storefront starter built with Qwik and Vendure
https://qwik-storefront.vendure.io
227 stars 87 forks source link

i18n: pass locale to shop-api #104

Open redaready opened 1 year ago

redaready commented 1 year ago

is there a way to pass the locale value of "extractLang(request.headers.get('accept-language'), request.url)" to shop-api call?

i have tried something like this, but a error raised: "Internal server error: Reading locale outside of context."

const executeRequest = async (options: Options) => {
    const locale = getLocale();
    const httpResponse = await fetch(`${ENV_VARIABLES.VITE_VENDURE_PUBLIC_URL}/?languageCode=${locale}`, options);
    return await extractTokenAndData(httpResponse);
};
gioboa commented 1 year ago

I will investigate in it

redaready commented 1 year ago

thanks gioboa, i have tried add "locale" to the option of requester, it works, but i don't think it's the best way to do the job because we are obliged to add locale option to many api calls...

diff --git a/src/providers/collections/collections.ts b/src/providers/collections/collections.ts
index 3034f63..2802a6d 100644
--- a/src/providers/collections/collections.ts
+++ b/src/providers/collections/collections.ts
@@ -1,9 +1,9 @@
 import gql from 'graphql-tag';
-import { sdk } from '~/graphql-wrapper';
 import { Collection } from '~/generated/graphql';
+import { sdk } from '~/graphql-wrapper';

-export const getCollections = async () => {
-   return await sdk.collections().then((res) => res.collections.items as Collection[]);
+export const getCollections = async (locale: string) => {
+   return await sdk.collections(undefined, { locale: locale }).then((res) => res.collections.items as Collection[]);
 };

 export const getCollectionBySlug = async (slug: string) => {
diff --git a/src/routes/layout.tsx b/src/routes/layout.tsx
index 9d8e4c2..eb14bfc 100644
--- a/src/routes/layout.tsx
+++ b/src/routes/layout.tsx
@@ -19,8 +19,9 @@ import { extractLang } from '~/utils/i18n';
 import Footer from '../components/footer/footer';
 import Header from '../components/header/header';

-export const useCollectionsLoader = routeLoader$(async () => {
-   return await getCollections();
+export const useCollectionsLoader = routeLoader$(async (requestEvent) => {
+   const locale = extractLang(requestEvent.headers.get('accept-language'), requestEvent.url.toString())
+   return await getCollections(locale);
 });

 export const useAvailableCountriesLoader = routeLoader$(async () => {
diff --git a/src/utils/api.ts b/src/utils/api.ts
index f03dc54..ee28204 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -1,7 +1,7 @@
 import { server$ } from '@builder.io/qwik-city';
 import { isBrowser } from '@builder.io/qwik/build';
 import { DocumentNode, print } from 'graphql/index';
-import { AUTH_TOKEN, HEADER_AUTH_TOKEN_KEY } from '~/constants';
+import { AUTH_TOKEN, DEFAULT_LOCALE, HEADER_AUTH_TOKEN_KEY } from '~/constants';
 import { ENV_VARIABLES } from '~/env';
 import { getCookie, setCookie } from '.';

@@ -12,22 +12,25 @@ type Options = { method: string; headers: Record<string, string>; body: string }
 export const requester = async <R, V>(
    doc: DocumentNode,
    vars?: V,
-   options: { token?: string } = { token: '' }
+   options: { token?: string, locale?: string } = { token: '' }
 ): Promise<R> => {
-   return execute<R, V>({ query: print(doc), variables: vars }, options.token);
+   return execute<R, V>({ query: print(doc), variables: vars }, options.token, options.locale || DEFAULT_LOCALE);
 };

 const execute = async <R, V = Record<string, any>>(
    body: ExecuteProps<V>,
-   authToken: string = ''
+   authToken: string = '',
+   locale: string
 ): Promise<R> => {
-   const options = { method: 'POST', headers: createHeaders(), body: JSON.stringify(body) };
+   const options = {
+       method: 'POST', headers: createHeaders(), body: JSON.stringify(body)
+   };
    if (authToken !== '') {
        options.headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}` };
    }
    const response: ResponseProps<R> = isBrowser
-       ? await executeOnTheServer(options)
-       : await executeRequest(options);
+       ? await executeOnTheServer(options, locale)
+       : await executeRequest(options, locale);

    if (isBrowser && response.token) {
        setCookie(AUTH_TOKEN, response.token, 365);
@@ -45,10 +48,10 @@ const createHeaders = () => {
    return headers;
 };

-const executeOnTheServer = server$(async (options: Options) => executeRequest(options));
+const executeOnTheServer = server$(async (options: Options, locale: string) => executeRequest(options, locale));

-const executeRequest = async (options: Options) => {
-   const httpResponse = await fetch(ENV_VARIABLES.VITE_VENDURE_PUBLIC_URL, options);
+const executeRequest = async (options: Options, locale?: string) => {
+   const httpResponse = await fetch(`${ENV_VARIABLES.VITE_VENDURE_PUBLIC_URL}/?languageCode=${locale}`, options);
    return await extractTokenAndData(httpResponse);
 };
giovramirez commented 1 year ago

@redaready, have you tried setting the locale in a Cookie just like it's done with the Vendure token?

You might be facing "Internal server error: Reading locale outside of context." due to the locale is set here. So probably the getLocale() function is called before the locale is set.

redaready commented 1 year ago

with some help of sourcegraph cody:

That error means you are calling getLocale() outside of a Qwik component or route. getLocale() relies on Qwik's locale context, so it can only be called within a Qwik component/route.

Some options to fix this:

Pass the locale as an argument to the function instead of calling getLocale():

function doSomething(locale: string) {
  // Use locale here instead of calling getLocale()
}

Wrap the call in a Qwik component:

component$(() => {
  const locale = getLocale();
  // Use locale here
});

Use a Qwik route and access the locale from the request event:

export const onRequest: RequestHandler = ({ locale }) => {
  // Use locale here
};

Store the locale in a variable outside of the Qwik context and pass that in:

let locale;

component$(() => {
  locale = getLocale();
});

function doSomething() {
  // Use locale here instead of calling getLocale()
}

Hope this helps resolve your error! Let me know if you have any other questions.

redaready commented 1 year ago

@giovramirez i think locale is a little bit diffrent with token, token is generated by vendure and then stored in cookie, locale on other hand, we must pass it to sdk calls at first invocations (ex: useCollectionsLoader and useAvailableCountriesLoader etc)... so i think we have no other choice than pass locale parameter to many sdk calls :-(

giovramirez commented 1 year ago

You're right @redaready. Besides, we can't use a Cookie since most of the calls are executed on the server, so it's not an option to get the locale from Cookie.

We have this interceptor where we can get the locale, store it outside of the Qwik context and use it globally within the app. However, I think for better readability of the codebase your approach of passing the locale parameter to every sdk looks better.

@gioboa any thoughts on this matter?

gioboa commented 1 year ago

I need to investigate into the problem, I will look at it in few days. You are more then welcome to suggest a solution with a PR

prasmalla commented 11 months ago

my approach was a global config variable for currencyCode and use it here

const executeRequest = async (options: Options) => {
    const httpResponse = await fetch(
        `${ENV_VARIABLES.VITE_VENDURE_PUBLIC_URL}?currencyCode=${getCurrencyCode()}`,
        options
    );
    return await extractTokenAndData(httpResponse);
};
gioboa commented 11 months ago

This sounds reasonable to me 👍 Thanks for sharing

redaready commented 11 months ago

can we change locale per request in this way? sometimes storefront has a Language Selector component, we need to get locale value from request context(cookie, path, or header) to call api. i think the locale value proprity chain could be

  1. user selection on web interface Language Selector (locale value persisted in cookie ou path, cookie prefered)
  2. user preference setting (stocked on server side, not in vendure now)
  3. server side app globle deflaut config (as @prasmalla said)
  4. browser language header
gioboa commented 11 months ago

Yep, if we change the path with a variable we can do whatever we want

prasmalla commented 11 months ago

@redaready we did as you mentioned storing the user selection in a cookie and updating the client-side global config on changes. vendure backend already supports it and there was an issue here that came up with this approach which has been fixed