lightnet 4.0.8 → 4.1.1

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 (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/exports/content.ts +5 -7
  3. package/package.json +11 -11
  4. package/src/astro-integration/config.ts +15 -25
  5. package/src/components/CategoriesSection.astro +2 -2
  6. package/src/components/MediaGallerySection.astro +4 -9
  7. package/src/components/MediaList.astro +13 -13
  8. package/src/components/VideoPlayer.astro +4 -4
  9. package/src/content/content-schema.ts +5 -292
  10. package/src/content/get-categories.ts +26 -29
  11. package/src/content/get-languages.ts +13 -26
  12. package/src/content/get-media-collections.ts +1 -1
  13. package/src/content/get-media-items.ts +1 -1
  14. package/src/content/get-media-types.ts +21 -14
  15. package/src/content/query-media-items.ts +2 -1
  16. package/src/content/schema/category.ts +40 -0
  17. package/src/content/schema/media-collection.ts +31 -0
  18. package/src/content/schema/media-item.ts +137 -0
  19. package/src/content/schema/media-type.ts +90 -0
  20. package/src/i18n/locals.d.ts +22 -0
  21. package/src/i18n/locals.ts +3 -1
  22. package/src/i18n/record-translation.ts +74 -0
  23. package/src/i18n/resolve-language.ts +12 -5
  24. package/src/i18n/translate-map.ts +129 -19
  25. package/src/i18n/translate.ts +38 -0
  26. package/src/i18n/translation-map-schema.ts +17 -0
  27. package/src/i18n/translations/TRANSLATION-STATUS.md +13 -41
  28. package/src/i18n/translations/ar.yml +1 -0
  29. package/src/i18n/translations/bn.yml +1 -0
  30. package/src/i18n/translations/de.yml +1 -0
  31. package/src/i18n/translations/en.yml +24 -21
  32. package/src/i18n/translations/es.yml +1 -0
  33. package/src/i18n/translations/fi.yml +1 -0
  34. package/src/i18n/translations/fr.yml +1 -0
  35. package/src/i18n/translations/hi.yml +1 -0
  36. package/src/i18n/translations/kk.yml +3 -0
  37. package/src/i18n/translations/pt.yml +1 -0
  38. package/src/i18n/translations/ru.yml +1 -0
  39. package/src/i18n/translations/uk.yml +1 -0
  40. package/src/i18n/translations/ur.yml +1 -0
  41. package/src/i18n/translations/zh.yml +1 -0
  42. package/src/i18n/translations.ts +5 -2
  43. package/src/layouts/Page.astro +3 -4
  44. package/src/layouts/components/Footer.astro +63 -10
  45. package/src/layouts/components/LanguagePicker.astro +2 -2
  46. package/src/layouts/components/MenuItem.astro +4 -4
  47. package/src/layouts/components/PageNavigation.astro +21 -29
  48. package/src/layouts/components/PageTitle.astro +4 -13
  49. package/src/pages/details-page/DefaultDetailsPage.astro +2 -15
  50. package/src/pages/details-page/components/AudioPanel.astro +5 -4
  51. package/src/pages/details-page/components/AudioPlayer.astro +4 -4
  52. package/src/pages/details-page/components/ContentSection.astro +42 -43
  53. package/src/pages/details-page/components/MediaCollection.astro +2 -6
  54. package/src/pages/details-page/components/main-details/OpenButton.astro +25 -19
  55. package/src/pages/details-page/components/main-details/ShareButton.astro +1 -1
  56. package/src/pages/details-page/components/more-details/Categories.astro +3 -5
  57. package/src/pages/details-page/components/more-details/Languages.astro +7 -3
  58. package/src/pages/details-page/utils/create-content-metadata.ts +40 -56
  59. package/src/pages/search-page/api/search.ts +1 -1
  60. package/src/pages/search-page/components/SearchFilter.astro +5 -5
  61. package/src/pages/search-page/components/SearchList.astro +22 -19
  62. package/src/utils/is-external-url.ts +34 -0
  63. package/src/utils/link-attributes.ts +10 -0
  64. package/src/utils/paths.ts +6 -0
  65. package/src/utils/urls.ts +12 -24
  66. package/src/astro-integration/validators/validate-inline-translations.ts +0 -51
@@ -1,8 +1,4 @@
1
- import {
2
- type TranslateMapFn,
3
- type TranslationMap,
4
- } from "../../../i18n/translate-map"
5
- import { isExternalUrl } from "../../../utils/urls"
1
+ import { isExternalUrl } from "../../../utils/is-external-url"
6
2
 
7
3
  export type UrlType =
8
4
  | "link"
@@ -15,49 +11,45 @@ export type UrlType =
15
11
 
16
12
  const KNOWN_EXTENSIONS: Record<
17
13
  string,
18
- { type: UrlType; canBeOpened?: boolean } | undefined
14
+ { type: UrlType; isDownload?: boolean } | undefined
19
15
  > = {
20
- htm: { type: "link", canBeOpened: true },
21
- html: { type: "link", canBeOpened: true },
22
- php: { type: "link", canBeOpened: true },
23
- json: { type: "source", canBeOpened: true },
24
- xml: { type: "source", canBeOpened: true },
25
- md: { type: "source", canBeOpened: true },
26
- svg: { type: "image", canBeOpened: true },
27
- jpg: { type: "image", canBeOpened: true },
28
- jpeg: { type: "image", canBeOpened: true },
29
- png: { type: "image", canBeOpened: true },
30
- gif: { type: "image", canBeOpened: true },
31
- ico: { type: "image", canBeOpened: true },
32
- webp: { type: "image", canBeOpened: true },
33
- mp3: { type: "audio", canBeOpened: true },
34
- wav: { type: "audio", canBeOpened: true },
35
- aac: { type: "audio", canBeOpened: true },
36
- ogg: { type: "audio", canBeOpened: true },
37
- mp4: { type: "video", canBeOpened: true },
38
- webm: { type: "video", canBeOpened: true },
39
- ogv: { type: "video", canBeOpened: true },
40
- pdf: { type: "text", canBeOpened: true },
41
- txt: { type: "text", canBeOpened: true },
42
- epub: { type: "text" },
43
- zip: { type: "package" },
44
- ppt: { type: "text" },
45
- pptx: { type: "text" },
46
- doc: { type: "text" },
47
- docx: { type: "text" },
16
+ htm: { type: "link" },
17
+ html: { type: "link" },
18
+ php: { type: "link" },
19
+ json: { type: "source" },
20
+ xml: { type: "source" },
21
+ md: { type: "source" },
22
+ svg: { type: "image" },
23
+ jpg: { type: "image" },
24
+ jpeg: { type: "image" },
25
+ png: { type: "image" },
26
+ gif: { type: "image" },
27
+ ico: { type: "image" },
28
+ webp: { type: "image" },
29
+ mp3: { type: "audio" },
30
+ wav: { type: "audio" },
31
+ aac: { type: "audio" },
32
+ ogg: { type: "audio" },
33
+ mp4: { type: "video" },
34
+ webm: { type: "video" },
35
+ ogv: { type: "video" },
36
+ pdf: { type: "text" },
37
+ txt: { type: "text" },
38
+ epub: { type: "text", isDownload: true },
39
+ zip: { type: "package", isDownload: true },
40
+ ppt: { type: "text", isDownload: true },
41
+ pptx: { type: "text", isDownload: true },
42
+ doc: { type: "text", isDownload: true },
43
+ docx: { type: "text", isDownload: true },
48
44
  } as const
49
45
 
50
- export function createContentMetadata(
51
- {
52
- url,
53
- label: customLabel,
54
- }: {
55
- url: string
56
- label?: TranslationMap
57
- },
58
- tMap: TranslateMapFn,
59
- context: { path: (string | number)[] },
60
- ) {
46
+ export function createContentMetadata({
47
+ url,
48
+ labelText: customLabel,
49
+ }: {
50
+ url: string
51
+ labelText?: string
52
+ }) {
61
53
  const isExternal = isExternalUrl(url)
62
54
  const path = isExternal ? new URL(url).pathname : url
63
55
 
@@ -72,24 +64,16 @@ export function createContentMetadata(
72
64
  ? lastPathSegment.slice(0, -(extension.length + 1))
73
65
  : undefined
74
66
 
75
- const labelText =
76
- (customLabel &&
77
- tMap(customLabel, {
78
- path: [...context.path, "label"],
79
- })) ??
80
- fileName ??
81
- linkName
67
+ const labelText = customLabel ?? fileName ?? linkName
82
68
  const type = KNOWN_EXTENSIONS[extension]?.type ?? "link"
83
- const canBeOpened =
84
- !hasExtension || !!KNOWN_EXTENSIONS[extension]?.canBeOpened
69
+ const isDownload = KNOWN_EXTENSIONS[extension]?.isDownload
85
70
 
86
71
  return {
87
72
  url,
88
73
  extension,
89
74
  isExternal,
90
75
  labelText,
91
- canBeOpened,
76
+ isDownload,
92
77
  type,
93
- target: isExternal ? "_blank" : "_self",
94
78
  } as const
95
79
  }
@@ -1,8 +1,8 @@
1
1
  import type { APIRoute } from "astro"
2
2
  import { getImage } from "astro:assets"
3
3
 
4
- import type { MediaItemEntry } from "../../../content/content-schema"
5
4
  import { getMediaItems } from "../../../content/get-media-items"
5
+ import type { MediaItemEntry } from "../../../content/schema/media-item"
6
6
  import { markdownToText } from "../../../utils/markdown"
7
7
  import type { SearchItem } from "./search-response"
8
8
 
@@ -2,22 +2,22 @@
2
2
  import config from "virtual:lightnet/config"
3
3
 
4
4
  import { getUsedCategories } from "../../../content/get-categories"
5
- import { getUsedLanguages } from "../../../content/get-languages"
5
+ import { getContentLanguages } from "../../../content/get-languages"
6
6
  import { getUsedMediaTypes } from "../../../content/get-media-types"
7
7
  import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
8
8
  import SearchFilterReact from "./SearchFilter.tsx"
9
9
 
10
- const { currentLocale, tMap } = Astro.locals.i18n
10
+ const { currentLocale, tConfigField, tContentField } = Astro.locals.i18n
11
11
 
12
- const categories = (await getUsedCategories(currentLocale, tMap)).map(
12
+ const categories = (await getUsedCategories(currentLocale, tContentField)).map(
13
13
  ({ id, labelText }) => ({ id, labelText }),
14
14
  )
15
15
 
16
- const mediaTypes = (await getUsedMediaTypes(currentLocale, tMap)).map(
16
+ const mediaTypes = (await getUsedMediaTypes(currentLocale, tContentField)).map(
17
17
  ({ id, labelText }) => ({ id, labelText }),
18
18
  )
19
19
 
20
- const languages = (await getUsedLanguages(currentLocale, tMap)).map(
20
+ const languages = (await getContentLanguages(currentLocale, tConfigField)).map(
21
21
  ({ code: id, labelText }) => ({ id, labelText }),
22
22
  )
23
23
 
@@ -2,42 +2,45 @@
2
2
  import { getCollection } from "astro:content"
3
3
 
4
4
  import { getUsedCategories } from "../../../content/get-categories"
5
- import { getUsedLanguages } from "../../../content/get-languages"
6
- import { getMediaTypes } from "../../../content/get-media-types"
5
+ import { getContentLanguages } from "../../../content/get-languages"
6
+ import { getTranslatedMediaTypes } from "../../../content/get-media-types"
7
7
  import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
8
8
  import SearchListReact from "./SearchList.tsx"
9
9
 
10
- const { currentLocale, tMap } = Astro.locals.i18n
10
+ const { currentLocale, tContentField, tConfigField } = Astro.locals.i18n
11
11
 
12
12
  const categories: Record<string, string> = {}
13
- for (const { id, labelText } of await getUsedCategories(currentLocale, tMap)) {
13
+ for (const { id, labelText } of await getUsedCategories(
14
+ currentLocale,
15
+ tContentField,
16
+ )) {
14
17
  categories[id] = labelText
15
18
  }
16
19
 
17
20
  const mediaTypes = Object.fromEntries(
18
- (await getMediaTypes()).map((type) => [
19
- type.id,
20
- {
21
- labelText: tMap(type.data.label, {
22
- path: ["media-types", type.id, "label"],
23
- }),
24
- icon: type.data.icon,
25
- coverImageStyle: type.data.coverImageStyle,
26
- },
27
- ]),
21
+ (await getTranslatedMediaTypes(currentLocale, tContentField)).map(
22
+ ({ labelText, icon, coverImageStyle, id }) => [
23
+ id,
24
+ {
25
+ labelText,
26
+ icon,
27
+ coverImageStyle,
28
+ },
29
+ ],
30
+ ),
28
31
  )
29
32
 
30
- const contentLanguagesList = await getUsedLanguages(currentLocale, tMap)
33
+ const contentLanguages = await getContentLanguages(currentLocale, tConfigField)
31
34
  const languages = Object.fromEntries(
32
- contentLanguagesList.map((language) => [
33
- language.code,
34
- { direction: language.direction, labelText: language.labelText },
35
+ contentLanguages.map(({ code, direction, labelText }) => [
36
+ code,
37
+ { direction, labelText },
35
38
  ]),
36
39
  )
37
40
 
38
41
  const mediaItemsTotal = (await getCollection("media")).length
39
42
 
40
- const showLanguage = contentLanguagesList.length > 1
43
+ const showLanguage = contentLanguages.length > 1
41
44
 
42
45
  const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
43
46
  "ln.search.no-results",
@@ -0,0 +1,34 @@
1
+ import config from "virtual:lightnet/config"
2
+
3
+ import { parseUrl } from "./urls"
4
+
5
+ /**
6
+ * Test if a given url is outside this site.
7
+ * Will return false if the url is relative or if it
8
+ * starts with the site config from astro config.
9
+ *
10
+ * @param url to test
11
+ * @returns is the url external?
12
+ */
13
+ export function isExternalUrl(url: string) {
14
+ const parsedUrl = parseUrl(url)
15
+ if (!parsedUrl) {
16
+ return false
17
+ }
18
+
19
+ if (config.internalDomains.includes(parsedUrl.hostname)) {
20
+ return false
21
+ }
22
+
23
+ const { SITE: site } = import.meta.env
24
+ if (!site) {
25
+ return true
26
+ }
27
+
28
+ const parsedSiteUrl = parseUrl(site)
29
+ if (!parsedSiteUrl) {
30
+ return true
31
+ }
32
+
33
+ return !parsedUrl.href.startsWith(parsedSiteUrl.href)
34
+ }
@@ -0,0 +1,10 @@
1
+ import { isExternalUrl } from "./is-external-url"
2
+
3
+ export function getLinkAttributes(href: string) {
4
+ const isExternal = isExternalUrl(href)
5
+ return {
6
+ href,
7
+ target: isExternal ? "_blank" : "_self",
8
+ rel: isExternal ? "noopener noreferrer" : undefined,
9
+ }
10
+ }
@@ -1,3 +1,5 @@
1
+ import { isAbsoluteUrl } from "./urls"
2
+
1
3
  /**
2
4
  * Prefix a site-internal path with Astro's configured base path.
3
5
  *
@@ -91,6 +93,10 @@ export function searchPagePath(
91
93
  * @returns resolved path. Eg. '/en/about' for input "en" and "/about"
92
94
  */
93
95
  export function localizePath(locale: string | undefined, path: string) {
96
+ if (isAbsoluteUrl(path)) {
97
+ return path
98
+ }
99
+
94
100
  return pathWithBase(
95
101
  `${locale ? `/${locale}` : ""}/${path.replace(/^\//, "")}`,
96
102
  )
package/src/utils/urls.ts CHANGED
@@ -1,28 +1,16 @@
1
- import config from "virtual:lightnet/config"
2
-
3
- /**
4
- * Test if a given url is outside this site.
5
- * Will return false if the url is relative or if it
6
- * starts with the site config from astro config.
7
- *
8
- * @param url to test
9
- * @returns is the url external?
10
- */
11
- export function isExternalUrl(url: string) {
12
- let parsedUrl
1
+ export function parseUrl(url: string) {
13
2
  try {
14
- // test if url is absolute
15
- parsedUrl = new URL(url)
3
+ return new URL(url)
16
4
  } catch {
17
- // url is relative
18
- return false
19
- }
20
- if (config.internalDomains.includes(parsedUrl.hostname)) {
21
- return false
22
- }
23
- const { SITE: site } = import.meta.env
24
- if (!site) {
25
- return true
5
+ // Support host-like values such as `example.com` without treating plain paths as external.
6
+ if (/^(localhost(?::\d+)?|[^/\s]+\.[^/\s]+)(?:[/?#]|$)/.test(url)) {
7
+ return new URL(`https://${url}`)
8
+ }
9
+
10
+ return null
26
11
  }
27
- return !url.startsWith(site)
12
+ }
13
+
14
+ export function isAbsoluteUrl(url: string) {
15
+ return !!parseUrl(url)
28
16
  }
@@ -1,51 +0,0 @@
1
- import { z } from "astro/zod"
2
-
3
- export const validateInlineTranslations = (
4
- config: {
5
- title: Record<string, string>
6
- languages: { label: Record<string, string> }[]
7
- mainMenu?: { label: Record<string, string> }[]
8
- logo?: { alt?: Record<string, string> }
9
- },
10
- locales: string[],
11
- defaultLocale: string,
12
- ctx: z.RefinementCtx,
13
- ) => {
14
- const validateInlineTranslation = (
15
- inlineTranslation: Record<string, string> | undefined,
16
- path: (string | number)[],
17
- ) => {
18
- if (!inlineTranslation) {
19
- return
20
- }
21
-
22
- if (!(defaultLocale in inlineTranslation)) {
23
- ctx.addIssue({
24
- code: "custom",
25
- message: `Missing translation for default locale "${defaultLocale}"`,
26
- path: [...path, defaultLocale],
27
- })
28
- }
29
-
30
- for (const locale of Object.keys(inlineTranslation)) {
31
- if (locales.includes(locale)) {
32
- continue
33
- }
34
-
35
- ctx.addIssue({
36
- code: "custom",
37
- message: `Invalid locale "${locale}". Inline translations only support configured site locales: ${locales.join(", ")}`,
38
- path: [...path, locale],
39
- })
40
- }
41
- }
42
-
43
- validateInlineTranslation(config.title, ["title"])
44
- for (const [index, language] of config.languages.entries()) {
45
- validateInlineTranslation(language.label, ["languages", index, "label"])
46
- }
47
- for (const [index, link] of (config.mainMenu ?? []).entries()) {
48
- validateInlineTranslation(link.label, ["mainMenu", index, "label"])
49
- }
50
- validateInlineTranslation(config.logo?.alt, ["logo", "alt"])
51
- }