lightnet 4.0.8 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/exports/content.ts +5 -7
- package/package.json +11 -11
- package/src/astro-integration/config.ts +15 -25
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/MediaGallerySection.astro +4 -9
- package/src/components/MediaList.astro +13 -13
- package/src/content/content-schema.ts +5 -292
- package/src/content/get-categories.ts +26 -29
- package/src/content/get-languages.ts +13 -26
- package/src/content/get-media-collections.ts +1 -1
- package/src/content/get-media-items.ts +1 -1
- package/src/content/get-media-types.ts +21 -14
- package/src/content/query-media-items.ts +2 -1
- package/src/content/schema/category.ts +40 -0
- package/src/content/schema/media-collection.ts +31 -0
- package/src/content/schema/media-item.ts +137 -0
- package/src/content/schema/media-type.ts +90 -0
- package/src/i18n/locals.d.ts +22 -0
- package/src/i18n/locals.ts +3 -1
- package/src/i18n/record-translation.ts +74 -0
- package/src/i18n/resolve-language.ts +12 -5
- package/src/i18n/translate-map.ts +129 -19
- package/src/i18n/translate.ts +38 -0
- package/src/i18n/translation-map-schema.ts +17 -0
- package/src/i18n/translations/TRANSLATION-STATUS.md +13 -41
- package/src/i18n/translations/ar.yml +1 -0
- package/src/i18n/translations/bn.yml +1 -0
- package/src/i18n/translations/de.yml +1 -0
- package/src/i18n/translations/es.yml +1 -0
- package/src/i18n/translations/fi.yml +1 -0
- package/src/i18n/translations/fr.yml +1 -0
- package/src/i18n/translations/hi.yml +1 -0
- package/src/i18n/translations/kk.yml +3 -0
- package/src/i18n/translations/pt.yml +1 -0
- package/src/i18n/translations/ru.yml +1 -0
- package/src/i18n/translations/uk.yml +1 -0
- package/src/i18n/translations/ur.yml +1 -0
- package/src/i18n/translations/zh.yml +1 -0
- package/src/i18n/translations.ts +5 -2
- package/src/layouts/Page.astro +3 -4
- package/src/layouts/components/Footer.astro +72 -10
- package/src/layouts/components/LanguagePicker.astro +2 -2
- package/src/layouts/components/PageNavigation.astro +17 -21
- package/src/layouts/components/PageTitle.astro +4 -13
- package/src/pages/details-page/DefaultDetailsPage.astro +2 -15
- package/src/pages/details-page/components/AudioPanel.astro +5 -4
- package/src/pages/details-page/components/ContentSection.astro +5 -4
- package/src/pages/details-page/components/MediaCollection.astro +2 -6
- package/src/pages/details-page/components/main-details/OpenButton.astro +20 -18
- package/src/pages/details-page/components/more-details/Categories.astro +3 -5
- package/src/pages/details-page/components/more-details/Languages.astro +7 -3
- package/src/pages/details-page/utils/create-content-metadata.ts +8 -22
- package/src/pages/search-page/api/search.ts +1 -1
- package/src/pages/search-page/components/SearchFilter.astro +5 -5
- package/src/pages/search-page/components/SearchList.astro +22 -19
- package/src/astro-integration/validators/validate-inline-translations.ts +0 -51
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { AstroError } from "astro/errors"
|
|
2
|
+
import type { CollectionKey } from "astro:content"
|
|
2
3
|
import config from "virtual:lightnet/config"
|
|
3
4
|
|
|
5
|
+
import type { ExtendedLightnetConfig } from "../astro-integration/config"
|
|
6
|
+
import { recordTranslation } from "./record-translation"
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* A map of translated values keyed by locale code.
|
|
6
10
|
*
|
|
@@ -12,14 +16,6 @@ import config from "virtual:lightnet/config"
|
|
|
12
16
|
*/
|
|
13
17
|
export type TranslationMap = Record<string, string | undefined>
|
|
14
18
|
|
|
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
19
|
/**
|
|
24
20
|
* Resolve a translation map for the current locale, falling back
|
|
25
21
|
* to configured default locale when needed.
|
|
@@ -32,7 +28,37 @@ export type TranslationMapContext = {
|
|
|
32
28
|
*/
|
|
33
29
|
export type TranslateMapFn = (
|
|
34
30
|
translationMap: TranslationMap,
|
|
35
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Describes where a translation map comes from so translation logs
|
|
33
|
+
* can point to the exact field in config or content.
|
|
34
|
+
*/
|
|
35
|
+
context: { path: (string | number)[] },
|
|
36
|
+
) => string
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve an inline translation map that belongs to a content entry.
|
|
40
|
+
*
|
|
41
|
+
* The entry is only used to provide a stable fallback path when translation-map
|
|
42
|
+
* metadata is missing during the current refactor.
|
|
43
|
+
*/
|
|
44
|
+
export type TranslateContentFieldFn = (
|
|
45
|
+
translationMap: TranslationMap,
|
|
46
|
+
contentEntry: {
|
|
47
|
+
id: string
|
|
48
|
+
collection: CollectionKey
|
|
49
|
+
data: Record<PropertyKey, unknown>
|
|
50
|
+
},
|
|
51
|
+
) => string
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve an inline translation map that belongs to the LightNet config.
|
|
55
|
+
*
|
|
56
|
+
* The config object is only used to keep the API explicit and to provide a
|
|
57
|
+
* stable fallback path when translation-map metadata is missing.
|
|
58
|
+
*/
|
|
59
|
+
export type TranslateConfigFieldFn = (
|
|
60
|
+
translationMap: TranslationMap,
|
|
61
|
+
config: ExtendedLightnetConfig,
|
|
36
62
|
) => string
|
|
37
63
|
|
|
38
64
|
/**
|
|
@@ -45,26 +71,110 @@ export type TranslateMapFn = (
|
|
|
45
71
|
* @param currentLocale The locale that should be resolved first before
|
|
46
72
|
* falling back to LightNet's configured default locale.
|
|
47
73
|
*/
|
|
48
|
-
export function useTranslateMap(currentLocale: string)
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
export function useTranslateMap(currentLocale: string) {
|
|
75
|
+
const tMap: TranslateMapFn = (translationMap, context) => {
|
|
76
|
+
const getKey = () => {
|
|
77
|
+
const keyCacheSymbol = Symbol.for("ln.key-cache")
|
|
78
|
+
if (hasOwnProperty(translationMap, keyCacheSymbol)) {
|
|
79
|
+
return translationMap[keyCacheSymbol] as string
|
|
80
|
+
}
|
|
81
|
+
const value = context.path.join(".")
|
|
82
|
+
Object.defineProperty(translationMap, keyCacheSymbol, { value })
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
85
|
+
const key = getKey()
|
|
86
|
+
recordTranslation({ key, values: translationMap, type: "map" })
|
|
87
|
+
|
|
88
|
+
const currentLocaleTranslation = translationMap[currentLocale]
|
|
89
|
+
if (currentLocaleTranslation) {
|
|
90
|
+
return currentLocaleTranslation
|
|
53
91
|
}
|
|
54
92
|
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
93
|
+
const defaultLocaleTranslation = translationMap[config.defaultLocale]
|
|
94
|
+
if (defaultLocaleTranslation) {
|
|
95
|
+
return defaultLocaleTranslation
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
const availableLocales = Object.keys(translationMap)
|
|
98
|
+
const availableLocales = Object.keys(translationMap).filter(
|
|
99
|
+
(key) => key !== "path",
|
|
100
|
+
)
|
|
61
101
|
const availableLocalesText = availableLocales.length
|
|
62
102
|
? availableLocales.map((locale) => `"${locale}"`).join(", ")
|
|
63
103
|
: "none"
|
|
64
104
|
|
|
65
105
|
throw new AstroError(
|
|
66
|
-
`Missing translation map value for "${
|
|
106
|
+
`Missing translation map value for "${key}" in locales "${currentLocale}" and "${config.defaultLocale}"`,
|
|
67
107
|
`Available locales: ${availableLocalesText}. Add a value for "${currentLocale}" or "${config.defaultLocale}" to this inline translation map.`,
|
|
68
108
|
)
|
|
69
109
|
}
|
|
110
|
+
|
|
111
|
+
const tContentField: TranslateContentFieldFn = (
|
|
112
|
+
translationMap,
|
|
113
|
+
contentEntry,
|
|
114
|
+
) => {
|
|
115
|
+
return tMap(translationMap, {
|
|
116
|
+
path: getMapPath(translationMap, contentEntry.data, [
|
|
117
|
+
"content",
|
|
118
|
+
contentEntry.collection,
|
|
119
|
+
contentEntry.id,
|
|
120
|
+
]),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tConfigField: TranslateConfigFieldFn = (translationMap, _config) => {
|
|
125
|
+
return tMap(translationMap, {
|
|
126
|
+
path: getMapPath(translationMap, _config, ["config"]),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { tMap, tContentField, tConfigField }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getMapPath(
|
|
134
|
+
translationMap: TranslationMap,
|
|
135
|
+
data: unknown,
|
|
136
|
+
path: string[],
|
|
137
|
+
) {
|
|
138
|
+
const pathCacheSymbol = Symbol.for("ln.path-cache")
|
|
139
|
+
|
|
140
|
+
if (hasOwnProperty(translationMap, pathCacheSymbol)) {
|
|
141
|
+
return translationMap[pathCacheSymbol] as string[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const findPath: (_data: unknown, _path: string[]) => string[] | undefined = (
|
|
145
|
+
_data,
|
|
146
|
+
_path,
|
|
147
|
+
) => {
|
|
148
|
+
if (!_data || typeof _data !== "object") {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (_data === translationMap) {
|
|
152
|
+
return _path
|
|
153
|
+
}
|
|
154
|
+
for (const [key, value] of Object.entries(_data)) {
|
|
155
|
+
const p = findPath(value, [..._path, key])
|
|
156
|
+
if (p) {
|
|
157
|
+
return p as string[]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const resolvedPath = findPath(data, path)
|
|
163
|
+
if (!resolvedPath) {
|
|
164
|
+
throw new AstroError(
|
|
165
|
+
`Invalid map context provided ${path} for could not find path for object ${JSON.stringify(translationMap)}`,
|
|
166
|
+
`Provided object ${JSON.stringify(data)}`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
Object.defineProperty(translationMap, pathCacheSymbol, {
|
|
170
|
+
value: resolvedPath,
|
|
171
|
+
})
|
|
172
|
+
return resolvedPath
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasOwnProperty<K extends PropertyKey>(
|
|
176
|
+
obj: unknown,
|
|
177
|
+
key: K,
|
|
178
|
+
): obj is Record<K, unknown> {
|
|
179
|
+
return !!obj && typeof obj === "object" && Object.hasOwn(obj, key)
|
|
70
180
|
}
|
package/src/i18n/translate.ts
CHANGED
|
@@ -3,6 +3,7 @@ import i18next, { type TOptions } from "i18next"
|
|
|
3
3
|
import config from "virtual:lightnet/config"
|
|
4
4
|
|
|
5
5
|
import { lazy } from "../utils/lazy"
|
|
6
|
+
import { recordTranslation } from "./record-translation"
|
|
6
7
|
import { type LightNetTranslationKey, loadTranslations } from "./translations"
|
|
7
8
|
|
|
8
9
|
// We add (string & NonNullable<unknown>) to preserve typescript autocompletion for known keys
|
|
@@ -68,6 +69,7 @@ export async function useTranslate(
|
|
|
68
69
|
const resolvedLocale = bcp47 ?? config.defaultLocale
|
|
69
70
|
const translations = await i18nextTranslations.get()
|
|
70
71
|
const availableTranslationKeys = new Set(await translationKeys.get())
|
|
72
|
+
recordAllTranslations()
|
|
71
73
|
|
|
72
74
|
const i18n = i18next.createInstance()
|
|
73
75
|
|
|
@@ -100,3 +102,39 @@ export async function useTranslate(
|
|
|
100
102
|
return value
|
|
101
103
|
}
|
|
102
104
|
}
|
|
105
|
+
|
|
106
|
+
async function recordAllTranslations() {
|
|
107
|
+
if (!import.meta.env.PROD) {
|
|
108
|
+
// only record translations during build time
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { locales } = config
|
|
113
|
+
const localesIncludingEnglish = [...locales.filter((l) => l !== "en"), "en"]
|
|
114
|
+
const keys = await translationKeys.get()
|
|
115
|
+
const translations = await i18nextTranslations.get()
|
|
116
|
+
|
|
117
|
+
const collectValues = (key: string, locales: string[]) => {
|
|
118
|
+
const values = {} as Record<string, string | undefined>
|
|
119
|
+
for (const locale of locales) {
|
|
120
|
+
values[locale] = translations[locale]?.translation[key]
|
|
121
|
+
}
|
|
122
|
+
return values
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
if (key.startsWith("ln.")) {
|
|
127
|
+
recordTranslation({
|
|
128
|
+
key,
|
|
129
|
+
values: collectValues(key, localesIncludingEnglish),
|
|
130
|
+
type: "lightnet",
|
|
131
|
+
})
|
|
132
|
+
} else {
|
|
133
|
+
recordTranslation({
|
|
134
|
+
key,
|
|
135
|
+
values: collectValues(key, locales),
|
|
136
|
+
type: "user",
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Translations by BCP-47 tag
|
|
5
|
+
* Must contain at least one localized value.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* {
|
|
9
|
+
* de: "Hallo",
|
|
10
|
+
* en: "Hello"
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
export const translationMapSchema = z
|
|
14
|
+
.record(z.string(), z.string().nonempty())
|
|
15
|
+
.refine((value) => Object.keys(value).length > 0, {
|
|
16
|
+
message: "Inline translations must contain at least one entry",
|
|
17
|
+
})
|
|
@@ -4,21 +4,15 @@ This autogenerated report shows all built-in languages and the current status of
|
|
|
4
4
|
|
|
5
5
|
## **AR** ([ar.yml](./ar.yml))
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- ln.details.edit
|
|
7
|
+
All keys have been translated. ✅
|
|
10
8
|
|
|
11
9
|
## **BN** ([bn.yml](./bn.yml))
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- ln.details.edit
|
|
11
|
+
All keys have been translated. ✅
|
|
16
12
|
|
|
17
13
|
## **DE** ([de.yml](./de.yml))
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- ln.details.edit
|
|
15
|
+
All keys have been translated. ✅
|
|
22
16
|
|
|
23
17
|
## **EN** ([en.yml](./en.yml))
|
|
24
18
|
|
|
@@ -26,62 +20,40 @@ All keys have been translated. ✅
|
|
|
26
20
|
|
|
27
21
|
## **ES** ([es.yml](./es.yml))
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- ln.details.edit
|
|
23
|
+
All keys have been translated. ✅
|
|
32
24
|
|
|
33
25
|
## **FI** ([fi.yml](./fi.yml))
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- ln.details.edit
|
|
27
|
+
All keys have been translated. ✅
|
|
38
28
|
|
|
39
29
|
## **FR** ([fr.yml](./fr.yml))
|
|
40
30
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- ln.details.edit
|
|
31
|
+
All keys have been translated. ✅
|
|
44
32
|
|
|
45
33
|
## **HI** ([hi.yml](./hi.yml))
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- ln.details.edit
|
|
35
|
+
All keys have been translated. ✅
|
|
50
36
|
|
|
51
37
|
## **KK** ([kk.yml](./kk.yml))
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- ln.previous
|
|
56
|
-
- ln.next
|
|
57
|
-
- ln.details.edit
|
|
39
|
+
All keys have been translated. ✅
|
|
58
40
|
|
|
59
41
|
## **PT** ([pt.yml](./pt.yml))
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- ln.details.edit
|
|
43
|
+
All keys have been translated. ✅
|
|
64
44
|
|
|
65
45
|
## **RU** ([ru.yml](./ru.yml))
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- ln.details.edit
|
|
47
|
+
All keys have been translated. ✅
|
|
70
48
|
|
|
71
49
|
## **UK** ([uk.yml](./uk.yml))
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- ln.details.edit
|
|
51
|
+
All keys have been translated. ✅
|
|
76
52
|
|
|
77
53
|
## **UR** ([ur.yml](./ur.yml))
|
|
78
54
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- ln.details.edit
|
|
55
|
+
All keys have been translated. ✅
|
|
82
56
|
|
|
83
57
|
## **ZH** ([zh.yml](./zh.yml))
|
|
84
58
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- ln.details.edit
|
|
59
|
+
All keys have been translated. ✅
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: جميع الفئات
|
|
|
17
17
|
ln.search.no-results: لا توجد نتائج
|
|
18
18
|
ln.details.open: فتح
|
|
19
19
|
ln.details.share: مشاركة
|
|
20
|
+
ln.details.edit: تحرير
|
|
20
21
|
ln.details.part-of-collection: جزء من مجموعة
|
|
21
22
|
ln.details.download: تنزيل
|
|
22
23
|
ln.share.url-copied-to-clipboard: تم نسخ الرابط إلى الحافظة
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: সকল বিভাগ
|
|
|
17
17
|
ln.search.no-results: কোনো ফলাফল নেই
|
|
18
18
|
ln.details.open: খুলুন
|
|
19
19
|
ln.details.share: শেয়ার করুন
|
|
20
|
+
ln.details.edit: সম্পাদনা করুন
|
|
20
21
|
ln.details.part-of-collection: সংগ্রহের অংশ
|
|
21
22
|
ln.details.download: ডাউনলোড করুন
|
|
22
23
|
ln.share.url-copied-to-clipboard: লিঙ্ক ক্লিপবোর্ডে কপি হয়েছে
|
|
@@ -11,6 +11,7 @@ ln.external-link: Externer Link
|
|
|
11
11
|
ln.details.open: Öffnen
|
|
12
12
|
ln.details.part-of-collection: Teil der Sammlung
|
|
13
13
|
ln.details.download: Download
|
|
14
|
+
ln.details.edit: Bearbeiten
|
|
14
15
|
ln.details.share: Teilen
|
|
15
16
|
ln.header.open-main-menu: Öffne Hauptmenü
|
|
16
17
|
ln.header.select-language: Sprache auswählen
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Todas las categorías
|
|
|
17
17
|
ln.search.no-results: Sin resultados
|
|
18
18
|
ln.details.open: Abrir
|
|
19
19
|
ln.details.share: Compartir
|
|
20
|
+
ln.details.edit: Editar
|
|
20
21
|
ln.details.part-of-collection: Parte de la colección
|
|
21
22
|
ln.details.download: Descargar
|
|
22
23
|
ln.share.url-copied-to-clipboard: Enlace copiado al portapapeles
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Kaikki kategoriat
|
|
|
17
17
|
ln.search.no-results: Ei tuloksia
|
|
18
18
|
ln.details.open: Avaa
|
|
19
19
|
ln.details.share: Jaa
|
|
20
|
+
ln.details.edit: Muokkaa
|
|
20
21
|
ln.details.part-of-collection: Osa kokoelmaa
|
|
21
22
|
ln.details.download: Lataa
|
|
22
23
|
ln.share.url-copied-to-clipboard: Linkki kopioitu leikepöydälle
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Toutes les catégories
|
|
|
17
17
|
ln.search.no-results: Aucun résultat
|
|
18
18
|
ln.details.open: Ouvrir
|
|
19
19
|
ln.details.share: Partager
|
|
20
|
+
ln.details.edit: Modifier
|
|
20
21
|
ln.details.part-of-collection: Fait partie de la collection
|
|
21
22
|
ln.details.download: Télécharger
|
|
22
23
|
ln.share.url-copied-to-clipboard: Lien copié dans le presse-papiers
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: सभी श्रेणियाँ
|
|
|
17
17
|
ln.search.no-results: कोई परिणाम नहीं मिला
|
|
18
18
|
ln.details.open: खोलें
|
|
19
19
|
ln.details.share: साझा करें
|
|
20
|
+
ln.details.edit: संपादित करें
|
|
20
21
|
ln.details.part-of-collection: संग्रह का भाग
|
|
21
22
|
ln.details.download: डाउनलोड करें
|
|
22
23
|
ln.share.url-copied-to-clipboard: लिंक क्लिपबोर्ड पर कॉपी हो गया है
|
|
@@ -6,6 +6,8 @@ ln.categories: Барлық санаттар
|
|
|
6
6
|
ln.language: Тіл
|
|
7
7
|
ln.languages: Тілдер
|
|
8
8
|
ln.type: Түрі
|
|
9
|
+
ln.previous: Алдыңғы
|
|
10
|
+
ln.next: Келесі
|
|
9
11
|
ln.external-link: Сыртқы сілтеме
|
|
10
12
|
ln.search.title: Іздеу
|
|
11
13
|
ln.search.placeholder: Медиа іздеу
|
|
@@ -15,6 +17,7 @@ ln.search.all-categories: Барлық санаттар
|
|
|
15
17
|
ln.search.no-results: Нәтижие жоқ
|
|
16
18
|
ln.details.open: Ашу
|
|
17
19
|
ln.details.share: Бөлісу
|
|
20
|
+
ln.details.edit: Өңдеу
|
|
18
21
|
ln.details.part-of-collection: Жинақ бөлімі
|
|
19
22
|
ln.details.download: Жүктеу
|
|
20
23
|
ln.share.url-copied-to-clipboard: Жүктеме көшірілді
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Todas as categorias
|
|
|
17
17
|
ln.search.no-results: Nenhum resultado
|
|
18
18
|
ln.details.open: Abrir
|
|
19
19
|
ln.details.share: Partilhar
|
|
20
|
+
ln.details.edit: Editar
|
|
20
21
|
ln.details.part-of-collection: Parte da coleção
|
|
21
22
|
ln.details.download: Transferir
|
|
22
23
|
ln.share.url-copied-to-clipboard: Ligação copiada para a área de transferência
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Все категории
|
|
|
17
17
|
ln.search.no-results: Нет результатов
|
|
18
18
|
ln.details.open: Открыть
|
|
19
19
|
ln.details.share: Поделиться
|
|
20
|
+
ln.details.edit: Редактировать
|
|
20
21
|
ln.details.part-of-collection: Часть коллекции
|
|
21
22
|
ln.details.download: Скачать
|
|
22
23
|
ln.share.url-copied-to-clipboard: Ссылка скопированa в буфер
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: Усі категорії
|
|
|
17
17
|
ln.search.no-results: Немає результатів
|
|
18
18
|
ln.details.open: Відкрити
|
|
19
19
|
ln.details.share: Поділитися
|
|
20
|
+
ln.details.edit: Редагувати
|
|
20
21
|
ln.details.part-of-collection: Частина колекції
|
|
21
22
|
ln.details.download: Завантажити
|
|
22
23
|
ln.share.url-copied-to-clipboard: Посилання скопійовано до буферу обміну
|
|
@@ -17,6 +17,7 @@ ln.search.all-categories: تمام زمرے
|
|
|
17
17
|
ln.search.no-results: کوئی نتیجہ نہیں ملا
|
|
18
18
|
ln.details.open: کھولیں
|
|
19
19
|
ln.details.share: شیئر کریں
|
|
20
|
+
ln.details.edit: ترمیم کریں
|
|
20
21
|
ln.details.part-of-collection: مجموعے کا حصہ
|
|
21
22
|
ln.details.download: ڈاؤن لوڈ کریں
|
|
22
23
|
ln.share.url-copied-to-clipboard: لنک کاپی ہو گیا
|
package/src/i18n/translations.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "astro/zod"
|
|
1
2
|
import YAML from "yaml"
|
|
2
3
|
|
|
3
4
|
const builtInTranslations = {
|
|
@@ -19,6 +20,8 @@ const builtInTranslations = {
|
|
|
19
20
|
|
|
20
21
|
type BuiltInLanguage = keyof typeof builtInTranslations
|
|
21
22
|
|
|
23
|
+
const translationFileSchema = z.record(z.string(), z.string())
|
|
24
|
+
|
|
22
25
|
const userTranslations = Object.fromEntries(
|
|
23
26
|
Object.entries(
|
|
24
27
|
import.meta.glob(["/src/translations/*.(yml|yaml)"], {
|
|
@@ -52,7 +55,7 @@ const loadBuiltInTranslations = async (
|
|
|
52
55
|
return {}
|
|
53
56
|
}
|
|
54
57
|
const yml = (await translationMap[bcp47]()).default
|
|
55
|
-
return YAML.parse(yml)
|
|
58
|
+
return translationFileSchema.parse(YAML.parse(yml))
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const loadUserTranslations = async (bcp47: string) => {
|
|
@@ -60,7 +63,7 @@ const loadUserTranslations = async (bcp47: string) => {
|
|
|
60
63
|
return {}
|
|
61
64
|
}
|
|
62
65
|
const yml = (await userTranslations[bcp47]()) as string
|
|
63
|
-
return YAML.parse(yml)
|
|
66
|
+
return translationFileSchema.parse(YAML.parse(yml))
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export type LightNetTranslationKey =
|
package/src/layouts/Page.astro
CHANGED
|
@@ -20,10 +20,9 @@ interface Props {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const { title, description, mainClass, locale } = Astro.props
|
|
23
|
-
const currentLocale
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
})
|
|
23
|
+
const { currentLocale: pathCurrentLocale, tConfigField } = Astro.locals.i18n
|
|
24
|
+
const currentLocale = locale ?? pathCurrentLocale
|
|
25
|
+
const configTitle = tConfigField(config.title, config)
|
|
27
26
|
const { direction } = resolveLanguage(currentLocale)
|
|
28
27
|
---
|
|
29
28
|
|
|
@@ -1,24 +1,86 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from "virtual:lightnet/config"
|
|
3
3
|
|
|
4
|
+
import { localizePath } from "../../utils/paths"
|
|
5
|
+
import { isExternalUrl } from "../../utils/urls"
|
|
4
6
|
import LightNetLogo from "./LightNetLogo.svg"
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
const { t, tConfigField, currentLocale } = Astro.locals.i18n
|
|
9
|
+
|
|
10
|
+
// Resolve optional footer text for the current locale.
|
|
11
|
+
const footerText = config.footerText
|
|
12
|
+
? tConfigField(config.footerText, config)
|
|
13
|
+
: undefined
|
|
14
|
+
|
|
15
|
+
// Prepare footer links using the same locale rules as the main menu.
|
|
16
|
+
const footerLinks = (config.footerLinks ?? []).map(
|
|
17
|
+
({ href, label, requiresLocale }) => {
|
|
18
|
+
const isExternal = isExternalUrl(href)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
href:
|
|
22
|
+
isExternal || !requiresLocale
|
|
23
|
+
? href
|
|
24
|
+
: localizePath(currentLocale, href),
|
|
25
|
+
label: tConfigField(label, config),
|
|
26
|
+
isExternal,
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// Hide the footer entirely when there is nothing to show.
|
|
32
|
+
const shouldRenderFooter =
|
|
33
|
+
config.credits || Boolean(footerText) || footerLinks.length > 0
|
|
34
|
+
|
|
35
|
+
if (!shouldRenderFooter) {
|
|
7
36
|
return
|
|
8
37
|
}
|
|
9
38
|
---
|
|
10
39
|
|
|
11
|
-
<footer class="w-full border-t border-gray-
|
|
40
|
+
<footer class="w-full border-t border-gray-200 bg-white">
|
|
12
41
|
<div
|
|
13
|
-
class="mx-auto flex w-full max-w-screen-xl justify-
|
|
42
|
+
class="mx-auto flex w-full max-w-screen-xl flex-col items-center justify-between gap-4 px-4 py-6 md:flex-row md:px-8"
|
|
14
43
|
>
|
|
15
|
-
<
|
|
16
|
-
class="flex items-center
|
|
17
|
-
href="https://lightnet.community"
|
|
18
|
-
target="_blank"
|
|
44
|
+
<div
|
|
45
|
+
class="flex flex-wrap items-center justify-center gap-2 text-sm text-gray-700 md:justify-start"
|
|
19
46
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
{footerText && <span>{footerText}</span>}
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
footerLinks.map((link, index) => (
|
|
51
|
+
<Fragment>
|
|
52
|
+
{(footerText || index > 0) && <span class="text-gray-400">·</span>}
|
|
53
|
+
<a
|
|
54
|
+
href={link.href}
|
|
55
|
+
target={link.isExternal ? "_blank" : undefined}
|
|
56
|
+
rel={link.isExternal ? "noopener noreferrer" : undefined}
|
|
57
|
+
class="underline-offset-4 hover:underline"
|
|
58
|
+
>
|
|
59
|
+
{link.label}
|
|
60
|
+
</a>
|
|
61
|
+
</Fragment>
|
|
62
|
+
))
|
|
63
|
+
}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
config.credits && (
|
|
68
|
+
<div class="flex items-center text-sm">
|
|
69
|
+
<a
|
|
70
|
+
class="flex items-center gap-2 text-gray-800 underline-offset-4 hover:underline"
|
|
71
|
+
href="https://lightnet.community"
|
|
72
|
+
target="_blank"
|
|
73
|
+
>
|
|
74
|
+
<img
|
|
75
|
+
src={LightNetLogo.src}
|
|
76
|
+
alt=""
|
|
77
|
+
class="h-5 w-auto"
|
|
78
|
+
loading="lazy"
|
|
79
|
+
/>
|
|
80
|
+
<span>{t("ln.footer.powered-by-lightnet")}</span>
|
|
81
|
+
</a>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
23
85
|
</div>
|
|
24
86
|
</footer>
|
|
@@ -6,7 +6,7 @@ import { localizePath, pathWithoutBase } from "../../utils/paths"
|
|
|
6
6
|
import Menu from "./Menu.astro"
|
|
7
7
|
import MenuItem from "./MenuItem.astro"
|
|
8
8
|
|
|
9
|
-
const { locales, currentLocale,
|
|
9
|
+
const { locales, currentLocale, tConfigField } = Astro.locals.i18n
|
|
10
10
|
|
|
11
11
|
const currentPath = pathWithoutBase(Astro.url.pathname)
|
|
12
12
|
const hasLocale =
|
|
@@ -18,7 +18,7 @@ const translations = (
|
|
|
18
18
|
await Promise.all(
|
|
19
19
|
locales.map(async (locale) => ({
|
|
20
20
|
locale,
|
|
21
|
-
label: resolveTranslatedLanguage(locale,
|
|
21
|
+
label: resolveTranslatedLanguage(locale, tConfigField).labelText,
|
|
22
22
|
active: locale === currentLocale,
|
|
23
23
|
href: currentPathWithLocale(locale),
|
|
24
24
|
})),
|