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 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
@@ -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": "3.10.1",
6
+ "version": "3.10.2",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -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: "Save",
22
- success: "Saved",
23
- error: "Failed",
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
- <form
35
- onSubmit={(e) => {
36
- e.preventDefault()
37
- form.handleSubmit()
38
- }}
39
- className="flex flex-col items-start gap-4"
40
- >
41
- <form.AppField
42
- name="commonId"
43
- children={(field) => <field.TextField label="Common ID" />}
44
- />
45
- <form.AppField
46
- name="title"
47
- children={(field) => <field.TextField label="Title" />}
48
- />
49
- <form.AppForm>
50
- <form.SubmitButton />
51
- <Toast id="invalid-form-data-toast" variant="error">
52
- <div className="font-bold text-gray-700">Invalid form data</div>
53
- Check the fields and try again.
54
- </Toast>
55
- </form.AppForm>
56
- </form>
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
- >Back to details page</a
31
+ >{t("ln.admin.back-to-details-page")}</a
28
32
  >
29
- <h1 class="mb-4 mt-8 text-lg">Edit media item</h1>
33
+ <h1 class="mb-4 mt-8 text-lg">{t("ln.admin.edit-media-item")}</h1>
30
34
 
31
- <EditForm mediaId={mediaId} mediaItem={mediaItemEntry.data} client:load />
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>
@@ -3,36 +3,43 @@ declare namespace App {
3
3
  /**
4
4
  * Provides internationalization helpers.
5
5
  */
6
- i18n: {
7
- /**
8
- * Translate a key to the language of the current locale.
9
- *
10
- * @param TranslationKey to be translated.
11
- */
12
- t: import("./translate").TranslateFn
6
+ i18n: I18n
7
+ }
8
+ }
13
9
 
14
- /**
15
- * The current locale or the default locale if the current locale is not available.
16
- *
17
- * In comparison to Astro.currentLocale this will always return a locale.
18
- * Use Astro.currentLocale if you want to know the locale that is included in the current path.
19
- */
20
- currentLocale: string
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
- * The current text direction. Left-to-right or right-to-left.
24
- */
25
- direction: "ltr" | "rtl"
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
- * The default locale as defined in the project configuration.
29
- */
30
- defaultLocale: string
26
+ /**
27
+ * The current text direction. Left-to-right or right-to-left.
28
+ */
29
+ direction: "ltr" | "rtl"
31
30
 
32
- /**
33
- * The available locales as defined in the project configuration.
34
- */
35
- locales: string[]
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
  }
@@ -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
+ }
@@ -22,18 +22,30 @@ const languageCodes = [
22
22
  ]
23
23
  const defaultLocale = resolveDefaultLocale(config)
24
24
 
25
- await i18next.init({
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: await prepareI18nextTranslations(),
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 = i18next.getFixedT<TranslationKey>(resolvedLocale)
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({ direction }: { direction: string }) {
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 { provideTranslations } from "../utils/search-filter-translations"
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 translations = provideTranslations(t)
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
- translations={translations}
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
- translations: Translations
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
- translations,
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 = (key: TranslationKey) => translations[key]
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 { provideTranslations } from "../utils/search-translations"
7
+ import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
8
8
  import SearchListReact from "./SearchList.tsx"
9
9
 
10
- const { t, currentLocale, direction } = Astro.locals.i18n
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
- currentLocale={currentLocale}
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
- currentLocale: string | undefined
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
- translations,
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 direction={direction} />
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
- }