lightnet 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # lightnet
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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.
8
+
9
+ - [#408](https://github.com/LightNetDev/LightNet/pull/408) [`d0aaa12`](https://github.com/LightNetDev/LightNet/commit/d0aaa12477bec601bcd10d46290f287dc2174b60) - Support a `{{year}}` placeholder in localized footer text.
10
+
11
+ ### Patch Changes
12
+
13
+ - [#407](https://github.com/LightNetDev/LightNet/pull/407) [`e5fa59b`](https://github.com/LightNetDev/LightNet/commit/e5fa59bbfce0aca158db08c91201fc8e03ecbaeb) - Update dependencies
14
+
15
+ - [#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.
16
+
17
+ ## 4.2.1
18
+
19
+ ### Patch Changes
20
+
21
+ - [#401](https://github.com/LightNetDev/LightNet/pull/401) [`b3db519`](https://github.com/LightNetDev/LightNet/commit/b3db519e7c0492b66f6aa26875cb91fe841fcdd5) - Adjusted footer link separator and wrapping behavior to use the updated CSS-only footer layout.
22
+
23
+ - [#404](https://github.com/LightNetDev/LightNet/pull/404) [`5acab49`](https://github.com/LightNetDev/LightNet/commit/5acab49ca79078edbba3868b9b7bce249b2fca8a) - Update dependencies.
24
+
3
25
  ## 4.2.0
4
26
 
5
27
  ### Minor 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.0",
6
+ "version": "4.3.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.5",
55
- "@iconify-json/lucide": "^1.2.108",
54
+ "@astrojs/react": "^5.0.7",
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.24",
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.2.0",
65
- "lucide-react": "^1.16.0",
66
- "marked": "^18.0.3",
67
- "postcss": "^8.5.14",
63
+ "fuse.js": "^7.4.2",
64
+ "i18next": "^26.3.1",
65
+ "lucide-react": "^1.17.0",
66
+ "marked": "^18.0.5",
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.14",
74
- "astro": "^6.3.3",
73
+ "@types/react": "^19.2.17",
74
+ "astro": "^6.4.4",
75
75
  "typescript": "^6.0.3",
76
- "vitest": "^4.1.6",
76
+ "vitest": "^4.1.8",
77
77
  "@internal/e2e-test-utils": "^0.0.1"
78
78
  },
79
79
  "engines": {
@@ -115,6 +115,10 @@ export const configSchema = z.object({
115
115
  credits: z.boolean().default(false),
116
116
  /**
117
117
  * Optional localized text to display in your site's footer.
118
+ *
119
+ * Use `{{year}}` to render the current calendar year.
120
+ *
121
+ * @example { en: "Copyright {{year}} LightNet" }
118
122
  */
119
123
  footerText: translationMapSchema.optional(),
120
124
  /**
@@ -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"
@@ -1,15 +1,18 @@
1
1
  ---
2
+ import { GlobeIcon } from "lucide-react"
2
3
  import config from "virtual:lightnet/config"
3
4
 
4
5
  import { getLinkAttributes } from "../../utils/link-attributes"
5
6
  import { localizePath } from "../../utils/paths"
7
+ import { formatFooterText } from "./format-footer-text"
8
+ import { getLanguageSelectionMenuItems } from "./get-language-selection-menu-items"
6
9
  import LightNetLogo from "./LightNetLogo.svg"
7
10
 
8
11
  const { t, tConfigField, currentLocale } = Astro.locals.i18n
9
12
 
10
13
  // Resolve optional footer text for the current locale.
11
14
  const footerText = config.footerText
12
- ? tConfigField(config.footerText, config)
15
+ ? formatFooterText(tConfigField(config.footerText, config))
13
16
  : undefined
14
17
 
15
18
  // Prepare footer links using the same locale rules as the main menu.
@@ -22,56 +25,125 @@ const footerLinks = (config.footerLinks ?? []).map(
22
25
  },
23
26
  )
24
27
 
28
+ const { links: languageMenuItems } = getLanguageSelectionMenuItems({
29
+ currentLocale,
30
+ pathname: Astro.url.pathname,
31
+ tConfigField,
32
+ })
33
+
34
+ const shouldShowLanguageSelector = languageMenuItems.length > 1
35
+
25
36
  // Hide the footer entirely when there is nothing to show.
26
37
  const shouldRenderFooter =
27
- config.credits || !!footerText || footerLinks.length > 0
38
+ config.credits ||
39
+ !!footerText ||
40
+ footerLinks.length > 0 ||
41
+ shouldShowLanguageSelector
28
42
 
29
43
  if (!shouldRenderFooter) {
30
44
  return
31
45
  }
46
+
47
+ const footerItems = [
48
+ ...(footerText ? [{ type: "text" as const, label: footerText }] : []),
49
+ ...footerLinks.map((link) => ({ type: "link" as const, ...link })),
50
+ ]
32
51
  ---
33
52
 
34
53
  <footer class="w-full border-t border-gray-200 bg-white">
35
- <div
36
- class="mx-auto flex w-full max-w-screen-xl flex-col items-center justify-between gap-4 px-4 py-6 md:flex-row md:px-8"
37
- >
38
- <div
39
- class="flex flex-wrap items-center justify-center gap-2 text-sm text-gray-700 md:justify-start"
40
- >
41
- {footerText && <span>{footerText}</span>}
42
-
54
+ <div class="mx-auto w-full max-w-screen-xl px-4 md:px-8">
55
+ <div class="flex flex-col gap-4 py-6">
43
56
  {
44
- footerLinks.map((link, index) => (
45
- <Fragment>
46
- {(footerText || index > 0) && <span class="text-gray-400">·</span>}
47
- <a
48
- {...getLinkAttributes(link.href)}
49
- class="underline-offset-4 hover:underline"
50
- >
51
- {link.label}
52
- </a>
53
- </Fragment>
54
- ))
57
+ shouldShowLanguageSelector && (
58
+ <div class="flex w-full justify-start">
59
+ <label class="inline-flex items-center gap-2 bg-white py-2 text-sm text-gray-800">
60
+ <GlobeIcon
61
+ className="h-4 w-4 shrink-0 text-gray-500"
62
+ aria-hidden="true"
63
+ />
64
+ <span class="sr-only">{t("ln.select-language")}</span>
65
+ <select
66
+ class="min-w-0 cursor-pointer appearance-none border-0 bg-transparent p-0 pr-0 text-sm font-medium text-gray-800 outline-none focus:ring-0"
67
+ aria-label={t("ln.select-language")}
68
+ data-footer-language-select
69
+ >
70
+ {languageMenuItems.map(({ href, label, locale, active }) => (
71
+ <option value={href} selected={active} lang={locale}>
72
+ {label}
73
+ </option>
74
+ ))}
75
+ </select>
76
+ </label>
77
+ </div>
78
+ )
55
79
  }
56
- </div>
57
80
 
58
- {
59
- config.credits && (
60
- <div class="flex items-center text-sm">
61
- <a
62
- class="flex items-center gap-2 text-gray-800 underline-offset-4 hover:underline"
63
- {...getLinkAttributes("https://lightnet.community")}
64
- >
65
- <img
66
- src={LightNetLogo.src}
67
- alt=""
68
- class="h-5 w-auto"
69
- loading="lazy"
70
- />
71
- <span>{t("ln.footer.powered-by-lightnet")}</span>
72
- </a>
73
- </div>
74
- )
75
- }
81
+ <div
82
+ class="flex w-full flex-col gap-4 text-sm text-gray-800 md:flex-row md:items-center md:justify-between md:gap-6"
83
+ >
84
+ {
85
+ footerItems.length > 0 && (
86
+ <div class="flex w-full flex-row flex-wrap items-center gap-x-2 gap-y-1 md:w-auto">
87
+ {footerItems.map((item, index) => (
88
+ <span class="inline-flex items-center gap-1.5">
89
+ <span
90
+ class:list={[
91
+ "w-3 shrink-0 justify-center text-center text-[0.8em] leading-none text-gray-400",
92
+ index === 0 ? "hidden" : "inline-flex",
93
+ ]}
94
+ aria-hidden="true"
95
+ >
96
+ &middot;
97
+ </span>
98
+ {item.type === "text" ? (
99
+ <span class="text-gray-800">{item.label}</span>
100
+ ) : (
101
+ <a
102
+ {...getLinkAttributes(item.href)}
103
+ class="text-gray-800 underline-offset-4 hover:underline"
104
+ >
105
+ {item.label}
106
+ </a>
107
+ )}
108
+ </span>
109
+ ))}
110
+ </div>
111
+ )
112
+ }
113
+
114
+ {
115
+ config.credits && (
116
+ <div class="flex w-full items-center justify-start md:w-auto md:justify-end">
117
+ <a
118
+ class="flex items-center gap-2 whitespace-nowrap text-gray-800 underline-offset-4 hover:underline"
119
+ {...getLinkAttributes("https://lightnet.community")}
120
+ >
121
+ <img
122
+ src={LightNetLogo.src}
123
+ alt=""
124
+ class="h-5 w-auto"
125
+ loading="lazy"
126
+ />
127
+ <span>{t("ln.footer.powered-by-lightnet")}</span>
128
+ </a>
129
+ </div>
130
+ )
131
+ }
132
+ </div>
133
+ </div>
76
134
  </div>
77
135
  </footer>
136
+
137
+ <script>
138
+ const footerLanguageSelect = document.querySelector<HTMLSelectElement>(
139
+ "[data-footer-language-select]",
140
+ )
141
+
142
+ footerLanguageSelect?.addEventListener("change", () => {
143
+ if (!footerLanguageSelect.value) {
144
+ return
145
+ }
146
+
147
+ window.location.assign(footerLanguageSelect.value)
148
+ })
149
+ </script>
@@ -1,43 +1,28 @@
1
1
  ---
2
2
  import { GlobeIcon } from "lucide-react"
3
3
 
4
- import { resolveTranslatedLanguage } from "../../i18n/resolve-language"
5
- import { localizePath, pathWithoutBase } from "../../utils/paths"
4
+ import { getLanguageSelectionMenuItems } from "./get-language-selection-menu-items"
6
5
  import Menu from "./Menu.astro"
7
6
  import MenuItem from "./MenuItem.astro"
8
7
 
9
- const { locales, currentLocale, tConfigField } = Astro.locals.i18n
8
+ const { currentLocale, tConfigField } = Astro.locals.i18n
10
9
 
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
- }
10
+ const {
11
+ currentPath,
12
+ hasLocale,
13
+ links: translations,
14
+ } = getLanguageSelectionMenuItems({
15
+ currentLocale,
16
+ pathname: Astro.url.pathname,
17
+ tConfigField,
18
+ })
34
19
 
35
20
  const disabled = !hasLocale && currentPath !== "/"
36
21
  ---
37
22
 
38
23
  {
39
24
  translations.length > 1 && (
40
- <Menu disabled={disabled} label="ln.header.select-language">
25
+ <Menu disabled={disabled} label="ln.select-language">
41
26
  <GlobeIcon slot="icon" />
42
27
  {translations.map(({ label, locale, active, href }) => (
43
28
  <MenuItem href={href} hreflang={locale} active={active}>
@@ -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
+ }