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.
- package/CHANGELOG.md +55 -0
- package/exports/content.ts +7 -2
- package/exports/i18n.ts +0 -1
- package/exports/index.ts +1 -5
- package/exports/utils.ts +0 -1
- package/package.json +26 -12
- package/src/astro-integration/config.ts +60 -49
- package/src/astro-integration/integration.ts +13 -24
- package/src/astro-integration/tailwind.ts +86 -0
- package/src/astro-integration/validators/validate-inline-translations.ts +51 -0
- package/src/astro-integration/validators/validate-languages.ts +39 -0
- package/src/astro-integration/virtual.d.ts +8 -6
- package/src/astro-integration/vite-plugin-lightnet-config.ts +29 -9
- package/src/components/CarouselSection.astro +7 -11
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HighlightSection.astro +4 -7
- package/src/components/Icon.tsx +2 -2
- package/src/components/MediaGallerySection.astro +88 -68
- package/src/components/MediaList.astro +9 -7
- package/src/components/SearchInput.astro +7 -4
- package/src/components/Section.astro +7 -5
- package/src/components/VideoPlayer.astro +2 -3
- package/src/content/content-schema.ts +129 -142
- package/src/content/get-categories.ts +52 -28
- package/src/content/get-languages.ts +29 -8
- package/src/content/get-media-collections.ts +43 -0
- package/src/content/get-media-types.ts +41 -7
- package/src/content/query-media-items.ts +23 -13
- package/src/i18n/bcp-47.ts +8 -0
- package/src/i18n/get-locale-paths.ts +1 -3
- package/src/i18n/locals.d.ts +21 -3
- package/src/i18n/locals.ts +18 -11
- package/src/i18n/resolve-current-locale.ts +18 -0
- package/src/i18n/resolve-language.ts +10 -5
- package/src/i18n/translate-map.ts +70 -0
- package/src/i18n/translate.ts +68 -47
- package/src/layouts/Page.astro +5 -3
- package/src/layouts/components/LanguagePicker.astro +22 -17
- package/src/layouts/components/Menu.astro +2 -5
- package/src/layouts/components/MenuItem.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +29 -29
- package/src/layouts/components/PageTitle.astro +23 -7
- package/src/pages/404Route.astro +2 -1
- package/src/pages/RootRoute.astro +6 -1
- package/src/pages/details-page/DefaultDetailsPage.astro +9 -2
- package/src/pages/details-page/DetailsPageRoute.astro +1 -2
- package/src/pages/details-page/components/AudioPanel.astro +7 -3
- package/src/pages/details-page/components/AudioPlayer.astro +2 -2
- package/src/pages/details-page/components/ContentSection.astro +67 -44
- package/src/pages/details-page/components/MediaCollection.astro +8 -4
- package/src/pages/details-page/components/MediaCollectionsSection.astro +3 -6
- package/src/pages/details-page/components/main-details/EditButton.astro +22 -10
- package/src/pages/details-page/components/main-details/OpenButton.astro +17 -12
- package/src/pages/details-page/components/main-details/ShareButton.astro +3 -2
- package/src/pages/details-page/components/more-details/Categories.astro +5 -3
- package/src/pages/details-page/components/more-details/Languages.astro +12 -7
- package/src/pages/details-page/utils/create-content-metadata.ts +24 -9
- package/src/pages/details-page/utils/get-translations.ts +6 -0
- package/src/pages/search-page/components/LoadingSkeleton.tsx +6 -5
- package/src/pages/search-page/components/SearchFilter.astro +10 -21
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.astro +10 -7
- package/src/pages/search-page/components/SearchListItem.tsx +5 -4
- package/src/pages/search-page/hooks/use-search.ts +5 -2
- package/src/utils/lazy.ts +20 -0
- package/src/utils/paths.ts +40 -3
- package/src/utils/urls.ts +1 -2
- package/src/utils/verify-schema.ts +12 -10
- package/tailwind.config.ts +1 -25
- package/__e2e__/admin.spec.ts +0 -20
- package/__e2e__/basics-fixture.ts +0 -77
- package/__e2e__/fixtures/basics/astro.config.mjs +0 -40
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +0 -21
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +0 -21
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +0 -21
- package/__e2e__/fixtures/basics/node_modules/.bin/tsc +0 -21
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +0 -21
- package/__e2e__/fixtures/basics/package.json +0 -21
- package/__e2e__/fixtures/basics/public/favicon.svg +0 -1
- package/__e2e__/fixtures/basics/public/files/example.pdf +0 -0
- package/__e2e__/fixtures/basics/src/assets/logo.png +0 -0
- package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +0 -3
- package/__e2e__/fixtures/basics/src/content/categories/teens.json +0 -3
- package/__e2e__/fixtures/basics/src/content/categories/theology.json +0 -3
- package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +0 -13
- package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +0 -12
- package/__e2e__/fixtures/basics/src/content/media/images/cover.jpg +0 -0
- package/__e2e__/fixtures/basics/src/content/media/images/how-to-kickflip--en.webp +0 -0
- package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +0 -15
- package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +0 -3
- package/__e2e__/fixtures/basics/src/content/media-types/audio.json +0 -7
- package/__e2e__/fixtures/basics/src/content/media-types/book.json +0 -9
- package/__e2e__/fixtures/basics/src/content/media-types/video.json +0 -7
- package/__e2e__/fixtures/basics/src/content.config.ts +0 -3
- package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +0 -16
- package/__e2e__/fixtures/basics/src/translations/de.yml +0 -9
- package/__e2e__/fixtures/basics/src/translations/en.yml +0 -9
- package/__e2e__/fixtures/basics/tailwind.config.mjs +0 -8
- package/__e2e__/global.teardown.ts +0 -5
- package/__e2e__/homepage.spec.ts +0 -123
- package/__e2e__/search.spec.ts +0 -14
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +0 -135
- package/__tests__/utils/markdown.spec.ts +0 -74
- package/__tests__/utils/urls.spec.ts +0 -27
- package/playwright.config.ts +0 -31
- package/src/astro-integration/project-context.ts +0 -5
- package/src/content/compare-media-collection-items.ts +0 -24
- package/src/i18n/resolve-default-locale.ts +0 -19
- package/src/i18n/resolve-locales.ts +0 -5
- package/src/pages/details-page/utils/get-collection-items.ts +0 -29
- package/vitest.config.js +0 -20
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { MediaItemEntry } from "./content-schema"
|
|
1
|
+
import type { MediaCollectionEntry, MediaItemEntry } from "./content-schema"
|
|
3
2
|
|
|
4
3
|
export type MediaItemQuery<TMediaItem extends MediaItemEntry> = {
|
|
5
4
|
/**
|
|
@@ -9,9 +8,7 @@ export type MediaItemQuery<TMediaItem extends MediaItemEntry> = {
|
|
|
9
8
|
type?: TMediaItem["data"]["type"]["id"]
|
|
10
9
|
language?: string
|
|
11
10
|
category?: NonNullable<TMediaItem["data"]["categories"]>[number]["id"]
|
|
12
|
-
collection?:
|
|
13
|
-
TMediaItem["data"]["collections"]
|
|
14
|
-
>[number]["collection"]["id"]
|
|
11
|
+
collection?: MediaCollectionEntry["id"]
|
|
15
12
|
}
|
|
16
13
|
orderBy?: "dateCreated" | "title"
|
|
17
14
|
limit?: number
|
|
@@ -19,6 +16,7 @@ export type MediaItemQuery<TMediaItem extends MediaItemEntry> = {
|
|
|
19
16
|
|
|
20
17
|
export const queryMediaItems = async <TMediaItem extends MediaItemEntry>(
|
|
21
18
|
allItems: Promise<TMediaItem[]>,
|
|
19
|
+
mediaCollections: Promise<MediaCollectionEntry[]>,
|
|
22
20
|
query: MediaItemQuery<TMediaItem>,
|
|
23
21
|
) => {
|
|
24
22
|
const { where = {}, orderBy, limit } = query
|
|
@@ -35,14 +33,18 @@ export const queryMediaItems = async <TMediaItem extends MediaItemEntry>(
|
|
|
35
33
|
(item) => !!item.data.categories?.find(({ id }) => id === where.category),
|
|
36
34
|
)
|
|
37
35
|
}
|
|
36
|
+
|
|
37
|
+
let mediaCollection: MediaCollectionEntry | undefined
|
|
38
38
|
if (where.collection) {
|
|
39
|
-
|
|
40
|
-
(
|
|
41
|
-
!!item.data.collections?.find(
|
|
42
|
-
({ collection }) => collection.id === where.collection,
|
|
43
|
-
),
|
|
39
|
+
mediaCollection = (await mediaCollections).find(
|
|
40
|
+
(mc) => mc.id === where.collection,
|
|
44
41
|
)
|
|
42
|
+
const mediaIdsInCollection = new Set(
|
|
43
|
+
mediaCollection?.data.mediaItems.map(({ id }) => id),
|
|
44
|
+
)
|
|
45
|
+
filters.push((item) => mediaIdsInCollection.has(item.id))
|
|
45
46
|
}
|
|
47
|
+
|
|
46
48
|
const combinedFilter = (item: TMediaItem) =>
|
|
47
49
|
filters.every((filter) => filter(item))
|
|
48
50
|
|
|
@@ -58,9 +60,17 @@ export const queryMediaItems = async <TMediaItem extends MediaItemEntry>(
|
|
|
58
60
|
item1.data.title.localeCompare(item2.data.title),
|
|
59
61
|
)
|
|
60
62
|
}
|
|
61
|
-
|
|
62
|
-
if (!orderBy &&
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
if (!orderBy && mediaCollection) {
|
|
65
|
+
const positionMap = new Map<string, number>()
|
|
66
|
+
mediaCollection.data.mediaItems.forEach((item, index) =>
|
|
67
|
+
positionMap.set(item.id, index),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
items.sort(
|
|
71
|
+
(item1, item2) =>
|
|
72
|
+
(positionMap.get(item1.id) ?? -1) - (positionMap.get(item2.id) ?? -1),
|
|
73
|
+
)
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
return items.slice(0, limit)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { GetStaticPaths } from "astro"
|
|
2
2
|
import config from "virtual:lightnet/config"
|
|
3
3
|
|
|
4
|
-
import { resolveLocales } from "./resolve-locales"
|
|
5
|
-
|
|
6
4
|
export const getLocalePaths = (() => {
|
|
7
|
-
return
|
|
5
|
+
return config.locales.map((locale) => ({ params: { locale } }))
|
|
8
6
|
}) satisfies GetStaticPaths
|
package/src/i18n/locals.d.ts
CHANGED
|
@@ -16,10 +16,28 @@ type I18n = {
|
|
|
16
16
|
t: import("./translate").TranslateFn
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Resolve a translation map to the language of the current locale.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Use this for inline translation maps, such as labels from config or content entries.
|
|
22
|
+
*
|
|
23
|
+
* @param translationMap Localized values keyed by locale code.
|
|
24
|
+
* @param context Describes where the map came from. Pass the original
|
|
25
|
+
* field path so missing-translation messages can point to the exact config
|
|
26
|
+
* or content field that needs a value, for example `["config", "title"]`
|
|
27
|
+
* or `["content", 0, "label"]`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* Astro.locals.i18n.tMap(config.title, {
|
|
31
|
+
* path: ["config", "title"],
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
tMap: import("./translate-map").TranslateMapFn
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The current locale resolved by LightNet from the URL pathname.
|
|
38
|
+
*
|
|
39
|
+
* If no supported locale is present in the pathname,
|
|
40
|
+
* this falls back to the configured default locale.
|
|
23
41
|
*/
|
|
24
42
|
currentLocale: string
|
|
25
43
|
|
package/src/i18n/locals.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import type { MiddlewareHandler } from "astro"
|
|
2
2
|
import config from "virtual:lightnet/config"
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { resolveCurrentLocaleFromPathname } from "./resolve-current-locale"
|
|
5
5
|
import { resolveLanguage } from "./resolve-language"
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { getTranslationKeys, useTranslate } from "./translate"
|
|
7
|
+
import { useTranslateMap } from "./translate-map"
|
|
8
8
|
|
|
9
|
-
export const onRequest: MiddlewareHandler = (
|
|
10
|
-
{ locals, currentLocale: astroCurrentLocale },
|
|
11
|
-
next,
|
|
12
|
-
) => {
|
|
9
|
+
export const onRequest: MiddlewareHandler = async ({ locals, url }, next) => {
|
|
13
10
|
if (!locals.i18n) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
11
|
+
const defaultLocale = config.defaultLocale
|
|
12
|
+
const locales = config.locales
|
|
13
|
+
const currentLocale = resolveCurrentLocaleFromPathname({
|
|
14
|
+
pathname: url.pathname,
|
|
15
|
+
base: import.meta.env.BASE_URL,
|
|
16
|
+
locales,
|
|
17
|
+
defaultLocale,
|
|
18
|
+
})
|
|
19
|
+
const [t, translationKeys] = await Promise.all([
|
|
20
|
+
useTranslate(currentLocale),
|
|
21
|
+
getTranslationKeys(),
|
|
22
|
+
])
|
|
23
|
+
const tMap = useTranslateMap(currentLocale)
|
|
18
24
|
const { direction } = resolveLanguage(currentLocale)
|
|
19
25
|
locals.i18n = {
|
|
20
26
|
t,
|
|
27
|
+
tMap,
|
|
21
28
|
currentLocale,
|
|
22
29
|
defaultLocale,
|
|
23
30
|
direction,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { pathWithoutBase } from "../utils/paths"
|
|
2
|
+
|
|
3
|
+
export function resolveCurrentLocaleFromPathname({
|
|
4
|
+
pathname,
|
|
5
|
+
base,
|
|
6
|
+
locales,
|
|
7
|
+
defaultLocale,
|
|
8
|
+
}: {
|
|
9
|
+
pathname: string
|
|
10
|
+
base?: string
|
|
11
|
+
locales: string[]
|
|
12
|
+
defaultLocale: string
|
|
13
|
+
}) {
|
|
14
|
+
const pathnameWithoutBase = base ? pathWithoutBase(pathname, base) : pathname
|
|
15
|
+
const firstPathSegment = pathnameWithoutBase.split("/")[1]
|
|
16
|
+
|
|
17
|
+
return locales.includes(firstPathSegment) ? firstPathSegment : defaultLocale
|
|
18
|
+
}
|
|
@@ -2,10 +2,10 @@ import { AstroError } from "astro/errors"
|
|
|
2
2
|
import i18next from "i18next"
|
|
3
3
|
import config from "virtual:lightnet/config"
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { TranslateMapFn } from "./translate-map"
|
|
6
6
|
|
|
7
7
|
const languages = Object.fromEntries(
|
|
8
|
-
config.languages.map((
|
|
8
|
+
config.languages.map((language) => [language.code, language]),
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
export const resolveLanguage = (bcp47: string) => {
|
|
@@ -14,7 +14,7 @@ export const resolveLanguage = (bcp47: string) => {
|
|
|
14
14
|
if (!language) {
|
|
15
15
|
throw new AstroError(
|
|
16
16
|
`Missing language code "${bcp47}"`,
|
|
17
|
-
`To fix the issue, add a language with the code "${bcp47}" to the LightNet configuration in your astro.config
|
|
17
|
+
`To fix the issue, add a language with the code "${bcp47}" to the LightNet configuration in your astro.config file.`,
|
|
18
18
|
)
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
@@ -23,10 +23,15 @@ export const resolveLanguage = (bcp47: string) => {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export const resolveTranslatedLanguage = (
|
|
26
|
+
export const resolveTranslatedLanguage = (
|
|
27
|
+
bcp47: string,
|
|
28
|
+
tMap: TranslateMapFn,
|
|
29
|
+
) => {
|
|
27
30
|
const language = resolveLanguage(bcp47)
|
|
28
31
|
return {
|
|
29
32
|
...language,
|
|
30
|
-
labelText:
|
|
33
|
+
labelText: tMap(language.label, {
|
|
34
|
+
path: ["languages", bcp47, "label"],
|
|
35
|
+
}),
|
|
31
36
|
}
|
|
32
37
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A map of translated values keyed by locale code.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* {
|
|
9
|
+
* en: "Hello",
|
|
10
|
+
* de: "Hallo"
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
export type TranslationMap = Record<string, string | undefined>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Describes where a translation map comes from so missing-value
|
|
17
|
+
* errors can point to the exact field in config or content.
|
|
18
|
+
*/
|
|
19
|
+
export type TranslationMapContext = {
|
|
20
|
+
path: (string | number)[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a translation map for the current locale, falling back
|
|
25
|
+
* to configured default locale when needed.
|
|
26
|
+
*
|
|
27
|
+
* @param translationMap Localized values keyed by locale code.
|
|
28
|
+
* @param context Describes where this map came from in config or content.
|
|
29
|
+
* The `path` is required so missing-translation errors can name the exact
|
|
30
|
+
* field that needs fixing, for example `["config", "title"]` or
|
|
31
|
+
* `["content", 0, "label"]`.
|
|
32
|
+
*/
|
|
33
|
+
export type TranslateMapFn = (
|
|
34
|
+
translationMap: TranslationMap,
|
|
35
|
+
context: TranslationMapContext,
|
|
36
|
+
) => string
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a locale-bound translation-map resolver.
|
|
40
|
+
*
|
|
41
|
+
* Use this for values that already contain their own translations,
|
|
42
|
+
* such as labels in config or content frontmatter. In contrast to `t`,
|
|
43
|
+
* this does not look up translation keys in translation files.
|
|
44
|
+
*
|
|
45
|
+
* @param currentLocale The locale that should be resolved first before
|
|
46
|
+
* falling back to LightNet's configured default locale.
|
|
47
|
+
*/
|
|
48
|
+
export function useTranslateMap(currentLocale: string): TranslateMapFn {
|
|
49
|
+
return (translationMap: TranslationMap, context: TranslationMapContext) => {
|
|
50
|
+
const currentLocaleValue = translationMap[currentLocale]
|
|
51
|
+
if (currentLocaleValue) {
|
|
52
|
+
return currentLocaleValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const defaultLocaleValue = translationMap[config.defaultLocale]
|
|
56
|
+
if (defaultLocaleValue) {
|
|
57
|
+
return defaultLocaleValue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const availableLocales = Object.keys(translationMap)
|
|
61
|
+
const availableLocalesText = availableLocales.length
|
|
62
|
+
? availableLocales.map((locale) => `"${locale}"`).join(", ")
|
|
63
|
+
: "none"
|
|
64
|
+
|
|
65
|
+
throw new AstroError(
|
|
66
|
+
`Missing translation map value for "${context.path.join(".")}" in locales "${currentLocale}" and "${config.defaultLocale}"`,
|
|
67
|
+
`Available locales: ${availableLocalesText}. Add a value for "${currentLocale}" or "${config.defaultLocale}" to this inline translation map.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/i18n/translate.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { AstroError } from "astro/errors"
|
|
|
2
2
|
import i18next, { type TOptions } from "i18next"
|
|
3
3
|
import config from "virtual:lightnet/config"
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { resolveLanguage } from "./resolve-language"
|
|
5
|
+
import { lazy } from "../utils/lazy"
|
|
7
6
|
import { type LightNetTranslationKey, loadTranslations } from "./translations"
|
|
8
7
|
|
|
9
8
|
// We add (string & NonNullable<unknown>) to preserve typescript autocompletion for known keys
|
|
@@ -11,67 +10,89 @@ export type TranslationKey =
|
|
|
11
10
|
| LightNetTranslationKey
|
|
12
11
|
| (string & NonNullable<unknown>)
|
|
13
12
|
|
|
14
|
-
export type TranslateFn = (
|
|
13
|
+
export type TranslateFn = (input: TranslationKey, options?: TOptions) => string
|
|
14
|
+
|
|
15
|
+
const fallbackLanguages = Object.fromEntries(
|
|
16
|
+
config.languages.map(({ code, fallbackLanguages }) => [
|
|
17
|
+
code,
|
|
18
|
+
fallbackLanguages,
|
|
19
|
+
]),
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
const languageCodes = [
|
|
17
23
|
...new Set(
|
|
18
24
|
config.languages
|
|
19
|
-
.filter((
|
|
20
|
-
.flatMap((
|
|
25
|
+
.filter(({ isSiteLanguage }) => isSiteLanguage)
|
|
26
|
+
.flatMap(({ code, fallbackLanguages }) => [
|
|
27
|
+
code,
|
|
28
|
+
...fallbackLanguages,
|
|
29
|
+
"en",
|
|
30
|
+
]),
|
|
21
31
|
),
|
|
22
32
|
]
|
|
23
|
-
const defaultLocale = resolveDefaultLocale(config)
|
|
24
33
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
]
|
|
34
|
+
const i18nextTranslations = lazy(async () => {
|
|
35
|
+
const result: Record<string, { translation: Record<string, string> }> = {}
|
|
36
|
+
for (const bcp47 of languageCodes) {
|
|
37
|
+
result[bcp47] = {
|
|
38
|
+
translation: await loadTranslations(bcp47),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
})
|
|
35
43
|
|
|
36
|
-
const
|
|
37
|
-
await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const translationKeys = lazy(async () => {
|
|
45
|
+
const translations = await i18nextTranslations.get()
|
|
46
|
+
return [
|
|
47
|
+
...new Set(
|
|
48
|
+
Object.values(translations)
|
|
49
|
+
.map(({ translation }) => translation)
|
|
50
|
+
.flatMap((oneLanguageTranslations) =>
|
|
51
|
+
Object.keys(oneLanguageTranslations),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
]
|
|
44
55
|
})
|
|
45
56
|
|
|
46
|
-
export function
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
export function getTranslationKeys() {
|
|
58
|
+
return translationKeys.get()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function useTranslate(
|
|
62
|
+
bcp47: string | undefined,
|
|
63
|
+
): Promise<TranslateFn> {
|
|
64
|
+
const resolvedLocale = bcp47 ?? config.defaultLocale
|
|
65
|
+
const translations = await i18nextTranslations.get()
|
|
66
|
+
const availableTranslationKeys = new Set(await translationKeys.get())
|
|
67
|
+
|
|
68
|
+
const i18n = i18next.createInstance({ showSupportNotice: false })
|
|
69
|
+
|
|
70
|
+
await i18n.init({
|
|
71
|
+
lng: config.defaultLocale,
|
|
72
|
+
// don't use name spacing
|
|
73
|
+
nsSeparator: false,
|
|
74
|
+
// only use flat keys
|
|
75
|
+
keySeparator: false,
|
|
76
|
+
resources: translations,
|
|
77
|
+
})
|
|
78
|
+
|
|
49
79
|
const fallbackLng = [
|
|
50
|
-
...
|
|
51
|
-
defaultLocale,
|
|
80
|
+
...(fallbackLanguages[resolvedLocale] ?? []),
|
|
81
|
+
config.defaultLocale,
|
|
52
82
|
"en",
|
|
53
83
|
]
|
|
54
|
-
return (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
return (input, options) => {
|
|
85
|
+
const t = i18n.getFixedT(resolvedLocale) as (
|
|
86
|
+
key: TranslationKey,
|
|
87
|
+
options?: TOptions,
|
|
88
|
+
) => string
|
|
89
|
+
const value = t(input, { fallbackLng, ...options })
|
|
90
|
+
if (value === input && !availableTranslationKeys.has(input)) {
|
|
60
91
|
throw new AstroError(
|
|
61
|
-
`Missing translation: '${
|
|
62
|
-
`To fix the issue, add a translation for '${
|
|
92
|
+
`Missing translation: '${input}' is undefined for language '${resolvedLocale}'.`,
|
|
93
|
+
`To fix the issue, add a translation for '${input}' to src/translations/${resolvedLocale}.yml`,
|
|
63
94
|
)
|
|
64
95
|
}
|
|
65
96
|
return value
|
|
66
97
|
}
|
|
67
98
|
}
|
|
68
|
-
|
|
69
|
-
async function prepareI18nextTranslations() {
|
|
70
|
-
const result: Record<string, { translation: Record<string, string> }> = {}
|
|
71
|
-
for (const bcp47 of languageCodes) {
|
|
72
|
-
result[bcp47] = {
|
|
73
|
-
translation: await loadTranslations(bcp47),
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return result
|
|
77
|
-
}
|
package/src/layouts/Page.astro
CHANGED
|
@@ -6,6 +6,7 @@ import CustomHead from "virtual:lightnet/components/CustomHead"
|
|
|
6
6
|
import config from "virtual:lightnet/config"
|
|
7
7
|
|
|
8
8
|
import { resolveLanguage } from "../i18n/resolve-language"
|
|
9
|
+
import { pathWithBase } from "../utils/paths"
|
|
9
10
|
import Favicon from "./components/Favicon.astro"
|
|
10
11
|
import Footer from "./components/Footer.astro"
|
|
11
12
|
import Header from "./components/Header.astro"
|
|
@@ -19,9 +20,10 @@ interface Props {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const { title, description, mainClass, locale } = Astro.props
|
|
22
|
-
const configTitle = Astro.locals.i18n.t(config.title)
|
|
23
|
-
|
|
24
23
|
const currentLocale = locale ?? Astro.locals.i18n.currentLocale
|
|
24
|
+
const configTitle = Astro.locals.i18n.tMap(config.title, {
|
|
25
|
+
path: ["config", "title"],
|
|
26
|
+
})
|
|
25
27
|
const { direction } = resolveLanguage(currentLocale)
|
|
26
28
|
---
|
|
27
29
|
|
|
@@ -34,7 +36,7 @@ const { direction } = resolveLanguage(currentLocale)
|
|
|
34
36
|
<title>{title ? `${title} | ${configTitle}` : configTitle}</title>
|
|
35
37
|
{description && <meta name="description" content={description} />}
|
|
36
38
|
{config.manifest && <link rel="manifest" href={config.manifest} />}
|
|
37
|
-
<link rel="prefetch" href="/api/internal/search.json" />
|
|
39
|
+
<link rel="prefetch" href={pathWithBase("/api/internal/search.json")} />
|
|
38
40
|
<Favicon />
|
|
39
41
|
<ViewTransition />
|
|
40
42
|
</head>
|
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { GlobeIcon } from "lucide-react"
|
|
3
|
+
|
|
2
4
|
import { resolveTranslatedLanguage } from "../../i18n/resolve-language"
|
|
3
|
-
import { localizePath } from "../../utils/paths"
|
|
5
|
+
import { localizePath, pathWithoutBase } from "../../utils/paths"
|
|
4
6
|
import Menu from "./Menu.astro"
|
|
5
7
|
import MenuItem from "./MenuItem.astro"
|
|
6
8
|
|
|
7
|
-
const {
|
|
9
|
+
const { locales, currentLocale, tMap } = Astro.locals.i18n
|
|
8
10
|
|
|
11
|
+
const currentPath = pathWithoutBase(Astro.url.pathname)
|
|
9
12
|
const hasLocale =
|
|
10
|
-
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
+
currentLocale &&
|
|
14
|
+
(currentPath.startsWith(`/${currentLocale}/`) ||
|
|
15
|
+
currentPath === `/${currentLocale}`)
|
|
13
16
|
|
|
14
|
-
const translations =
|
|
15
|
-
.
|
|
16
|
-
locale
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const translations = (
|
|
18
|
+
await Promise.all(
|
|
19
|
+
locales.map(async (locale) => ({
|
|
20
|
+
locale,
|
|
21
|
+
label: resolveTranslatedLanguage(locale, tMap).labelText,
|
|
22
|
+
active: locale === currentLocale,
|
|
23
|
+
href: currentPathWithLocale(locale),
|
|
24
|
+
})),
|
|
25
|
+
)
|
|
26
|
+
).sort((a, b) => a.label.localeCompare(b.label))
|
|
22
27
|
|
|
23
28
|
function currentPathWithLocale(locale: string) {
|
|
24
|
-
const currentPath = Astro.url.pathname
|
|
25
29
|
const currentPathWithoutLocale = hasLocale
|
|
26
|
-
? currentPath.slice(
|
|
30
|
+
? currentPath.slice(currentLocale.length + 1)
|
|
27
31
|
: currentPath
|
|
28
32
|
return localizePath(locale, currentPathWithoutLocale)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
const disabled = !hasLocale &&
|
|
35
|
+
const disabled = !hasLocale && currentPath !== "/"
|
|
32
36
|
---
|
|
33
37
|
|
|
34
38
|
{
|
|
35
39
|
translations.length > 1 && (
|
|
36
|
-
<Menu disabled={disabled}
|
|
40
|
+
<Menu disabled={disabled} label="ln.header.select-language">
|
|
41
|
+
<GlobeIcon slot="icon" />
|
|
37
42
|
{translations.map(({ label, locale, active, href }) => (
|
|
38
43
|
<MenuItem href={href} hreflang={locale} active={active}>
|
|
39
44
|
{label}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
import Icon from "../../components/Icon"
|
|
3
|
-
|
|
4
2
|
interface Props {
|
|
5
|
-
icon: string
|
|
6
3
|
label: string
|
|
7
4
|
disabled?: boolean
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
const {
|
|
7
|
+
const { label, disabled } = Astro.props
|
|
11
8
|
---
|
|
12
9
|
|
|
13
10
|
<ln-menu class="relative flex h-full items-center">
|
|
@@ -17,7 +14,7 @@ const { icon, label, disabled } = Astro.props
|
|
|
17
14
|
aria-label={Astro.locals.i18n.t(label)}
|
|
18
15
|
class="flex rounded-md p-3 text-gray-600 hover:text-primary disabled:text-gray-300"
|
|
19
16
|
>
|
|
20
|
-
<
|
|
17
|
+
<slot name="icon" />
|
|
21
18
|
</button>
|
|
22
19
|
|
|
23
20
|
<ul
|
|
@@ -13,7 +13,7 @@ const { href, hreflang, active, target } = Astro.props
|
|
|
13
13
|
href={href}
|
|
14
14
|
hreflang={hreflang}
|
|
15
15
|
target={target}
|
|
16
|
-
class="flex items-center gap-
|
|
16
|
+
class="flex items-center gap-2 px-6 py-3 decoration-gray-800"
|
|
17
17
|
class:list={[active ? "font-bold" : "hover:underline"]}
|
|
18
18
|
>
|
|
19
19
|
<slot />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { ExternalLinkIcon, MenuIcon, SearchIcon } from "lucide-react"
|
|
2
3
|
import config from "virtual:lightnet/config"
|
|
3
4
|
|
|
4
|
-
import Icon from "../../components/Icon"
|
|
5
5
|
import { localizePath, searchPagePath } from "../../utils/paths"
|
|
6
6
|
import { isExternalUrl } from "../../utils/urls"
|
|
7
7
|
import LanguagePicker from "./LanguagePicker.astro"
|
|
@@ -9,26 +9,28 @@ import Menu from "./Menu.astro"
|
|
|
9
9
|
import MenuItem from "./MenuItem.astro"
|
|
10
10
|
|
|
11
11
|
const currentPath = Astro.url.pathname
|
|
12
|
-
const
|
|
13
|
-
const isExternal = isExternalUrl(href)
|
|
14
|
-
const path =
|
|
15
|
-
isExternal || !requiresLocale
|
|
16
|
-
? href
|
|
17
|
-
: localizePath(Astro.currentLocale, href)
|
|
18
|
-
const isActive =
|
|
19
|
-
!isExternal &&
|
|
20
|
-
(currentPath === localizePath(Astro.currentLocale, href) ||
|
|
21
|
-
currentPath === localizePath(Astro.currentLocale, `${href}/`) ||
|
|
22
|
-
currentPath === href)
|
|
23
|
-
return {
|
|
24
|
-
path,
|
|
25
|
-
isExternal,
|
|
26
|
-
label,
|
|
27
|
-
isActive,
|
|
28
|
-
}
|
|
29
|
-
})
|
|
12
|
+
const { t, tMap, currentLocale } = Astro.locals.i18n
|
|
30
13
|
|
|
31
|
-
const
|
|
14
|
+
const items = (config.mainMenu ?? []).map(
|
|
15
|
+
({ href, label, requiresLocale }, index) => {
|
|
16
|
+
const isExternal = isExternalUrl(href)
|
|
17
|
+
const path =
|
|
18
|
+
isExternal || !requiresLocale ? href : localizePath(currentLocale, href)
|
|
19
|
+
const isActive =
|
|
20
|
+
!isExternal &&
|
|
21
|
+
(currentPath === localizePath(currentLocale, href) ||
|
|
22
|
+
currentPath === localizePath(currentLocale, `${href}/`) ||
|
|
23
|
+
currentPath === href)
|
|
24
|
+
return {
|
|
25
|
+
path,
|
|
26
|
+
isExternal,
|
|
27
|
+
labelText: tMap(label, {
|
|
28
|
+
path: ["config", "mainMenu", index, "label"],
|
|
29
|
+
}),
|
|
30
|
+
isActive,
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
)
|
|
32
34
|
---
|
|
33
35
|
|
|
34
36
|
<nav class="-me-3 flex items-center">
|
|
@@ -38,9 +40,9 @@ const t = Astro.locals.i18n.t
|
|
|
38
40
|
class="flex p-3 text-gray-600 hover:text-primary"
|
|
39
41
|
aria-label={t("ln.search.title")}
|
|
40
42
|
data-astro-prefetch="viewport"
|
|
41
|
-
href={searchPagePath(
|
|
43
|
+
href={searchPagePath(currentLocale)}
|
|
42
44
|
>
|
|
43
|
-
<
|
|
45
|
+
<SearchIcon />
|
|
44
46
|
</a>
|
|
45
47
|
)
|
|
46
48
|
}
|
|
@@ -49,19 +51,17 @@ const t = Astro.locals.i18n.t
|
|
|
49
51
|
|
|
50
52
|
{
|
|
51
53
|
!!items.length && (
|
|
52
|
-
<Menu
|
|
53
|
-
|
|
54
|
+
<Menu label="ln.header.open-main-menu">
|
|
55
|
+
<MenuIcon slot="icon" />
|
|
56
|
+
{items.map(({ labelText, path, isActive, isExternal }) => (
|
|
54
57
|
<MenuItem
|
|
55
58
|
href={path}
|
|
56
59
|
active={isActive}
|
|
57
60
|
target={isExternal ? "_blank" : "_self"}
|
|
58
61
|
>
|
|
59
|
-
{
|
|
62
|
+
{labelText}
|
|
60
63
|
{isExternal && (
|
|
61
|
-
<
|
|
62
|
-
className="mdi--external-link shrink-0 text-base"
|
|
63
|
-
ariaLabel=""
|
|
64
|
-
/>
|
|
64
|
+
<ExternalLinkIcon size="1.2rem" className="shrink-0" />
|
|
65
65
|
)}
|
|
66
66
|
</MenuItem>
|
|
67
67
|
))}
|