@prezly/theme-kit-nextjs 2.0.1-1 → 2.1.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 (259) hide show
  1. package/README.md +15 -15
  2. package/build/adapter-nextjs/api-handlers/fetchGalleries.js +21 -31
  3. package/build/adapter-nextjs/api-handlers/fetchGalleries.js.map +1 -1
  4. package/build/adapter-nextjs/api-handlers/fetchStories.js +23 -41
  5. package/build/adapter-nextjs/api-handlers/fetchStories.js.map +1 -1
  6. package/build/adapter-nextjs/api-handlers/index.js +7 -18
  7. package/build/adapter-nextjs/api-handlers/index.js.map +1 -1
  8. package/build/adapter-nextjs/getNewsroomServerSideProps.js +13 -20
  9. package/build/adapter-nextjs/getNewsroomServerSideProps.js.map +1 -1
  10. package/build/adapter-nextjs/getNewsroomStaticProps.js +13 -19
  11. package/build/adapter-nextjs/getNewsroomStaticProps.js.map +1 -1
  12. package/build/adapter-nextjs/index.js +21 -69
  13. package/build/adapter-nextjs/index.js.map +1 -1
  14. package/build/adapter-nextjs/page-props/category.js +96 -122
  15. package/build/adapter-nextjs/page-props/category.js.map +1 -1
  16. package/build/adapter-nextjs/page-props/gallery.js +65 -91
  17. package/build/adapter-nextjs/page-props/gallery.js.map +1 -1
  18. package/build/adapter-nextjs/page-props/galleryAlbum.js +49 -68
  19. package/build/adapter-nextjs/page-props/galleryAlbum.js.map +1 -1
  20. package/build/adapter-nextjs/page-props/home.js +66 -87
  21. package/build/adapter-nextjs/page-props/home.js.map +1 -1
  22. package/build/adapter-nextjs/page-props/index.js +40 -136
  23. package/build/adapter-nextjs/page-props/index.js.map +1 -1
  24. package/build/adapter-nextjs/page-props/lib/types.js +3 -1
  25. package/build/adapter-nextjs/page-props/search.js +28 -29
  26. package/build/adapter-nextjs/page-props/search.js.map +1 -1
  27. package/build/adapter-nextjs/page-props/sitemap/SitemapBuilder.js +46 -32
  28. package/build/adapter-nextjs/page-props/sitemap/SitemapBuilder.js.map +1 -1
  29. package/build/adapter-nextjs/page-props/sitemap/createPaths.js +17 -17
  30. package/build/adapter-nextjs/page-props/sitemap/createPaths.js.map +1 -1
  31. package/build/adapter-nextjs/page-props/sitemap/getServerSideProps.js +33 -37
  32. package/build/adapter-nextjs/page-props/sitemap/getServerSideProps.js.map +1 -1
  33. package/build/adapter-nextjs/page-props/sitemap/index.js +23 -41
  34. package/build/adapter-nextjs/page-props/sitemap/index.js.map +1 -1
  35. package/build/adapter-nextjs/page-props/sitemap/types.js +3 -1
  36. package/build/adapter-nextjs/page-props/story.js +60 -81
  37. package/build/adapter-nextjs/page-props/story.js.map +1 -1
  38. package/build/adapter-nextjs/page-props/storyPreview.js +24 -31
  39. package/build/adapter-nextjs/page-props/storyPreview.js.map +1 -1
  40. package/build/adapter-nextjs/processRequest.js +23 -38
  41. package/build/adapter-nextjs/processRequest.js.map +1 -1
  42. package/build/adapter-nextjs/processStaticRequest.js +14 -23
  43. package/build/adapter-nextjs/processStaticRequest.js.map +1 -1
  44. package/build/components-nextjs/PageSeo/PageSeo.js +77 -89
  45. package/build/components-nextjs/PageSeo/PageSeo.js.map +1 -1
  46. package/build/components-nextjs/PageSeo/index.js +19 -25
  47. package/build/components-nextjs/PageSeo/index.js.map +1 -1
  48. package/build/components-nextjs/PageSeo/lib/getAbsoluteUrl.js +8 -9
  49. package/build/components-nextjs/PageSeo/lib/getAbsoluteUrl.js.map +1 -1
  50. package/build/components-nextjs/PageSeo/lib/types.js +3 -1
  51. package/build/components-nextjs/StorySeo/StorySeo.js +46 -74
  52. package/build/components-nextjs/StorySeo/StorySeo.js.map +1 -1
  53. package/build/components-nextjs/StorySeo/index.js +5 -11
  54. package/build/components-nextjs/StorySeo/index.js.map +1 -1
  55. package/build/components-nextjs/document.js +44 -36
  56. package/build/components-nextjs/document.js.map +1 -1
  57. package/build/components-nextjs/index.js +9 -25
  58. package/build/components-nextjs/index.js.map +1 -1
  59. package/build/data-fetching/api/PrezlyApi.js +187 -210
  60. package/build/data-fetching/api/PrezlyApi.js.map +1 -1
  61. package/build/data-fetching/api/getPrezlyApi.js +19 -25
  62. package/build/data-fetching/api/getPrezlyApi.js.map +1 -1
  63. package/build/data-fetching/api/index.js +16 -14
  64. package/build/data-fetching/api/index.js.map +1 -1
  65. package/build/data-fetching/api/queries.js +41 -80
  66. package/build/data-fetching/api/queries.js.map +1 -1
  67. package/build/data-fetching/index.js +18 -36
  68. package/build/data-fetching/index.js.map +1 -1
  69. package/build/data-fetching/lib/getAlgoliaSettings.js +20 -26
  70. package/build/data-fetching/lib/getAlgoliaSettings.js.map +1 -1
  71. package/build/data-fetching/lib/getEnvVariables.js +36 -40
  72. package/build/data-fetching/lib/getEnvVariables.js.map +1 -1
  73. package/build/data-fetching/lib/index.js +11 -32
  74. package/build/data-fetching/lib/index.js.map +1 -1
  75. package/build/data-fetching/lib/isSdkError.js +6 -7
  76. package/build/data-fetching/lib/isSdkError.js.map +1 -1
  77. package/build/data-fetching/lib/isUuid.js +6 -7
  78. package/build/data-fetching/lib/isUuid.js.map +1 -1
  79. package/build/data-fetching/types.js +3 -1
  80. package/build/index.js +25 -103
  81. package/build/index.js.map +1 -1
  82. package/build/infinite-loading/index.js +23 -41
  83. package/build/infinite-loading/index.js.map +1 -1
  84. package/build/infinite-loading/types.js +3 -1
  85. package/build/infinite-loading/useInfiniteGalleriesLoading.js +35 -46
  86. package/build/infinite-loading/useInfiniteGalleriesLoading.js.map +1 -1
  87. package/build/infinite-loading/useInfiniteLoading.js +41 -54
  88. package/build/infinite-loading/useInfiniteLoading.js.map +1 -1
  89. package/build/infinite-loading/useInfiniteStoriesLoading.js +48 -60
  90. package/build/infinite-loading/useInfiniteStoriesLoading.js.map +1 -1
  91. package/build/intl/index.d.ts +1 -1
  92. package/build/intl/index.js +24 -47
  93. package/build/intl/index.js.map +1 -1
  94. package/build/intl/languages.js +110 -150
  95. package/build/intl/languages.js.map +1 -1
  96. package/build/intl/locale.d.ts +1 -0
  97. package/build/intl/locale.js +104 -58
  98. package/build/intl/locale.js.map +1 -1
  99. package/build/intl/localeConfig.d.ts +2 -3
  100. package/build/intl/localeConfig.js +13 -28
  101. package/build/intl/localeConfig.js.map +1 -1
  102. package/build/intl/localeList.d.ts +2 -4
  103. package/build/intl/localeList.js +108 -7
  104. package/build/intl/localeList.js.map +1 -1
  105. package/build/intl/localeObject.js +78 -83
  106. package/build/intl/localeObject.js.map +1 -1
  107. package/build/newsroom-context/context.js +24 -42
  108. package/build/newsroom-context/context.js.map +1 -1
  109. package/build/newsroom-context/hooks/index.js +27 -125
  110. package/build/newsroom-context/hooks/index.js.map +1 -1
  111. package/build/newsroom-context/hooks/useAlgoliaSettings.js +8 -9
  112. package/build/newsroom-context/hooks/useAlgoliaSettings.js.map +1 -1
  113. package/build/newsroom-context/hooks/useCategories.js +8 -9
  114. package/build/newsroom-context/hooks/useCategories.js.map +1 -1
  115. package/build/newsroom-context/hooks/useCompanyInformation.js +8 -9
  116. package/build/newsroom-context/hooks/useCompanyInformation.js.map +1 -1
  117. package/build/newsroom-context/hooks/useCurrentCategory.js +8 -9
  118. package/build/newsroom-context/hooks/useCurrentCategory.js.map +1 -1
  119. package/build/newsroom-context/hooks/useCurrentLocale.js +8 -9
  120. package/build/newsroom-context/hooks/useCurrentLocale.js.map +1 -1
  121. package/build/newsroom-context/hooks/useCurrentStory.js +8 -9
  122. package/build/newsroom-context/hooks/useCurrentStory.js.map +1 -1
  123. package/build/newsroom-context/hooks/useGetLinkLocaleSlug.js +19 -21
  124. package/build/newsroom-context/hooks/useGetLinkLocaleSlug.js.map +1 -1
  125. package/build/newsroom-context/hooks/useGetTranslationUrl.js +54 -56
  126. package/build/newsroom-context/hooks/useGetTranslationUrl.js.map +1 -1
  127. package/build/newsroom-context/hooks/useLanguages.js +8 -9
  128. package/build/newsroom-context/hooks/useLanguages.js.map +1 -1
  129. package/build/newsroom-context/hooks/useNewsroom.js +8 -9
  130. package/build/newsroom-context/hooks/useNewsroom.js.map +1 -1
  131. package/build/newsroom-context/hooks/useNewsroomContext.js +14 -15
  132. package/build/newsroom-context/hooks/useNewsroomContext.js.map +1 -1
  133. package/build/newsroom-context/index.d.ts +1 -2
  134. package/build/newsroom-context/index.js +20 -32
  135. package/build/newsroom-context/index.js.map +1 -1
  136. package/build/sentry/client.js +7 -8
  137. package/build/sentry/client.js.map +1 -1
  138. package/build/sentry/common.js +27 -30
  139. package/build/sentry/common.js.map +1 -1
  140. package/build/sentry/index.js +7 -18
  141. package/build/sentry/index.js.map +1 -1
  142. package/build/sentry/server.js +7 -8
  143. package/build/sentry/server.js.map +1 -1
  144. package/build/tsconfig.tsbuildinfo +1 -0
  145. package/build/types.js +3 -1
  146. package/build/utils/boilerplate.js +18 -11
  147. package/build/utils/boilerplate.js.map +1 -1
  148. package/build/utils/constants.js +6 -11
  149. package/build/utils/constants.js.map +1 -1
  150. package/build/utils/getAssetsUrl.js +7 -8
  151. package/build/utils/getAssetsUrl.js.map +1 -1
  152. package/build/utils/getDataRequestLink.js +10 -10
  153. package/build/utils/getDataRequestLink.js.map +1 -1
  154. package/build/utils/getLocalizedCategoryData.js +29 -37
  155. package/build/utils/getLocalizedCategoryData.js.map +1 -1
  156. package/build/utils/getNewsroomLogoUrl.js +18 -19
  157. package/build/utils/getNewsroomLogoUrl.js.map +1 -1
  158. package/build/utils/getPrivacyPortalUrl.js +11 -15
  159. package/build/utils/getPrivacyPortalUrl.js.map +1 -1
  160. package/build/utils/getStoryPublicationDate.js +12 -13
  161. package/build/utils/getStoryPublicationDate.js.map +1 -1
  162. package/build/utils/getUploadcareGroupUrl.js +8 -9
  163. package/build/utils/getUploadcareGroupUrl.js.map +1 -1
  164. package/build/utils/index.js +26 -114
  165. package/build/utils/index.js.map +1 -1
  166. package/build/utils/isEmbargoStory.js +6 -7
  167. package/build/utils/isEmbargoStory.js.map +1 -1
  168. package/package.json +12 -26
  169. package/src/@types/global.d.ts +7 -0
  170. package/src/__mocks__/languages.ts +416 -0
  171. package/src/__mocks__/story.ts +359 -0
  172. package/src/adapter-nextjs/api-handlers/fetchGalleries.ts +24 -0
  173. package/src/adapter-nextjs/api-handlers/fetchStories.ts +26 -0
  174. package/src/adapter-nextjs/api-handlers/index.ts +2 -0
  175. package/src/adapter-nextjs/getNewsroomServerSideProps.ts +29 -0
  176. package/src/adapter-nextjs/getNewsroomStaticProps.ts +29 -0
  177. package/src/adapter-nextjs/index.ts +6 -0
  178. package/src/adapter-nextjs/page-props/category.ts +157 -0
  179. package/src/adapter-nextjs/page-props/gallery.ts +114 -0
  180. package/src/adapter-nextjs/page-props/galleryAlbum.ts +89 -0
  181. package/src/adapter-nextjs/page-props/home.ts +113 -0
  182. package/src/adapter-nextjs/page-props/index.ts +26 -0
  183. package/src/adapter-nextjs/page-props/lib/types.ts +8 -0
  184. package/src/adapter-nextjs/page-props/search.ts +53 -0
  185. package/src/adapter-nextjs/page-props/sitemap/SitemapBuilder.ts +56 -0
  186. package/src/adapter-nextjs/page-props/sitemap/createPaths.ts +21 -0
  187. package/src/adapter-nextjs/page-props/sitemap/getServerSideProps.ts +43 -0
  188. package/src/adapter-nextjs/page-props/sitemap/index.ts +5 -0
  189. package/src/adapter-nextjs/page-props/sitemap/types.ts +5 -0
  190. package/src/adapter-nextjs/page-props/story.ts +94 -0
  191. package/src/adapter-nextjs/page-props/storyPreview.ts +33 -0
  192. package/src/adapter-nextjs/processRequest.ts +49 -0
  193. package/src/adapter-nextjs/processStaticRequest.ts +32 -0
  194. package/src/components-nextjs/PageSeo/PageSeo.tsx +131 -0
  195. package/src/components-nextjs/PageSeo/index.ts +2 -0
  196. package/src/components-nextjs/PageSeo/lib/getAbsoluteUrl.ts +7 -0
  197. package/src/components-nextjs/PageSeo/lib/types.ts +4 -0
  198. package/src/components-nextjs/StorySeo/StorySeo.tsx +80 -0
  199. package/src/components-nextjs/StorySeo/index.ts +1 -0
  200. package/src/components-nextjs/document.tsx +38 -0
  201. package/src/components-nextjs/index.ts +3 -0
  202. package/src/data-fetching/api/PrezlyApi.ts +304 -0
  203. package/src/data-fetching/api/getPrezlyApi.ts +24 -0
  204. package/src/data-fetching/api/index.ts +1 -0
  205. package/src/data-fetching/api/queries.ts +51 -0
  206. package/src/data-fetching/index.ts +3 -0
  207. package/src/data-fetching/lib/getAlgoliaSettings.ts +29 -0
  208. package/src/data-fetching/lib/getEnvVariables.ts +46 -0
  209. package/src/data-fetching/lib/index.ts +4 -0
  210. package/src/data-fetching/lib/isSdkError.ts +5 -0
  211. package/src/data-fetching/lib/isUuid.ts +5 -0
  212. package/src/data-fetching/types.ts +14 -0
  213. package/src/index.ts +9 -0
  214. package/src/infinite-loading/index.ts +4 -0
  215. package/src/infinite-loading/types.ts +5 -0
  216. package/src/infinite-loading/useInfiniteGalleriesLoading.ts +53 -0
  217. package/src/infinite-loading/useInfiniteLoading.ts +66 -0
  218. package/src/infinite-loading/useInfiniteStoriesLoading.ts +81 -0
  219. package/src/intl/index.ts +4 -0
  220. package/src/intl/languages.test.ts +232 -0
  221. package/src/intl/languages.ts +233 -0
  222. package/src/intl/locale.test.ts +95 -0
  223. package/src/intl/locale.ts +140 -0
  224. package/src/intl/localeConfig.js +23 -0
  225. package/src/intl/localeList.js +110 -0
  226. package/src/intl/localeObject.test.ts +81 -0
  227. package/src/intl/localeObject.ts +99 -0
  228. package/src/newsroom-context/context.tsx +115 -0
  229. package/src/newsroom-context/hooks/index.ts +11 -0
  230. package/src/newsroom-context/hooks/useAlgoliaSettings.ts +7 -0
  231. package/src/newsroom-context/hooks/useCategories.ts +7 -0
  232. package/src/newsroom-context/hooks/useCompanyInformation.ts +7 -0
  233. package/src/newsroom-context/hooks/useCurrentCategory.ts +7 -0
  234. package/src/newsroom-context/hooks/useCurrentLocale.ts +7 -0
  235. package/src/newsroom-context/hooks/useCurrentStory.ts +7 -0
  236. package/src/newsroom-context/hooks/useGetLinkLocaleSlug.ts +37 -0
  237. package/src/newsroom-context/hooks/useGetTranslationUrl.ts +97 -0
  238. package/src/newsroom-context/hooks/useLanguages.ts +7 -0
  239. package/src/newsroom-context/hooks/useNewsroom.ts +7 -0
  240. package/src/newsroom-context/hooks/useNewsroomContext.ts +15 -0
  241. package/src/newsroom-context/index.ts +2 -0
  242. package/src/sentry/client.ts +7 -0
  243. package/src/sentry/common.ts +27 -0
  244. package/src/sentry/index.ts +2 -0
  245. package/src/sentry/server.ts +7 -0
  246. package/src/types.ts +51 -0
  247. package/src/utils/boilerplate.ts +27 -0
  248. package/src/utils/constants.ts +3 -0
  249. package/src/utils/getAssetsUrl.test.ts +9 -0
  250. package/src/utils/getAssetsUrl.ts +9 -0
  251. package/src/utils/getDataRequestLink.ts +14 -0
  252. package/src/utils/getLocalizedCategoryData.ts +47 -0
  253. package/src/utils/getNewsroomLogoUrl.ts +22 -0
  254. package/src/utils/getPrivacyPortalUrl.ts +25 -0
  255. package/src/utils/getStoryPublicationDate.tsx +15 -0
  256. package/src/utils/getUploadcareGroupUrl.ts +13 -0
  257. package/src/utils/index.ts +10 -0
  258. package/src/utils/isEmbargoStory.ts +10 -0
  259. package/build/@types/global.d.js +0 -1
@@ -0,0 +1,233 @@
1
+ import type { CultureRef, NewsroomLanguageSettings, Story } from '@prezly/sdk';
2
+
3
+ import { DUMMY_DEFAULT_LOCALE } from './locale';
4
+ import { LocaleObject } from './localeObject';
5
+
6
+ function isOnlyCulture(culture: CultureRef, languages: NewsroomLanguageSettings[]): boolean {
7
+ const numberOfLanguages = languages.filter(
8
+ ({ locale: { language_code } }) => language_code === culture.language_code,
9
+ ).length;
10
+
11
+ return numberOfLanguages === 1;
12
+ }
13
+
14
+ /**
15
+ * @returns the display name of the locale in its native language
16
+ *
17
+ * If there's only one culture used in a specific language,
18
+ * we strip the culture name completely.
19
+ *
20
+ * Examples:
21
+ * - English (Global), Spanish (Spain)
22
+ * - -> English, Spanish
23
+ * - English (Global), English (UK), Spanish (Spain)
24
+ * - -> English (Global), English (UK), Spanish
25
+ */
26
+ export function getLanguageDisplayName(
27
+ language: NewsroomLanguageSettings,
28
+ languages: NewsroomLanguageSettings[],
29
+ ): string {
30
+ const { locale } = language;
31
+
32
+ if (isOnlyCulture(locale, languages)) {
33
+ const cultureNameIndex = locale.native_name.indexOf('(');
34
+
35
+ if (cultureNameIndex !== -1) {
36
+ return locale.native_name.slice(0, cultureNameIndex - 1);
37
+ }
38
+ }
39
+
40
+ return locale.native_name;
41
+ }
42
+
43
+ /**
44
+ * @returns Language set as default in the Newsroom Settings.
45
+ */
46
+ export function getDefaultLanguage(languages: NewsroomLanguageSettings[]) {
47
+ return languages.find(({ is_default }) => is_default) || languages[0];
48
+ }
49
+
50
+ /**
51
+ * @returns Only languages having at least one published story
52
+ */
53
+ export function getUsedLanguages(languages: NewsroomLanguageSettings[]) {
54
+ return languages.filter((language) => language.public_stories_count > 0);
55
+ }
56
+
57
+ export function getLanguageByExactLocaleCode(
58
+ languages: NewsroomLanguageSettings[],
59
+ locale: LocaleObject,
60
+ ) {
61
+ const localeCode = locale.toUnderscoreCode();
62
+ return languages.find(({ code }) => code === localeCode);
63
+ }
64
+
65
+ // See https://github.com/prezly/prezly/blob/master/lib/model/CulturePeer.php#L123
66
+ export function getLanguageByNeutralLocaleCode(
67
+ languages: NewsroomLanguageSettings[],
68
+ locale: LocaleObject,
69
+ ) {
70
+ const neutralLanguageCode = locale.toNeutralLanguageCode();
71
+
72
+ // Prefer default language
73
+ const defaultLanguage = getDefaultLanguage(languages);
74
+ // TODO: This doesn't align with other methods logic
75
+ if (defaultLanguage.code === locale.toUnderscoreCode()) {
76
+ return defaultLanguage;
77
+ }
78
+
79
+ // Try to look in used cultures first (giving priority to used ones)
80
+ const usedLanguages = getUsedLanguages(languages);
81
+ const usedLanguage = usedLanguages.find(
82
+ ({ code }) =>
83
+ LocaleObject.fromAnyCode(code).toNeutralLanguageCode() === neutralLanguageCode,
84
+ );
85
+ if (usedLanguage) {
86
+ return usedLanguage;
87
+ }
88
+
89
+ // Search in all languages
90
+ return languages.find(
91
+ ({ code }) =>
92
+ LocaleObject.fromAnyCode(code).toNeutralLanguageCode() === neutralLanguageCode,
93
+ );
94
+ }
95
+
96
+ // See https://github.com/prezly/prezly/blob/master/lib/model/CulturePeer.php#L159
97
+ export function getLanguageByShortRegionCode(
98
+ languages: NewsroomLanguageSettings[],
99
+ locale: LocaleObject,
100
+ ) {
101
+ const shortRegionCode = locale.toRegionCode();
102
+
103
+ // Prefer default language
104
+ const defaultLanguage = getDefaultLanguage(languages);
105
+ if (LocaleObject.fromAnyCode(defaultLanguage.code).toRegionCode() === shortRegionCode) {
106
+ return defaultLanguage;
107
+ }
108
+
109
+ // Try to look in used cultures first (giving priority to used ones)
110
+ const usedLanguages = getUsedLanguages(languages);
111
+ const usedLanguage = usedLanguages.find(
112
+ ({ code }) => LocaleObject.fromAnyCode(code).toRegionCode() === shortRegionCode,
113
+ );
114
+ if (usedLanguage) {
115
+ return usedLanguage;
116
+ }
117
+
118
+ // Search in all languages
119
+ return languages.find(
120
+ ({ code }) => LocaleObject.fromAnyCode(code).toRegionCode() === shortRegionCode,
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Retrieve a culture with fallback to legacy codes support.
126
+ * Pulled from https://github.com/prezly/prezly/blob/de9900d9890a33502780494aa3fb85c9a732b3c3/lib/model/CulturePeer.php#L91-L114
127
+ */
128
+ export function getLanguageFromNextLocaleIsoCode(
129
+ languages: NewsroomLanguageSettings[],
130
+ nextLocaleIsoCode?: string,
131
+ ): NewsroomLanguageSettings | undefined {
132
+ const defaultLanguage = getDefaultLanguage(languages);
133
+
134
+ if (!nextLocaleIsoCode || nextLocaleIsoCode === DUMMY_DEFAULT_LOCALE) {
135
+ return defaultLanguage;
136
+ }
137
+
138
+ const locale = LocaleObject.fromAnyCode(nextLocaleIsoCode);
139
+ let targetLanguage: NewsroomLanguageSettings | undefined;
140
+
141
+ if (nextLocaleIsoCode.length >= 2 && nextLocaleIsoCode.length <= 4) {
142
+ // The order of methods here is reversed from `getShortestLocaleCode`, so that the logic "unwraps" the possible variants with no collisions
143
+ targetLanguage =
144
+ getLanguageByExactLocaleCode(languages, locale) ||
145
+ getLanguageByShortRegionCode(languages, locale) ||
146
+ getLanguageByNeutralLocaleCode(languages, locale);
147
+ } else {
148
+ targetLanguage = getLanguageByExactLocaleCode(languages, locale);
149
+ }
150
+
151
+ return targetLanguage;
152
+ }
153
+
154
+ export function getLanguageFromStory(languages: NewsroomLanguageSettings[], story: Story) {
155
+ const { code: storyLocaleCode } = story.culture;
156
+
157
+ return languages.find(({ code }) => code === storyLocaleCode);
158
+ }
159
+
160
+ /**
161
+ * Extracts company information for the selected locale
162
+ */
163
+ export function getCompanyInformation(languages: NewsroomLanguageSettings[], locale: LocaleObject) {
164
+ const currentLanguage =
165
+ getLanguageByExactLocaleCode(languages, locale) || getDefaultLanguage(languages);
166
+
167
+ return currentLanguage.company_information;
168
+ }
169
+
170
+ /**
171
+ * Get newsroom notifications for the selected locale
172
+ */
173
+ export function getNotifications(languages: NewsroomLanguageSettings[], locale: LocaleObject) {
174
+ const currentLanguage =
175
+ getLanguageByExactLocaleCode(languages, locale) || getDefaultLanguage(languages);
176
+
177
+ return currentLanguage.notifications;
178
+ }
179
+
180
+ /**
181
+ * Get the shortest locale code possible from full locale code
182
+ * First: try shorting to neutral language code (there should be no locales with the same language code)
183
+ * Then: try shorting to region code (there should be no locales with the same region code)
184
+ * Finally: return the original locale code (shorting is not possible)
185
+ *
186
+ * @param locale A LocaleObject constructed from FULL locale code (taken straight from the selected language)
187
+ */
188
+ export function getShortestLocaleCode(
189
+ languages: NewsroomLanguageSettings[],
190
+ locale: LocaleObject,
191
+ ): string | false {
192
+ const localeCode = locale.toUnderscoreCode();
193
+ const defaultLanguage = getDefaultLanguage(languages);
194
+ // If it's a default locale, return false (no locale needed in URL)
195
+ if (localeCode === defaultLanguage.code) {
196
+ return false;
197
+ }
198
+
199
+ // Try shorting to neutral language code
200
+ const neutralLanguageCode = locale.toNeutralLanguageCode();
201
+ const matchingLanguagesByNeutralCode = languages.filter(
202
+ ({ code }) =>
203
+ LocaleObject.fromAnyCode(code).toNeutralLanguageCode() === neutralLanguageCode ||
204
+ code === neutralLanguageCode,
205
+ );
206
+ if (matchingLanguagesByNeutralCode.length === 1) {
207
+ return neutralLanguageCode;
208
+ }
209
+
210
+ // Try shorting to region code
211
+ const shortRegionCode = locale.toRegionCode();
212
+ const matchingLanguagesByRegionCode = languages.filter(
213
+ ({ code }) => LocaleObject.fromAnyCode(code).toRegionCode() === shortRegionCode,
214
+ );
215
+ // Prevent collision with neutral language codes
216
+ const lowerCasedShortRegionCode = shortRegionCode.toLowerCase();
217
+ const mathchingNeutralLanguagesByRegionCode = languages.filter(
218
+ ({ code }) =>
219
+ LocaleObject.fromAnyCode(code).toNeutralLanguageCode() === lowerCasedShortRegionCode ||
220
+ code === lowerCasedShortRegionCode,
221
+ );
222
+ if (
223
+ matchingLanguagesByRegionCode.length === 1 &&
224
+ // If there are 2 or more matching neutral languages, it means that there are no languages that can be shortened to neutral code
225
+ mathchingNeutralLanguagesByRegionCode.length !== 1 &&
226
+ // We don't want just numbers in our region code
227
+ Number.isNaN(Number(shortRegionCode))
228
+ ) {
229
+ return shortRegionCode;
230
+ }
231
+
232
+ return localeCode;
233
+ }
@@ -0,0 +1,95 @@
1
+ import { jest } from '@jest/globals';
2
+ import {
3
+ DEFAULT_LOCALE,
4
+ DUMMY_DEFAULT_LOCALE,
5
+ getLocaleDirection,
6
+ getRedirectToCanonicalLocale,
7
+ getSupportedLocaleIsoCode,
8
+ } from './locale';
9
+ import { LocaleObject } from './localeObject';
10
+
11
+ describe('getSupportedLocaleIsoCode', () => {
12
+ it('returns a code by exact match', () => {
13
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('en'))).toBe('en');
14
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('af'))).toBe('af');
15
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('pt-BR'))).toBe('pt-BR');
16
+ });
17
+
18
+ it('returns a code by neutral language code match', () => {
19
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('en-US'))).toBe('en');
20
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('de-DE'))).toBe('de');
21
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('uk-UA'))).toBe('uk');
22
+ });
23
+
24
+ it('returns custom mapped codes for Chinese locales', () => {
25
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('zh-Hant'))).toBe('zh-TW');
26
+ expect(getSupportedLocaleIsoCode(LocaleObject.fromAnyCode('zh-HK'))).toBe('zh-CN');
27
+ });
28
+
29
+ it('returns default locale code for unsupported languages', () => {
30
+ const consoleSpy = jest.spyOn(console, 'warn');
31
+
32
+ // @ts-expect-error
33
+ expect(getSupportedLocaleIsoCode(new LocaleObject('aa'))).toBe(DEFAULT_LOCALE);
34
+ expect(consoleSpy).toHaveBeenCalledWith(
35
+ 'Unsupported locale provided. Please use `LocaleObject.fromAnyCode` to ensure you are using a correct language code',
36
+ );
37
+ });
38
+ });
39
+
40
+ describe('getRedirectToCanonicalLocale', () => {
41
+ it('returns undefined when no locale or dummy locale is provided', () => {
42
+ expect(getRedirectToCanonicalLocale('en', undefined, '/')).toBe(undefined);
43
+ expect(getRedirectToCanonicalLocale('en', DUMMY_DEFAULT_LOCALE, '/')).toBe(undefined);
44
+ });
45
+
46
+ it('returns undefined when locale is already canonical', () => {
47
+ expect(getRedirectToCanonicalLocale('en', 'en', '/')).toBe(undefined);
48
+ expect(getRedirectToCanonicalLocale('nl_BE', 'nl-be', '/')).toBe(undefined);
49
+ });
50
+
51
+ it('returns a redirect object when requested locale is not canonical', () => {
52
+ expect(getRedirectToCanonicalLocale(false, 'en-us', '/test')).toEqual({
53
+ destination: '/test',
54
+ permanent: false,
55
+ });
56
+
57
+ expect(getRedirectToCanonicalLocale('en', 'en-us', '/test')).toEqual({
58
+ destination: '/en/test',
59
+ permanent: false,
60
+ });
61
+
62
+ expect(getRedirectToCanonicalLocale('en', 'en-us', 'test')).toEqual({
63
+ destination: '/en/test',
64
+ permanent: false,
65
+ });
66
+ });
67
+
68
+ it('preserves query parameters', () => {
69
+ expect(
70
+ getRedirectToCanonicalLocale('en', 'en-us', '/test', {
71
+ page: '1',
72
+ tags: ['story', 'media'],
73
+ }),
74
+ ).toEqual({
75
+ destination: '/en/test?page=1&tags=story&tags=media',
76
+ permanent: false,
77
+ });
78
+ });
79
+ });
80
+
81
+ describe('getLocaleDirection', () => {
82
+ it('returns `rtl` for right-to-left languages', () => {
83
+ const RTL_LANGUAGES = ['ar', 'ar-AE', 'ar-BH', 'ar-QA', 'ar-SA', 'he-IL', 'ur-PK'];
84
+ RTL_LANGUAGES.forEach((language) => {
85
+ expect(getLocaleDirection(LocaleObject.fromAnyCode(language))).toBe('rtl');
86
+ });
87
+ });
88
+
89
+ it('returns `ltr` for other languages', () => {
90
+ const LTR_LANGUAGES = ['en', 'en-US', 'fr-FR', 'fil', 'pt-BR', 'nl', 'nl-BE'];
91
+ LTR_LANGUAGES.forEach((language) => {
92
+ expect(getLocaleDirection(LocaleObject.fromAnyCode(language))).toBe('ltr');
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,140 @@
1
+ import type { Redirect } from 'next';
2
+ import type { ParsedUrlQuery } from 'querystring';
3
+ import { stringify } from 'querystring';
4
+
5
+ import { LocaleObject } from './localeObject';
6
+
7
+ export const DEFAULT_LOCALE = 'en';
8
+ // We use pseudo locale used for localization testing, to reliably determine if we need to fallback to the default newsroom language
9
+ export const DUMMY_DEFAULT_LOCALE = 'qps-ploc';
10
+
11
+ /**
12
+ * This a list of locales that Prezly has translations for. Each code represents a language file in the `@prezly/themes-intl-messages` package.
13
+ * See https://github.com/prezly/themes-intl-messages for more info.
14
+ */
15
+ const SUPPORTED_LOCALES = [
16
+ 'af',
17
+ 'ar',
18
+ 'az',
19
+ 'be',
20
+ 'bg',
21
+ 'cs',
22
+ 'da',
23
+ 'de',
24
+ 'el',
25
+ 'en',
26
+ 'es',
27
+ 'et',
28
+ 'fi',
29
+ 'fil',
30
+ 'fr',
31
+ 'ga',
32
+ 'he',
33
+ 'hi',
34
+ 'hr',
35
+ 'hu',
36
+ 'id',
37
+ 'it',
38
+ 'ja',
39
+ 'kk',
40
+ 'ko',
41
+ 'lt',
42
+ 'lv',
43
+ 'mt',
44
+ 'nl',
45
+ 'no',
46
+ 'pl',
47
+ 'pt-BR',
48
+ 'pt',
49
+ 'ro',
50
+ 'ru',
51
+ 'sk',
52
+ 'sl',
53
+ 'sv',
54
+ 'sw',
55
+ 'th',
56
+ 'tr',
57
+ 'uk',
58
+ 'ur',
59
+ 'uz',
60
+ 'vi',
61
+ 'zh-CN',
62
+ 'zh-TW',
63
+ ];
64
+
65
+ /**
66
+ * Use this function to determine which translations file you should load from `@prezly/themes-intl-mesages` package.
67
+ * See https://github.com/prezly/theme-nextjs-bea/blob/main/utils/lang.ts for a usage example.
68
+ */
69
+ export function getSupportedLocaleIsoCode(locale: LocaleObject): string {
70
+ const localeIsoCode = locale.toHyphenCode();
71
+
72
+ const isSupportedLocale =
73
+ localeIsoCode.length >= 2 && SUPPORTED_LOCALES.includes(localeIsoCode);
74
+ if (isSupportedLocale) {
75
+ return localeIsoCode;
76
+ }
77
+
78
+ const language = locale.toNeutralLanguageCode();
79
+ const isSupportedLanguage = SUPPORTED_LOCALES.includes(language);
80
+ if (isSupportedLanguage) {
81
+ return language;
82
+ }
83
+
84
+ // Custom mapping for Chinese locales
85
+ if (localeIsoCode.toLowerCase() === 'zh-hant') {
86
+ return 'zh-TW';
87
+ }
88
+ if (localeIsoCode.toLowerCase() === 'zh-hk') {
89
+ return 'zh-CN';
90
+ }
91
+
92
+ // This code should never be reached, because locale code check is happening in `LocaleObject.fromAnyCode`
93
+ // eslint-disable-next-line no-console
94
+ console.warn(
95
+ 'Unsupported locale provided. Please use `LocaleObject.fromAnyCode` to ensure you are using a correct language code',
96
+ );
97
+ return DEFAULT_LOCALE;
98
+ }
99
+
100
+ export function getRedirectToCanonicalLocale(
101
+ shortestLocaleCode: string | false,
102
+ nextLocaleIsoCode: string | undefined,
103
+ redirectPath: string,
104
+ query?: ParsedUrlQuery,
105
+ ): Redirect | undefined {
106
+ // No need to redirect if no particular locale was requested by the application
107
+ if (!nextLocaleIsoCode || nextLocaleIsoCode === DUMMY_DEFAULT_LOCALE) {
108
+ return undefined;
109
+ }
110
+
111
+ const shortestLocaleSlug = shortestLocaleCode
112
+ ? LocaleObject.fromAnyCode(shortestLocaleCode).toUrlSlug()
113
+ : shortestLocaleCode;
114
+
115
+ if (shortestLocaleSlug !== nextLocaleIsoCode) {
116
+ const prefixedPath =
117
+ redirectPath && !redirectPath.startsWith('/') ? `/${redirectPath}` : redirectPath;
118
+
119
+ const urlQuery = query ? `?${stringify(query)}` : '';
120
+
121
+ return {
122
+ destination: shortestLocaleSlug
123
+ ? `/${shortestLocaleSlug}${prefixedPath}${urlQuery}`
124
+ : `${prefixedPath}${urlQuery}`,
125
+ permanent: false,
126
+ };
127
+ }
128
+
129
+ return undefined;
130
+ }
131
+
132
+ export function getLocaleDirection(locale: LocaleObject): 'ltr' | 'rtl' {
133
+ const neutralLanguageCode = locale.toNeutralLanguageCode();
134
+
135
+ if (['ar', 'he', 'ur'].includes(neutralLanguageCode)) {
136
+ return 'rtl';
137
+ }
138
+
139
+ return 'ltr';
140
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * This list is pulled from the main Prezly application. The underscores are replaced with dashes.
3
+ * Together with permutation performed lower in the code,
4
+ * the exported list represents all of the possible locale codes that Next.js theme application might accept.
5
+ */
6
+ const supportedLocales = require('./localeList');
7
+
8
+ const lowercaseLocales = supportedLocales.map((l) => l.toLowerCase());
9
+
10
+ // Get all permutations for the shorter locale codes supported by Prezly (e.g. short region codes and neutral language codes)
11
+ const lowercaseLocalPermutations = Array.from(
12
+ new Set([
13
+ ...lowercaseLocales,
14
+ ...lowercaseLocales.map((code) => code.split('-')[0]),
15
+ ...lowercaseLocales
16
+ .map((code) => code.split('-')[1])
17
+ .filter(Boolean)
18
+ // Remove number-only codes from possible permutations (like `419` for `es-419`)
19
+ .filter((code) => Number.isNaN(Number(code))),
20
+ ]),
21
+ ).sort();
22
+
23
+ module.exports = lowercaseLocalPermutations;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * This list is pulled from the main Prezly application. The underscores are replaced with dashes.
3
+ */
4
+ module.exports = [
5
+ 'af-ZA',
6
+ 'ar',
7
+ 'ar-AE',
8
+ 'ar-BH',
9
+ 'ar-QA',
10
+ 'ar-SA',
11
+ 'az',
12
+ 'be',
13
+ 'bg-BG',
14
+ 'ca',
15
+ 'cs-CZ',
16
+ 'da-DK',
17
+ 'de',
18
+ 'de-DE',
19
+ 'de-BE',
20
+ 'de-AT',
21
+ 'de-CH',
22
+ 'el-GR',
23
+ 'en',
24
+ 'en-US',
25
+ 'en-AE',
26
+ 'en-AU',
27
+ 'en-BD',
28
+ 'en-BE',
29
+ 'en-BH',
30
+ 'en-CA',
31
+ 'en-CN',
32
+ 'en-DE',
33
+ 'en-DK',
34
+ 'en-GB',
35
+ 'en-HK',
36
+ 'en-ID',
37
+ 'en-IE',
38
+ 'en-IL',
39
+ 'en-IN',
40
+ 'en-JM',
41
+ 'en-JP',
42
+ 'en-KH',
43
+ 'en-KR',
44
+ 'en-LK',
45
+ 'en-MV',
46
+ 'en-MM',
47
+ 'en-MY',
48
+ 'en-NP',
49
+ 'en-NZ',
50
+ 'en-PH',
51
+ 'en-PK',
52
+ 'en-QA',
53
+ 'en-SA',
54
+ 'en-SG',
55
+ 'en-TH',
56
+ 'en-TW',
57
+ 'en-VN',
58
+ 'en-ZA',
59
+ 'es',
60
+ 'es-ES',
61
+ 'es-MX',
62
+ 'et-EE',
63
+ 'es-419',
64
+ 'fi-FI',
65
+ 'fil',
66
+ 'fr',
67
+ 'fr-BE',
68
+ 'fr-FR',
69
+ 'fr-CA',
70
+ 'fr-LU',
71
+ 'ga-IE',
72
+ 'he-IL',
73
+ 'hi-IN',
74
+ 'hr-HR',
75
+ 'hu-HU',
76
+ 'id-ID',
77
+ 'it-IT',
78
+ 'ja-JP',
79
+ 'kk',
80
+ 'ko-KR',
81
+ 'ky',
82
+ 'lt-LT',
83
+ 'lv-LV',
84
+ 'mt-MT',
85
+ 'ms-MY',
86
+ 'nb-NO',
87
+ 'nl',
88
+ 'nl-BE',
89
+ 'nl-NL',
90
+ 'pl-PL',
91
+ 'pt',
92
+ 'pt-BR',
93
+ 'pt-PT',
94
+ 'ro-RO',
95
+ 'ru-RU',
96
+ 'sk-SK',
97
+ 'sl-SI',
98
+ 'sv-SE',
99
+ 'sw',
100
+ 'th-TH',
101
+ 'tr-TR',
102
+ 'uk-UA',
103
+ 'ur-PK',
104
+ 'uz',
105
+ 'vi-VN',
106
+ 'zh-Hant',
107
+ 'zh-HK',
108
+ 'zh-TW',
109
+ 'zh-CN',
110
+ ];
@@ -0,0 +1,81 @@
1
+ import { LocaleObject } from './localeObject';
2
+
3
+ it('throws when created from invalid code', () => {
4
+ // @ts-expect-error
5
+ expect(() => new LocaleObject('en_US')).toThrow('Invalid locale code provided!');
6
+ // @ts-expect-error
7
+ expect(() => new LocaleObject('1234')).toThrow('Invalid locale code provided!');
8
+ // @ts-expect-error
9
+ expect(() => new LocaleObject('*!en-US')).toThrow('Invalid locale code provided!');
10
+ });
11
+
12
+ it('throws when created with unsupported code', () => {
13
+ const unsupportedCode = 'unknown-code';
14
+ expect(() => LocaleObject.fromAnyCode(unsupportedCode)).toThrow(
15
+ `Unsupported locale code provided: "${unsupportedCode}"`,
16
+ );
17
+ });
18
+
19
+ it('accepts any valid code type and converts it to hyphen code consistently', () => {
20
+ expect(LocaleObject.fromAnyCode('en').toHyphenCode()).toBe('en');
21
+ expect(LocaleObject.fromAnyCode('en-us').toHyphenCode()).toBe('en-US');
22
+ expect(LocaleObject.fromAnyCode('en-US').toHyphenCode()).toBe('en-US');
23
+ expect(LocaleObject.fromAnyCode('EN-US').toHyphenCode()).toBe('en-US');
24
+ expect(LocaleObject.fromAnyCode('en_us').toHyphenCode()).toBe('en-US');
25
+ expect(LocaleObject.fromAnyCode('en_US').toHyphenCode()).toBe('en-US');
26
+ expect(LocaleObject.fromAnyCode('EN_US').toHyphenCode()).toBe('en-US');
27
+
28
+ expect(LocaleObject.fromAnyCode('es-419').toHyphenCode()).toBe('es-419');
29
+ expect(LocaleObject.fromAnyCode('es_419').toHyphenCode()).toBe('es-419');
30
+ });
31
+
32
+ it('converts to underscore code consistently', () => {
33
+ expect(LocaleObject.fromAnyCode('en').toUnderscoreCode()).toBe('en');
34
+ expect(LocaleObject.fromAnyCode('en-us').toUnderscoreCode()).toBe('en_US');
35
+ expect(LocaleObject.fromAnyCode('en-US').toUnderscoreCode()).toBe('en_US');
36
+ expect(LocaleObject.fromAnyCode('EN-US').toUnderscoreCode()).toBe('en_US');
37
+ expect(LocaleObject.fromAnyCode('en_us').toUnderscoreCode()).toBe('en_US');
38
+ expect(LocaleObject.fromAnyCode('en_US').toUnderscoreCode()).toBe('en_US');
39
+ expect(LocaleObject.fromAnyCode('EN_US').toUnderscoreCode()).toBe('en_US');
40
+ });
41
+
42
+ it('converts to URL slug consistently', () => {
43
+ expect(LocaleObject.fromAnyCode('en').toUrlSlug()).toBe('en');
44
+ expect(LocaleObject.fromAnyCode('en-us').toUrlSlug()).toBe('en-us');
45
+ expect(LocaleObject.fromAnyCode('en-US').toUrlSlug()).toBe('en-us');
46
+ expect(LocaleObject.fromAnyCode('EN-US').toUrlSlug()).toBe('en-us');
47
+ expect(LocaleObject.fromAnyCode('en_us').toUrlSlug()).toBe('en-us');
48
+ expect(LocaleObject.fromAnyCode('en_US').toUrlSlug()).toBe('en-us');
49
+ expect(LocaleObject.fromAnyCode('EN_US').toUrlSlug()).toBe('en-us');
50
+ });
51
+
52
+ it('converts to neutral language code consistently', () => {
53
+ expect(LocaleObject.fromAnyCode('en').toNeutralLanguageCode()).toBe('en');
54
+ expect(LocaleObject.fromAnyCode('en-us').toNeutralLanguageCode()).toBe('en');
55
+ expect(LocaleObject.fromAnyCode('en-US').toNeutralLanguageCode()).toBe('en');
56
+ expect(LocaleObject.fromAnyCode('EN-US').toNeutralLanguageCode()).toBe('en');
57
+ expect(LocaleObject.fromAnyCode('en_us').toNeutralLanguageCode()).toBe('en');
58
+ expect(LocaleObject.fromAnyCode('en_US').toNeutralLanguageCode()).toBe('en');
59
+ expect(LocaleObject.fromAnyCode('EN_US').toNeutralLanguageCode()).toBe('en');
60
+ });
61
+
62
+ it('converts to region code consistently', () => {
63
+ expect(LocaleObject.fromAnyCode('en').toRegionCode()).toBe('EN');
64
+ expect(LocaleObject.fromAnyCode('en-us').toRegionCode()).toBe('US');
65
+ expect(LocaleObject.fromAnyCode('en-US').toRegionCode()).toBe('US');
66
+ expect(LocaleObject.fromAnyCode('EN-US').toRegionCode()).toBe('US');
67
+ expect(LocaleObject.fromAnyCode('en_us').toRegionCode()).toBe('US');
68
+ expect(LocaleObject.fromAnyCode('en_US').toRegionCode()).toBe('US');
69
+ expect(LocaleObject.fromAnyCode('EN_US').toRegionCode()).toBe('US');
70
+ });
71
+
72
+ it('can compare two objects', () => {
73
+ const enUsLocale = LocaleObject.fromAnyCode('en-US');
74
+ const enUsLocaleDuplicate = LocaleObject.fromAnyCode('en-US');
75
+ const deDeLocale = LocaleObject.fromAnyCode('de-DE');
76
+
77
+ expect(enUsLocale.isEqual(enUsLocaleDuplicate)).toBe(true);
78
+ expect(enUsLocaleDuplicate.isEqual(enUsLocale)).toBe(true);
79
+ expect(enUsLocale.isEqual(deDeLocale)).toBe(false);
80
+ expect(deDeLocale.isEqual(enUsLocale)).toBe(false);
81
+ });