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
@@ -1,5 +1,4 @@
1
- import { compareMediaCollectionItems } from "./compare-media-collection-items"
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?: NonNullable<
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
- filters.push(
40
- (item) =>
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
- const { collection } = where
62
- if (!orderBy && collection) {
63
- items.sort((a, b) => compareMediaCollectionItems(a, b, collection))
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)
@@ -0,0 +1,8 @@
1
+ export const isBcp47 = (value: string) => {
2
+ try {
3
+ new Intl.Locale(value)
4
+ return true
5
+ } catch {
6
+ return false
7
+ }
8
+ }
@@ -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 resolveLocales(config).map((locale) => ({ params: { locale } }))
5
+ return config.locales.map((locale) => ({ params: { locale } }))
8
6
  }) satisfies GetStaticPaths
@@ -16,10 +16,28 @@ type I18n = {
16
16
  t: import("./translate").TranslateFn
17
17
 
18
18
  /**
19
- * The current locale or the default locale if the current locale is not available.
19
+ * Resolve a translation map to the language of the current locale.
20
20
  *
21
- * In comparison to Astro.currentLocale this will always return a locale.
22
- * Use Astro.currentLocale if you want to know the locale that is included in the current path.
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
 
@@ -1,23 +1,30 @@
1
1
  import type { MiddlewareHandler } from "astro"
2
2
  import config from "virtual:lightnet/config"
3
3
 
4
- import { resolveDefaultLocale } from "./resolve-default-locale"
4
+ import { resolveCurrentLocaleFromPathname } from "./resolve-current-locale"
5
5
  import { resolveLanguage } from "./resolve-language"
6
- import { resolveLocales } from "./resolve-locales"
7
- import { translationKeys, useTranslate } from "./translate"
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 t = useTranslate(astroCurrentLocale)
15
- const defaultLocale = resolveDefaultLocale(config)
16
- const locales = resolveLocales(config)
17
- const currentLocale = astroCurrentLocale ?? defaultLocale
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 { TranslateFn } from "./translate"
5
+ import type { TranslateMapFn } from "./translate-map"
6
6
 
7
7
  const languages = Object.fromEntries(
8
- config.languages.map((lang) => [lang.code, lang]),
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.mjs file.`,
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 = (bcp47: string, t: TranslateFn) => {
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: t(language.label),
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
+ }
@@ -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 { resolveDefaultLocale } from "./resolve-default-locale"
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 = (key: TranslationKey, options?: TOptions) => string
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((lng) => lng.isSiteLanguage)
20
- .flatMap((lng) => [lng.code, ...lng.fallbackLanguages, "en"]),
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 translations = await prepareI18nextTranslations()
26
- export const translationKeys = [
27
- ...new Set(
28
- Object.values(translations)
29
- .map(({ translation }) => translation)
30
- .flatMap((oneLanguageTranslations) =>
31
- Object.keys(oneLanguageTranslations),
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 i18n = i18next.createInstance({ showSupportNotice: false })
37
- await i18n.init({
38
- lng: defaultLocale,
39
- // don't use name spacing
40
- nsSeparator: false,
41
- // only use flat keys
42
- keySeparator: false,
43
- resources: translations,
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 useTranslate(bcp47: string | undefined): TranslateFn {
47
- const resolvedLocale = bcp47 ?? defaultLocale
48
- const t = i18n.getFixedT<TranslationKey>(resolvedLocale)
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
- ...resolveLanguage(resolvedLocale).fallbackLanguages,
51
- defaultLocale,
80
+ ...(fallbackLanguages[resolvedLocale] ?? []),
81
+ config.defaultLocale,
52
82
  "en",
53
83
  ]
54
- return (key, options) => {
55
- const value = t(key, { fallbackLng, ...options })
56
- // i18next will return the key if no translation is found.
57
- // If a value starts with ln. or x. we consider it to be
58
- // a untranslated translation key.
59
- if (value.match(/^(?:ln|x)\../i)) {
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: '${key}' is undefined for language '${resolvedLocale}'.`,
62
- `To fix the issue, add a translation for '${key}' to src/translations/${resolvedLocale}.yml`,
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
- }
@@ -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 { t, locales } = Astro.locals.i18n
9
+ const { locales, currentLocale, tMap } = Astro.locals.i18n
8
10
 
11
+ const currentPath = pathWithoutBase(Astro.url.pathname)
9
12
  const hasLocale =
10
- Astro.currentLocale &&
11
- (Astro.url.pathname.startsWith(`/${Astro.currentLocale}/`) ||
12
- Astro.url.pathname === `/${Astro.currentLocale}`)
13
+ currentLocale &&
14
+ (currentPath.startsWith(`/${currentLocale}/`) ||
15
+ currentPath === `/${currentLocale}`)
13
16
 
14
- const translations = locales
15
- .map((locale) => ({
16
- locale,
17
- label: resolveTranslatedLanguage(locale, t).labelText,
18
- active: locale === Astro.currentLocale,
19
- href: currentPathWithLocale(locale),
20
- }))
21
- .sort((a, b) => a.label.localeCompare(b.label))
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(Astro.currentLocale.length + 1)
30
+ ? currentPath.slice(currentLocale.length + 1)
27
31
  : currentPath
28
32
  return localizePath(locale, currentPathWithoutLocale)
29
33
  }
30
34
 
31
- const disabled = !hasLocale && Astro.url.pathname !== "/"
35
+ const disabled = !hasLocale && currentPath !== "/"
32
36
  ---
33
37
 
34
38
  {
35
39
  translations.length > 1 && (
36
- <Menu disabled={disabled} icon="mdi--web" label="ln.header.select-language">
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 { icon, label, disabled } = Astro.props
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
- <Icon className={icon} ariaLabel="" />
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-3 px-6 py-3 decoration-gray-800"
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 items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
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 t = Astro.locals.i18n.t
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(Astro.currentLocale)}
43
+ href={searchPagePath(currentLocale)}
42
44
  >
43
- <Icon className="mdi--magnify" ariaLabel="" />
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 icon="mdi--menu" label="ln.header.open-main-menu">
53
- {items.map(({ label, path, isActive, isExternal }) => (
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
- {t(label)}
62
+ {labelText}
60
63
  {isExternal && (
61
- <Icon
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
  ))}