lightnet 3.10.1 → 3.10.2
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 +6 -0
- package/package.json +1 -1
- package/src/admin/components/form/FieldErrors.tsx +4 -1
- package/src/admin/components/form/SubmitButton.tsx +6 -4
- package/src/admin/i18n/translations/en.yml +10 -0
- package/src/admin/pages/media/EditForm.tsx +38 -23
- package/src/admin/pages/media/EditRoute.astro +12 -3
- package/src/admin/types/media-item.ts +4 -2
- package/src/i18n/locals.d.ts +35 -28
- package/src/i18n/locals.ts +2 -1
- package/src/i18n/react/i18n-context.ts +32 -0
- package/src/i18n/react/prepare-i18n-config.ts +31 -0
- package/src/i18n/react/useI18n.ts +15 -0
- package/src/i18n/translate.ts +15 -3
- package/src/pages/search-page/components/LoadingSkeleton.tsx +3 -1
- package/src/pages/search-page/components/SearchFilter.astro +12 -3
- package/src/pages/search-page/components/SearchFilter.tsx +4 -7
- package/src/pages/search-page/components/SearchList.astro +7 -6
- package/src/pages/search-page/components/SearchList.tsx +12 -15
- package/src/pages/search-page/components/SearchListItem.tsx +2 -4
- package/src/pages/search-page/utils/search-filter-translations.ts +0 -20
- package/src/pages/search-page/utils/search-translations.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.10.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#325](https://github.com/LightNetDev/LightNet/pull/325) [`07b71f4`](https://github.com/LightNetDev/LightNet/commit/07b71f4d44f8e005aafb48bedc347f14c71e182a) Thanks [@smn-cds](https://github.com/smn-cds)! - Refactor internal react i18n logic.
|
|
8
|
+
|
|
3
9
|
## 3.10.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { AnyFieldMeta } from "@tanstack/react-form"
|
|
2
2
|
|
|
3
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
4
|
+
|
|
3
5
|
type FieldErrorsProps = {
|
|
4
6
|
meta: AnyFieldMeta
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
|
|
10
|
+
const { t } = useI18n()
|
|
8
11
|
if (!meta.isTouched || meta.isValid) return null
|
|
9
12
|
|
|
10
13
|
return (
|
|
11
14
|
<ul className="my-2 flex flex-col gap-1" role="alert">
|
|
12
15
|
{meta.errors.map((error) => (
|
|
13
16
|
<li className="text-sm text-rose-800" key={error.code}>
|
|
14
|
-
{error.message}
|
|
17
|
+
{t(error.message)}
|
|
15
18
|
</li>
|
|
16
19
|
))}
|
|
17
20
|
</ul>
|
|
@@ -2,6 +2,7 @@ import { useStore } from "@tanstack/react-form"
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react"
|
|
3
3
|
|
|
4
4
|
import Icon from "../../../components/Icon"
|
|
5
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
5
6
|
import { useFormContext } from "./form-context"
|
|
6
7
|
|
|
7
8
|
const SUCCESS_DURATION_MS = 2000
|
|
@@ -18,9 +19,9 @@ const buttonStateClasses = {
|
|
|
18
19
|
} as const
|
|
19
20
|
|
|
20
21
|
const buttonLabels = {
|
|
21
|
-
idle: "
|
|
22
|
-
success: "
|
|
23
|
-
error: "
|
|
22
|
+
idle: "ln.admin.save",
|
|
23
|
+
success: "ln.admin.saved",
|
|
24
|
+
error: "ln.admin.failed",
|
|
24
25
|
} as const
|
|
25
26
|
|
|
26
27
|
const icons = {
|
|
@@ -31,6 +32,7 @@ const icons = {
|
|
|
31
32
|
|
|
32
33
|
export default function SubmitButton() {
|
|
33
34
|
const form = useFormContext()
|
|
35
|
+
const { t } = useI18n()
|
|
34
36
|
const { submissionAttempts, isSubmitting, isSubmitSuccessful } = useStore(
|
|
35
37
|
form.store,
|
|
36
38
|
(state) => ({
|
|
@@ -48,7 +50,7 @@ export default function SubmitButton() {
|
|
|
48
50
|
return (
|
|
49
51
|
<button className={buttonClass} type="submit" disabled={isSubmitting}>
|
|
50
52
|
{icon && <Icon className={icon} ariaLabel="" />}
|
|
51
|
-
{label}
|
|
53
|
+
{t(label)}
|
|
52
54
|
</button>
|
|
53
55
|
)
|
|
54
56
|
}
|
|
@@ -1 +1,11 @@
|
|
|
1
1
|
ln.admin.edit: Edit
|
|
2
|
+
ln.admin.save: Save
|
|
3
|
+
ln.admin.saved: Saved
|
|
4
|
+
ln.admin.failed: Failed
|
|
5
|
+
ln.admin.edit-media-item: Edit media item
|
|
6
|
+
ln.admin.back-to-details-page: Back to details page
|
|
7
|
+
ln.admin.title: Title
|
|
8
|
+
ln.admin.common-id: Common ID
|
|
9
|
+
ln.admin.toast.invalid-data.title: Invalid form data
|
|
10
|
+
ln.admin.toast.invalid-data.hint: Check the fields and try again.
|
|
11
|
+
ln.admin.errors.non-empty-string: String must contain at least 1 character(s)
|
|
@@ -2,6 +2,11 @@ import { revalidateLogic } from "@tanstack/react-form"
|
|
|
2
2
|
|
|
3
3
|
import { showToastById } from "../../../components/showToast"
|
|
4
4
|
import Toast from "../../../components/Toast"
|
|
5
|
+
import {
|
|
6
|
+
createI18n,
|
|
7
|
+
type I18nConfig,
|
|
8
|
+
I18nContext,
|
|
9
|
+
} from "../../../i18n/react/i18n-context"
|
|
5
10
|
import { useAppForm } from "../../components/form"
|
|
6
11
|
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
7
12
|
import { updateMediaItem } from "./media-item-store"
|
|
@@ -9,9 +14,11 @@ import { updateMediaItem } from "./media-item-store"
|
|
|
9
14
|
export default function EditForm({
|
|
10
15
|
mediaId,
|
|
11
16
|
mediaItem,
|
|
17
|
+
i18nConfig,
|
|
12
18
|
}: {
|
|
13
19
|
mediaId: string
|
|
14
20
|
mediaItem: MediaItem
|
|
21
|
+
i18nConfig: I18nConfig
|
|
15
22
|
}) {
|
|
16
23
|
const form = useAppForm({
|
|
17
24
|
defaultValues: mediaItem,
|
|
@@ -29,30 +36,38 @@ export default function EditForm({
|
|
|
29
36
|
showToastById("invalid-form-data-toast")
|
|
30
37
|
},
|
|
31
38
|
})
|
|
39
|
+
const i18n = createI18n(i18nConfig)
|
|
40
|
+
const { t } = i18n
|
|
32
41
|
|
|
33
42
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
e
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
<I18nContext.Provider value={i18n}>
|
|
44
|
+
<form
|
|
45
|
+
onSubmit={(e) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
form.handleSubmit()
|
|
48
|
+
}}
|
|
49
|
+
className="flex flex-col items-start gap-4"
|
|
50
|
+
>
|
|
51
|
+
<form.AppField
|
|
52
|
+
name="commonId"
|
|
53
|
+
children={(field) => (
|
|
54
|
+
<field.TextField label={t("ln.admin.common-id")} />
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
<form.AppField
|
|
58
|
+
name="title"
|
|
59
|
+
children={(field) => <field.TextField label={t("ln.admin.title")} />}
|
|
60
|
+
/>
|
|
61
|
+
<form.AppForm>
|
|
62
|
+
<form.SubmitButton />
|
|
63
|
+
<Toast id="invalid-form-data-toast" variant="error">
|
|
64
|
+
<div className="font-bold text-gray-700">
|
|
65
|
+
{t("ln.admin.toast.invalid-data.title")}
|
|
66
|
+
</div>
|
|
67
|
+
{t("ln.admin.toast.invalid-data.hint")}
|
|
68
|
+
</Toast>
|
|
69
|
+
</form.AppForm>
|
|
70
|
+
</form>
|
|
71
|
+
</I18nContext.Provider>
|
|
57
72
|
)
|
|
58
73
|
}
|
|
@@ -4,6 +4,7 @@ import { getCollection } from "astro:content"
|
|
|
4
4
|
import config from "virtual:lightnet/config"
|
|
5
5
|
|
|
6
6
|
import { getRawMediaItem } from "../../../content/get-media-items"
|
|
7
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
7
8
|
import { resolveLocales } from "../../../i18n/resolve-locales"
|
|
8
9
|
import Page from "../../../layouts/Page.astro"
|
|
9
10
|
import EditForm from "./EditForm"
|
|
@@ -17,6 +18,9 @@ export const getStaticPaths = (async () => {
|
|
|
17
18
|
|
|
18
19
|
const { mediaId } = Astro.params
|
|
19
20
|
const mediaItemEntry = await getRawMediaItem(mediaId)
|
|
21
|
+
|
|
22
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, ["ln.admin.*"])
|
|
23
|
+
const { t } = Astro.locals.i18n
|
|
20
24
|
---
|
|
21
25
|
|
|
22
26
|
<Page>
|
|
@@ -24,10 +28,15 @@ const mediaItemEntry = await getRawMediaItem(mediaId)
|
|
|
24
28
|
<a
|
|
25
29
|
class="underline"
|
|
26
30
|
href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
|
|
27
|
-
>
|
|
31
|
+
>{t("ln.admin.back-to-details-page")}</a
|
|
28
32
|
>
|
|
29
|
-
<h1 class="mb-4 mt-8 text-lg">
|
|
33
|
+
<h1 class="mb-4 mt-8 text-lg">{t("ln.admin.edit-media-item")}</h1>
|
|
30
34
|
|
|
31
|
-
<EditForm
|
|
35
|
+
<EditForm
|
|
36
|
+
mediaId={mediaId}
|
|
37
|
+
mediaItem={mediaItemEntry.data}
|
|
38
|
+
i18nConfig={i18nConfig}
|
|
39
|
+
client:load
|
|
40
|
+
/>
|
|
32
41
|
</div>
|
|
33
42
|
</Page>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "astro/zod"
|
|
2
2
|
|
|
3
|
+
const NON_EMPTY_STRING = "ln.admin.errors.non-empty-string"
|
|
4
|
+
|
|
3
5
|
export const mediaItemSchema = z.object({
|
|
4
|
-
commonId: z.string().nonempty(),
|
|
5
|
-
title: z.string().nonempty(),
|
|
6
|
+
commonId: z.string().nonempty(NON_EMPTY_STRING),
|
|
7
|
+
title: z.string().nonempty(NON_EMPTY_STRING),
|
|
6
8
|
})
|
|
7
9
|
|
|
8
10
|
export type MediaItem = z.infer<typeof mediaItemSchema>
|
package/src/i18n/locals.d.ts
CHANGED
|
@@ -3,36 +3,43 @@ declare namespace App {
|
|
|
3
3
|
/**
|
|
4
4
|
* Provides internationalization helpers.
|
|
5
5
|
*/
|
|
6
|
-
i18n:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* @param TranslationKey to be translated.
|
|
11
|
-
*/
|
|
12
|
-
t: import("./translate").TranslateFn
|
|
6
|
+
i18n: I18n
|
|
7
|
+
}
|
|
8
|
+
}
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
type I18n = {
|
|
11
|
+
/**
|
|
12
|
+
* Translate a key to the language of the current locale.
|
|
13
|
+
*
|
|
14
|
+
* @param TranslationKey to be translated.
|
|
15
|
+
*/
|
|
16
|
+
t: import("./translate").TranslateFn
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The current locale or the default locale if the current locale is not available.
|
|
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.
|
|
23
|
+
*/
|
|
24
|
+
currentLocale: string
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The current text direction. Left-to-right or right-to-left.
|
|
28
|
+
*/
|
|
29
|
+
direction: "ltr" | "rtl"
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The default locale as defined in the project configuration.
|
|
33
|
+
*/
|
|
34
|
+
defaultLocale: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The available locales as defined in the project configuration.
|
|
38
|
+
*/
|
|
39
|
+
locales: string[]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* All available translation keys.
|
|
43
|
+
*/
|
|
44
|
+
translationKeys: string[]
|
|
38
45
|
}
|
package/src/i18n/locals.ts
CHANGED
|
@@ -4,7 +4,7 @@ import config from "virtual:lightnet/config"
|
|
|
4
4
|
import { resolveDefaultLocale } from "./resolve-default-locale"
|
|
5
5
|
import { resolveLanguage } from "./resolve-language"
|
|
6
6
|
import { resolveLocales } from "./resolve-locales"
|
|
7
|
-
import { useTranslate } from "./translate"
|
|
7
|
+
import { translationKeys, useTranslate } from "./translate"
|
|
8
8
|
|
|
9
9
|
export const onRequest: MiddlewareHandler = (
|
|
10
10
|
{ locals, currentLocale: astroCurrentLocale },
|
|
@@ -22,6 +22,7 @@ export const onRequest: MiddlewareHandler = (
|
|
|
22
22
|
defaultLocale,
|
|
23
23
|
direction,
|
|
24
24
|
locales,
|
|
25
|
+
translationKeys,
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
return next()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
export type I18n = {
|
|
4
|
+
t: (key: string) => string
|
|
5
|
+
currentLocale: string
|
|
6
|
+
direction: "rtl" | "ltr"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type I18nConfig = Omit<I18n, "t"> & {
|
|
10
|
+
translations: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const I18nContext = createContext<I18n | undefined>(undefined)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates the runtime i18n helpers given a prepared configuration.
|
|
17
|
+
* Wraps the raw translation dictionary with a lookup that throws on missing keys.
|
|
18
|
+
*/
|
|
19
|
+
export const createI18n = ({
|
|
20
|
+
translations,
|
|
21
|
+
currentLocale,
|
|
22
|
+
direction,
|
|
23
|
+
}: I18nConfig) => {
|
|
24
|
+
const t = (key: string) => {
|
|
25
|
+
const translated = translations[key]
|
|
26
|
+
if (!translated) {
|
|
27
|
+
throw new Error(`Missing translation for key ${key}`)
|
|
28
|
+
}
|
|
29
|
+
return translated
|
|
30
|
+
}
|
|
31
|
+
return { t, currentLocale, direction }
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepares the configuration object passed from an Astro page to the React i18n context.
|
|
3
|
+
* Resolves every requested translation key (supporting wildcard suffixes like `ln.dashboard.*`)
|
|
4
|
+
* so the React island receives only the strings it needs.
|
|
5
|
+
*
|
|
6
|
+
* @param i18n i18n helpers sourced from `Astro.locals`.
|
|
7
|
+
* @param translationKeys Specific keys (or wildcard groups) required by the React component.
|
|
8
|
+
* @returns A configuration object containing the resolved config.
|
|
9
|
+
*/
|
|
10
|
+
export const prepareI18nConfig = (
|
|
11
|
+
{ t, translationKeys: allKeys, currentLocale, direction }: I18n,
|
|
12
|
+
translationKeys: string[],
|
|
13
|
+
) => {
|
|
14
|
+
const resolveTranslations = (key: string) => {
|
|
15
|
+
if (key.endsWith("*")) {
|
|
16
|
+
const keyPrefix = key.slice(0, -1)
|
|
17
|
+
return allKeys
|
|
18
|
+
.filter((k) => k.startsWith(keyPrefix))
|
|
19
|
+
.map((k) => [k, t(k)])
|
|
20
|
+
}
|
|
21
|
+
return [[key, t(key)]]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
translations: Object.fromEntries(
|
|
26
|
+
translationKeys.flatMap(resolveTranslations),
|
|
27
|
+
) as Record<string, string>,
|
|
28
|
+
currentLocale,
|
|
29
|
+
direction,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useContext } from "react"
|
|
2
|
+
|
|
3
|
+
import { I18nContext } from "./i18n-context"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves the current i18n helpers from context.
|
|
7
|
+
* Must be called inside a React tree wrapped with `I18nContext.Provider`, otherwise throws.
|
|
8
|
+
*/
|
|
9
|
+
export const useI18n = () => {
|
|
10
|
+
const i18n = useContext(I18nContext)
|
|
11
|
+
if (!i18n) {
|
|
12
|
+
throw new Error("No i18n context has been provided")
|
|
13
|
+
}
|
|
14
|
+
return i18n
|
|
15
|
+
}
|
package/src/i18n/translate.ts
CHANGED
|
@@ -22,18 +22,30 @@ const languageCodes = [
|
|
|
22
22
|
]
|
|
23
23
|
const defaultLocale = resolveDefaultLocale(config)
|
|
24
24
|
|
|
25
|
-
await
|
|
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
|
+
]
|
|
35
|
+
|
|
36
|
+
const i18n = i18next.createInstance()
|
|
37
|
+
await i18n.init({
|
|
26
38
|
lng: defaultLocale,
|
|
27
39
|
// don't use name spacing
|
|
28
40
|
nsSeparator: false,
|
|
29
41
|
// only use flat keys
|
|
30
42
|
keySeparator: false,
|
|
31
|
-
resources:
|
|
43
|
+
resources: translations,
|
|
32
44
|
})
|
|
33
45
|
|
|
34
46
|
export function useTranslate(bcp47: string | undefined): TranslateFn {
|
|
35
47
|
const resolvedLocale = bcp47 ?? defaultLocale
|
|
36
|
-
const t =
|
|
48
|
+
const t = i18n.getFixedT<TranslationKey>(resolvedLocale)
|
|
37
49
|
const fallbackLng = [
|
|
38
50
|
...resolveLanguage(resolvedLocale).fallbackLanguages,
|
|
39
51
|
defaultLocale,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Icon from "../../../components/Icon"
|
|
2
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
2
3
|
|
|
3
|
-
export default function LoadingSkeleton(
|
|
4
|
+
export default function LoadingSkeleton() {
|
|
5
|
+
const { direction } = useI18n()
|
|
4
6
|
return (
|
|
5
7
|
<div className="flex h-52 animate-pulse items-center overflow-hidden py-2 sm:h-64">
|
|
6
8
|
<div className="h-36 w-36 shrink-0 rounded-md bg-gray-200"></div>
|
|
@@ -4,7 +4,7 @@ import config from "virtual:lightnet/config"
|
|
|
4
4
|
import { getUsedCategories } from "../../../content/get-categories"
|
|
5
5
|
import { contentLanguages } from "../../../content/get-languages"
|
|
6
6
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
-
import {
|
|
7
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
8
8
|
import SearchFilterReact from "./SearchFilter.tsx"
|
|
9
9
|
|
|
10
10
|
const { t, currentLocale } = Astro.locals.i18n
|
|
@@ -49,7 +49,16 @@ if (
|
|
|
49
49
|
) {
|
|
50
50
|
initialLanguageFilter = currentLocale
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
53
|
+
"ln.search.title",
|
|
54
|
+
"ln.language",
|
|
55
|
+
"ln.search.placeholder",
|
|
56
|
+
"ln.search.all-languages",
|
|
57
|
+
"ln.type",
|
|
58
|
+
"ln.search.all-types",
|
|
59
|
+
"ln.category",
|
|
60
|
+
"ln.search.all-categories",
|
|
61
|
+
])
|
|
53
62
|
---
|
|
54
63
|
|
|
55
64
|
<SearchFilterReact
|
|
@@ -57,7 +66,7 @@ const translations = provideTranslations(t)
|
|
|
57
66
|
languages={languages}
|
|
58
67
|
mediaTypes={mediaTypes}
|
|
59
68
|
categories={categories}
|
|
60
|
-
|
|
69
|
+
i18nConfig={i18nConfig}
|
|
61
70
|
languageFilterEnabled={languageFilterEnabled}
|
|
62
71
|
typesFilterEnabled={typesFilterEnabled}
|
|
63
72
|
categoriesFilterEnabled={categoriesFilterEnabled}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
2
|
|
|
3
3
|
import Icon from "../../../components/Icon"
|
|
4
|
+
import { createI18n, type I18nConfig } from "../../../i18n/react/i18n-context"
|
|
4
5
|
import { useDebounce } from "../hooks/use-debounce"
|
|
5
6
|
import { useSearchQueryParam } from "../hooks/use-search-query-param"
|
|
6
|
-
import type {
|
|
7
|
-
TranslationKey,
|
|
8
|
-
Translations,
|
|
9
|
-
} from "../utils/search-filter-translations"
|
|
10
7
|
import { CATEGORY, LANGUAGE, SEARCH, TYPE } from "../utils/search-query"
|
|
11
8
|
import Select from "./Select"
|
|
12
9
|
|
|
@@ -16,7 +13,7 @@ interface Props {
|
|
|
16
13
|
languages: FilterValue[]
|
|
17
14
|
categories: FilterValue[]
|
|
18
15
|
mediaTypes: FilterValue[]
|
|
19
|
-
|
|
16
|
+
i18nConfig: I18nConfig
|
|
20
17
|
languageFilterEnabled: boolean
|
|
21
18
|
typesFilterEnabled: boolean
|
|
22
19
|
categoriesFilterEnabled: boolean
|
|
@@ -26,7 +23,7 @@ interface Props {
|
|
|
26
23
|
export default function SearchFilter({
|
|
27
24
|
categories,
|
|
28
25
|
mediaTypes,
|
|
29
|
-
|
|
26
|
+
i18nConfig,
|
|
30
27
|
languages,
|
|
31
28
|
languageFilterEnabled,
|
|
32
29
|
typesFilterEnabled,
|
|
@@ -40,7 +37,7 @@ export default function SearchFilter({
|
|
|
40
37
|
|
|
41
38
|
const searchInput = useRef<HTMLInputElement | null>(null)
|
|
42
39
|
|
|
43
|
-
const t = (
|
|
40
|
+
const { t } = createI18n(i18nConfig)
|
|
44
41
|
|
|
45
42
|
const debouncedSetSearch = useDebounce((value: string) => {
|
|
46
43
|
setSearch(value)
|
|
@@ -4,10 +4,10 @@ import { getCollection } from "astro:content"
|
|
|
4
4
|
import { getUsedCategories } from "../../../content/get-categories"
|
|
5
5
|
import { contentLanguages } from "../../../content/get-languages"
|
|
6
6
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
-
import {
|
|
7
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
8
8
|
import SearchListReact from "./SearchList.tsx"
|
|
9
9
|
|
|
10
|
-
const { t, currentLocale
|
|
10
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
11
11
|
|
|
12
12
|
const categories: Record<string, string> = {}
|
|
13
13
|
for (const { id, name } of await getUsedCategories(currentLocale, t)) {
|
|
@@ -34,15 +34,16 @@ const languages = Object.fromEntries(
|
|
|
34
34
|
|
|
35
35
|
const mediaItemsTotal = (await getCollection("media")).length
|
|
36
36
|
|
|
37
|
-
const translations = provideTranslations(t)
|
|
38
37
|
const showLanguage = contentLanguages.length > 1
|
|
38
|
+
|
|
39
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
40
|
+
"ln.search.no-results",
|
|
41
|
+
])
|
|
39
42
|
---
|
|
40
43
|
|
|
41
44
|
<SearchListReact
|
|
42
45
|
client:load
|
|
43
|
-
|
|
44
|
-
direction={direction}
|
|
45
|
-
translations={translations}
|
|
46
|
+
i18nConfig={i18nConfig}
|
|
46
47
|
categories={categories}
|
|
47
48
|
mediaTypes={mediaTypes}
|
|
48
49
|
languages={languages}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
2
|
import { useEffect, useRef, useState } from "react"
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
createI18n,
|
|
6
|
+
type I18nConfig,
|
|
7
|
+
I18nContext,
|
|
8
|
+
} from "../../../i18n/react/i18n-context"
|
|
4
9
|
import { useSearch } from "../hooks/use-search"
|
|
5
|
-
import type { TranslationKey, Translations } from "../utils/search-translations"
|
|
6
10
|
import LoadingSkeleton from "./LoadingSkeleton"
|
|
7
11
|
import SearchListItem, {
|
|
8
12
|
type MediaType,
|
|
@@ -10,9 +14,7 @@ import SearchListItem, {
|
|
|
10
14
|
} from "./SearchListItem"
|
|
11
15
|
|
|
12
16
|
interface Props {
|
|
13
|
-
|
|
14
|
-
translations: Translations
|
|
15
|
-
direction: "rtl" | "ltr"
|
|
17
|
+
i18nConfig: I18nConfig
|
|
16
18
|
categories: Record<string, string>
|
|
17
19
|
languages: Record<string, TranslatedLanguage>
|
|
18
20
|
showLanguage: boolean
|
|
@@ -21,11 +23,9 @@ interface Props {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export default function SearchList({
|
|
24
|
-
currentLocale,
|
|
25
26
|
categories,
|
|
26
|
-
|
|
27
|
+
i18nConfig,
|
|
27
28
|
languages,
|
|
28
|
-
direction,
|
|
29
29
|
showLanguage,
|
|
30
30
|
mediaTypes,
|
|
31
31
|
mediaItemsTotal,
|
|
@@ -63,11 +63,10 @@ export default function SearchList({
|
|
|
63
63
|
observer.disconnect()
|
|
64
64
|
}
|
|
65
65
|
}, [])
|
|
66
|
-
|
|
67
|
-
const t = (key: TranslationKey) => translations[key]
|
|
66
|
+
const i18n = createI18n(i18nConfig)
|
|
68
67
|
|
|
69
68
|
return (
|
|
70
|
-
|
|
69
|
+
<I18nContext.Provider value={i18n}>
|
|
71
70
|
<div ref={listRef} className="px-4 md:px-8">
|
|
72
71
|
<ol
|
|
73
72
|
className="relative w-full divide-y divide-gray-200"
|
|
@@ -89,13 +88,11 @@ export default function SearchList({
|
|
|
89
88
|
}}
|
|
90
89
|
>
|
|
91
90
|
{isLoading ? (
|
|
92
|
-
<LoadingSkeleton
|
|
91
|
+
<LoadingSkeleton />
|
|
93
92
|
) : (
|
|
94
93
|
<SearchListItem
|
|
95
94
|
item={item}
|
|
96
|
-
direction={direction}
|
|
97
95
|
showLanguage={showLanguage}
|
|
98
|
-
currentLocale={currentLocale}
|
|
99
96
|
categories={categories}
|
|
100
97
|
languages={languages}
|
|
101
98
|
mediaTypes={mediaTypes}
|
|
@@ -108,9 +105,9 @@ export default function SearchList({
|
|
|
108
105
|
</div>
|
|
109
106
|
{!results.length && !isLoading && (
|
|
110
107
|
<div className="mt-24 text-center font-bold text-gray-500">
|
|
111
|
-
{t("ln.search.no-results")}
|
|
108
|
+
{i18n.t("ln.search.no-results")}
|
|
112
109
|
</div>
|
|
113
110
|
)}
|
|
114
|
-
|
|
111
|
+
</I18nContext.Provider>
|
|
115
112
|
)
|
|
116
113
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import CoverImageDecorator from "../../../components/CoverImageDecorator"
|
|
2
2
|
import Icon from "../../../components/Icon"
|
|
3
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
3
4
|
import { detailsPagePath } from "../../../utils/paths"
|
|
4
5
|
import type { SearchItem } from "../api/search-response"
|
|
5
6
|
|
|
@@ -16,8 +17,6 @@ export type TranslatedLanguage = {
|
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
item: SearchItem
|
|
19
|
-
currentLocale: string | undefined
|
|
20
|
-
direction: "rtl" | "ltr"
|
|
21
20
|
categories: Record<string, string>
|
|
22
21
|
languages: Record<string, TranslatedLanguage>
|
|
23
22
|
showLanguage: boolean
|
|
@@ -26,13 +25,12 @@ interface Props {
|
|
|
26
25
|
|
|
27
26
|
export default function SearchListItem({
|
|
28
27
|
item,
|
|
29
|
-
currentLocale,
|
|
30
28
|
categories,
|
|
31
29
|
languages,
|
|
32
|
-
direction,
|
|
33
30
|
showLanguage,
|
|
34
31
|
mediaTypes,
|
|
35
32
|
}: Props) {
|
|
33
|
+
const { currentLocale, direction } = useI18n()
|
|
36
34
|
const coverImageStyle = mediaTypes[item.type].coverImageStyle
|
|
37
35
|
return (
|
|
38
36
|
<a
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
const translationKeys = [
|
|
2
|
-
"ln.search.title",
|
|
3
|
-
"ln.language",
|
|
4
|
-
"ln.search.placeholder",
|
|
5
|
-
"ln.search.all-languages",
|
|
6
|
-
"ln.type",
|
|
7
|
-
"ln.search.all-types",
|
|
8
|
-
"ln.category",
|
|
9
|
-
"ln.search.all-categories",
|
|
10
|
-
] as const
|
|
11
|
-
|
|
12
|
-
export type TranslationKey = (typeof translationKeys)[number]
|
|
13
|
-
|
|
14
|
-
export type Translations = Record<TranslationKey, string>
|
|
15
|
-
|
|
16
|
-
export const provideTranslations = (translate: (key: string) => string) => {
|
|
17
|
-
return Object.fromEntries(
|
|
18
|
-
translationKeys.map((key) => [key, translate(key)]),
|
|
19
|
-
) as Translations
|
|
20
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const translationKeys = ["ln.search.no-results"] as const
|
|
2
|
-
|
|
3
|
-
export type TranslationKey = (typeof translationKeys)[number]
|
|
4
|
-
|
|
5
|
-
export type Translations = Record<TranslationKey, string>
|
|
6
|
-
|
|
7
|
-
export const provideTranslations = (translate: (key: string) => string) => {
|
|
8
|
-
return Object.fromEntries(
|
|
9
|
-
translationKeys.map((key) => [key, translate(key)]),
|
|
10
|
-
) as Translations
|
|
11
|
-
}
|