lightnet 4.2.1 → 4.4.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 (35) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +9 -9
  3. package/src/astro-integration/config.ts +19 -11
  4. package/src/i18n/translations/ar.yml +1 -1
  5. package/src/i18n/translations/bn.yml +1 -1
  6. package/src/i18n/translations/de.yml +1 -1
  7. package/src/i18n/translations/en.yml +1 -1
  8. package/src/i18n/translations/es.yml +1 -1
  9. package/src/i18n/translations/fi.yml +1 -1
  10. package/src/i18n/translations/fr.yml +1 -1
  11. package/src/i18n/translations/hi.yml +1 -1
  12. package/src/i18n/translations/kk.yml +1 -1
  13. package/src/i18n/translations/pt.yml +1 -1
  14. package/src/i18n/translations/ru.yml +1 -1
  15. package/src/i18n/translations/uk.yml +1 -1
  16. package/src/i18n/translations/ur.yml +1 -1
  17. package/src/i18n/translations/zh.yml +1 -1
  18. package/src/i18n/translations.ts +1 -1
  19. package/src/layouts/Page.astro +27 -6
  20. package/src/layouts/components/footer/Credits.astro +14 -0
  21. package/src/layouts/components/footer/Footer.astro +88 -0
  22. package/src/layouts/components/footer/LanguageSelectionMenu.astro +44 -0
  23. package/src/layouts/components/{LightNetLogo.svg → footer/LightNetLogo.svg} +1 -1
  24. package/src/layouts/components/footer/format-footer-text.ts +14 -0
  25. package/src/layouts/components/get-language-selection-menu-items.ts +47 -0
  26. package/src/layouts/components/header/LanguageSelectionMenu.astro +34 -0
  27. package/src/layouts/components/{MenuItem.astro → header/MenuItem.astro} +1 -1
  28. package/src/layouts/components/{PageNavigation.astro → header/PageNavigation.astro} +4 -4
  29. package/src/layouts/components/{PageTitle.astro → header/PageTitle.astro} +1 -1
  30. package/src/layouts/components/Footer.astro +0 -110
  31. package/src/layouts/components/LanguagePicker.astro +0 -49
  32. /package/src/layouts/components/{Header.astro → header/Header.astro} +0 -0
  33. /package/src/layouts/components/{Menu.astro → header/Menu.astro} +0 -0
  34. /package/src/layouts/components/{Favicon.astro → meta/Favicon.astro} +0 -0
  35. /package/src/layouts/components/{ViewTransition.astro → meta/ViewTransition.astro} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # lightnet
2
2
 
3
+ ## 4.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#409](https://github.com/LightNetDev/LightNet/pull/409) [`6d214f6`](https://github.com/LightNetDev/LightNet/commit/6d214f6b620dc00b8860da50006778649e977d59) - Add ` disallowSearchIndexing` settings to exclude entire site or pages from search engines.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#409](https://github.com/LightNetDev/LightNet/pull/409) [`6d214f6`](https://github.com/LightNetDev/LightNet/commit/6d214f6b620dc00b8860da50006778649e977d59) - Improved footer layout
12
+
13
+ ## 4.3.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#405](https://github.com/LightNetDev/LightNet/pull/405) [`522ffae`](https://github.com/LightNetDev/LightNet/commit/522ffaed830b9e68f5479567ee326c5439ea0f27) - Add a footer language selector with shared locale-path logic and a footer-specific accessible label.
18
+
19
+ - [#408](https://github.com/LightNetDev/LightNet/pull/408) [`d0aaa12`](https://github.com/LightNetDev/LightNet/commit/d0aaa12477bec601bcd10d46290f287dc2174b60) - Support a `{{year}}` placeholder in localized footer text.
20
+
21
+ ### Patch Changes
22
+
23
+ - [#407](https://github.com/LightNetDev/LightNet/pull/407) [`e5fa59b`](https://github.com/LightNetDev/LightNet/commit/e5fa59bbfce0aca158db08c91201fc8e03ecbaeb) - Update dependencies
24
+
25
+ - [#405](https://github.com/LightNetDev/LightNet/pull/405) [`522ffae`](https://github.com/LightNetDev/LightNet/commit/522ffaed830b9e68f5479567ee326c5439ea0f27) - Rename the shared language selection translation key to `ln.select-language` and update all locale files, comments, and call sites. This changes the public translation key used by downstream custom translations.
26
+
3
27
  ## 4.2.1
4
28
 
5
29
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "LightNet makes it easy to run your own digital media library.",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "4.2.1",
6
+ "version": "4.4.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -51,29 +51,29 @@
51
51
  "tailwindcss": ">=3.4.0 <4.0.0"
52
52
  },
53
53
  "dependencies": {
54
- "@astrojs/react": "^5.0.6",
54
+ "@astrojs/react": "^5.0.7",
55
55
  "@iconify-json/lucide": "^1.2.111",
56
56
  "@iconify-json/mdi": "^1.2.3",
57
57
  "@iconify/tailwind": "^1.2.0",
58
58
  "@tailwindcss/typography": "^0.5.19",
59
- "@tanstack/react-virtual": "^3.13.26",
59
+ "@tanstack/react-virtual": "^3.14.2",
60
60
  "autoprefixer": "^10.5.0",
61
61
  "embla-carousel": "^8.6.0",
62
62
  "embla-carousel-wheel-gestures": "^8.1.0",
63
- "fuse.js": "^7.3.0",
64
- "i18next": "^26.3.0",
63
+ "fuse.js": "^7.4.2",
64
+ "i18next": "^26.3.1",
65
65
  "lucide-react": "^1.17.0",
66
- "marked": "^18.0.4",
66
+ "marked": "^18.0.5",
67
67
  "postcss": "^8.5.15",
68
68
  "postcss-load-config": "^6.0.1",
69
69
  "yaml": "^2.9.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@playwright/test": "^1.60.0",
73
- "@types/react": "^19.2.15",
74
- "astro": "^6.4.2",
73
+ "@types/react": "^19.2.17",
74
+ "astro": "^6.4.4",
75
75
  "typescript": "^6.0.3",
76
- "vitest": "^4.1.7",
76
+ "vitest": "^4.1.8",
77
77
  "@internal/e2e-test-utils": "^0.0.1"
78
78
  },
79
79
  "engines": {
@@ -110,18 +110,10 @@ export const configSchema = z.object({
110
110
  */
111
111
  favicon: faviconSchema.array().optional(),
112
112
  /**
113
- * Enable displaying a “Powered by LightNet” link in your site’s footer.
113
+ * If set too true, this sets robots meta tag to no index to tell search engines like google to
114
+ * not add any page of this media library to their search results.
114
115
  */
115
- credits: z.boolean().default(false),
116
- /**
117
- * Optional localized text to display in your site's footer.
118
- */
119
- footerText: translationMapSchema.optional(),
120
- /**
121
- * Optional links to display in your site's footer.
122
- */
123
- footerLinks: z.array(linkSchema).optional(),
124
-
116
+ disallowSearchIndexing: z.boolean().optional(),
125
117
  /**
126
118
  * Link to manifest file within public/ folder
127
119
  */
@@ -182,6 +174,22 @@ export const configSchema = z.object({
182
174
  * @example "./src/components/MyHeadTag.astro"
183
175
  */
184
176
  headComponent: z.string().optional(),
177
+ /**
178
+ * Enable displaying a “Powered by LightNet” link in your site’s footer.
179
+ */
180
+ credits: z.boolean().default(false),
181
+ /**
182
+ * Optional localized text to display in your site's footer.
183
+ *
184
+ * Use `{{year}}` to render the current calendar year.
185
+ *
186
+ * @example { en: "Copyright {{year}} LightNet" }
187
+ */
188
+ footerText: translationMapSchema.optional(),
189
+ /**
190
+ * Optional links to display in your site's footer.
191
+ */
192
+ footerLinks: z.array(linkSchema).optional(),
185
193
  /**
186
194
  * Path to an Astro component to be added at the bottom of all pages.
187
195
  *
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: افتح القائمة الرئيسية
2
- ln.header.select-language: اختر اللغة
2
+ ln.select-language: اختر اللغة
3
3
  ln.home.title: الصفحة الرئيسية
4
4
  ln.category: الفئة
5
5
  ln.categories: الفئات
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: প্রধান মেনু খুলুন
2
- ln.header.select-language: ভাষা নির্বাচন করুন
2
+ ln.select-language: ভাষা নির্বাচন করুন
3
3
  ln.home.title: হোম
4
4
  ln.category: বিভাগ
5
5
  ln.categories: বিভাগসমূহ
@@ -14,7 +14,7 @@ ln.details.download: Download
14
14
  ln.details.edit: Bearbeiten
15
15
  ln.details.share: Teilen
16
16
  ln.header.open-main-menu: Öffne Hauptmenü
17
- ln.header.select-language: Sprache auswählen
17
+ ln.select-language: Sprache auswählen
18
18
  ln.home.title: Startseite
19
19
  ln.search.all-categories: Alle Kategorien
20
20
  ln.search.all-languages: Alle Sprachen
@@ -10,7 +10,7 @@ ln.header.open-main-menu: Open main menu
10
10
  #
11
11
  # English: Select language
12
12
  # Used on: https://kuuluu.org/en (not visible)
13
- ln.header.select-language: Select language
13
+ ln.select-language: Select language
14
14
 
15
15
  # Display name for the home page.
16
16
  #
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Abrir menú principal
2
- ln.header.select-language: Seleccionar idioma
2
+ ln.select-language: Seleccionar idioma
3
3
  ln.home.title: Inicio
4
4
  ln.category: Categoría
5
5
  ln.categories: Categorías
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Avaa päävalikko
2
- ln.header.select-language: Valitse kieli
2
+ ln.select-language: Valitse kieli
3
3
  ln.home.title: Etusivu
4
4
  ln.category: Kategoria
5
5
  ln.categories: Kategoriat
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Ouvrir le menu principal
2
- ln.header.select-language: Sélectionner la langue
2
+ ln.select-language: Sélectionner la langue
3
3
  ln.home.title: Accueil
4
4
  ln.category: Catégorie
5
5
  ln.categories: Catégories
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: मुख्य मेनू खोलें
2
- ln.header.select-language: भाषा चुनें
2
+ ln.select-language: भाषा चुनें
3
3
  ln.home.title: मुखपृष्ठ
4
4
  ln.category: श्रेणी
5
5
  ln.categories: श्रेणियाँ
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Бастапқы мәзірді ашу
2
- ln.header.select-language: Тіл таңдау
2
+ ln.select-language: Тіл таңдау
3
3
  ln.home.title: Басты бет
4
4
  ln.category: Санат
5
5
  ln.categories: Барлық санаттар
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Abrir menu principal
2
- ln.header.select-language: Escolher idioma
2
+ ln.select-language: Escolher idioma
3
3
  ln.home.title: Início
4
4
  ln.category: Categoria
5
5
  ln.categories: Categorias
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Открыть главное меню
2
- ln.header.select-language: Выберите язык
2
+ ln.select-language: Выберите язык
3
3
  ln.home.title: Главная
4
4
  ln.category: Категория
5
5
  ln.categories: Категории
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: Відкрити головне меню
2
- ln.header.select-language: Оберіть мову
2
+ ln.select-language: Оберіть мову
3
3
  ln.home.title: Головна
4
4
  ln.category: Категорія
5
5
  ln.categories: Категорії
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: مرکزی مینو کھولیں
2
- ln.header.select-language: زبان منتخب کریں
2
+ ln.select-language: زبان منتخب کریں
3
3
  ln.home.title: ہوم
4
4
  ln.category: زمرہ
5
5
  ln.categories: زمروں
@@ -1,5 +1,5 @@
1
1
  ln.header.open-main-menu: 打开主菜单
2
- ln.header.select-language: 选择语言
2
+ ln.select-language: 选择语言
3
3
  ln.home.title: 首页
4
4
  ln.category: 分类
5
5
  ln.categories: 分类
@@ -82,7 +82,7 @@ export type LightNetTranslationKey =
82
82
  | "ln.details.part-of-collection"
83
83
  | "ln.details.download"
84
84
  | "ln.header.open-main-menu"
85
- | "ln.header.select-language"
85
+ | "ln.select-language"
86
86
  | "ln.home.title"
87
87
  | "ln.search.all-categories"
88
88
  | "ln.search.all-languages"
@@ -7,10 +7,10 @@ import config from "virtual:lightnet/config"
7
7
 
8
8
  import { resolveLanguage } from "../i18n/resolve-language"
9
9
  import { pathWithBase } from "../utils/paths"
10
- import Favicon from "./components/Favicon.astro"
11
- import Footer from "./components/Footer.astro"
12
- import Header from "./components/Header.astro"
13
- import ViewTransition from "./components/ViewTransition.astro"
10
+ import Footer from "./components/footer/Footer.astro"
11
+ import Header from "./components/header/Header.astro"
12
+ import Favicon from "./components/meta/Favicon.astro"
13
+ import ViewTransition from "./components/meta/ViewTransition.astro"
14
14
 
15
15
  export interface Props {
16
16
  /**
@@ -37,10 +37,26 @@ export interface Props {
37
37
  * analytics providers that choose to respect it. e.g. @lightnet/plausible-analytics
38
38
  */
39
39
  disableShouldTrack?: boolean
40
+ /**
41
+ * Disallow search indexing for this page from search tools like google.
42
+ * This tells them to not index content on this page.
43
+ *
44
+ * There is also a global setting to disable search indexing for all pages.
45
+ * The page specific setting will take precedence.
46
+ *
47
+ * default: false
48
+ */
49
+ disallowSearchIndexing?: boolean
40
50
  }
41
51
 
42
- const { title, description, mainClass, locale, disableShouldTrack } =
43
- Astro.props
52
+ const {
53
+ title,
54
+ description,
55
+ mainClass,
56
+ locale,
57
+ disableShouldTrack,
58
+ disallowSearchIndexing,
59
+ } = Astro.props
44
60
  const { currentLocale: pathCurrentLocale, tConfigField } = Astro.locals.i18n
45
61
  const currentLocale = locale ?? pathCurrentLocale
46
62
  const configTitle = tConfigField(config.title, config)
@@ -56,6 +72,11 @@ const { direction } = resolveLanguage(currentLocale)
56
72
  <head>
57
73
  <meta charset="UTF-8" />
58
74
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
75
+ {
76
+ (disallowSearchIndexing ?? config.disallowSearchIndexing) && (
77
+ <meta name="robots" content="noindex" />
78
+ )
79
+ }
59
80
  {CustomHead && <CustomHead />}
60
81
  <title>{title ? `${title} | ${configTitle}` : configTitle}</title>
61
82
  {description && <meta name="description" content={description} />}
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { getLinkAttributes } from "../../../utils/link-attributes"
3
+ import LightNetLogo from "./LightNetLogo.svg"
4
+
5
+ const { t } = Astro.locals.i18n
6
+ ---
7
+
8
+ <a
9
+ class="inline-flex items-center gap-2 whitespace-nowrap underline-offset-4 hover:underline"
10
+ {...getLinkAttributes("https://lightnet.community")}
11
+ >
12
+ <img src={LightNetLogo.src} alt="" class="h-5 w-auto" loading="lazy" />
13
+ <span>{t("ln.footer.powered-by-lightnet")}</span>
14
+ </a>
@@ -0,0 +1,88 @@
1
+ ---
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import { getLinkAttributes } from "../../../utils/link-attributes"
5
+ import { localizePath } from "../../../utils/paths"
6
+ import Credits from "./Credits.astro"
7
+ import { formatFooterText } from "./format-footer-text"
8
+ import LanguageSelectionMenu from "./LanguageSelectionMenu.astro"
9
+
10
+ const { tConfigField, currentLocale } = Astro.locals.i18n
11
+
12
+ // Resolve optional footer text for the current locale.
13
+ const footerText = config.footerText
14
+ ? formatFooterText(tConfigField(config.footerText, config))
15
+ : undefined
16
+
17
+ // Prepare footer links using the same locale rules as the main menu.
18
+ const footerLinks = (config.footerLinks ?? []).map(
19
+ ({ href, label, requiresLocale }) => {
20
+ return {
21
+ href: requiresLocale ? localizePath(currentLocale, href) : href,
22
+ label: tConfigField(label, config),
23
+ }
24
+ },
25
+ )
26
+
27
+ const shouldShowLanguageSelectionMenu = config.locales.length > 1
28
+
29
+ // Hide the footer entirely when there is nothing to show.
30
+ const shouldRenderFooter =
31
+ config.credits ||
32
+ !!footerText ||
33
+ footerLinks.length > 0 ||
34
+ shouldShowLanguageSelectionMenu
35
+
36
+ if (!shouldRenderFooter) {
37
+ return
38
+ }
39
+
40
+ const footerItems = [
41
+ ...(footerText ? [{ type: "text" as const, label: footerText }] : []),
42
+ ...footerLinks.map((link) => ({ type: "link" as const, ...link })),
43
+ ]
44
+ ---
45
+
46
+ <footer class="w-full border-t border-gray-200 bg-white text-sm text-gray-800">
47
+ <div
48
+ class="mx-auto flex w-full max-w-screen-xl px-4 py-4 md:px-8 md:py-6"
49
+ class:list={footerItems.length === 0
50
+ ? "flex-col justify-between gap-4 sm:flex-row sm:items-center"
51
+ : "flex-col gap-2"}
52
+ >
53
+ {shouldShowLanguageSelectionMenu && <LanguageSelectionMenu />}
54
+ {
55
+ (footerItems.length > 0 || config.credits) && (
56
+ <div class="flex shrink flex-col gap-6 md:flex-row md:items-start md:justify-between">
57
+ {footerItems.length > 0 && (
58
+ <div class="flex w-full min-w-0 flex-1 shrink flex-wrap">
59
+ {footerItems.map((item, index) => (
60
+ <span class="inline-flex">
61
+ {item.type === "text" ? (
62
+ <span>{item.label}</span>
63
+ ) : (
64
+ <a
65
+ {...getLinkAttributes(item.href)}
66
+ class="underline-offset-4 hover:underline"
67
+ >
68
+ {item.label}
69
+ </a>
70
+ )}
71
+ {index !== footerItems.length - 1 && (
72
+ <span
73
+ class="shrink-0 px-2 text-gray-400"
74
+ aria-hidden="true"
75
+ >
76
+ &middot;
77
+ </span>
78
+ )}
79
+ </span>
80
+ ))}
81
+ </div>
82
+ )}
83
+ {config.credits && <Credits />}
84
+ </div>
85
+ )
86
+ }
87
+ </div>
88
+ </footer>
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { GlobeIcon } from "lucide-react"
3
+
4
+ import { getLanguageSelectionMenuItems } from "../get-language-selection-menu-items"
5
+
6
+ const { t, tConfigField, currentLocale } = Astro.locals.i18n
7
+
8
+ const { links: languageMenuItems } = getLanguageSelectionMenuItems({
9
+ currentLocale,
10
+ pathname: Astro.url.pathname,
11
+ tConfigField,
12
+ })
13
+ ---
14
+
15
+ <label class="inline-flex items-center gap-2 py-2 font-medium">
16
+ <GlobeIcon className="h-5 w-5 shrink-0 text-gray-600" aria-hidden="true" />
17
+ <span class="sr-only">{t("ln.select-language")}</span>
18
+ <select
19
+ class="min-w-0 cursor-pointer appearance-none border-0 bg-transparent p-0 pr-0 outline-none focus:ring-0"
20
+ aria-label={t("ln.select-language")}
21
+ data-footer-language-select
22
+ >
23
+ {
24
+ languageMenuItems.map(({ href, label, locale, active }) => (
25
+ <option value={href} selected={active} lang={locale}>
26
+ {label}
27
+ </option>
28
+ ))
29
+ }
30
+ </select>
31
+ </label>
32
+ <script>
33
+ const footerLanguageSelect = document.querySelector<HTMLSelectElement>(
34
+ "[data-footer-language-select]",
35
+ )
36
+
37
+ footerLanguageSelect?.addEventListener("change", () => {
38
+ if (!footerLanguageSelect.value) {
39
+ return
40
+ }
41
+
42
+ window.location.assign(footerLanguageSelect.value)
43
+ })
44
+ </script>
@@ -1 +1 @@
1
- <svg viewBox="16 16 141 173" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M88.007 180.642c-21.767-5.846-39.688-19.304-40.574-42.862-.887-23.558 17.751-43.334 45.426-40.84v-7.844c.011-4.03 4.977-5.495 8.021-2.928l29.136 23.729c2.155 1.821 2.15 5.16-.019 7.08l-29.117 24.152c-3.059 2.72-8.031.681-8.02-3.349v-8.005c-2.916-.453-5.137-.579-6.708-.453-25.207 2.016-23.083 44.6 20.454 42.388 18.705-.951 49.148-21.73 50.394-53.985 0-48.62-60.4-64.433-45.205-97.762 1.529-3.354-4.627-4.867-8.501-3.393-15.573 4.29-32.752 13.714-42.526 41.693-1.251 3.581-2.824 12.038-4.168 15.558-1.242 3.533-3.034 11.445-7.996 11.025-6.501-.55-4.236-11.168-2.815-15.74 1.197-3.854-4.14-7.109-9.028-2.073-8.3 8.549-16.598 19.982-19.804 37.823-1.259 7.001-1.327 16.903.101 25.208 6.204 36.183 37.705 59.276 68.356 58.915 7.58-.09 7.787-6.942 2.593-8.337Z" fill="#374151"/></svg>
1
+ <svg viewBox="16 16 141 173" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M88.007 180.642c-21.767-5.846-39.688-19.304-40.574-42.862-.887-23.558 17.751-43.334 45.426-40.84v-7.844c.011-4.03 4.977-5.495 8.021-2.928l29.136 23.729c2.155 1.821 2.15 5.16-.019 7.08l-29.117 24.152c-3.059 2.72-8.031.681-8.02-3.349v-8.005c-2.916-.453-5.137-.579-6.708-.453-25.207 2.016-23.083 44.6 20.454 42.388 18.705-.951 49.148-21.73 50.394-53.985 0-48.62-60.4-64.433-45.205-97.762 1.529-3.354-4.627-4.867-8.501-3.393-15.573 4.29-32.752 13.714-42.526 41.693-1.251 3.581-2.824 12.038-4.168 15.558-1.242 3.533-3.034 11.445-7.996 11.025-6.501-.55-4.236-11.168-2.815-15.74 1.197-3.854-4.14-7.109-9.028-2.073-8.3 8.549-16.598 19.982-19.804 37.823-1.259 7.001-1.327 16.903.101 25.208 6.204 36.183 37.705 59.276 68.356 58.915 7.58-.09 7.787-6.942 2.593-8.337Z" fill="#4b5563"/></svg>
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Replaces supported placeholders in localized footer text.
3
+ *
4
+ * Use `{{year}}` to render the current calendar year.
5
+ *
6
+ * @param text Localized footer text from LightNet config.
7
+ * @param currentYear Year to render. Defaults to the current system year.
8
+ */
9
+ export const formatFooterText = (
10
+ text: string,
11
+ currentYear = new Date().getFullYear(),
12
+ ) => {
13
+ return text.replaceAll("{{year}}", String(currentYear))
14
+ }
@@ -0,0 +1,47 @@
1
+ import config from "virtual:lightnet/config"
2
+
3
+ import { resolveLanguage } from "../../i18n/resolve-language"
4
+ import type { TranslateConfigFieldFn } from "../../i18n/translate-map"
5
+ import { localizePath, pathWithoutBase } from "../../utils/paths"
6
+
7
+ export type LanguageLink = {
8
+ active: boolean
9
+ href: string
10
+ label: string
11
+ locale: string
12
+ }
13
+
14
+ export function getLanguageSelectionMenuItems({
15
+ currentLocale,
16
+ pathname,
17
+ tConfigField,
18
+ }: {
19
+ currentLocale: string
20
+ pathname: string
21
+ tConfigField: TranslateConfigFieldFn
22
+ }) {
23
+ const currentPath = pathWithoutBase(pathname)
24
+ const hasLocale = Boolean(
25
+ currentLocale &&
26
+ (currentPath.startsWith(`/${currentLocale}/`) ||
27
+ currentPath === `/${currentLocale}`),
28
+ )
29
+
30
+ const links = config.locales
31
+ .map((locale) => ({
32
+ locale,
33
+ label: tConfigField(resolveLanguage(locale).label, config),
34
+ active: locale === currentLocale,
35
+ href: localizePath(
36
+ locale,
37
+ hasLocale ? currentPath.slice(currentLocale.length + 1) : currentPath,
38
+ ),
39
+ }))
40
+ .sort((a, b) => a.label.localeCompare(b.label, currentLocale))
41
+
42
+ return {
43
+ currentPath,
44
+ hasLocale,
45
+ links,
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ import { GlobeIcon } from "lucide-react"
3
+
4
+ import { getLanguageSelectionMenuItems } from "../get-language-selection-menu-items"
5
+ import Menu from "./Menu.astro"
6
+ import MenuItem from "./MenuItem.astro"
7
+
8
+ const { currentLocale, tConfigField } = Astro.locals.i18n
9
+
10
+ const {
11
+ currentPath,
12
+ hasLocale,
13
+ links: translations,
14
+ } = getLanguageSelectionMenuItems({
15
+ currentLocale,
16
+ pathname: Astro.url.pathname,
17
+ tConfigField,
18
+ })
19
+
20
+ const disabled = !hasLocale && currentPath !== "/"
21
+ ---
22
+
23
+ {
24
+ translations.length > 1 && (
25
+ <Menu disabled={disabled} label="ln.select-language">
26
+ <GlobeIcon slot="icon" />
27
+ {translations.map(({ label, locale, active, href }) => (
28
+ <MenuItem href={href} hreflang={locale} active={active}>
29
+ {label}
30
+ </MenuItem>
31
+ ))}
32
+ </Menu>
33
+ )
34
+ }
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getLinkAttributes } from "../../utils/link-attributes"
2
+ import { getLinkAttributes } from "../../../utils/link-attributes"
3
3
 
4
4
  interface Props {
5
5
  href: string
@@ -2,9 +2,9 @@
2
2
  import { ExternalLinkIcon, MenuIcon, SearchIcon } from "lucide-react"
3
3
  import config from "virtual:lightnet/config"
4
4
 
5
- import { isExternalUrl } from "../../utils/is-external-url"
6
- import { localizePath, searchPagePath } from "../../utils/paths"
7
- import LanguagePicker from "./LanguagePicker.astro"
5
+ import { isExternalUrl } from "../../../utils/is-external-url"
6
+ import { localizePath, searchPagePath } from "../../../utils/paths"
7
+ import LanguageSelectionMenu from "./LanguageSelectionMenu.astro"
8
8
  import Menu from "./Menu.astro"
9
9
  import MenuItem from "./MenuItem.astro"
10
10
 
@@ -40,7 +40,7 @@ const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
40
40
  )
41
41
  }
42
42
 
43
- <LanguagePicker />
43
+ <LanguageSelectionMenu />
44
44
 
45
45
  {
46
46
  !!items.length && (
@@ -4,7 +4,7 @@ import { Image } from "astro:assets"
4
4
  import config from "virtual:lightnet/config"
5
5
  import logo from "virtual:lightnet/logo"
6
6
 
7
- import { localizePath } from "../../utils/paths"
7
+ import { localizePath } from "../../../utils/paths"
8
8
 
9
9
  function getWidth(logo: ImageMetadata) {
10
10
  const size = config.logo?.size ?? 28
@@ -1,110 +0,0 @@
1
- ---
2
- import config from "virtual:lightnet/config"
3
-
4
- import { getLinkAttributes } from "../../utils/link-attributes"
5
- import { localizePath } from "../../utils/paths"
6
- import LightNetLogo from "./LightNetLogo.svg"
7
-
8
- const { t, tConfigField, currentLocale } = Astro.locals.i18n
9
-
10
- // Resolve optional footer text for the current locale.
11
- const footerText = config.footerText
12
- ? tConfigField(config.footerText, config)
13
- : undefined
14
-
15
- type FooterTextItem = {
16
- label: string
17
- type: "text"
18
- }
19
-
20
- type FooterLinkItem = {
21
- href: string
22
- label: string
23
- type: "link"
24
- }
25
-
26
- // Prepare footer links using the same locale rules as the main menu.
27
- const footerLinks = (config.footerLinks ?? []).map(
28
- ({ href, label, requiresLocale }) => {
29
- return {
30
- href: requiresLocale ? localizePath(currentLocale, href) : href,
31
- label: tConfigField(label, config),
32
- }
33
- },
34
- )
35
-
36
- // Hide the footer entirely when there is nothing to show.
37
- const shouldRenderFooter =
38
- config.credits || !!footerText || footerLinks.length > 0
39
-
40
- if (!shouldRenderFooter) {
41
- return
42
- }
43
-
44
- const footerItems: Array<FooterTextItem | FooterLinkItem> = [
45
- ...(footerText ? [{ type: "text" as const, label: footerText }] : []),
46
- ...footerLinks.map((link): FooterLinkItem => ({ type: "link", ...link })),
47
- ]
48
- ---
49
-
50
- <footer class="w-full border-t border-gray-200 bg-white">
51
- <div class="mx-auto w-full max-w-screen-xl px-4 md:px-8">
52
- <div
53
- class="flex flex-col items-start justify-between gap-6 py-6 sm:flex-row sm:items-start"
54
- >
55
- <div
56
- class="flex w-full flex-col items-start gap-3 text-sm text-gray-800 sm:min-w-0 sm:flex-1 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2"
57
- >
58
- {
59
- footerItems.length > 0 && (
60
- <div class="flex w-full flex-row flex-wrap items-center gap-x-2 gap-y-1 sm:w-auto">
61
- {footerItems.map((item, index) => (
62
- <span class="inline-flex items-center gap-1.5">
63
- <span class="text-gray-800">
64
- {item.type === "text" ? (
65
- item.label
66
- ) : (
67
- <a
68
- {...getLinkAttributes(item.href)}
69
- class="underline-offset-4 hover:underline"
70
- >
71
- {item.label}
72
- </a>
73
- )}
74
- </span>
75
- {index < footerItems.length - 1 && (
76
- <span
77
- class="text-[0.8em] leading-none text-gray-400"
78
- aria-hidden="true"
79
- >
80
- &middot;
81
- </span>
82
- )}
83
- </span>
84
- ))}
85
- </div>
86
- )
87
- }
88
- </div>
89
-
90
- {
91
- config.credits && (
92
- <div class="flex w-full items-center text-sm sm:w-auto sm:flex-none sm:justify-end">
93
- <a
94
- class="flex items-center gap-2 whitespace-nowrap text-gray-800 underline-offset-4 hover:underline"
95
- {...getLinkAttributes("https://lightnet.community")}
96
- >
97
- <img
98
- src={LightNetLogo.src}
99
- alt=""
100
- class="h-5 w-auto"
101
- loading="lazy"
102
- />
103
- <span>{t("ln.footer.powered-by-lightnet")}</span>
104
- </a>
105
- </div>
106
- )
107
- }
108
- </div>
109
- </div>
110
- </footer>
@@ -1,49 +0,0 @@
1
- ---
2
- import { GlobeIcon } from "lucide-react"
3
-
4
- import { resolveTranslatedLanguage } from "../../i18n/resolve-language"
5
- import { localizePath, pathWithoutBase } from "../../utils/paths"
6
- import Menu from "./Menu.astro"
7
- import MenuItem from "./MenuItem.astro"
8
-
9
- const { locales, currentLocale, tConfigField } = Astro.locals.i18n
10
-
11
- const currentPath = pathWithoutBase(Astro.url.pathname)
12
- const hasLocale =
13
- currentLocale &&
14
- (currentPath.startsWith(`/${currentLocale}/`) ||
15
- currentPath === `/${currentLocale}`)
16
-
17
- const translations = (
18
- await Promise.all(
19
- locales.map(async (locale) => ({
20
- locale,
21
- label: resolveTranslatedLanguage(locale, tConfigField).labelText,
22
- active: locale === currentLocale,
23
- href: currentPathWithLocale(locale),
24
- })),
25
- )
26
- ).sort((a, b) => a.label.localeCompare(b.label))
27
-
28
- function currentPathWithLocale(locale: string) {
29
- const currentPathWithoutLocale = hasLocale
30
- ? currentPath.slice(currentLocale.length + 1)
31
- : currentPath
32
- return localizePath(locale, currentPathWithoutLocale)
33
- }
34
-
35
- const disabled = !hasLocale && currentPath !== "/"
36
- ---
37
-
38
- {
39
- translations.length > 1 && (
40
- <Menu disabled={disabled} label="ln.header.select-language">
41
- <GlobeIcon slot="icon" />
42
- {translations.map(({ label, locale, active, href }) => (
43
- <MenuItem href={href} hreflang={locale} active={active}>
44
- {label}
45
- </MenuItem>
46
- ))}
47
- </Menu>
48
- )
49
- }