tolgee / next-app-intl-example

5 stars 0 forks source link

SSR doesn't work when using namespace #2

Closed heejinp closed 6 months ago

heejinp commented 7 months ago

Hi,

When I modify getStaticData function in shared.ts file like this:

export async function getStaticData(languages: string[], namespaces: string[]) {
  const result: Record<string, any> = {};
  for (const namespace of namespaces) {
    for (const lang of languages) {
      result[`${lang}:${namespace}`] = (
        await import(`../i18n/${namespace}/${lang}.json`)
      ).default;
    }
  }
  return result;
}

it shows a blank page in the JavaScript disabled mode, otherwise(enable js or not using namespace) it works. Do the docs(https://tolgee.io/js-sdk/integrations/react/next/app-router) cover the case for using a namespace too?

dependencies:

 "dependencies": {
    "@tolgee/format-icu": "5.23.3",
    "@tolgee/react": "5.23.3",
    "@tolgee/web": "5.23.3",
    "next": "14.1.4",
    "next-intl": "^3.2.3",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
stepan662 commented 7 months ago

Hey, yeah I'll update the exmple app, I think the issue might be in different syntax for empty namespace.

stepan662 commented 7 months ago

Should be something like this:

export async function getStaticData(
  languages: string[],
  namespaces: string[] = ['']
) {
  const result: Record<string, any> = {};
  for (const lang of languages) {
    for (const namespace of namespaces) {
      if (namespace) {
        result[`${lang}:${namespace}`] = (
          await import(`../i18n/${namespace}/${lang}.json`)
        ).default;
      } else {
        result[lang] = (await import(`../i18n/${lang}.json`)).default;
      }
    }
  }
  return result;
}
heejinp commented 7 months ago

Should be something like this:

export async function getStaticData(
  languages: string[],
  namespaces: string[] = ['']
) {
  const result: Record<string, any> = {};
  for (const lang of languages) {
    for (const namespace of namespaces) {
      if (namespace) {
        result[`${lang}:${namespace}`] = (
          await import(`../i18n/${namespace}/${lang}.json`)
        ).default;
      } else {
        result[lang] = (await import(`../i18n/${lang}.json`)).default;
      }
    }
  }
  return result;
}

yup I thought it might be the syntax thing too, so tried your snippet and still getting the fallback text. before that, it seems like the namespace is partially working out in CSR like this:

image

I'll be waiting for the update! thanks

stepan662 commented 7 months ago

Hmm, could you elaborate more? I think there must be some other problem, do you have it correctly connected to some project in Tolgee, with appropriate strings?

heejinp commented 7 months ago

Hmm, could you elaborate more? I think there must be some other problem, do you have it correctly connected to some project in Tolgee, with appropriate strings?

Okay, I am testing in the demo app and translation works fine without namespace but:

The result object of getStaticData is like:

{
  'en:test': {
    'add-item-add-button': 'Add',
    'add-item-input-placeholder': 'New list item',
    'app-title': 'What To Pack',
    'delete-item-button': 'Delete',
    'menu-item-translation-methods': 'Translation methods',
    'send-via-email': 'Send via e-mail',
    'share-button': 'Share',
    this_is_a_key: 'This is a key',
    this_is_a_key_with_params: 'This is key with params {key} {key2}',
    this_is_a_key_with_tags: 'This is a key with tags <b>bold</b> <b><i>{key}</i></b>'
  },
  'fr:test': {
    'add-item-add-button': 'Ajouter',
    'add-item-input-placeholder': 'Nouveau élément de la liste',
    'app-title': 'Quoi emballer',
    'delete-item-button': 'Supprimer',
    'menu-item-translation-methods': 'Méthodes de la traduction',
    'send-via-email': 'Envoyer par e-mail',
    'share-button': 'Partager',
    this_is_a_key: "C'est un clé",
    this_is_a_key_with_params: "C'est la clé avec paramètres {key} {key2}",
    this_is_a_key_with_tags: "C'est la clé avec des tags <b>bold</b> <b><i>{key}</i></b>"
  },
  'en:common': {
    'add-item-add-button': 'Add',
    'add-item-input-placeholder': 'New list item',
    'app-title': 'What To Pack',
    'delete-item-button': 'Delete',
    'menu-item-translation-methods': 'Translation methods',
    'send-via-email': 'Send via e-mail',
    'share-button': 'Share',
    this_is_a_key: 'This is a key',
    this_is_a_key_with_params: 'This is key with params {key} {key2}',
    this_is_a_key_with_tags: 'This is a key with tags <b>bold</b> <b><i>{key}</i></b>'
  },
 ..... and more ...
}

These are the only changes I made, and no error was thrown from the await import() syntax but I think both issues are coming from this part.

stepan662 commented 6 months ago

Could you also share tolgee configuration and how you use it in the code?

heejinp commented 6 months ago

Could you also share tolgee configuration and how you use it in the code?

client.tsx

'use client';

import { TolgeeBase } from './shared';
import { TolgeeProvider, useTolgeeSSR } from '@tolgee/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

type Props = {
  locales: any;
  locale: string;
  children: React.ReactNode;
};

const tolgee = TolgeeBase().init();

export const TolgeeNextProvider = ({ locale, locales, children }: Props) => {
  const tolgeeSSR = useTolgeeSSR(tolgee, locale, locales);
  const router = useRouter();

  useEffect(() => {
    const { unsubscribe } = tolgeeSSR.on('permanentChange', () => {
      router.refresh();
    });

    return () => unsubscribe();
  }, [tolgeeSSR, router]);

  return (
    <TolgeeProvider
      tolgee={tolgeeSSR}
      options={{ useSuspense: false }}
      fallback="Loading"
    >
      {children}
    </TolgeeProvider>
  );
};

server.tsx

import { useLocale } from 'next-intl';

import { TolgeeBase, ALL_LOCALES, getStaticData } from './shared';
import { createServerInstance } from '@tolgee/react/server';

export const { getTolgee, getTranslate, T } = createServerInstance({
  getLocale: useLocale,
  createTolgee: async (locale) =>
    TolgeeBase().init({
      staticData: await getStaticData(ALL_LOCALES),
      observerOptions: {
        fullKeyEncode: true,
      },
      language: locale,
      fetch: async (input, init) => {
        const data = await fetch(input, { ...init, next: { revalidate: 0 } });
        return data;
      },
    }),
});

shared.tsx

import { FormatIcu } from "@tolgee/format-icu";
import { DevTools, Tolgee } from "@tolgee/web";

const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;

export const ALL_LOCALES = ["en", "cs", "de", "fr"];

export const DEFAULT_LOCALE = "en";

export async function getStaticData(
  languages: string[],
  namespaces: string[] = ["test", "common"]
) {
  const result: Record<string, any> = {};
  for (const namespace of namespaces) {
    for (const lang of languages) {
      if (namespace) {
        result[`${lang}:${namespace}`] = (
          await import(`../i18n/${namespace}/${lang}.json`).catch((error) => {
            console.log(error);
          })
        ).default;
      } else {
        result[lang] = (
          await import(`../i18n/${lang}.json`).catch((error) => {
            console.log(error);
          })
        ).default;
      }
    }
  }
  console.log(result);

  return result;
}

export function TolgeeBase() {
  return Tolgee().use(FormatIcu()).use(DevTools()).updateDefaults({
    apiKey,
    apiUrl,
    fallbackLanguage: "en",
  });
}

about middleware.ts, navigation.ts files, and the rest of the components/pages I didn't touch them at all from the demo.

stepan662 commented 6 months ago

Ok, I think everything is correct, except, you are not telling Tolgee to use the namespaces that you've provided.

// in shared.ts
export function TolgeeBase() {
  return Tolgee()
    .use(FormatIcu())
    .use(DevTools())
    .updateDefaults({
      apiKey,
      apiUrl,
      fallbackLanguage: 'en',
      defaultNs: 'test',            // <– this tells Tolgee that this is a default namespace, if not specified
      ns: ['test', 'common'],   // <- this says what namespaces should be available after start, important for DevTools
    });
}

Then if you want to use test namespace you can just call t function without the namespace definition. For common namespace, use t('key_name', { ns: 'common' }).

heejinp commented 6 months ago

Ok, I think everything is correct, except, you are not telling Tolgee to use the namespaces that you've provided.

// in shared.ts
export function TolgeeBase() {
  return Tolgee()
    .use(FormatIcu())
    .use(DevTools())
    .updateDefaults({
      apiKey,
      apiUrl,
      fallbackLanguage: 'en',
      defaultNs: 'test',            // <– this tells Tolgee that this is a default namespace, if not specified
      ns: ['test', 'common'],   // <- this says what namespaces should be available after start, important for DevTools
    });
}

Then if you want to use test namespace you can just call t function without the namespace definition. For common namespace, use t('key_name', { ns: 'common' }).

Ahh you're right 🤦 After adding defaultNS and ns it started working with correct translations / in js disabled mode in every languages. Thanks a lot!