From 38f3acbd85bbffe24fef6c27698ce3b1bc89d49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Wed, 30 Oct 2024 13:48:04 +0100 Subject: [PATCH] feat: simplify next-js integration (#3391) --- .github/workflows/publish-examples-check.yml | 3 +- .github/workflows/publish-examples.yml | 1 + e2e/cypress/e2e/next-app-intl/dev.cy.ts | 40 ++ e2e/cypress/e2e/next-app-intl/prod.cy.ts | 33 ++ e2e/cypress/e2e/next-app/dev.cy.ts | 2 +- e2e/cypress/e2e/next-app/prod.cy.ts | 2 +- package.json | 1 + .../core/src/Controller/State/initState.ts | 4 +- .../format-icu/src/createFormatIcu.test.ts | 8 +- packages/format-icu/src/createFormatIcu.ts | 6 +- packages/react/src/TolgeeProvider.tsx | 39 +- packages/react/src/useTolgeeSSR.ts | 26 +- packages/web/src/package/LanguageDetector.ts | 36 +- .../tools/detectLanguageFromHeaders.ts | 10 + .../package/tools/getHeaderLanguages.test.ts | 36 ++ .../src/package/tools/getHeaderLanguages.ts | 15 + packages/web/src/package/typedIndex.ts | 4 +- pnpm-lock.yaml | 349 +++++++++--------- scripts/e2eRunner/config.ts | 20 + testapps/next-app-intl/.gitignore | 37 ++ .../{next-app => next-app-intl}/CHANGELOG.md | 0 testapps/next-app-intl/README.md | 23 ++ .../i18n => next-app-intl/messages}/cs.json | 0 .../i18n => next-app-intl/messages}/de.json | 0 .../i18n => next-app-intl/messages}/en.json | 0 .../i18n => next-app-intl/messages}/fr.json | 0 testapps/next-app-intl/next.config.js | 10 + testapps/next-app-intl/package.json | 28 ++ testapps/next-app-intl/public/favicon.ico | Bin 0 -> 15086 bytes testapps/next-app-intl/public/img/appLogo.svg | 15 + .../next-app-intl/public/img/background.svg | 5 + testapps/next-app-intl/public/img/iconAdd.svg | 17 + .../next-app-intl/public/img/iconMail.svg | 8 + .../next-app-intl/public/img/iconShare.svg | 16 + .../src/app/[locale]/Todos.tsx | 0 .../src/app/[locale]/[...rest]/page.tsx | 0 .../src/app/[locale]/layout.tsx | 10 +- .../src/app/[locale]/page.tsx | 0 .../TranslationMethodsClient.tsx | 0 .../TranslationMethodsServer.tsx | 0 .../app/[locale]/translation-methods/page.tsx | 19 + testapps/next-app-intl/src/app/layout.tsx | 12 + testapps/next-app-intl/src/app/not-found.tsx | 17 + testapps/next-app-intl/src/app/page.tsx | 6 + testapps/next-app-intl/src/app/style.css | 250 +++++++++++++ .../src/components/LangSelector.tsx | 32 ++ .../next-app-intl/src/components/Navbar.tsx | 11 + .../src/i18n/request.ts | 0 .../src/middleware.ts | 6 +- .../src/navigation.ts | 4 +- testapps/next-app-intl/src/tolgee/client.tsx | 40 ++ testapps/next-app-intl/src/tolgee/server.tsx | 20 + testapps/next-app-intl/src/tolgee/shared.ts | 36 ++ testapps/next-app-intl/tsconfig.json | 41 ++ testapps/next-app/README.md | 3 +- testapps/next-app/next.config.js | 6 +- testapps/next-app/package.json | 1 - testapps/next-app/src/app/Todos.tsx | 87 +++++ .../app/[locale]/translation-methods/page.tsx | 27 -- testapps/next-app/src/app/layout.tsx | 23 +- testapps/next-app/src/app/page.tsx | 27 +- .../TranslationMethodsClient.tsx | 84 +++++ .../TranslationMethodsServer.tsx | 83 +++++ .../src/app/translation-methods/page.tsx | 19 + .../next-app/src/components/LangSelector.tsx | 11 +- testapps/next-app/src/tolgee/client.tsx | 24 +- testapps/next-app/src/tolgee/language.ts | 26 ++ testapps/next-app/src/tolgee/server.tsx | 15 +- testapps/next-app/src/tolgee/shared.ts | 4 +- testapps/next-internal/pages/index.tsx | 8 +- testapps/next/messages/cs.json | 12 + testapps/next/messages/de.json | 12 + testapps/next/messages/en.json | 12 + testapps/next/messages/fr.json | 12 + .../i18n => messages}/namespaced/cs.json | 0 .../i18n => messages}/namespaced/de.json | 0 .../i18n => messages}/namespaced/en.json | 0 .../i18n => messages}/namespaced/fr.json | 0 testapps/next/src/pages/_app.tsx | 34 ++ testapps/next/src/pages/index.tsx | 20 +- .../next/src/pages/translation-methods.tsx | 20 +- testapps/next/src/tolgee.ts | 31 ++ testapps/next/src/tolgeeNext.tsx | 69 ---- testapps/vue-ssr/.gitignore | 2 + testapps/vue-ssr/tsconfig.tsbuildinfo | 1 - 85 files changed, 1562 insertions(+), 409 deletions(-) create mode 100644 e2e/cypress/e2e/next-app-intl/dev.cy.ts create mode 100644 e2e/cypress/e2e/next-app-intl/prod.cy.ts create mode 100644 packages/web/src/package/tools/detectLanguageFromHeaders.ts create mode 100644 packages/web/src/package/tools/getHeaderLanguages.test.ts create mode 100644 packages/web/src/package/tools/getHeaderLanguages.ts create mode 100644 testapps/next-app-intl/.gitignore rename testapps/{next-app => next-app-intl}/CHANGELOG.md (100%) create mode 100644 testapps/next-app-intl/README.md rename testapps/{next/public/i18n => next-app-intl/messages}/cs.json (100%) rename testapps/{next/public/i18n => next-app-intl/messages}/de.json (100%) rename testapps/{next/public/i18n => next-app-intl/messages}/en.json (100%) rename testapps/{next/public/i18n => next-app-intl/messages}/fr.json (100%) create mode 100644 testapps/next-app-intl/next.config.js create mode 100644 testapps/next-app-intl/package.json create mode 100644 testapps/next-app-intl/public/favicon.ico create mode 100644 testapps/next-app-intl/public/img/appLogo.svg create mode 100644 testapps/next-app-intl/public/img/background.svg create mode 100644 testapps/next-app-intl/public/img/iconAdd.svg create mode 100644 testapps/next-app-intl/public/img/iconMail.svg create mode 100644 testapps/next-app-intl/public/img/iconShare.svg rename testapps/{next-app => next-app-intl}/src/app/[locale]/Todos.tsx (100%) rename testapps/{next-app => next-app-intl}/src/app/[locale]/[...rest]/page.tsx (100%) rename testapps/{next-app => next-app-intl}/src/app/[locale]/layout.tsx (63%) rename testapps/{next-app => next-app-intl}/src/app/[locale]/page.tsx (100%) rename testapps/{next-app => next-app-intl}/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx (100%) rename testapps/{next-app => next-app-intl}/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx (100%) create mode 100644 testapps/next-app-intl/src/app/[locale]/translation-methods/page.tsx create mode 100644 testapps/next-app-intl/src/app/layout.tsx create mode 100644 testapps/next-app-intl/src/app/not-found.tsx create mode 100644 testapps/next-app-intl/src/app/page.tsx create mode 100644 testapps/next-app-intl/src/app/style.css create mode 100644 testapps/next-app-intl/src/components/LangSelector.tsx create mode 100644 testapps/next-app-intl/src/components/Navbar.tsx rename testapps/{next-app => next-app-intl}/src/i18n/request.ts (100%) rename testapps/{next-app => next-app-intl}/src/middleware.ts (74%) rename testapps/{next-app => next-app-intl}/src/navigation.ts (64%) create mode 100644 testapps/next-app-intl/src/tolgee/client.tsx create mode 100644 testapps/next-app-intl/src/tolgee/server.tsx create mode 100644 testapps/next-app-intl/src/tolgee/shared.ts create mode 100644 testapps/next-app-intl/tsconfig.json create mode 100644 testapps/next-app/src/app/Todos.tsx delete mode 100644 testapps/next-app/src/app/[locale]/translation-methods/page.tsx create mode 100644 testapps/next-app/src/app/translation-methods/TranslationMethodsClient.tsx create mode 100644 testapps/next-app/src/app/translation-methods/TranslationMethodsServer.tsx create mode 100644 testapps/next-app/src/app/translation-methods/page.tsx create mode 100644 testapps/next-app/src/tolgee/language.ts create mode 100644 testapps/next/messages/cs.json create mode 100644 testapps/next/messages/de.json create mode 100644 testapps/next/messages/en.json create mode 100644 testapps/next/messages/fr.json rename testapps/next/{public/i18n => messages}/namespaced/cs.json (100%) rename testapps/next/{public/i18n => messages}/namespaced/de.json (100%) rename testapps/next/{public/i18n => messages}/namespaced/en.json (100%) rename testapps/next/{public/i18n => messages}/namespaced/fr.json (100%) create mode 100644 testapps/next/src/pages/_app.tsx create mode 100644 testapps/next/src/tolgee.ts delete mode 100644 testapps/next/src/tolgeeNext.tsx delete mode 100644 testapps/vue-ssr/tsconfig.tsbuildinfo diff --git a/.github/workflows/publish-examples-check.yml b/.github/workflows/publish-examples-check.yml index 30bffc4bc2..831eabf177 100644 --- a/.github/workflows/publish-examples-check.yml +++ b/.github/workflows/publish-examples-check.yml @@ -15,8 +15,9 @@ jobs: [ 'react', 'next', - 'next-app', 'vue', + 'next-app', + 'next-app-intl', 'svelte', 'ngx', 'react-i18next', diff --git a/.github/workflows/publish-examples.yml b/.github/workflows/publish-examples.yml index 1db8d1ae8e..ba2357041f 100644 --- a/.github/workflows/publish-examples.yml +++ b/.github/workflows/publish-examples.yml @@ -18,6 +18,7 @@ jobs: 'next', 'vue', 'next-app', + 'next-app-intl', 'svelte', 'ngx', 'react-i18next', diff --git a/e2e/cypress/e2e/next-app-intl/dev.cy.ts b/e2e/cypress/e2e/next-app-intl/dev.cy.ts new file mode 100644 index 0000000000..2dd5d6f8a5 --- /dev/null +++ b/e2e/cypress/e2e/next-app-intl/dev.cy.ts @@ -0,0 +1,40 @@ +import { exampleAppTest } from '../../common/exampleAppTest'; +import { translationMethodsTest } from '../../common/translationMethodsTest'; +import { exampleAppDevTest } from '../../common/exampleAppDevTest'; + +context( + 'Next with app router (with next-intl) in dev mode', + { retries: 5 }, + () => { + const url = 'http://localhost:8125'; + const translationMethods = url + '/en/translation-methods'; + exampleAppTest(url); + translationMethodsTest(translationMethods, { + en: [ + { text: 'This is a key', count: 2 }, + { text: 'This is key with params value value2', count: 6 }, + { + text: 'This is a key with tags bold value', + count: 2, + testId: 'translationWithTags', + }, + { text: 'Translation in translation', count: 2 }, + ], + de: [ + { text: 'Dies ist ein Schlüssel', count: 2 }, + { + text: 'Dies ist ein Schlüssel mit den Parametern value value2', + count: 6, + }, + { + text: 'Dies ist ein Schlüssel mit den Tags bold value', + count: 2, + testId: 'translationWithTags', + }, + { text: 'Translation in translation', count: 2 }, + ], + }); + + exampleAppDevTest(url, { noLoading: true }); + } +); diff --git a/e2e/cypress/e2e/next-app-intl/prod.cy.ts b/e2e/cypress/e2e/next-app-intl/prod.cy.ts new file mode 100644 index 0000000000..050944866d --- /dev/null +++ b/e2e/cypress/e2e/next-app-intl/prod.cy.ts @@ -0,0 +1,33 @@ +import { exampleAppTest } from '../../common/exampleAppTest'; +import { translationMethodsTest } from '../../common/translationMethodsTest'; + +context('Next with app router (with next-intl) in prod mode', () => { + const url = 'http://localhost:8127'; + const translationMethods = url + '/en/translation-methods'; + exampleAppTest(url); + translationMethodsTest(translationMethods, { + en: [ + { text: 'This is a key', count: 2 }, + { text: 'This is key with params value value2', count: 6 }, + { + text: 'This is a key with tags bold value', + count: 2, + testId: 'translationWithTags', + }, + { text: 'Translation in translation', count: 2 }, + ], + de: [ + { text: 'Dies ist ein Schlüssel', count: 2 }, + { + text: 'Dies ist ein Schlüssel mit den Parametern value value2', + count: 6, + }, + { + text: 'Dies ist ein Schlüssel mit den Tags bold value', + count: 2, + testId: 'translationWithTags', + }, + { text: 'Translation in translation', count: 2 }, + ], + }); +}); diff --git a/e2e/cypress/e2e/next-app/dev.cy.ts b/e2e/cypress/e2e/next-app/dev.cy.ts index 4da42b53c6..29695ed2d9 100644 --- a/e2e/cypress/e2e/next-app/dev.cy.ts +++ b/e2e/cypress/e2e/next-app/dev.cy.ts @@ -4,7 +4,7 @@ import { exampleAppDevTest } from '../../common/exampleAppDevTest'; context('Next with app router in dev mode', { retries: 5 }, () => { const url = 'http://localhost:8122'; - const translationMethods = url + '/en/translation-methods'; + const translationMethods = url + '/translation-methods'; exampleAppTest(url); translationMethodsTest(translationMethods, { en: [ diff --git a/e2e/cypress/e2e/next-app/prod.cy.ts b/e2e/cypress/e2e/next-app/prod.cy.ts index 5d96718a29..95fda3f04f 100644 --- a/e2e/cypress/e2e/next-app/prod.cy.ts +++ b/e2e/cypress/e2e/next-app/prod.cy.ts @@ -3,7 +3,7 @@ import { translationMethodsTest } from '../../common/translationMethodsTest'; context('Next with app router in prod mode', () => { const url = 'http://localhost:8121'; - const translationMethods = url + '/en/translation-methods'; + const translationMethods = url + '/translation-methods'; exampleAppTest(url); translationMethodsTest(translationMethods, { en: [ diff --git a/package.json b/package.json index 401a62ed04..c47f68c544 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "develop:react-i18next": "npm run develop -- --scope=@tolgee/react-i18next-testapp", "develop:vue-i18next": "npm run develop -- --scope=@tolgee/vue-i18next-testapp", "develop:next-app": "npm run develop -- --scope=@tolgee/next-app-testapp", + "develop:next-app-intl": "npm run develop -- --scope=@tolgee/next-app-intl-testapp", "develop:vue-ssr": "npm run develop -- --scope=@tolgee/vue-ssr-testapp", "build:e2e": "turbo run build:e2e --cache-dir='.turbo'", "test:e2e": "pnpm run build:e2e && pnpm --prefix e2e run start", diff --git a/packages/core/src/Controller/State/initState.ts b/packages/core/src/Controller/State/initState.ts index 350018fd9a..2b557ad609 100644 --- a/packages/core/src/Controller/State/initState.ts +++ b/packages/core/src/Controller/State/initState.ts @@ -81,8 +81,8 @@ export type TolgeeOptionsInternal = { * * ```ts * { - * 'locale': - * 'locale:namespace': + * 'language': + * 'language:namespace': * } * ``` */ diff --git a/packages/format-icu/src/createFormatIcu.test.ts b/packages/format-icu/src/createFormatIcu.test.ts index 5e6f48f4b6..bf2d692c51 100644 --- a/packages/format-icu/src/createFormatIcu.test.ts +++ b/packages/format-icu/src/createFormatIcu.test.ts @@ -12,10 +12,10 @@ describe('format icu', () => { expect(result).toEqual('result is 42,000'); }); - it('fixes invalid locale', () => { + it('fixes invalid language', () => { const formatter = createFormatIcu() as any; - expect(formatter.getLocale('en_GB')).toEqual('en-GB'); - expect(formatter.getLocale('en_GB-nonsenceeeee')).toEqual('en-GB'); - expect(formatter.getLocale('cs CZ')).toEqual('cs-CZ'); + expect(formatter.getLanguage('en_GB')).toEqual('en-GB'); + expect(formatter.getLanguage('en_GB-nonsenceeeee')).toEqual('en-GB'); + expect(formatter.getLanguage('cs CZ')).toEqual('cs-CZ'); }); }); diff --git a/packages/format-icu/src/createFormatIcu.ts b/packages/format-icu/src/createFormatIcu.ts index c8ff891eaa..1785a045dc 100644 --- a/packages/format-icu/src/createFormatIcu.ts +++ b/packages/format-icu/src/createFormatIcu.ts @@ -12,7 +12,7 @@ export const createFormatIcu = (): FinalFormatterMiddleware => { } } - function getLocale(language: string) { + function getLanguage(language: string) { if (!locales.get(language)) { let localeCandidate: string = String(language).replace(/[^a-zA-Z]/g, '-'); while (!isLocaleValid(localeCandidate)) { @@ -33,12 +33,12 @@ export const createFormatIcu = (): FinalFormatterMiddleware => { (p) => typeof p === 'function' ); - const locale = getLocale(language); + const locale = getLanguage(language); return new IntlMessageFormat(translation, locale, undefined, { ignoreTag, }).format(params); }; - return Object.freeze({ getLocale, format }); + return Object.freeze({ getLanguage, format }); }; diff --git a/packages/react/src/TolgeeProvider.tsx b/packages/react/src/TolgeeProvider.tsx index 91b2c0f6ab..65622d63ce 100644 --- a/packages/react/src/TolgeeProvider.tsx +++ b/packages/react/src/TolgeeProvider.tsx @@ -1,6 +1,7 @@ import React, { Suspense, useEffect, useState } from 'react'; -import { TolgeeInstance } from '@tolgee/web'; +import { TolgeeInstance, TolgeeStaticData } from '@tolgee/web'; import { ReactOptions, TolgeeReactContext } from './types'; +import { useTolgeeSSR } from './useTolgeeSSR'; export const DEFAULT_REACT_OPTIONS: ReactOptions = { useSuspense: true, @@ -20,11 +21,31 @@ export const getProviderInstance = () => { let LAST_TOLGEE_INSTANCE: TolgeeInstance | undefined = undefined; +export type SSROptions = { + /** + * Hard set language to this value, use together with `staticData` + */ + language?: string; + /** + * If provided, static data will be hard set to Tolgee cache for initial render + */ + staticData?: TolgeeStaticData; +}; + export interface TolgeeProviderProps { children?: React.ReactNode; tolgee: TolgeeInstance; options?: ReactOptions; fallback?: React.ReactNode; + /** + * use this option if you use SSR + * + * You can pass staticData and language + * which will be set to tolgee instance for the initial render + * + * Don't switch between ssr and non-ssr dynamically + */ + ssr?: SSROptions | boolean; } export const TolgeeProvider: React.FC = ({ @@ -32,9 +53,8 @@ export const TolgeeProvider: React.FC = ({ options, children, fallback, + ssr, }) => { - const [loading, setLoading] = useState(!tolgee.isLoaded()); - // prevent restarting tolgee unnecesarly // however if the instance change on hot-reloading // we want to restart @@ -56,6 +76,15 @@ export const TolgeeProvider: React.FC = ({ } }, [tolgee]); + let tolgeeSSR = tolgee; + + const { language, staticData } = ( + typeof ssr !== 'object' ? {} : ssr + ) as SSROptions; + tolgeeSSR = useTolgeeSSR(tolgee, language, staticData, Boolean(ssr)); + + const [loading, setLoading] = useState(!tolgeeSSR.isLoaded()); + const optionsWithDefault = { ...DEFAULT_REACT_OPTIONS, ...options }; const TolgeeProviderContext = getProviderInstance(); @@ -63,7 +92,7 @@ export const TolgeeProvider: React.FC = ({ if (optionsWithDefault.useSuspense) { return ( {loading ? ( fallback @@ -76,7 +105,7 @@ export const TolgeeProvider: React.FC = ({ return ( {loading ? fallback : children} diff --git a/packages/react/src/useTolgeeSSR.ts b/packages/react/src/useTolgeeSSR.ts index 2fe779e74a..a152ba06cc 100644 --- a/packages/react/src/useTolgeeSSR.ts +++ b/packages/react/src/useTolgeeSSR.ts @@ -24,39 +24,43 @@ function getTolgeeWithDeactivatedWrapper( * * It also ensures that the first render is done without wrapping and so it avoids * "client different than server" issues. - * + * * * @param tolgeeInstance initialized Tolgee instance * @param language language that is obtained outside of Tolgee on the server and client * @param staticData static data for the language + * @param enabled if set to false, no action is taken */ export function useTolgeeSSR( tolgeeInstance: TolgeeInstance, language?: string, - staticData?: TolgeeStaticData | undefined + staticData?: TolgeeStaticData | undefined, + enabled = true ) { const [noWrappingTolgee] = useState(() => getTolgeeWithDeactivatedWrapper(tolgeeInstance) ); - const [initialRender, setInitialRender] = useState(true); + const [initialRender, setInitialRender] = useState(enabled); useEffect(() => { setInitialRender(false); }, []); useMemo(() => { - // we have to prepare tolgee before rendering children - // so translations are available right away - // events emitting must be off, to not trigger re-render while rendering - tolgeeInstance.setEmitterActive(false); - tolgeeInstance.addStaticData(staticData); - tolgeeInstance.changeLanguage(language!); - tolgeeInstance.setEmitterActive(true); + if (enabled) { + // we have to prepare tolgee before rendering children + // so translations are available right away + // events emitting must be off, to not trigger re-render while rendering + tolgeeInstance.setEmitterActive(false); + tolgeeInstance.addStaticData(staticData); + tolgeeInstance.changeLanguage(language!); + tolgeeInstance.setEmitterActive(true); + } }, [language, staticData, tolgeeInstance]); useState(() => { // running this function only on first render - if (!tolgeeInstance.isLoaded()) { + if (!tolgeeInstance.isLoaded() && enabled) { // warning user, that static data provided are not sufficient // for proper SSR render const missingRecords = tolgeeInstance diff --git a/packages/web/src/package/LanguageDetector.ts b/packages/web/src/package/LanguageDetector.ts index c9cc986bc2..a2cf29bad5 100644 --- a/packages/web/src/package/LanguageDetector.ts +++ b/packages/web/src/package/LanguageDetector.ts @@ -1,27 +1,31 @@ import type { LanguageDetectorMiddleware, TolgeePlugin } from '@tolgee/core'; import { throwIfSSR } from './tools/isSSR'; +export function detectLanguage(language: string, availableLanguages: string[]) { + const exactMatch = availableLanguages.find((l) => l === language); + if (exactMatch) { + return exactMatch; + } + + const getTwoLetters = (fullTag: string) => + fullTag.replace(/^(.+?)(-.*)?$/, '$1'); + + const preferredTwoLetter = getTwoLetters(window.navigator.language); + const twoLetterMatch = availableLanguages.find( + (l) => getTwoLetters(l) === preferredTwoLetter + ); + if (twoLetterMatch) { + return twoLetterMatch; + } + return undefined; +} + export function createLanguageDetector(): LanguageDetectorMiddleware { return { getLanguage({ availableLanguages }) { throwIfSSR('LanguageDetector'); const preferred = window.navigator.language; - const exactMatch = availableLanguages.find((l) => l === preferred); - if (exactMatch) { - return exactMatch; - } - - const getTwoLetters = (fullTag: string) => - fullTag.replace(/^(.+?)(-.*)?$/, '$1'); - - const preferredTwoLetter = getTwoLetters(window.navigator.language); - const twoLetterMatch = availableLanguages.find( - (l) => getTwoLetters(l) === preferredTwoLetter - ); - if (twoLetterMatch) { - return twoLetterMatch; - } - return undefined; + return detectLanguage(preferred, availableLanguages); }, }; } diff --git a/packages/web/src/package/tools/detectLanguageFromHeaders.ts b/packages/web/src/package/tools/detectLanguageFromHeaders.ts new file mode 100644 index 0000000000..83622a68f5 --- /dev/null +++ b/packages/web/src/package/tools/detectLanguageFromHeaders.ts @@ -0,0 +1,10 @@ +import { detectLanguage } from '../LanguageDetector'; +import { getHeaderLanguages } from './getHeaderLanguages'; + +export const detectLanguageFromHeaders = ( + headers: Headers, + availableLanguages: string[] +) => { + const languages = getHeaderLanguages(headers); + return languages[0] && detectLanguage(languages[0], availableLanguages); +}; diff --git a/packages/web/src/package/tools/getHeaderLanguages.test.ts b/packages/web/src/package/tools/getHeaderLanguages.test.ts new file mode 100644 index 0000000000..42e5c9b491 --- /dev/null +++ b/packages/web/src/package/tools/getHeaderLanguages.test.ts @@ -0,0 +1,36 @@ +import { getHeaderLanguages } from './getHeaderLanguages'; + +describe('language from headers', () => { + it('parses correctly single language', () => { + const headers = new Headers(); + headers.set('Accept-Language', 'en-US'); + expect(getHeaderLanguages(headers)).toEqual(['en-US']); + }); + + it('parses correctly multiple languages', () => { + const headers = new Headers(); + headers.set('Accept-Language', 'en-US, en'); + expect(getHeaderLanguages(headers)).toEqual(['en-US', 'en']); + }); + + it('parses correctly star', () => { + const headers = new Headers(); + headers.set('Accept-Language', '*'); + expect(getHeaderLanguages(headers)).toEqual([]); + }); + + it('parses correctly weighted languages', () => { + const headers = new Headers(); + headers.set('Accept-Language', 'fr-CH, fr;q=0.9'); + expect(getHeaderLanguages(headers)).toEqual(['fr-CH', 'fr']); + }); + + it('parses correctly weighted languages with star', () => { + const headers = new Headers(); + headers.set( + 'Accept-Language', + 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5' + ); + expect(getHeaderLanguages(headers)).toEqual(['fr-CH', 'fr', 'en', 'de']); + }); +}); diff --git a/packages/web/src/package/tools/getHeaderLanguages.ts b/packages/web/src/package/tools/getHeaderLanguages.ts new file mode 100644 index 0000000000..e925b90951 --- /dev/null +++ b/packages/web/src/package/tools/getHeaderLanguages.ts @@ -0,0 +1,15 @@ +export function getHeaderLanguages(headers: Headers) { + const acceptLanguageHeader = headers.get('Accept-Language'); + if (!acceptLanguageHeader) { + return []; + } + // Split the header into locales based on commas + const locales = acceptLanguageHeader.split(',').map((locale) => { + // Remove whitespace and split by ';' to get only the locale part + const [localePart] = locale.trim().split(';'); + return localePart; + }); + + // Filter out any empty strings and return the unique locales + return [...new Set(locales.filter((locale) => locale && locale !== '*'))]; +} diff --git a/packages/web/src/package/typedIndex.ts b/packages/web/src/package/typedIndex.ts index 08d0b144fc..a772d4525c 100644 --- a/packages/web/src/package/typedIndex.ts +++ b/packages/web/src/package/typedIndex.ts @@ -3,8 +3,10 @@ export { DevBackend } from './DevBackend'; export { getProjectIdFromApiKey } from './tools/decodeApiKey'; export { BrowserExtensionPlugin } from './BrowserExtensionPlugin/BrowserExtensionPlugin'; export { LanguageStorage } from './LanguageStorage'; -export { LanguageDetector } from './LanguageDetector'; +export { LanguageDetector, detectLanguage } from './LanguageDetector'; +export { detectLanguageFromHeaders } from './tools/detectLanguageFromHeaders'; export { BackendFetch } from './BackendFetch'; +export { isSSR } from './tools/isSSR'; export { TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE, TOLGEE_ATTRIBUTE_NAME, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 275ce8ba1a..b17c9026ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -800,9 +800,6 @@ importers: next: specifier: 14.2.13 version: 14.2.13(@babel/core@7.25.7)(react-dom@18.3.1)(react@18.3.1) - next-intl: - specifier: 3.19.4 - version: 3.19.4(next@14.2.13)(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -823,6 +820,43 @@ importers: specifier: ^5.3.2 version: 5.3.2 + testapps/next-app-intl: + dependencies: + '@tolgee/format-icu': + specifier: 5.29.5 + version: link:../../packages/format-icu + '@tolgee/react': + specifier: 5.29.5 + version: link:../../packages/react + '@tolgee/web': + specifier: 5.29.5 + version: link:../../packages/web + next: + specifier: 14.2.13 + version: 14.2.13(@babel/core@7.25.7)(react-dom@18.3.1)(react@18.3.1) + next-intl: + specifier: 3.19.4 + version: 3.19.4(next@14.2.13)(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.11.17 + '@types/react': + specifier: ^18.2.42 + version: 18.2.74 + '@types/react-dom': + specifier: ^18.2.17 + version: 18.2.23 + typescript: + specifier: ^5.3.2 + version: 5.4.5 + testapps/next-internal: dependencies: '@tolgee/react': @@ -7374,7 +7408,7 @@ packages: '@jest/schemas': 28.1.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.8.2 + '@types/node': 18.14.6 '@types/yargs': 17.0.22 chalk: 4.1.2 dev: true @@ -10380,7 +10414,7 @@ packages: /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 17.0.45 + '@types/node': 18.14.6 /@types/html-minifier-terser@6.1.0: resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -10484,6 +10518,7 @@ packages: /@types/node@17.0.45: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + dev: true /@types/node@18.13.0: resolution: {integrity: sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==} @@ -10504,11 +10539,13 @@ packages: resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==} dependencies: undici-types: 5.26.5 + dev: true /@types/node@22.7.4: resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} dependencies: undici-types: 6.19.8 + dev: true /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -11945,7 +11982,7 @@ packages: '@vue/shared': 3.2.47 estree-walker: 2.0.2 magic-string: 0.25.9 - postcss: 8.4.38 + postcss: 8.4.47 source-map: 0.6.1 /@vue/compiler-sfc@3.5.11: @@ -13127,7 +13164,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001464 + caniuse-lite: 1.0.30001667 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.1.0 @@ -13143,7 +13180,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001605 + caniuse-lite: 1.0.30001667 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.0 @@ -13925,7 +13962,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001566 + caniuse-lite: 1.0.30001667 electron-to-chromium: 1.4.603 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.21.4) @@ -13936,7 +13973,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001566 + caniuse-lite: 1.0.30001667 electron-to-chromium: 1.4.325 node-releases: 2.0.10 update-browserslist-db: 1.0.10(browserslist@4.21.5) @@ -13947,7 +13984,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001605 + caniuse-lite: 1.0.30001667 electron-to-chromium: 1.4.676 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) @@ -14160,21 +14197,14 @@ packages: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001605 + caniuse-lite: 1.0.30001667 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 dev: true - /caniuse-lite@1.0.30001464: - resolution: {integrity: sha512-oww27MtUmusatpRpCGSOneQk2/l5czXANDSFvsc7VuOQ86s3ANhZetpwXNf1zY/zdfP63Xvjz325DAdAoES13g==} - dev: true - - /caniuse-lite@1.0.30001566: - resolution: {integrity: sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==} - dev: true - /caniuse-lite@1.0.30001605: resolution: {integrity: sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==} + dev: false /caniuse-lite@1.0.30001663: resolution: {integrity: sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==} @@ -15487,13 +15517,13 @@ packages: postcss: 8.4.28 dev: true - /css-declaration-sorter@6.3.1(postcss@8.4.38): + /css-declaration-sorter@6.3.1(postcss@8.4.47): resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /css-loader@6.10.0(webpack@5.90.3): @@ -15525,12 +15555,12 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.38) - postcss: 8.4.38 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.38) - postcss-modules-local-by-default: 4.0.0(postcss@8.4.38) - postcss-modules-scope: 3.0.0(postcss@8.4.38) - postcss-modules-values: 4.0.0(postcss@8.4.38) + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.47) + postcss-modules-local-by-default: 4.0.0(postcss@8.4.47) + postcss-modules-scope: 3.0.0(postcss@8.4.47) + postcss-modules-values: 4.0.0(postcss@8.4.47) postcss-value-parser: 4.2.0 semver: 7.6.3 webpack: 5.74.0 @@ -15555,9 +15585,9 @@ packages: esbuild: optional: true dependencies: - cssnano: 5.1.13(postcss@8.4.38) + cssnano: 5.1.13(postcss@8.4.47) jest-worker: 27.5.1 - postcss: 8.4.38 + postcss: 8.4.47 schema-utils: 4.0.0 serialize-javascript: 6.0.2 source-map: 0.6.1 @@ -15661,42 +15691,42 @@ packages: postcss-unique-selectors: 5.1.1(postcss@8.4.28) dev: true - /cssnano-preset-default@5.2.12(postcss@8.4.38): + /cssnano-preset-default@5.2.12(postcss@8.4.47): resolution: {integrity: sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - css-declaration-sorter: 6.3.1(postcss@8.4.38) - cssnano-utils: 3.1.0(postcss@8.4.38) - postcss: 8.4.38 - postcss-calc: 8.2.4(postcss@8.4.38) - postcss-colormin: 5.3.0(postcss@8.4.38) - postcss-convert-values: 5.1.2(postcss@8.4.38) - postcss-discard-comments: 5.1.2(postcss@8.4.38) - postcss-discard-duplicates: 5.1.0(postcss@8.4.38) - postcss-discard-empty: 5.1.1(postcss@8.4.38) - postcss-discard-overridden: 5.1.0(postcss@8.4.38) - postcss-merge-longhand: 5.1.6(postcss@8.4.38) - postcss-merge-rules: 5.1.2(postcss@8.4.38) - postcss-minify-font-values: 5.1.0(postcss@8.4.38) - postcss-minify-gradients: 5.1.1(postcss@8.4.38) - postcss-minify-params: 5.1.3(postcss@8.4.38) - postcss-minify-selectors: 5.2.1(postcss@8.4.38) - postcss-normalize-charset: 5.1.0(postcss@8.4.38) - postcss-normalize-display-values: 5.1.0(postcss@8.4.38) - postcss-normalize-positions: 5.1.1(postcss@8.4.38) - postcss-normalize-repeat-style: 5.1.1(postcss@8.4.38) - postcss-normalize-string: 5.1.0(postcss@8.4.38) - postcss-normalize-timing-functions: 5.1.0(postcss@8.4.38) - postcss-normalize-unicode: 5.1.0(postcss@8.4.38) - postcss-normalize-url: 5.1.0(postcss@8.4.38) - postcss-normalize-whitespace: 5.1.1(postcss@8.4.38) - postcss-ordered-values: 5.1.3(postcss@8.4.38) - postcss-reduce-initial: 5.1.0(postcss@8.4.38) - postcss-reduce-transforms: 5.1.0(postcss@8.4.38) - postcss-svgo: 5.1.0(postcss@8.4.38) - postcss-unique-selectors: 5.1.1(postcss@8.4.38) + css-declaration-sorter: 6.3.1(postcss@8.4.47) + cssnano-utils: 3.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-calc: 8.2.4(postcss@8.4.47) + postcss-colormin: 5.3.0(postcss@8.4.47) + postcss-convert-values: 5.1.2(postcss@8.4.47) + postcss-discard-comments: 5.1.2(postcss@8.4.47) + postcss-discard-duplicates: 5.1.0(postcss@8.4.47) + postcss-discard-empty: 5.1.1(postcss@8.4.47) + postcss-discard-overridden: 5.1.0(postcss@8.4.47) + postcss-merge-longhand: 5.1.6(postcss@8.4.47) + postcss-merge-rules: 5.1.2(postcss@8.4.47) + postcss-minify-font-values: 5.1.0(postcss@8.4.47) + postcss-minify-gradients: 5.1.1(postcss@8.4.47) + postcss-minify-params: 5.1.3(postcss@8.4.47) + postcss-minify-selectors: 5.2.1(postcss@8.4.47) + postcss-normalize-charset: 5.1.0(postcss@8.4.47) + postcss-normalize-display-values: 5.1.0(postcss@8.4.47) + postcss-normalize-positions: 5.1.1(postcss@8.4.47) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.47) + postcss-normalize-string: 5.1.0(postcss@8.4.47) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.47) + postcss-normalize-unicode: 5.1.0(postcss@8.4.47) + postcss-normalize-url: 5.1.0(postcss@8.4.47) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.47) + postcss-ordered-values: 5.1.3(postcss@8.4.47) + postcss-reduce-initial: 5.1.0(postcss@8.4.47) + postcss-reduce-transforms: 5.1.0(postcss@8.4.47) + postcss-svgo: 5.1.0(postcss@8.4.47) + postcss-unique-selectors: 5.1.1(postcss@8.4.47) dev: true /cssnano-utils@3.1.0(postcss@8.4.28): @@ -15708,13 +15738,13 @@ packages: postcss: 8.4.28 dev: true - /cssnano-utils@3.1.0(postcss@8.4.38): + /cssnano-utils@3.1.0(postcss@8.4.47): resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /cssnano@5.1.13(postcss@8.4.28): @@ -15729,15 +15759,15 @@ packages: yaml: 1.10.2 dev: true - /cssnano@5.1.13(postcss@8.4.38): + /cssnano@5.1.13(postcss@8.4.47): resolution: {integrity: sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-preset-default: 5.2.12(postcss@8.4.38) + cssnano-preset-default: 5.2.12(postcss@8.4.47) lilconfig: 2.0.6 - postcss: 8.4.38 + postcss: 8.4.47 yaml: 1.10.2 dev: true @@ -19313,15 +19343,6 @@ packages: safer-buffer: 2.1.2 dev: true - /icss-utils@5.1.0(postcss@8.4.38): - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - postcss: 8.4.38 - dev: true - /icss-utils@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -19473,7 +19494,7 @@ packages: /injection-js@2.4.0: resolution: {integrity: sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: true /inline-source-map@0.6.2: @@ -20187,7 +20208,7 @@ packages: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 17.0.45 + '@types/node': 18.14.6 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -20241,7 +20262,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.17 + '@types/node': 18.14.6 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -20713,7 +20734,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 17.0.45 + '@types/node': 18.14.6 jest-mock: 27.5.1 jest-util: 27.5.1 dev: true @@ -20736,7 +20757,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.17 + '@types/node': 18.14.6 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -20850,7 +20871,7 @@ packages: '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 17.0.45 + '@types/node': 18.14.6 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -21320,7 +21341,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.4 + '@types/node': 18.14.6 chalk: 4.1.2 cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 @@ -21342,7 +21363,7 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 17.0.45 + '@types/node': 18.14.6 graceful-fs: 4.2.11 dev: true @@ -22406,7 +22427,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.4.1 + tslib: 2.7.0 dev: true /lru-cache@10.2.2: @@ -24755,12 +24776,12 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-calc@8.2.4(postcss@8.4.38): + /postcss-calc@8.2.4(postcss@8.4.47): resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: postcss: ^8.2.2 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 dev: true @@ -24778,7 +24799,7 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-colormin@5.3.0(postcss@8.4.38): + /postcss-colormin@5.3.0(postcss@8.4.47): resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -24787,7 +24808,7 @@ packages: browserslist: 4.23.0 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -24802,14 +24823,14 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-convert-values@5.1.2(postcss@8.4.38): + /postcss-convert-values@5.1.2(postcss@8.4.47): resolution: {integrity: sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -24822,13 +24843,13 @@ packages: postcss: 8.4.28 dev: true - /postcss-discard-comments@5.1.2(postcss@8.4.38): + /postcss-discard-comments@5.1.2(postcss@8.4.47): resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-discard-duplicates@5.1.0(postcss@8.4.28): @@ -24840,13 +24861,13 @@ packages: postcss: 8.4.28 dev: true - /postcss-discard-duplicates@5.1.0(postcss@8.4.38): + /postcss-discard-duplicates@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-discard-empty@5.1.1(postcss@8.4.28): @@ -24858,13 +24879,13 @@ packages: postcss: 8.4.28 dev: true - /postcss-discard-empty@5.1.1(postcss@8.4.38): + /postcss-discard-empty@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-discard-overridden@5.1.0(postcss@8.4.28): @@ -24876,13 +24897,13 @@ packages: postcss: 8.4.28 dev: true - /postcss-discard-overridden@5.1.0(postcss@8.4.38): + /postcss-discard-overridden@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-load-config@3.1.4(postcss@8.4.47)(ts-node@10.9.1): @@ -24954,15 +24975,15 @@ packages: stylehacks: 5.1.0(postcss@8.4.28) dev: true - /postcss-merge-longhand@5.1.6(postcss@8.4.38): + /postcss-merge-longhand@5.1.6(postcss@8.4.47): resolution: {integrity: sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - stylehacks: 5.1.0(postcss@8.4.38) + stylehacks: 5.1.0(postcss@8.4.47) dev: true /postcss-merge-rules@5.1.2(postcss@8.4.28): @@ -24978,7 +24999,7 @@ packages: postcss-selector-parser: 6.1.2 dev: true - /postcss-merge-rules@5.1.2(postcss@8.4.38): + /postcss-merge-rules@5.1.2(postcss@8.4.47): resolution: {integrity: sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -24986,8 +25007,8 @@ packages: dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 3.1.0(postcss@8.4.47) + postcss: 8.4.47 postcss-selector-parser: 6.1.2 dev: true @@ -25001,13 +25022,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-minify-font-values@5.1.0(postcss@8.4.38): + /postcss-minify-font-values@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25023,15 +25044,15 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-minify-gradients@5.1.1(postcss@8.4.38): + /postcss-minify-gradients@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 3.1.0(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25047,15 +25068,15 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-minify-params@5.1.3(postcss@8.4.38): + /postcss-minify-params@5.1.3(postcss@8.4.47): resolution: {integrity: sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.23.0 - cssnano-utils: 3.1.0(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 3.1.0(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25069,25 +25090,16 @@ packages: postcss-selector-parser: 6.1.2 dev: true - /postcss-minify-selectors@5.2.1(postcss@8.4.38): + /postcss-minify-selectors@5.2.1(postcss@8.4.47): resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-selector-parser: 6.1.2 dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.38): - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - postcss: 8.4.38 - dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.47): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} @@ -25097,14 +25109,14 @@ packages: postcss: 8.4.47 dev: true - /postcss-modules-local-by-default@4.0.0(postcss@8.4.38): + /postcss-modules-local-by-default@4.0.0(postcss@8.4.47): resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.38) - postcss: 8.4.38 + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 postcss-selector-parser: 6.0.10 postcss-value-parser: 4.2.0 dev: true @@ -25121,13 +25133,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope@3.0.0(postcss@8.4.38): + /postcss-modules-scope@3.0.0(postcss@8.4.47): resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-selector-parser: 6.0.10 dev: true @@ -25141,16 +25153,6 @@ packages: postcss-selector-parser: 6.1.2 dev: true - /postcss-modules-values@4.0.0(postcss@8.4.38): - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - dependencies: - icss-utils: 5.1.0(postcss@8.4.38) - postcss: 8.4.38 - dev: true - /postcss-modules-values@4.0.0(postcss@8.4.47): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} @@ -25170,13 +25172,13 @@ packages: postcss: 8.4.28 dev: true - /postcss-normalize-charset@5.1.0(postcss@8.4.38): + /postcss-normalize-charset@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-normalize-display-values@5.1.0(postcss@8.4.28): @@ -25189,13 +25191,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-display-values@5.1.0(postcss@8.4.38): + /postcss-normalize-display-values@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25209,13 +25211,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-positions@5.1.1(postcss@8.4.38): + /postcss-normalize-positions@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25229,13 +25231,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-repeat-style@5.1.1(postcss@8.4.38): + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25249,13 +25251,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-string@5.1.0(postcss@8.4.38): + /postcss-normalize-string@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25269,13 +25271,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-timing-functions@5.1.0(postcss@8.4.38): + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25290,14 +25292,14 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-unicode@5.1.0(postcss@8.4.38): + /postcss-normalize-unicode@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25312,14 +25314,14 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-url@5.1.0(postcss@8.4.38): + /postcss-normalize-url@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: normalize-url: 6.1.0 - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25333,13 +25335,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-normalize-whitespace@5.1.1(postcss@8.4.38): + /postcss-normalize-whitespace@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25354,14 +25356,14 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-ordered-values@5.1.3(postcss@8.4.38): + /postcss-ordered-values@5.1.3(postcss@8.4.47): resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - cssnano-utils: 3.1.0(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 3.1.0(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25376,7 +25378,7 @@ packages: postcss: 8.4.28 dev: true - /postcss-reduce-initial@5.1.0(postcss@8.4.38): + /postcss-reduce-initial@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: @@ -25384,7 +25386,7 @@ packages: dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - postcss: 8.4.38 + postcss: 8.4.47 dev: true /postcss-reduce-transforms@5.1.0(postcss@8.4.28): @@ -25397,13 +25399,13 @@ packages: postcss-value-parser: 4.2.0 dev: true - /postcss-reduce-transforms@5.1.0(postcss@8.4.38): + /postcss-reduce-transforms@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 dev: true @@ -25452,13 +25454,13 @@ packages: svgo: 2.8.0 dev: true - /postcss-svgo@5.1.0(postcss@8.4.38): + /postcss-svgo@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-value-parser: 4.2.0 svgo: 2.8.0 dev: true @@ -25473,13 +25475,13 @@ packages: postcss-selector-parser: 6.1.2 dev: true - /postcss-unique-selectors@5.1.1(postcss@8.4.38): + /postcss-unique-selectors@5.1.1(postcss@8.4.47): resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: - postcss: 8.4.38 + postcss: 8.4.47 postcss-selector-parser: 6.1.2 dev: true @@ -25501,7 +25503,7 @@ packages: dependencies: nanoid: 3.3.7 picocolors: 1.1.0 - source-map-js: 1.2.0 + source-map-js: 1.2.1 dev: true /postcss@8.4.31: @@ -25546,7 +25548,6 @@ packages: nanoid: 3.3.7 picocolors: 1.1.0 source-map-js: 1.2.1 - dev: true /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} @@ -28060,14 +28061,14 @@ packages: postcss-selector-parser: 6.1.2 dev: true - /stylehacks@5.1.0(postcss@8.4.38): + /stylehacks@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.47 postcss-selector-parser: 6.1.2 dev: true @@ -29018,10 +29019,6 @@ packages: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} dev: true - /tslib@2.4.1: - resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} - dev: true - /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} dev: true @@ -29330,9 +29327,11 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true /undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: true /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} diff --git a/scripts/e2eRunner/config.ts b/scripts/e2eRunner/config.ts index 98cbaa7053..ac76979c08 100644 --- a/scripts/e2eRunner/config.ts +++ b/scripts/e2eRunner/config.ts @@ -167,6 +167,26 @@ export const config: Config = { }, }, }, + 'next-app-intl': { + commandLineServices: { + dev: { + command: 'npm run dev -- -p 8125', + cwd: path.resolve(__dirname, '../../testapps/next-app-intl/'), + waitForOutput: 'Ready in ', + environment: { + NEXT_BUILD_DIR: 'dist-e2e', + NEXT_PUBLIC_TOLGEE_API_URL: 'http://localhost:8202', + NEXT_PUBLIC_TOLGEE_API_KEY: + 'examples-admin-imported-project-implicit', + }, + }, + prod: { + command: 'npm run start -- -p 8127', + cwd: path.resolve(__dirname, '../../testapps/next-app-intl/'), + waitForOutput: 'Ready in ', + }, + }, + }, 'vue-ssr': { commandLineServices: { dev: { diff --git a/testapps/next-app-intl/.gitignore b/testapps/next-app-intl/.gitignore new file mode 100644 index 0000000000..5c9d7dd3de --- /dev/null +++ b/testapps/next-app-intl/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build +/dist-e2e + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/testapps/next-app/CHANGELOG.md b/testapps/next-app-intl/CHANGELOG.md similarity index 100% rename from testapps/next-app/CHANGELOG.md rename to testapps/next-app-intl/CHANGELOG.md diff --git a/testapps/next-app-intl/README.md b/testapps/next-app-intl/README.md new file mode 100644 index 0000000000..bd36c58988 --- /dev/null +++ b/testapps/next-app-intl/README.md @@ -0,0 +1,23 @@ +# Tolgee with next.js 14 app router DEMO + +This repo demonstrates how to use `tolgee` with new `next.js` app router. + +An example based on `next14` app folder with `tolgee` and `next-intl` package. + +## Setup + +1. Clone this repo +2. Run `npm i` +3. Run `npm run dev` + +## Setup tolgee credentials (optional) + +4. Create project in Tolgee platform +5. Add `.env.development.local` file to base folder of this project with an API key to your project + +``` +NEXT_PUBLIC_TOLGEE_API_URL=https://app.tolgee.io +NEXT_PUBLIC_TOLGEE_API_KEY= +``` + +6. Re-run `npm run dev` diff --git a/testapps/next/public/i18n/cs.json b/testapps/next-app-intl/messages/cs.json similarity index 100% rename from testapps/next/public/i18n/cs.json rename to testapps/next-app-intl/messages/cs.json diff --git a/testapps/next/public/i18n/de.json b/testapps/next-app-intl/messages/de.json similarity index 100% rename from testapps/next/public/i18n/de.json rename to testapps/next-app-intl/messages/de.json diff --git a/testapps/next/public/i18n/en.json b/testapps/next-app-intl/messages/en.json similarity index 100% rename from testapps/next/public/i18n/en.json rename to testapps/next-app-intl/messages/en.json diff --git a/testapps/next/public/i18n/fr.json b/testapps/next-app-intl/messages/fr.json similarity index 100% rename from testapps/next/public/i18n/fr.json rename to testapps/next-app-intl/messages/fr.json diff --git a/testapps/next-app-intl/next.config.js b/testapps/next-app-intl/next.config.js new file mode 100644 index 0000000000..385ea474f6 --- /dev/null +++ b/testapps/next-app-intl/next.config.js @@ -0,0 +1,10 @@ +const createNextIntlPlugin = require('next-intl/plugin'); + +const withNextIntl = createNextIntlPlugin(); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + distDir: process.env.NEXT_BUILD_DIR || '.next', +}; + +module.exports = withNextIntl(nextConfig); diff --git a/testapps/next-app-intl/package.json b/testapps/next-app-intl/package.json new file mode 100644 index 0000000000..6743bd4994 --- /dev/null +++ b/testapps/next-app-intl/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tolgee/next-app-intl-testapp", + "version": "5.29.5", + "private": true, + "scripts": { + "dev": "next dev", + "develop": "next dev", + "build": "next build", + "build:e2e": "NEXT_BUILD_DIR=dist-e2e next build", + "start": "next start", + "clean": "rm -rf .next dist-e2e" + }, + "dependencies": { + "@tolgee/format-icu": "5.29.5", + "@tolgee/react": "5.29.5", + "@tolgee/web": "5.29.5", + "next": "14.2.13", + "next-intl": "3.19.4", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", + "typescript": "^5.3.2" + } +} diff --git a/testapps/next-app-intl/public/favicon.ico b/testapps/next-app-intl/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4ddd8fff7086c240ed21438ca7d96f1b8dae0bc0 GIT binary patch literal 15086 zcmdU$O=uNY6vrnYO%#n9m0G$<)C8)FRuGb41&a%z2%-zOrYkKQ7LVcV8tZ8{r_I(J}2Xwd3WZ$*OwePxifd}IsgB? z@7{AiX5x7@UY)mPi>I{JyV~w~9iHd4w)*$0Jnt9fty0SKPwPGJD;0Q8Wn|)gA|*d3 zZJyV>ZcgVi%JR2KUOJ_pNiRu%l#WaPk^bu|*W~exGL*#z+iFOiKxv{cq~A-QNoSnvZWVz$%y2 z$kB(=jC597D#v2>V1NZC*rkY~fpzbN^>mc9}aM-le$W`3Rbh-!*zlSeaS_(t57xidzf?h9!_wJ6XMpeG;F(2?%@^} zTZz5QCkusdm&t8;0X=2;v z=4NyA=FO7%Pfbmky?gg2#`ihHeJl-sYxshdXBjsq7mY z8_mUw7gO^e8yhp7ot>$$ea>(%X}z;`>sB*5I-05ur%#_Y#Iv;R=W{QKf7*DOB3>4% zC)`u_AKPw8<7vuxS%^Q}Q;%QEpEOB4Eg@cp>j3vu@iWMsYu9*MV!RCVhdXmXI6u|B zOFS(xUWR$WJ@NV>s=meIX&K^WkiT6&bc&Dj`YnpP&5w(xWr&wS{&xM=By8vPv*j6; zM#a-I#mgXnIKv%r-oL19&_}`Ra6BzzybR*m{R{jzNz1x_B%V%BPv_?4GR8~GAFgn= z!d{p?v3*{0N<1AJ8Zs|lyeMUT8O4LM2F@IU!kI)_e}g@H_L#xJL9=w}(kLEj^5Mu? zPa!Gm`4#bW_3G7WI7ZpTonKL(#c+POu291V^0w=Gv7X=BY8=qTLOVb2w&WaA-W4pg z_ZPfVNg|8+{gJIFb*3*>V?2NfcG7m2$vk0h`@i3x4*QtFGv;}uQrr+*nI9_oTi_dv zxOXgioLZQ(?e_y}6iZ{#ZwxL^&=9>|`hvL_vv zayCHQqFj^5GrQ-*#;)IWnNEy;<($6sycVvrp4Uj4(FrcKN zvwpkUcIKIsPTVT}NSgmnYgOv+w20N4r9IMbr9Vq2rL(?rO&-rELs@J}6~-g|aZs9- zPD^QmIHgQhiVe2-NF(pcs9(zWSaEo9&#=V@zLH3X3Xe$n+2j1XJi`aR@aZBuRp=+F zi|-_5@QLp@fnz1GEl+$$wFAG=4^msUQYjeW{!ptt`bniQ!+ya6(@K{v%63*JUtzrP zJy>8OjL&bR;WD}Vy1F`(R_p8QS z3)aiW1e0>+L40 z-*@!(_U7itD4r4d!TA^bRO^q1h6b~2*|K1rT(A3mOt7s~bNyvGRCE1lIqcL$qL`%e zF71v_e7gwkz@DKR`wz>H{y;x@CUvP_p)%OwBfQ>LmXi=0*h5xh{~07$vHiE{t1C-pXV!RjLzkh$YPT0;{WAHuhnKNhJu)lWgTA}x{_V=CVx2>(su;#M@ zA9w|M`q$Xc<8RBbW@GJ!#C#`=ZNB}iEh&4Pk&zMe=+Pt7-riman>=juzhBUvYv%ET z2M + + + + + + \ No newline at end of file diff --git a/testapps/next-app-intl/public/img/background.svg b/testapps/next-app-intl/public/img/background.svg new file mode 100644 index 0000000000..bb305cae2c --- /dev/null +++ b/testapps/next-app-intl/public/img/background.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/testapps/next-app-intl/public/img/iconAdd.svg b/testapps/next-app-intl/public/img/iconAdd.svg new file mode 100644 index 0000000000..e33b444b6a --- /dev/null +++ b/testapps/next-app-intl/public/img/iconAdd.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testapps/next-app-intl/public/img/iconMail.svg b/testapps/next-app-intl/public/img/iconMail.svg new file mode 100644 index 0000000000..145f498464 --- /dev/null +++ b/testapps/next-app-intl/public/img/iconMail.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/testapps/next-app-intl/public/img/iconShare.svg b/testapps/next-app-intl/public/img/iconShare.svg new file mode 100644 index 0000000000..936a052814 --- /dev/null +++ b/testapps/next-app-intl/public/img/iconShare.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/testapps/next-app/src/app/[locale]/Todos.tsx b/testapps/next-app-intl/src/app/[locale]/Todos.tsx similarity index 100% rename from testapps/next-app/src/app/[locale]/Todos.tsx rename to testapps/next-app-intl/src/app/[locale]/Todos.tsx diff --git a/testapps/next-app/src/app/[locale]/[...rest]/page.tsx b/testapps/next-app-intl/src/app/[locale]/[...rest]/page.tsx similarity index 100% rename from testapps/next-app/src/app/[locale]/[...rest]/page.tsx rename to testapps/next-app-intl/src/app/[locale]/[...rest]/page.tsx diff --git a/testapps/next-app/src/app/[locale]/layout.tsx b/testapps/next-app-intl/src/app/[locale]/layout.tsx similarity index 63% rename from testapps/next-app/src/app/[locale]/layout.tsx rename to testapps/next-app-intl/src/app/[locale]/layout.tsx index a7902765b5..f1b76da923 100644 --- a/testapps/next-app/src/app/[locale]/layout.tsx +++ b/testapps/next-app-intl/src/app/[locale]/layout.tsx @@ -1,7 +1,7 @@ import { notFound } from 'next/navigation'; import { ReactNode } from 'react'; import { TolgeeNextProvider } from '@/tolgee/client'; -import { ALL_LOCALES, getStaticData } from '@/tolgee/shared'; +import { ALL_LANGUAGES, getStaticData } from '@/tolgee/shared'; type Props = { children: ReactNode; @@ -12,18 +12,18 @@ export default async function LocaleLayout({ children, params: { locale }, }: Props) { - if (!ALL_LOCALES.includes(locale)) { + if (!ALL_LANGUAGES.includes(locale)) { notFound(); } // it's important you provide all data which are needed for initial render - // so current locale and also fallback locales + necessary namespaces - const locales = await getStaticData([locale, 'en']); + // so current language and also fallback languages + necessary namespaces + const staticData = await getStaticData([locale, 'en']); return ( - + {children} diff --git a/testapps/next-app/src/app/[locale]/page.tsx b/testapps/next-app-intl/src/app/[locale]/page.tsx similarity index 100% rename from testapps/next-app/src/app/[locale]/page.tsx rename to testapps/next-app-intl/src/app/[locale]/page.tsx diff --git a/testapps/next-app/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx similarity index 100% rename from testapps/next-app/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx rename to testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsClient.tsx diff --git a/testapps/next-app/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx b/testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx similarity index 100% rename from testapps/next-app/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx rename to testapps/next-app-intl/src/app/[locale]/translation-methods/TranslationMethodsServer.tsx diff --git a/testapps/next-app-intl/src/app/[locale]/translation-methods/page.tsx b/testapps/next-app-intl/src/app/[locale]/translation-methods/page.tsx new file mode 100644 index 0000000000..d89ddb0894 --- /dev/null +++ b/testapps/next-app-intl/src/app/[locale]/translation-methods/page.tsx @@ -0,0 +1,19 @@ +import { Link } from '@/navigation'; +import { Navbar } from '@/components/Navbar'; + +import { TranslationMethodsServer } from './TranslationMethodsServer'; +import { TranslationMethodsClient } from './TranslationMethodsClient'; + +export default async function AboutPage() { + return ( +
+ +
+ The example app +
+
+ + +
+ ); +} diff --git a/testapps/next-app-intl/src/app/layout.tsx b/testapps/next-app-intl/src/app/layout.tsx new file mode 100644 index 0000000000..afa7e0cfb2 --- /dev/null +++ b/testapps/next-app-intl/src/app/layout.tsx @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; +import './style.css'; + +type Props = { + children: ReactNode; +}; + +// Since we have a `not-found.tsx` page on the root, a layout file +// is required, even if it's just passing children through. +export default function RootLayout({ children }: Props) { + return children; +} diff --git a/testapps/next-app-intl/src/app/not-found.tsx b/testapps/next-app-intl/src/app/not-found.tsx new file mode 100644 index 0000000000..ed4705cb1f --- /dev/null +++ b/testapps/next-app-intl/src/app/not-found.tsx @@ -0,0 +1,17 @@ +'use client'; + +import Error from 'next/error'; + +// Render the default Next.js 404 page when a route +// is requested that doesn't match the middleware and +// therefore doesn't have a locale associated with it. + +export default function NotFound() { + return ( + + + + + + ); +} diff --git a/testapps/next-app-intl/src/app/page.tsx b/testapps/next-app-intl/src/app/page.tsx new file mode 100644 index 0000000000..d82c1dae43 --- /dev/null +++ b/testapps/next-app-intl/src/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation'; + +// This page only renders when the app is built statically (output: 'export') +export default function RootPage() { + redirect('/en'); +} diff --git a/testapps/next-app-intl/src/app/style.css b/testapps/next-app-intl/src/app/style.css new file mode 100644 index 0000000000..3e271a3f94 --- /dev/null +++ b/testapps/next-app-intl/src/app/style.css @@ -0,0 +1,250 @@ +@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400'); + +:root * { + box-sizing: border-box; +} + +:root *:focus { + outline-offset: 3px; + outline-color: rgb(84, 84, 84); +} + +:root { + font-family: 'Ubuntu', sans-serif; + font-weight: 400; + font-size: 16px; +} + +:root::before { + content: ''; + position: fixed; + inset: 0; + background-image: url('/img/background.svg'); + background-size: 100%; + background-repeat: no-repeat; + background-position-y: bottom; + background-position-x: center; + background-color: #4079ec; +} + +body { + margin: 0; + padding: 0; + position: relative; + min-height: 100vh; + padding-bottom: 40px; + color: white; +} + +.example { + max-width: 800px; + margin: 0 auto; + padding: 0 20px; + height: 100%; +} + +.button { + color: white; + cursor: pointer; + border: 0; + padding: 16px 20px; + background-color: #1f2d40; + border-radius: 4px; + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; + text-transform: uppercase; + transition: background-color; +} + +button:hover { + background-color: #344762; +} + +button[disabled] { + background-color: rgba(117, 117, 117, 0.87); + cursor: default; +} + +button img { + user-drag: none; + -webkit-user-drag: none; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.navbar { + display: flex; + justify-content: space-between; +} + +.navbar a { + text-decoration: none; + color: white; + border: 1px solid rgba(255, 255, 255, 0.51); + border-radius: 4px; + display: flex; + align-items: center; + padding: 8px 12px; + margin: 16px 0px; + font-size: 16px; +} + +.navbar .lang-selector { + position: relative; + text-decoration: none; + color: white; + border: 1px solid rgba(255, 255, 255, 0.51); + border-radius: 4px; + display: flex; + align-items: center; + padding: 8px 18px 8px 12px; + margin: 16px 0px; + font-size: 16px; + background: transparent; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + width: 110px; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position-x: calc(100% - 8px); + background-position-y: 9px; +} + +header { + display: grid; + margin-bottom: 20px; + margin-top: 8px; + color: white; + justify-items: center; +} + +header h1 { + padding: 0; + margin: 0; + font-size: 48px; + font-weight: 300; + font-style: normal; +} + +.items { + min-height: 500px; + background-color: #ffffffcc; + backdrop-filter: blur(8px); + border-radius: 10px; + display: flex; + flex-direction: column; + height: 100%; +} + +.items__new-item { + display: flex; + padding: 32px; + gap: 20px; +} + +.items__new-item input { + flex-grow: 1; + background-color: white; + border: none; + box-sizing: border-box; + border-radius: 6px; + padding: 8px 16px; + font-size: 18px; +} + +.items__list { + margin: 32px; + flex-grow: 1; +} + +.item { + display: flex; + padding: 10px 0px; + margin: 0px 10px; + color: black; + border-bottom: 1px solid white; +} + +.item__text { + font-size: 18px; + flex-grow: 1; +} + +.item button { + font-size: 15px; + background: none; + border: none; + cursor: pointer; + text-transform: uppercase; + font-weight: 300; +} + +.items__buttons { + align-self: flex-end; + margin: 32px; + display: flex; + gap: 20px; +} + +.translation-methods { + max-width: 1000px; + margin: 0 auto; + padding: 0 20px; + height: 100%; +} + +.translation-methods .tiles { + margin: 50px auto 0; + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + color: black; +} + +.translation-methods .tiles > div { + background-color: #ffffffcc; + backdrop-filter: blur(8px); + display: grid; + grid-template-rows: auto 1fr; + align-items: center; + padding: 20px; + border-radius: 5px; + min-height: 120px; + gap: 10px; +} + +.translation-methods .tiles > div > div { + text-align: center; +} + +.translation-methods h1 { + padding: 0; + margin: 0; + color: #1f385b; + font-size: 19px; + font-weight: 500; +} + +.load-more-section { + display: flex; + margin-top: 20px; + justify-content: center; +} + +.translation-methods .section-title { + position: relative; + top: 30px; + color: white; +} + +.translation-methods .section-title { + position: relative; + top: 30px; + color: white; +} diff --git a/testapps/next-app-intl/src/components/LangSelector.tsx b/testapps/next-app-intl/src/components/LangSelector.tsx new file mode 100644 index 0000000000..695df6c643 --- /dev/null +++ b/testapps/next-app-intl/src/components/LangSelector.tsx @@ -0,0 +1,32 @@ +'use client'; + +import React, { ChangeEvent, useTransition } from 'react'; +import { useTolgee } from '@tolgee/react'; +import { usePathname, useRouter } from '@/navigation'; + +export const LangSelector: React.FC = () => { + const tolgee = useTolgee(['language']); + const language = tolgee.getLanguage(); + const router = useRouter(); + const pathname = usePathname(); + const [_, startTransition] = useTransition(); + + function onSelectChange(event: ChangeEvent) { + const nextLanguage = event.target.value; + startTransition(() => { + router.replace(pathname, { locale: nextLanguage }); + }); + } + return ( + + ); +}; diff --git a/testapps/next-app-intl/src/components/Navbar.tsx b/testapps/next-app-intl/src/components/Navbar.tsx new file mode 100644 index 0000000000..c90ad50d64 --- /dev/null +++ b/testapps/next-app-intl/src/components/Navbar.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { LangSelector } from './LangSelector'; + +export const Navbar = ({ children }: React.PropsWithChildren) => { + return ( +
+ {children} + +
+ ); +}; diff --git a/testapps/next-app/src/i18n/request.ts b/testapps/next-app-intl/src/i18n/request.ts similarity index 100% rename from testapps/next-app/src/i18n/request.ts rename to testapps/next-app-intl/src/i18n/request.ts diff --git a/testapps/next-app/src/middleware.ts b/testapps/next-app-intl/src/middleware.ts similarity index 74% rename from testapps/next-app/src/middleware.ts rename to testapps/next-app-intl/src/middleware.ts index 06efe16a7f..4ae29969ec 100644 --- a/testapps/next-app/src/middleware.ts +++ b/testapps/next-app-intl/src/middleware.ts @@ -1,11 +1,11 @@ import createMiddleware from 'next-intl/middleware'; -import { ALL_LOCALES, DEFAULT_LOCALE } from '@/tolgee/shared'; +import { ALL_LANGUAGES, DEFAULT_LANGUAGE } from '@/tolgee/shared'; // read more about next-intl middleware configuration // https://next-intl-docs.vercel.app/docs/routing/middleware#locale-prefix export default createMiddleware({ - locales: ALL_LOCALES, - defaultLocale: DEFAULT_LOCALE, + locales: ALL_LANGUAGES, + defaultLocale: DEFAULT_LANGUAGE, localePrefix: 'as-needed', }); diff --git a/testapps/next-app/src/navigation.ts b/testapps/next-app-intl/src/navigation.ts similarity index 64% rename from testapps/next-app/src/navigation.ts rename to testapps/next-app-intl/src/navigation.ts index 4a81932c87..8af9279326 100644 --- a/testapps/next-app/src/navigation.ts +++ b/testapps/next-app-intl/src/navigation.ts @@ -1,7 +1,7 @@ import { createSharedPathnamesNavigation } from 'next-intl/navigation'; -import { ALL_LOCALES } from './tolgee/shared'; +import { ALL_LANGUAGES } from './tolgee/shared'; // read more about next-intl library // https://next-intl-docs.vercel.app export const { Link, redirect, usePathname, useRouter } = - createSharedPathnamesNavigation({ locales: ALL_LOCALES }); + createSharedPathnamesNavigation({ locales: ALL_LANGUAGES }); diff --git a/testapps/next-app-intl/src/tolgee/client.tsx b/testapps/next-app-intl/src/tolgee/client.tsx new file mode 100644 index 0000000000..d2776d8996 --- /dev/null +++ b/testapps/next-app-intl/src/tolgee/client.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect } from 'react'; +import { TolgeeProvider, TolgeeStaticData } from '@tolgee/react'; +import { useRouter } from 'next/navigation'; +import { TolgeeBase } from './shared'; + +type Props = { + staticData: TolgeeStaticData; + language: string; + children: React.ReactNode; +}; + +const tolgee = TolgeeBase().init(); + +export const TolgeeNextProvider = ({ + language, + staticData, + children, +}: Props) => { + const router = useRouter(); + + useEffect(() => { + const { unsubscribe } = tolgee.on('permanentChange', () => { + router.refresh(); + }); + return () => unsubscribe(); + }, [tolgee, router]); + + return ( + + {children} + + ); +}; diff --git a/testapps/next-app-intl/src/tolgee/server.tsx b/testapps/next-app-intl/src/tolgee/server.tsx new file mode 100644 index 0000000000..98e69cab02 --- /dev/null +++ b/testapps/next-app-intl/src/tolgee/server.tsx @@ -0,0 +1,20 @@ +import { getLocale } from 'next-intl/server'; + +import { TolgeeBase, ALL_LANGUAGES, getStaticData } from './shared'; +import { createServerInstance } from '@tolgee/react/server'; + +export const { getTolgee, getTranslate, T } = createServerInstance({ + getLocale: getLocale, + createTolgee: async (language) => + TolgeeBase().init({ + // including all languages + // on server we are not concerned about bundle size + staticData: await getStaticData(ALL_LANGUAGES), + observerOptions: { + fullKeyEncode: true, + }, + language, + fetch: async (input, init) => + fetch(input, { ...init, next: { revalidate: 0 } }), + }), +}); diff --git a/testapps/next-app-intl/src/tolgee/shared.ts b/testapps/next-app-intl/src/tolgee/shared.ts new file mode 100644 index 0000000000..2e57971c8c --- /dev/null +++ b/testapps/next-app-intl/src/tolgee/shared.ts @@ -0,0 +1,36 @@ +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_LANGUAGES = ['en', 'cs', 'de', 'fr']; + +export const DEFAULT_LANGUAGE = 'en'; + +export async function getStaticData( + languages: string[], + namespaces: string[] = [''] +) { + const result: Record = {}; + for (const lang of languages) { + for (const namespace of namespaces) { + if (namespace) { + result[`${lang}:${namespace}`] = ( + await import(`../../messages/${namespace}/${lang}.json`) + ).default; + } else { + result[lang] = (await import(`../../messages/${lang}.json`)).default; + } + } + } + return result; +} + +export function TolgeeBase() { + return Tolgee().use(FormatIcu()).use(DevTools()).updateDefaults({ + apiKey, + apiUrl, + fallbackLanguage: 'en', + }); +} diff --git a/testapps/next-app-intl/tsconfig.json b/testapps/next-app-intl/tsconfig.json new file mode 100644 index 0000000000..ba91618e36 --- /dev/null +++ b/testapps/next-app-intl/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + }, + "resolveJsonModule": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "dist-e2e/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/testapps/next-app/README.md b/testapps/next-app/README.md index bd36c58988..e5c3ff032e 100644 --- a/testapps/next-app/README.md +++ b/testapps/next-app/README.md @@ -1,8 +1,9 @@ +> This repo is just a dummy. Submit issues [in monorepo](https://github.com/tolgee/tolgee-js) or [check the source code here](https://github.com/tolgee/tolgee-js/tree/main/testapps/next-app). # Tolgee with next.js 14 app router DEMO This repo demonstrates how to use `tolgee` with new `next.js` app router. -An example based on `next14` app folder with `tolgee` and `next-intl` package. +An example based on `next14` app folder with `tolgee` package and cookie-based localization. ## Setup diff --git a/testapps/next-app/next.config.js b/testapps/next-app/next.config.js index 385ea474f6..52ed1efedc 100644 --- a/testapps/next-app/next.config.js +++ b/testapps/next-app/next.config.js @@ -1,10 +1,6 @@ -const createNextIntlPlugin = require('next-intl/plugin'); - -const withNextIntl = createNextIntlPlugin(); - /** @type {import('next').NextConfig} */ const nextConfig = { distDir: process.env.NEXT_BUILD_DIR || '.next', }; -module.exports = withNextIntl(nextConfig); +module.exports = nextConfig; diff --git a/testapps/next-app/package.json b/testapps/next-app/package.json index c6f2af0c77..50d59b4a4b 100644 --- a/testapps/next-app/package.json +++ b/testapps/next-app/package.json @@ -15,7 +15,6 @@ "@tolgee/react": "5.29.5", "@tolgee/web": "5.29.5", "next": "14.2.13", - "next-intl": "3.19.4", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/testapps/next-app/src/app/Todos.tsx b/testapps/next-app/src/app/Todos.tsx new file mode 100644 index 0000000000..fc8cb20ae0 --- /dev/null +++ b/testapps/next-app/src/app/Todos.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { FormEvent, useEffect, useState } from 'react'; +import { T, useTranslate } from '@tolgee/react'; + +const getInitialItems = () => { + const items: string[] = + typeof window !== undefined + ? JSON.parse(localStorage.getItem('tolgee-example-app-items') || '[]') + : []; + + return items?.length + ? items + : ['Passport', 'Maps and directions', 'Travel guide']; +}; + +export const Todos = () => { + const { t } = useTranslate(); + + const [newItemValue, setNewItemValue] = useState(''); + const [items, setItems] = useState([]); + + useEffect(() => { + setItems(getInitialItems()); + }, []); + + const updateLocalstorage = (items: string[]) => { + localStorage.setItem('tolgee-example-app-items', JSON.stringify(items)); + }; + + const onAdd = (e: FormEvent) => { + e.preventDefault(); + const newItems = [...items, newItemValue]; + setItems(newItems); + updateLocalstorage(newItems); + setNewItemValue(''); + }; + + const onDelete = (index: number) => () => { + const newItems = items.filter((_, i) => i !== index); + setItems(newItems); + updateLocalstorage(newItems); + }; + + const onAction = (action: string) => () => { + alert('action: ' + action); + }; + + return ( +
+
+ setNewItemValue(e.target.value)} + placeholder={t('add-item-input-placeholder')} + /> + +
+
+ {items.map((item, i) => ( +
+
{item}
+ +
+ ))} +
+
+ + +
+
+ ); +}; diff --git a/testapps/next-app/src/app/[locale]/translation-methods/page.tsx b/testapps/next-app/src/app/[locale]/translation-methods/page.tsx deleted file mode 100644 index 7a2eadc36c..0000000000 --- a/testapps/next-app/src/app/[locale]/translation-methods/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { getLocale } from 'next-intl/server'; -import { Link } from '@/navigation'; -import { Navbar } from '@/components/Navbar'; -import { getStaticData } from '@/tolgee/shared'; -import { TolgeeNextProvider } from '@/tolgee/client'; - -import { TranslationMethodsServer } from './TranslationMethodsServer'; -import { TranslationMethodsClient } from './TranslationMethodsClient'; - -export default async function AboutPage() { - const locale = await getLocale(); - const locales = await getStaticData(['en', locale]); - - return ( - -
- -
- The example app -
-
- - -
-
- ); -} diff --git a/testapps/next-app/src/app/layout.tsx b/testapps/next-app/src/app/layout.tsx index afa7e0cfb2..de4f141297 100644 --- a/testapps/next-app/src/app/layout.tsx +++ b/testapps/next-app/src/app/layout.tsx @@ -1,12 +1,27 @@ import { ReactNode } from 'react'; +import { TolgeeNextProvider } from '@/tolgee/client'; +import { getStaticData } from '@/tolgee/shared'; +import { getLanguage } from '@/tolgee/language'; import './style.css'; type Props = { children: ReactNode; + params: { locale: string }; }; -// Since we have a `not-found.tsx` page on the root, a layout file -// is required, even if it's just passing children through. -export default function RootLayout({ children }: Props) { - return children; +export default async function LocaleLayout({ children }: Props) { + const locale = await getLanguage(); + // it's important you provide all data which are needed for initial render + // so current language and also fallback languages + necessary namespaces + const staticData = await getStaticData([locale, 'en']); + + return ( + + + + {children} + + + + ); } diff --git a/testapps/next-app/src/app/page.tsx b/testapps/next-app/src/app/page.tsx index d82c1dae43..3bf42204fd 100644 --- a/testapps/next-app/src/app/page.tsx +++ b/testapps/next-app/src/app/page.tsx @@ -1,6 +1,25 @@ -import { redirect } from 'next/navigation'; +import { getTranslate } from '@/tolgee/server'; -// This page only renders when the app is built statically (output: 'export') -export default function RootPage() { - redirect('/en'); +import { Navbar } from '@/components/Navbar'; +import Link from 'next/link'; +import { Todos } from './Todos'; + +export default async function IndexPage() { + const t = await getTranslate(); + return ( +
+
+ + + {t('menu-item-translation-methods')} + + +
+ +

{t('app-title')}

+
+ +
+
+ ); } diff --git a/testapps/next-app/src/app/translation-methods/TranslationMethodsClient.tsx b/testapps/next-app/src/app/translation-methods/TranslationMethodsClient.tsx new file mode 100644 index 0000000000..79765ae1e0 --- /dev/null +++ b/testapps/next-app/src/app/translation-methods/TranslationMethodsClient.tsx @@ -0,0 +1,84 @@ +'use client'; +import { T, useTranslate } from '@tolgee/react'; + +export const TranslationMethodsClient = () => { + const { t } = useTranslate(); + + return ( + <> +

Client

+
+
+

T component with params

+
+ +
+
+ +
+

T component with noWrap

+
+ +
+
+ +
+

T component with interpolation

+
+ + , + i: , + key: 'value', + }} + > + Hey + + +
+
+ +
+

t function with params

+
+ {t('this_is_a_key_with_params', { key: 'value', key2: 'value2' })} +
+
+ +
+

t function with noWrap

+
{t('this_is_a_key', { noWrap: true })}
+
+ +
+

Translation in translation

+
+
+ + {t('translation_inner', 'Translation')} + + ), + }} + > + {'Translation in translation'} + +
+
+
+
+ + ); +}; diff --git a/testapps/next-app/src/app/translation-methods/TranslationMethodsServer.tsx b/testapps/next-app/src/app/translation-methods/TranslationMethodsServer.tsx new file mode 100644 index 0000000000..5f04566ec9 --- /dev/null +++ b/testapps/next-app/src/app/translation-methods/TranslationMethodsServer.tsx @@ -0,0 +1,83 @@ +import { T, getTranslate } from '@/tolgee/server'; + +export const TranslationMethodsServer = async () => { + const t = await getTranslate(); + + return ( + <> +

Server

+
+
+

T component with params

+
+ +
+
+ +
+

T component with noWrap

+
+ +
+
+ +
+

T component with interpolation

+
+ + , + i: , + key: 'value', + }} + > + Hey + + +
+
+ +
+

t function with params

+
+ {t('this_is_a_key_with_params', { key: 'value', key2: 'value2' })} +
+
+ +
+

t function with noWrap

+
{t('this_is_a_key', { noWrap: true })}
+
+ +
+

Translation in translation

+
+
+ + {t('translation_inner', 'Translation')} + + ), + }} + > + {'Translation in translation'} + +
+
+
+
+ + ); +}; diff --git a/testapps/next-app/src/app/translation-methods/page.tsx b/testapps/next-app/src/app/translation-methods/page.tsx new file mode 100644 index 0000000000..942e7d1b4c --- /dev/null +++ b/testapps/next-app/src/app/translation-methods/page.tsx @@ -0,0 +1,19 @@ +import { Navbar } from '@/components/Navbar'; + +import { TranslationMethodsServer } from './TranslationMethodsServer'; +import { TranslationMethodsClient } from './TranslationMethodsClient'; +import Link from 'next/link'; + +export default async function AboutPage() { + return ( +
+ +
+ The example app +
+
+ + +
+ ); +} diff --git a/testapps/next-app/src/components/LangSelector.tsx b/testapps/next-app/src/components/LangSelector.tsx index 46b6586150..7e2c02c2d0 100644 --- a/testapps/next-app/src/components/LangSelector.tsx +++ b/testapps/next-app/src/components/LangSelector.tsx @@ -1,21 +1,16 @@ 'use client'; -import React, { ChangeEvent, useTransition } from 'react'; +import React, { ChangeEvent } from 'react'; import { useTolgee } from '@tolgee/react'; -import { usePathname, useRouter } from '@/navigation'; +import { setLanguage } from '@/tolgee/language'; export const LangSelector: React.FC = () => { const tolgee = useTolgee(['language']); const locale = tolgee.getLanguage(); - const router = useRouter(); - const pathname = usePathname(); - const [_, startTransition] = useTransition(); function onSelectChange(event: ChangeEvent) { const nextLocale = event.target.value; - startTransition(() => { - router.replace(pathname, { locale: nextLocale }); - }); + setLanguage(nextLocale); } return (