lightnet 3.12.2 → 4.0.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 (111) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/exports/content.ts +7 -2
  3. package/exports/i18n.ts +0 -1
  4. package/exports/index.ts +1 -5
  5. package/exports/utils.ts +0 -1
  6. package/package.json +26 -12
  7. package/src/astro-integration/config.ts +60 -49
  8. package/src/astro-integration/integration.ts +13 -24
  9. package/src/astro-integration/tailwind.ts +86 -0
  10. package/src/astro-integration/validators/validate-inline-translations.ts +51 -0
  11. package/src/astro-integration/validators/validate-languages.ts +39 -0
  12. package/src/astro-integration/virtual.d.ts +8 -6
  13. package/src/astro-integration/vite-plugin-lightnet-config.ts +29 -9
  14. package/src/components/CarouselSection.astro +7 -11
  15. package/src/components/CategoriesSection.astro +2 -2
  16. package/src/components/HighlightSection.astro +4 -7
  17. package/src/components/Icon.tsx +2 -2
  18. package/src/components/MediaGallerySection.astro +88 -68
  19. package/src/components/MediaList.astro +9 -7
  20. package/src/components/SearchInput.astro +7 -4
  21. package/src/components/Section.astro +7 -5
  22. package/src/components/VideoPlayer.astro +2 -3
  23. package/src/content/content-schema.ts +129 -142
  24. package/src/content/get-categories.ts +52 -28
  25. package/src/content/get-languages.ts +29 -8
  26. package/src/content/get-media-collections.ts +43 -0
  27. package/src/content/get-media-types.ts +41 -7
  28. package/src/content/query-media-items.ts +23 -13
  29. package/src/i18n/bcp-47.ts +8 -0
  30. package/src/i18n/get-locale-paths.ts +1 -3
  31. package/src/i18n/locals.d.ts +21 -3
  32. package/src/i18n/locals.ts +18 -11
  33. package/src/i18n/resolve-current-locale.ts +18 -0
  34. package/src/i18n/resolve-language.ts +10 -5
  35. package/src/i18n/translate-map.ts +70 -0
  36. package/src/i18n/translate.ts +68 -47
  37. package/src/layouts/Page.astro +5 -3
  38. package/src/layouts/components/LanguagePicker.astro +22 -17
  39. package/src/layouts/components/Menu.astro +2 -5
  40. package/src/layouts/components/MenuItem.astro +1 -1
  41. package/src/layouts/components/PageNavigation.astro +29 -29
  42. package/src/layouts/components/PageTitle.astro +23 -7
  43. package/src/pages/404Route.astro +2 -1
  44. package/src/pages/RootRoute.astro +6 -1
  45. package/src/pages/details-page/DefaultDetailsPage.astro +9 -2
  46. package/src/pages/details-page/DetailsPageRoute.astro +1 -2
  47. package/src/pages/details-page/components/AudioPanel.astro +7 -3
  48. package/src/pages/details-page/components/AudioPlayer.astro +2 -2
  49. package/src/pages/details-page/components/ContentSection.astro +67 -44
  50. package/src/pages/details-page/components/MediaCollection.astro +8 -4
  51. package/src/pages/details-page/components/MediaCollectionsSection.astro +3 -6
  52. package/src/pages/details-page/components/main-details/EditButton.astro +22 -10
  53. package/src/pages/details-page/components/main-details/OpenButton.astro +17 -12
  54. package/src/pages/details-page/components/main-details/ShareButton.astro +3 -2
  55. package/src/pages/details-page/components/more-details/Categories.astro +5 -3
  56. package/src/pages/details-page/components/more-details/Languages.astro +12 -7
  57. package/src/pages/details-page/utils/create-content-metadata.ts +24 -9
  58. package/src/pages/details-page/utils/get-translations.ts +6 -0
  59. package/src/pages/search-page/components/LoadingSkeleton.tsx +6 -5
  60. package/src/pages/search-page/components/SearchFilter.astro +10 -21
  61. package/src/pages/search-page/components/SearchFilter.tsx +2 -2
  62. package/src/pages/search-page/components/SearchList.astro +10 -7
  63. package/src/pages/search-page/components/SearchListItem.tsx +5 -4
  64. package/src/pages/search-page/hooks/use-search.ts +5 -2
  65. package/src/utils/lazy.ts +20 -0
  66. package/src/utils/paths.ts +40 -3
  67. package/src/utils/urls.ts +1 -2
  68. package/src/utils/verify-schema.ts +12 -10
  69. package/tailwind.config.ts +1 -25
  70. package/__e2e__/admin.spec.ts +0 -20
  71. package/__e2e__/basics-fixture.ts +0 -77
  72. package/__e2e__/fixtures/basics/astro.config.mjs +0 -40
  73. package/__e2e__/fixtures/basics/node_modules/.bin/astro +0 -21
  74. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +0 -21
  75. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +0 -21
  76. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +0 -21
  77. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +0 -21
  78. package/__e2e__/fixtures/basics/package.json +0 -21
  79. package/__e2e__/fixtures/basics/public/favicon.svg +0 -1
  80. package/__e2e__/fixtures/basics/public/files/example.pdf +0 -0
  81. package/__e2e__/fixtures/basics/src/assets/logo.png +0 -0
  82. package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +0 -3
  83. package/__e2e__/fixtures/basics/src/content/categories/teens.json +0 -3
  84. package/__e2e__/fixtures/basics/src/content/categories/theology.json +0 -3
  85. package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +0 -13
  86. package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +0 -12
  87. package/__e2e__/fixtures/basics/src/content/media/images/cover.jpg +0 -0
  88. package/__e2e__/fixtures/basics/src/content/media/images/how-to-kickflip--en.webp +0 -0
  89. package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +0 -15
  90. package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +0 -3
  91. package/__e2e__/fixtures/basics/src/content/media-types/audio.json +0 -7
  92. package/__e2e__/fixtures/basics/src/content/media-types/book.json +0 -9
  93. package/__e2e__/fixtures/basics/src/content/media-types/video.json +0 -7
  94. package/__e2e__/fixtures/basics/src/content.config.ts +0 -3
  95. package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +0 -16
  96. package/__e2e__/fixtures/basics/src/translations/de.yml +0 -9
  97. package/__e2e__/fixtures/basics/src/translations/en.yml +0 -9
  98. package/__e2e__/fixtures/basics/tailwind.config.mjs +0 -8
  99. package/__e2e__/global.teardown.ts +0 -5
  100. package/__e2e__/homepage.spec.ts +0 -123
  101. package/__e2e__/search.spec.ts +0 -14
  102. package/__tests__/pages/details-page/create-content-metadata.spec.ts +0 -135
  103. package/__tests__/utils/markdown.spec.ts +0 -74
  104. package/__tests__/utils/urls.spec.ts +0 -27
  105. package/playwright.config.ts +0 -31
  106. package/src/astro-integration/project-context.ts +0 -5
  107. package/src/content/compare-media-collection-items.ts +0 -24
  108. package/src/i18n/resolve-default-locale.ts +0 -19
  109. package/src/i18n/resolve-locales.ts +0 -5
  110. package/src/pages/details-page/utils/get-collection-items.ts +0 -29
  111. package/vitest.config.js +0 -20
@@ -14,33 +14,49 @@ function getWidth(logo: ImageMetadata) {
14
14
  return Math.floor(size * Math.max(1, logo.width / logo.height))
15
15
  }
16
16
 
17
- const logoAlt =
18
- config.logo?.alt ?? (config.logo?.replacesTitle ? config.title : "")
17
+ const { currentLocale, tMap } = Astro.locals.i18n
19
18
 
20
- const { t } = Astro.locals.i18n
19
+ const logoAlt = (() => {
20
+ if (config.logo?.alt) {
21
+ return tMap(config.logo.alt, {
22
+ path: ["config", "logo", "alt"],
23
+ })
24
+ }
25
+ if (config.logo?.replacesTitle) {
26
+ return tMap(config.title, {
27
+ path: ["config", "title"],
28
+ })
29
+ }
30
+ return ""
31
+ })()
21
32
  ---
22
33
 
23
34
  <a
24
35
  class="flex items-center gap-3 font-bold text-gray-600 md:text-lg"
25
- href={localizePath(Astro.currentLocale, "/")}
36
+ href={localizePath(currentLocale, "/")}
26
37
  >
27
38
  {
28
39
  logo &&
29
40
  (logo.format === "svg" ? (
30
41
  <img
31
42
  src={logo.src}
32
- alt={t(logoAlt)}
43
+ alt={logoAlt}
33
44
  style={`width:${getWidth(logo)}px;`}
34
45
  />
35
46
  ) : (
36
47
  <Image
37
48
  src={logo}
38
- alt={t(logoAlt)}
49
+ alt={logoAlt}
39
50
  style={`width:${getWidth(logo)}px;`}
40
51
  width={getWidth(logo)}
41
52
  densities={[1.5, 2, 3]}
42
53
  loading="eager"
43
54
  />
44
55
  ))
45
- }{!config.logo?.replacesTitle && t(config.title)}</a
56
+ }{
57
+ !config.logo?.replacesTitle &&
58
+ tMap(config.title, {
59
+ path: ["config", "title"],
60
+ })
61
+ }</a
46
62
  >
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Page from "../layouts/Page.astro"
3
+ import { localizePath } from "../utils/paths"
3
4
  ---
4
5
 
5
6
  <Page>
@@ -8,7 +9,7 @@ import Page from "../layouts/Page.astro"
8
9
  {Astro.locals.i18n.t("ln.404.page-not-found")}
9
10
  </p>
10
11
  <a
11
- href={`/${Astro.locals.i18n.defaultLocale}`}
12
+ href={localizePath(Astro.locals.i18n.defaultLocale, "/")}
12
13
  class="rounded-2xl bg-gray-200 px-8 py-4 text-sm font-bold transition-colors ease-in-out hover:bg-gray-400"
13
14
  >{Astro.locals.i18n.t("ln.404.go-to-the-home-page")}</a
14
15
  >
@@ -1,3 +1,8 @@
1
1
  ---
2
- return Astro.rewrite(`/${Astro.locals.i18n.defaultLocale}`)
2
+ return Astro.rewrite(
3
+ (await import("../utils/paths")).localizePath(
4
+ Astro.locals.i18n.defaultLocale,
5
+ "/",
6
+ ),
7
+ )
3
8
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import type { TranslationMap } from "../../i18n/translate-map"
2
3
  import ContentSection from "./components/ContentSection.astro"
3
4
  import DescriptionSection from "./components/DescriptionSection.astro"
4
5
  import DetailsPage from "./components/DetailsPage.astro"
@@ -9,10 +10,16 @@ import MoreDetailsSection from "./components/MoreDetailsSection.astro"
9
10
 
10
11
  export type Props = {
11
12
  mediaId: string
12
- openActionLabel?: string
13
+ openActionLabel?: TranslationMap
13
14
  }
14
15
 
15
- const { mediaId, openActionLabel = "ln.details.open" } = Astro.props
16
+ const defaultOpenActionLabel = Object.fromEntries(
17
+ Astro.locals.i18n.locales.map((locale) => [
18
+ locale,
19
+ Astro.locals.i18n.t("ln.details.open", { lng: locale }),
20
+ ]),
21
+ )
22
+ const { mediaId, openActionLabel = defaultOpenActionLabel } = Astro.props
16
23
  ---
17
24
 
18
25
  <DetailsPage mediaId={mediaId}>
@@ -6,14 +6,13 @@ import config from "virtual:lightnet/config"
6
6
 
7
7
  import { getMediaItem } from "../../content/get-media-items"
8
8
  import { getMediaType } from "../../content/get-media-types"
9
- import { resolveLocales } from "../../i18n/resolve-locales"
10
9
  import AudioDetailsPage from "./AudioDetailsPage.astro"
11
10
  import DefaultDetailsPage from "./DefaultDetailsPage.astro"
12
11
  import VideoDetailsPage from "./VideoDetailsPage.astro"
13
12
 
14
13
  export const getStaticPaths = (async () => {
15
14
  const mediaItems = await getCollection("media")
16
- return resolveLocales(config).flatMap((locale) =>
15
+ return config.locales.flatMap((locale) =>
17
16
  mediaItems.map(({ id: mediaId }) => ({ params: { mediaId, locale } })),
18
17
  )
19
18
  }) satisfies GetStaticPaths
@@ -9,11 +9,15 @@ interface Props {
9
9
  }
10
10
 
11
11
  const { mediaId, className } = Astro.props
12
- const { t } = Astro.locals.i18n
12
+ const { tMap } = Astro.locals.i18n
13
13
 
14
14
  const item = await getMediaItem(mediaId)
15
15
 
16
- const content = item.data.content.map((c) => createContentMetadata(c))
16
+ const content = item.data.content.map((contentItem, index) =>
17
+ createContentMetadata(contentItem, tMap, {
18
+ path: ["media-items", mediaId, "content", index],
19
+ }),
20
+ )
17
21
  ---
18
22
 
19
23
  <ol
@@ -24,7 +28,7 @@ const content = item.data.content.map((c) => createContentMetadata(c))
24
28
  {
25
29
  content.map((c) => (
26
30
  <li>
27
- {content.length > 1 && <div class="mb-3 font-bold">{t(c.label)}</div>}
31
+ {content.length > 1 && <div class="mb-3 font-bold">{c.labelText}</div>}
28
32
  <AudioPlayer className="w-full" src={c.url} />
29
33
  </li>
30
34
  ))
@@ -1,5 +1,5 @@
1
1
  ---
2
- import Icon from "../../../components/Icon"
2
+ import { ExternalLinkIcon } from "lucide-react"
3
3
 
4
4
  type Props = {
5
5
  src: string
@@ -37,7 +37,7 @@ const isMp3 = audioExtension.endsWith(".mp3")
37
37
  </span>
38
38
  <div class="flex h-full grow flex-col gap-3">
39
39
  <span class="flex items-center gap-2 font-semibold text-gray-900">
40
- <Icon className="mdi--external-link" ariaLabel="" />
40
+ <ExternalLinkIcon className="shrink-0" />
41
41
  {t("ln.external-link")}
42
42
  </span>
43
43
  <div class="h-2 w-full rounded-full bg-gray-200">
@@ -1,5 +1,17 @@
1
1
  ---
2
- import Icon from "../../../components/Icon"
2
+ import {
3
+ ChevronRightIcon,
4
+ CodeIcon,
5
+ DownloadIcon,
6
+ ImageIcon,
7
+ LinkIcon,
8
+ type LucideIcon,
9
+ MusicIcon,
10
+ PackageIcon,
11
+ TextIcon,
12
+ VideoIcon,
13
+ } from "lucide-react"
14
+
3
15
  import { getMediaItem } from "../../../content/get-media-items"
4
16
  import {
5
17
  createContentMetadata,
@@ -18,19 +30,23 @@ const item = await getMediaItem(mediaId)
18
30
  if (item.data.content.length < minimumItems) {
19
31
  return
20
32
  }
33
+ const { t, tMap, direction } = Astro.locals.i18n
21
34
 
22
- const content = item.data.content.map((c) => createContentMetadata(c))
23
- const typeIcons: { [k in UrlType]: string } = {
24
- audio: "mdi--music",
25
- text: "mdi--text",
26
- source: "mdi--code-tags",
27
- link: "mdi--link-variant",
28
- image: "mdi--image-outline",
29
- video: "mdi--video-outline",
30
- package: "mdi--zip-box-outline",
35
+ const content = item.data.content.map((contentItem, index) =>
36
+ createContentMetadata(contentItem, tMap, {
37
+ path: ["media-items", mediaId, "content", index],
38
+ }),
39
+ )
40
+ const typeIcons: Record<UrlType, LucideIcon> = {
41
+ audio: MusicIcon,
42
+ text: TextIcon,
43
+ source: CodeIcon,
44
+ link: LinkIcon,
45
+ image: ImageIcon,
46
+ video: VideoIcon,
47
+ package: PackageIcon,
31
48
  } as const
32
-
33
- const { t, direction } = Astro.locals.i18n
49
+ const iconDirectionClass = direction === "rtl" ? "scale-x-[-1]" : ""
34
50
  ---
35
51
 
36
52
  <ol
@@ -38,39 +54,46 @@ const { t, direction } = Astro.locals.i18n
38
54
  >
39
55
  {
40
56
  content.map(
41
- ({ extension, label, type, canBeOpened, url, target }, index) => (
42
- <li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
43
- <a
44
- href={url}
45
- target={target}
46
- class="flex items-center justify-between py-8"
47
- >
48
- <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800">
49
- <Icon
50
- className={`${typeIcons[type]} text-gray-200`}
51
- ariaLabel=""
52
- />
53
- </span>
57
+ ({ extension, labelText, type, canBeOpened, url, target }, index) => {
58
+ const TypeIcon = typeIcons[type]
59
+
60
+ return (
61
+ <li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
62
+ <a
63
+ href={url}
64
+ target={target}
65
+ class="flex items-center justify-between py-8"
66
+ >
67
+ <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800 text-gray-200">
68
+ <TypeIcon size="1.2rem" />
69
+ </span>
54
70
 
55
- <div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
56
- {t(label)}
57
- </div>
58
- <div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
59
- {extension}
60
- </div>
61
- <Icon
62
- className={`${canBeOpened ? "mdi--chevron-right" : "mdi--download"} shrink-0 bg-gray-600 group-hover:bg-gray-800`}
63
- ariaLabel={
64
- canBeOpened ? t("ln.details.open") : t("ln.details.download")
65
- }
66
- flipIcon={direction === "rtl"}
67
- />
68
- </a>
69
- {index !== content.length - 1 && (
70
- <div class="h-px w-full bg-gray-300" />
71
- )}
72
- </li>
73
- ),
71
+ <div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
72
+ {labelText}
73
+ </div>
74
+ <div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
75
+ {extension}
76
+ </div>
77
+ {canBeOpened ? (
78
+ <>
79
+ <ChevronRightIcon
80
+ className={`shrink-0 text-gray-600 group-hover:text-gray-800 ${iconDirectionClass}`}
81
+ />
82
+ <span class="sr-only">{t("ln.details.open")}</span>
83
+ </>
84
+ ) : (
85
+ <>
86
+ <DownloadIcon className="shrink-0 text-gray-600 group-hover:text-gray-800" />
87
+ <span class="sr-only">{t("ln.details.download")}</span>
88
+ </>
89
+ )}
90
+ </a>
91
+ {index !== content.length - 1 && (
92
+ <div class="h-px w-full bg-gray-300" />
93
+ )}
94
+ </li>
95
+ )
96
+ },
74
97
  )
75
98
  }
76
99
  </ol>
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  import { AstroError } from "astro/errors"
3
3
  import { getEntry } from "astro:content"
4
+ import { getEntries } from "astro:content"
4
5
 
5
6
  import MediaList from "../../../components/MediaList.astro"
6
- import { getCollectionItems } from "../utils/get-collection-items"
7
7
 
8
8
  interface Props {
9
9
  collectionId: string
@@ -18,7 +18,7 @@ if (!collection) {
18
18
  `To fix the issue, add a media-collection at "src/content/media-collections/${collectionId}.json".`,
19
19
  )
20
20
  }
21
- const items = (await getCollectionItems(collection.id)).map((item) => ({
21
+ const items = (await getEntries(collection.data.mediaItems)).map((item) => ({
22
22
  ...item,
23
23
  disabled: item.id === disableItem,
24
24
  }))
@@ -26,7 +26,7 @@ const items = (await getCollectionItems(collection.id)).map((item) => ({
26
26
  if (items.length < 2) {
27
27
  return
28
28
  }
29
- const t = Astro.locals.i18n.t
29
+ const { t, tMap } = Astro.locals.i18n
30
30
  ---
31
31
 
32
32
  <section class="mx-auto mt-16 max-w-screen-md px-4 md:mt-20 md:px-8">
@@ -34,7 +34,11 @@ const t = Astro.locals.i18n.t
34
34
  {t("ln.details.part-of-collection")}
35
35
  </h2>
36
36
  <h3 class="mb-6 text-lg font-bold text-gray-700 sm:mb-8">
37
- {t(collection.data.label)}
37
+ {
38
+ tMap(collection.data.label, {
39
+ path: ["media-collections", collectionId, "label"],
40
+ })
41
+ }
38
42
  </h3>
39
43
  <MediaList items={items} />
40
44
  </section>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getMediaItem } from "../../../content/get-media-items"
2
+ import { getCollectionsForMediaItem } from "../../../content/get-media-collections"
3
3
  import MediaCollection from "./MediaCollection.astro"
4
4
 
5
5
  interface Props {
@@ -7,15 +7,12 @@ interface Props {
7
7
  }
8
8
 
9
9
  const { mediaId } = Astro.props
10
- const item = await getMediaItem(mediaId)
11
10
 
12
- const collections = item.data.collections?.map(
13
- ({ collection }) => collection.id,
14
- )
11
+ const collections = await getCollectionsForMediaItem(mediaId)
15
12
  ---
16
13
 
17
14
  {
18
- collections?.map((id) => (
15
+ collections.map((id) => (
19
16
  <MediaCollection collectionId={id} disableItem={mediaId} />
20
17
  ))
21
18
  }
@@ -1,5 +1,10 @@
1
1
  ---
2
- import Icon from "../../../../components/Icon"
2
+ import { SquarePenIcon } from "lucide-react"
3
+ import mediaItemEditButtonController from "virtual:lightnet/components/media-item-edit-button-controller"
4
+
5
+ if (!mediaItemEditButtonController) {
6
+ return
7
+ }
3
8
 
4
9
  interface Props {
5
10
  mediaId: string
@@ -11,18 +16,25 @@ const { mediaId } = Astro.props
11
16
  <a
12
17
  class="hidden cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
13
18
  id="edit-btn"
14
- data-admin-enabled={false}
15
- href={`/admin/media/${mediaId}`}
16
- ><Icon className="mdi--square-edit-outline" ariaLabel="" />
19
+ data-media-id={mediaId}
20
+ href="#"
21
+ ><SquarePenIcon />
17
22
  {Astro.locals.i18n.t("ln.details.edit")}</a
18
23
  >
19
24
  <script>
25
+ import mediaItemEditButtonController from "virtual:lightnet/components/media-item-edit-button-controller"
26
+
20
27
  const btn: HTMLAnchorElement | null = document.querySelector("#edit-btn")
21
- const showEditButton =
22
- btn?.dataset.adminEnabled === "true" &&
23
- (import.meta.env.DEV || localStorage.getItem("ln-admin-enabled") === "true")
24
- if (showEditButton) {
25
- btn?.classList.remove("hidden")
26
- btn?.classList.add("flex")
28
+ const mediaId = btn?.dataset.mediaId
29
+
30
+ if (
31
+ mediaItemEditButtonController &&
32
+ btn &&
33
+ mediaId &&
34
+ mediaItemEditButtonController.shouldShow()
35
+ ) {
36
+ btn.href = mediaItemEditButtonController.createHref(mediaId)
37
+ btn.classList.remove("hidden")
38
+ btn.classList.add("flex")
27
39
  }
28
40
  </script>
@@ -1,16 +1,21 @@
1
1
  ---
2
- import Icon from "../../../../components/Icon"
2
+ import { ExternalLinkIcon } from "lucide-react"
3
+
3
4
  import { getMediaItem } from "../../../../content/get-media-items"
5
+ import type { TranslationMap } from "../../../../i18n/translate-map"
4
6
  import { createContentMetadata } from "../../utils/create-content-metadata"
5
7
 
6
8
  interface Props {
7
9
  mediaId: string
8
- openActionLabel: string
10
+ openActionLabel: TranslationMap
9
11
  className?: string
10
12
  }
11
13
  const { mediaId, openActionLabel, className } = Astro.props
14
+ const { tMap } = Astro.locals.i18n
12
15
  const item = await getMediaItem(mediaId)
13
- const content = createContentMetadata(item.data.content[0])
16
+ const content = createContentMetadata(item.data.content[0], tMap, {
17
+ path: ["media-items", mediaId, "content", 0],
18
+ })
14
19
  ---
15
20
 
16
21
  <a
@@ -20,17 +25,17 @@ const content = createContentMetadata(item.data.content[0])
20
25
  hreflang={item.data.language}
21
26
  class:list={[className]}
22
27
  >
23
- {
24
- content.isExternal && (
25
- <Icon
26
- ariaLabel={Astro.locals.i18n.t("ln.external-link")}
27
- className={`shrink-0 mdi--external-link`}
28
- />
29
- )
30
- }
28
+ {content.isExternal && <ExternalLinkIcon className="shrink-0" />}
31
29
  {
32
30
  content.canBeOpened
33
- ? Astro.locals.i18n.t(openActionLabel)
31
+ ? tMap(openActionLabel, {
32
+ path: [
33
+ "media-types",
34
+ item.data.type.id,
35
+ "detailsPage",
36
+ "openActionLabel",
37
+ ],
38
+ })
34
39
  : Astro.locals.i18n.t("ln.details.download")
35
40
  }
36
41
  </a>
@@ -1,5 +1,6 @@
1
1
  ---
2
- import Icon from "../../../../components/Icon"
2
+ import { ForwardIcon } from "lucide-react"
3
+
3
4
  import Toast from "../../../../components/Toast"
4
5
 
5
6
  interface Props {
@@ -11,7 +12,7 @@ interface Props {
11
12
  class="flex cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
12
13
  class:list={[Astro.props.className]}
13
14
  id="share-btn"
14
- ><Icon className="mdi--share" ariaLabel="" />
15
+ ><ForwardIcon />
15
16
  {Astro.locals.i18n.t("ln.details.share")}</button
16
17
  >
17
18
  <Toast id="share-success" variant="success">
@@ -10,7 +10,7 @@ interface Props {
10
10
 
11
11
  const item = await getMediaItem(Astro.props.mediaId)
12
12
 
13
- const { t } = Astro.locals.i18n
13
+ const { t, tMap, currentLocale } = Astro.locals.i18n
14
14
 
15
15
  const categories = await Promise.all(
16
16
  item.data.categories?.map(({ id }) => getCategory(id)) ?? [],
@@ -25,11 +25,13 @@ const categories = await Promise.all(
25
25
  {categories.map(async (category) => (
26
26
  <li class="flex rounded-lg bg-gray-200 px-4 py-1 text-gray-500 hover:bg-gray-300">
27
27
  <a
28
- href={searchPagePath(Astro.currentLocale, {
28
+ href={searchPagePath(currentLocale, {
29
29
  category: category.id,
30
30
  })}
31
31
  >
32
- {t(category.label)}
32
+ {tMap(category.label, {
33
+ path: ["categories", category.id, "label"],
34
+ })}
33
35
  </a>
34
36
  </li>
35
37
  ))}
@@ -14,23 +14,28 @@ const item = await getMediaItem(Astro.props.mediaId)
14
14
  const { mediaId } = Astro.props
15
15
  const translations = await getTranslations(mediaId)
16
16
 
17
- const { t } = Astro.locals.i18n
17
+ const { t, tMap, currentLocale } = Astro.locals.i18n
18
+ const currentLanguage = resolveTranslatedLanguage(item.data.language, tMap)
19
+ const translationLanguages = await Promise.all(
20
+ translations.map(async (translation) => ({
21
+ ...translation,
22
+ labelText: resolveTranslatedLanguage(translation.language, tMap).labelText,
23
+ })),
24
+ )
18
25
  ---
19
26
 
20
27
  <div>
21
28
  <Label>{translations.length ? t("ln.languages") : t("ln.language")}</Label>
22
29
  <ul class="flex flex-wrap gap-2">
23
- <li class="py-1 pe-2 text-gray-800">
24
- {resolveTranslatedLanguage(item.data.language, t).labelText}
25
- </li>
30
+ <li class="py-1 pe-2 text-gray-800">{currentLanguage.labelText}</li>
26
31
  {
27
- translations.map((translation) => (
32
+ translationLanguages.map((translation) => (
28
33
  <li class="flex rounded-lg border border-gray-200 px-4 py-1 text-gray-600 hover:bg-gray-200">
29
34
  <a
30
- href={detailsPagePath(Astro.currentLocale, translation)}
35
+ href={detailsPagePath(currentLocale, translation)}
31
36
  hreflang={translation.language}
32
37
  >
33
- {resolveTranslatedLanguage(translation.language, t).labelText}
38
+ {translation.labelText}
34
39
  </a>
35
40
  </li>
36
41
  ))
@@ -1,3 +1,7 @@
1
+ import {
2
+ type TranslateMapFn,
3
+ type TranslationMap,
4
+ } from "../../../i18n/translate-map"
1
5
  import { isExternalUrl } from "../../../utils/urls"
2
6
 
3
7
  export type UrlType =
@@ -43,13 +47,17 @@ const KNOWN_EXTENSIONS: Record<
43
47
  docx: { type: "text" },
44
48
  } as const
45
49
 
46
- export function createContentMetadata({
47
- url,
48
- label: customLabel,
49
- }: {
50
- url: string
51
- label?: string
52
- }) {
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
+ ) {
53
61
  const isExternal = isExternalUrl(url)
54
62
  const path = isExternal ? new URL(url).pathname : url
55
63
 
@@ -63,7 +71,14 @@ export function createContentMetadata({
63
71
  const fileName = hasExtension
64
72
  ? lastPathSegment.slice(0, -(extension.length + 1))
65
73
  : undefined
66
- const label = customLabel || fileName || linkName
74
+
75
+ const labelText =
76
+ (customLabel &&
77
+ tMap(customLabel, {
78
+ path: [...context.path, "label"],
79
+ })) ??
80
+ fileName ??
81
+ linkName
67
82
  const type = KNOWN_EXTENSIONS[extension]?.type ?? "link"
68
83
  const canBeOpened =
69
84
  !hasExtension || !!KNOWN_EXTENSIONS[extension]?.canBeOpened
@@ -72,7 +87,7 @@ export function createContentMetadata({
72
87
  url,
73
88
  extension,
74
89
  isExternal,
75
- label,
90
+ labelText,
76
91
  canBeOpened,
77
92
  type,
78
93
  target: isExternal ? "_blank" : "_self",
@@ -3,6 +3,9 @@ import { getMediaItem, getMediaItems } from "../../../content/get-media-items"
3
3
  const groupItemsByCommonId = async () => {
4
4
  const items = await getMediaItems()
5
5
  items.forEach(({ id, data: { commonId, language } }) => {
6
+ if (!commonId) {
7
+ return
8
+ }
6
9
  if (!itemsByCommonId.has(commonId)) {
7
10
  itemsByCommonId.set(commonId, [])
8
11
  }
@@ -24,6 +27,9 @@ export const getTranslations = async (mediaId: string) => {
24
27
  await groupItemsByCommonId()
25
28
  }
26
29
  const item = await getMediaItem(mediaId)
30
+ if (!item.data.commonId) {
31
+ return []
32
+ }
27
33
  const sameCommonId = itemsByCommonId.get(item.data.commonId)
28
34
  if (!sameCommonId) {
29
35
  return []
@@ -1,8 +1,11 @@
1
- import Icon from "../../../components/Icon"
1
+ import { ChevronRightIcon } from "lucide-react"
2
+
2
3
  import { useI18n } from "../../../i18n/react/use-i18n"
3
4
 
4
5
  export default function LoadingSkeleton() {
5
6
  const { direction } = useI18n()
7
+ const iconDirectionClass = direction === "rtl" ? "scale-x-[-1]" : ""
8
+
6
9
  return (
7
10
  <ul>
8
11
  {Array.from({ length: 8 }, (_, index) => (
@@ -16,10 +19,8 @@ export default function LoadingSkeleton() {
16
19
  <div className="h-4 w-3/4 rounded-md bg-gray-200 md:h-6"></div>
17
20
  <div className="h-4 w-5/6 rounded-md bg-gray-200 md:h-6"></div>
18
21
  </div>
19
- <Icon
20
- className="my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 mdi--chevron-right sm:block"
21
- flipIcon={direction === "rtl"}
22
- ariaLabel=""
22
+ <ChevronRightIcon
23
+ className={`my-auto me-4 ms-2 hidden shrink-0 text-gray-300 sm:block ${iconDirectionClass}`}
23
24
  />
24
25
  </li>
25
26
  ))}