@prezly/theme-kit-nextjs 3.2.5-test.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/build/components-nextjs/PageSeo/PageSeo.js +9 -14
  2. package/build/components-nextjs/PageSeo/PageSeo.js.map +1 -1
  3. package/build/components-nextjs/document.js.map +1 -1
  4. package/build/intl/getFallbackLocale.d.ts +10 -0
  5. package/build/intl/getFallbackLocale.js +24 -0
  6. package/build/intl/getFallbackLocale.js.map +1 -0
  7. package/build/intl/index.d.ts +2 -1
  8. package/build/intl/index.js +2 -1
  9. package/build/intl/index.js.map +1 -1
  10. package/build/intl/locale.d.ts +3 -0
  11. package/build/intl/locale.js.map +1 -1
  12. package/build/intl/localeObject.d.ts +5 -0
  13. package/build/intl/localeObject.js +7 -0
  14. package/build/intl/localeObject.js.map +1 -1
  15. package/build/newsroom-context/index.d.ts +1 -1
  16. package/build/newsroom-context/index.js.map +1 -1
  17. package/build/seo/getAlternateLanguageLinks.d.ts +4 -0
  18. package/build/seo/getAlternateLanguageLinks.js +84 -0
  19. package/build/seo/getAlternateLanguageLinks.js.map +1 -0
  20. package/build/seo/index.d.ts +1 -0
  21. package/build/seo/index.js +18 -0
  22. package/build/seo/index.js.map +1 -0
  23. package/build/seo/utils/bindLanguagesWithLocales.d.ts +11 -0
  24. package/build/seo/utils/bindLanguagesWithLocales.js +23 -0
  25. package/build/seo/utils/bindLanguagesWithLocales.js.map +1 -0
  26. package/build/seo/utils/createAlternateLanguageLink.d.ts +5 -0
  27. package/build/seo/utils/createAlternateLanguageLink.js +15 -0
  28. package/build/seo/utils/createAlternateLanguageLink.js.map +1 -0
  29. package/build/tsconfig.tsbuildinfo +1 -0
  30. package/package.json +24 -7
  31. package/src/components-nextjs/PageSeo/PageSeo.tsx +16 -24
  32. package/src/components-nextjs/document.tsx +0 -1
  33. package/src/intl/getFallbackLocale.ts +29 -0
  34. package/src/intl/index.ts +2 -1
  35. package/src/intl/locale.ts +5 -1
  36. package/src/intl/localeObject.ts +8 -0
  37. package/src/newsroom-context/index.ts +1 -1
  38. package/src/seo/getAlternateLanguageLinks.test.ts +164 -0
  39. package/src/seo/getAlternateLanguageLinks.ts +115 -0
  40. package/src/seo/index.ts +1 -0
  41. package/src/seo/utils/bindLanguagesWithLocales.ts +31 -0
  42. package/src/seo/utils/createAlternateLanguageLink.ts +17 -0
@@ -0,0 +1,164 @@
1
+ import { NewsroomLanguageSettings } from '@prezly/sdk';
2
+ import { LocaleCode, LocaleObject } from '../intl';
3
+ import { getAlternateLanguageLinks } from './getAlternateLanguageLinks';
4
+
5
+ function lang(code: LocaleCode, isDefault = false) {
6
+ return { code, is_default: isDefault } as NewsroomLanguageSettings;
7
+ }
8
+
9
+ function getTranslationUrl(locale: LocaleObject) {
10
+ return `translationUrl/${locale.toHyphenCode()}`;
11
+ }
12
+
13
+ function generateTranslationUrl(locale: LocaleObject) {
14
+ const translationUrl = getTranslationUrl(locale);
15
+
16
+ return `http://localhost:3000/${locale.toNeutralLanguageCode()}/${translationUrl}`;
17
+ }
18
+
19
+ describe('getAlternateLanguageLinks', () => {
20
+ it('should handle one en-GB non-default language', () => {
21
+ const links = getAlternateLanguageLinks([lang('en-GB')], generateTranslationUrl);
22
+
23
+ expect(links).toMatchObject([
24
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en' },
25
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
26
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'x-default' },
27
+ ]);
28
+ });
29
+
30
+ it('should handle one en-GB default language', () => {
31
+ const links = getAlternateLanguageLinks([lang('en-GB', true)], generateTranslationUrl);
32
+
33
+ expect(links).toMatchObject([
34
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en' },
35
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
36
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'x-default' },
37
+ ]);
38
+ });
39
+
40
+ it('should handle two full locales', () => {
41
+ const links = getAlternateLanguageLinks(
42
+ [lang('en-GB', true), lang('fr-FR')],
43
+ generateTranslationUrl,
44
+ );
45
+
46
+ expect(links).toMatchObject([
47
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en' },
48
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
49
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr' },
50
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
51
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'x-default' },
52
+ ]);
53
+ });
54
+
55
+ it('should use en-GB locale as x-default even if other locale is set as default', () => {
56
+ const links = getAlternateLanguageLinks(
57
+ [lang('en-GB'), lang('fr-FR', true)],
58
+ generateTranslationUrl,
59
+ );
60
+
61
+ expect(links).toMatchObject([
62
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en' },
63
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
64
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr' },
65
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
66
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'x-default' },
67
+ ]);
68
+ });
69
+
70
+ it('should use default locale as x-default when there is eng locale', () => {
71
+ const links = getAlternateLanguageLinks(
72
+ [lang('ru-RU'), lang('fr-FR', true)],
73
+ generateTranslationUrl,
74
+ );
75
+
76
+ expect(links).toMatchObject([
77
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr' },
78
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
79
+ { href: 'http://localhost:3000/ru/translationUrl/ru-RU', hrefLang: 'ru' },
80
+ { href: 'http://localhost:3000/ru/translationUrl/ru-RU', hrefLang: 'ru-RU' },
81
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'x-default' },
82
+ ]);
83
+ });
84
+
85
+ it('should include provided region independent locale', () => {
86
+ const links = getAlternateLanguageLinks(
87
+ [lang('en-US'), lang('fr'), lang('fr-FR', true)],
88
+ generateTranslationUrl,
89
+ );
90
+
91
+ expect(links).toMatchObject([
92
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en' },
93
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en-US' },
94
+ { href: 'http://localhost:3000/fr/translationUrl/fr', hrefLang: 'fr' },
95
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
96
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'x-default' },
97
+ ]);
98
+ });
99
+
100
+ it('should use provided en region independent locale as x-default', () => {
101
+ const links = getAlternateLanguageLinks(
102
+ [lang('en'), lang('en-US'), lang('fr'), lang('fr-FR')],
103
+ generateTranslationUrl,
104
+ );
105
+
106
+ expect(links).toMatchObject([
107
+ { href: 'http://localhost:3000/en/translationUrl/en', hrefLang: 'en' },
108
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en-US' },
109
+ { href: 'http://localhost:3000/fr/translationUrl/fr', hrefLang: 'fr' },
110
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
111
+ { href: 'http://localhost:3000/en/translationUrl/en', hrefLang: 'x-default' },
112
+ ]);
113
+ });
114
+
115
+ it('should fallback en link to en-US locale when there is en-GB locale', () => {
116
+ const links = getAlternateLanguageLinks(
117
+ [lang('en-GB'), lang('en-US')],
118
+ generateTranslationUrl,
119
+ );
120
+
121
+ expect(links).toMatchObject([
122
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en' },
123
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
124
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en-US' },
125
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'x-default' },
126
+ ]);
127
+ });
128
+
129
+ it('should should omit locales if getTranslationUrl is undefined', () => {
130
+ const links = getAlternateLanguageLinks(
131
+ [lang('en-US'), lang('fr'), lang('fr-FR', true)],
132
+ (locale) => {
133
+ const translationUrl =
134
+ locale.toHyphenCode() === 'en-US' ? undefined : getTranslationUrl(locale);
135
+
136
+ if (translationUrl) {
137
+ return generateTranslationUrl(locale);
138
+ }
139
+
140
+ return undefined;
141
+ },
142
+ );
143
+
144
+ expect(links).toMatchObject([
145
+ { href: 'http://localhost:3000/fr/translationUrl/fr', hrefLang: 'fr' },
146
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'fr-FR' },
147
+ { href: 'http://localhost:3000/fr/translationUrl/fr-FR', hrefLang: 'x-default' },
148
+ ]);
149
+ });
150
+
151
+ it('should should not duplicate links when equal languages passed', () => {
152
+ const links = getAlternateLanguageLinks(
153
+ [lang('en-GB'), lang('en-GB'), lang('en-US'), lang('en-US')],
154
+ generateTranslationUrl,
155
+ );
156
+
157
+ expect(links).toMatchObject([
158
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en' },
159
+ { href: 'http://localhost:3000/en/translationUrl/en-GB', hrefLang: 'en-GB' },
160
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'en-US' },
161
+ { href: 'http://localhost:3000/en/translationUrl/en-US', hrefLang: 'x-default' },
162
+ ]);
163
+ });
164
+ });
@@ -0,0 +1,115 @@
1
+ import type { NewsroomLanguageSettings } from '@prezly/sdk';
2
+
3
+ import type { AlternateLanguageLink } from '../components-nextjs/PageSeo';
4
+ import type { LocaleObject } from '../intl';
5
+ import { getFallbackLocale } from '../intl';
6
+
7
+ import { bindLanguagesWithLocales } from './utils/bindLanguagesWithLocales';
8
+ import { createAlternateLanguageLink } from './utils/createAlternateLanguageLink';
9
+
10
+ // We use english locale as fallback since English is the most common language
11
+ const ENG_FALLBACK = 'en';
12
+ const DEFAULT_HREF_LANG = 'x-default';
13
+
14
+ export function getAlternateLanguageLinks(
15
+ availableLanguages: NewsroomLanguageSettings[],
16
+ generateTranslationUrl: (locale: LocaleObject) => string | undefined,
17
+ ): AlternateLanguageLink[] {
18
+ const languagesWithLocales = bindLanguagesWithLocales(availableLanguages);
19
+ const hrefLangLinks: Array<AlternateLanguageLink> = [];
20
+
21
+ function pushLocaleToLinks(locale: LocaleObject, hrefLang?: string) {
22
+ const link = createAlternateLanguageLink(locale, generateTranslationUrl);
23
+
24
+ if (!link) {
25
+ return;
26
+ }
27
+
28
+ if (hrefLang) {
29
+ hrefLangLinks.push({ ...link, hrefLang });
30
+ } else {
31
+ hrefLangLinks.push(link);
32
+ }
33
+ }
34
+
35
+ function populateDirectLinks() {
36
+ languagesWithLocales.forEach((binds) =>
37
+ binds.forEach(({ locale }) => pushLocaleToLinks(locale)),
38
+ );
39
+ }
40
+
41
+ function populateRegionIndependentLinks() {
42
+ languagesWithLocales.forEach((binds, regionIndependentLocaleCode) => {
43
+ // When there is no region independent locale of provided locales
44
+ // we still need something as a region independent locale
45
+ const localesArray = Array.from(binds.values()).map(({ locale }) => locale);
46
+
47
+ const hasRegionIndependentLocale = localesArray.some(
48
+ (locale) => locale.isRegionIndependent,
49
+ );
50
+
51
+ if (hasRegionIndependentLocale) {
52
+ return;
53
+ }
54
+
55
+ // Try to find some possible fallback for the regionIndependentLocaleCode from hardcoded list
56
+ // We try to find fallback from provided translations of current regionIndependentLocaleCode
57
+ // If there is no defined fallback in the list we will use just first translation as a region independent translation
58
+ const fallback =
59
+ getFallbackLocale(regionIndependentLocaleCode, localesArray) ?? localesArray.at(0);
60
+
61
+ if (fallback) {
62
+ pushLocaleToLinks(fallback, regionIndependentLocaleCode);
63
+ }
64
+ });
65
+ }
66
+
67
+ function populateDefault() {
68
+ let isDefaultAddedToLinks = false;
69
+
70
+ // First we try to find region independent english locale (en)
71
+ // If the story has just region independent `en` translation we will use it as default hreflang
72
+ languagesWithLocales.forEach((binds) =>
73
+ binds.forEach(({ locale }) => {
74
+ if (locale.isRegionIndependent && locale.toNeutralLanguageCode() === ENG_FALLBACK) {
75
+ pushLocaleToLinks(locale, DEFAULT_HREF_LANG);
76
+ isDefaultAddedToLinks = true;
77
+ }
78
+ }),
79
+ );
80
+
81
+ if (!isDefaultAddedToLinks) {
82
+ // If there is no explicit region independent `en` translation for this story
83
+ // we will try to find some link that already has `en` fallback
84
+ const fallbackFromRegionIndependentLocales = hrefLangLinks.find(
85
+ (alternate) => alternate.hrefLang === ENG_FALLBACK,
86
+ );
87
+
88
+ if (fallbackFromRegionIndependentLocales) {
89
+ hrefLangLinks.push({
90
+ ...fallbackFromRegionIndependentLocales,
91
+ hrefLang: DEFAULT_HREF_LANG,
92
+ });
93
+ isDefaultAddedToLinks = true;
94
+ }
95
+ }
96
+
97
+ if (!isDefaultAddedToLinks) {
98
+ // If there is no english locale at all we will use is_default language (provided from server)
99
+ languagesWithLocales.forEach((binds) =>
100
+ binds.forEach(({ locale, language }) => {
101
+ if (language.is_default) {
102
+ pushLocaleToLinks(locale, DEFAULT_HREF_LANG);
103
+ isDefaultAddedToLinks = true;
104
+ }
105
+ }),
106
+ );
107
+ }
108
+ }
109
+
110
+ populateDirectLinks();
111
+ populateRegionIndependentLinks();
112
+ populateDefault();
113
+
114
+ return hrefLangLinks.sort((a, b) => a.hrefLang.localeCompare(b.hrefLang));
115
+ }
@@ -0,0 +1 @@
1
+ export * from './getAlternateLanguageLinks';
@@ -0,0 +1,31 @@
1
+ import type { NewsroomLanguageSettings } from '@prezly/sdk';
2
+
3
+ import type { LangCode, LocaleCode } from '../../intl';
4
+ import { LocaleObject } from '../../intl';
5
+
6
+ interface LocaleWithLanguage {
7
+ language: NewsroomLanguageSettings;
8
+ locale: LocaleObject;
9
+ }
10
+
11
+ /**
12
+ * Group languages by their region independent language code (e.g. "en" for "en-US" and "en-GB").
13
+ */
14
+ export function bindLanguagesWithLocales(languages: NewsroomLanguageSettings[]) {
15
+ const localesByLangCode = new Map<LangCode, Map<LocaleCode, LocaleWithLanguage>>();
16
+
17
+ languages.forEach((language) => {
18
+ const languageLocale = LocaleObject.fromAnyCode(language.code);
19
+ const regionIndependentLanguageCode = languageLocale.toNeutralLanguageCode();
20
+ let currentLocale = localesByLangCode.get(regionIndependentLanguageCode);
21
+
22
+ if (!currentLocale) {
23
+ currentLocale = new Map();
24
+ localesByLangCode.set(regionIndependentLanguageCode, currentLocale);
25
+ }
26
+
27
+ currentLocale.set(languageLocale.toHyphenCode(), { language, locale: languageLocale });
28
+ });
29
+
30
+ return localesByLangCode;
31
+ }
@@ -0,0 +1,17 @@
1
+ import type { LocaleObject } from '../../intl';
2
+
3
+ export function createAlternateLanguageLink(
4
+ locale: LocaleObject,
5
+ generateTranslationUrl: (locale: LocaleObject) => string | undefined,
6
+ ) {
7
+ const translationUrl = generateTranslationUrl(locale);
8
+
9
+ if (!translationUrl) {
10
+ return undefined;
11
+ }
12
+
13
+ return {
14
+ hrefLang: locale.toHyphenCode(),
15
+ href: translationUrl,
16
+ };
17
+ }