@secorto/i18n 0.0.4

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/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @secorto/i18n
2
+
3
+ Reusable building blocks for multilingual content systems.
4
+
5
+ ## Goals
6
+
7
+ - Framework-agnostic core.
8
+ - Build-time validation.
9
+ - Translation-aware content navigation.
10
+ - Localized tags.
11
+ - Astro adapters.
12
+ - Documentation-first architecture.
@@ -0,0 +1,299 @@
1
+ //#region src/core/locale.d.ts
2
+ interface Locales<L extends string> {
3
+ readonly all: readonly L[];
4
+ fromString(lang: string | undefined): L;
5
+ isValid(lang: string): lang is L;
6
+ }
7
+ declare function createLocales<L extends string>(locales: readonly L[]): Locales<L>;
8
+ //#endregion
9
+ //#region src/core/translationLink.d.ts
10
+ /**
11
+ * Represents a translation that is available and can be accessed by users.
12
+ */
13
+ type AvailableLink<L extends string> = {
14
+ type: 'available';
15
+ href: string;
16
+ locale: L;
17
+ };
18
+ /**
19
+ * Represents a translation that does not exist for the given locale.
20
+ */
21
+ type MissingLink<L extends string> = {
22
+ type: 'missing';
23
+ href: null;
24
+ locale: L;
25
+ };
26
+ /**
27
+ * Represents a translation that exists as a draft but is not yet publicly available.
28
+ */
29
+ type DraftLink<L extends string> = {
30
+ type: 'draft';
31
+ href: string;
32
+ locale: L;
33
+ };
34
+ /**
35
+ * Represents the state of a translation for a locale.
36
+ */
37
+ type TranslationLink<L extends string> = AvailableLink<L> | MissingLink<L> | DraftLink<L>;
38
+ /**
39
+ * Represents a translation link that can be accessed, either as a published
40
+ * translation (`available`) or as a draft (`draft`).
41
+ */
42
+ type AccessibleTranslationLink<L extends string> = AvailableLink<L> | DraftLink<L>;
43
+ /**
44
+ * Creates an available translation link.
45
+ */
46
+ declare function availableLink<L extends string>(href: string, lang: L): AvailableLink<L>;
47
+ /**
48
+ * Creates a missing translation link.
49
+ */
50
+ declare function missingLink<L extends string>(lang: L): MissingLink<L>;
51
+ /**
52
+ * Creates a draft translation link.
53
+ */
54
+ declare function draftLink<L extends string>(href: string, lang: L): DraftLink<L>;
55
+ /** Returns whether the link is accessible. */
56
+ declare function isAccessible<L extends string>(link: TranslationLink<L>): link is AccessibleTranslationLink<L>;
57
+ /** Returns whether the link is available. */
58
+ declare function isAvailable<L extends string>(link: TranslationLink<L>): link is AvailableLink<L>;
59
+ /** Returns whether the link is a draft. */
60
+ declare function isDraft<L extends string>(link: TranslationLink<L>): link is DraftLink<L>;
61
+ /** Returns whether the link is missing. */
62
+ declare function isMissing<L extends string>(link: TranslationLink<L>): link is MissingLink<L>;
63
+ /**
64
+ * Resolves the default accessible translation link from a collection of links.
65
+ *
66
+ * Selection priority:
67
+ * 1. An `available` link matching `defaultLang`.
68
+ * 2. The first `available` link.
69
+ * 3. A `draft` link matching `defaultLang`.
70
+ * 4. The first `draft` link.
71
+ *
72
+ * @template L Type representing the supported locales.
73
+ * @param links Translation links to evaluate.
74
+ * @param defaultLang Preferred locale to prioritize during selection.
75
+ * @returns The selected accessible translation link.
76
+ *
77
+ * @throws {Error} If `links` is empty or if no accessible link exists.
78
+ */
79
+ declare function resolveDefaultAccessibleLink<L extends string>(links: TranslationLink<L>[], defaultLang: L): AccessibleTranslationLink<L>;
80
+ //#endregion
81
+ //#region src/core/standalone.d.ts
82
+ /**
83
+ * Represents a localized standalone page.
84
+ */
85
+ interface StandalonePageEntry {
86
+ /**
87
+ * Route to the page.
88
+ */
89
+ route: string;
90
+ /**
91
+ * Indicates whether the page exists only as a draft.
92
+ */
93
+ draft?: boolean;
94
+ }
95
+ /**
96
+ * Maps a page identifier to its localized standalone page entries.
97
+ *
98
+ * The first key represents the page identifier and the nested keys represent
99
+ * locales.
100
+ */
101
+ type StandalonePageIndex<K extends string, L extends string> = Record<K, Partial<Record<L, StandalonePageEntry>>>;
102
+ /**
103
+ * Creates translation links for a standalone page.
104
+ *
105
+ * Validates that:
106
+ * - the translation key is indexed
107
+ * - the current route belongs to the indexed translation group
108
+ */
109
+ declare function createStandalonePageLinks<L extends string>(path: string, translationKey: string, index: StandalonePageIndex<string, L>, locales: Locales<L>): TranslationLink<L>[];
110
+ //#endregion
111
+ //#region src/core/extract-id.d.ts
112
+ /**
113
+ * Extracts the locale and cleanId from an entryId of the form "es/my-post".
114
+ * Locale validation is delegated to the Locales value object.
115
+ *
116
+ * @param entryId Raw entry identifier (e.g., "es/my-post")
117
+ * @param locales Locales value object created via createLocales()
118
+ * @returns An object containing the validated locale and cleanId
119
+ * @throws Error if the entryId is malformed or the locale is invalid
120
+ */
121
+ declare function extractCleanId<L extends string>(entryId: string, locales: Locales<L>): {
122
+ locale: L;
123
+ id: string;
124
+ };
125
+ //#endregion
126
+ //#region src/section/routes.d.ts
127
+ type SectionDictionary<Section extends string, Language extends string, TValue> = Record<Section, Record<Language, TValue>>;
128
+ /**
129
+ * Value object that encapsulates localized slugs per section and exposes
130
+ * a stable API for building localized URLs.
131
+ *
132
+ * Invariants:
133
+ * - Each (locale, slug) pair must be unique across all sections.
134
+ * - The object is constructed exclusively through `createSectionRoutes`.
135
+ *
136
+ * @template Section - Section keys (e.g., 'blog', 'talk').
137
+ * @template Language - Locale keys (e.g., 'es', 'en').
138
+ */
139
+ interface SectionRoutes<Section extends string, Language extends string> {
140
+ /**
141
+ * Raw dictionary of localized slugs per section.
142
+ * This structure is immutable once the value object is created.
143
+ */
144
+ readonly routes: Record<Section, Record<Language, string>>;
145
+ /**
146
+ * Returns the configured section identifiers.
147
+ */
148
+ getSections(): Section[];
149
+ /**
150
+ * Returns the localized slug for a section.
151
+ *
152
+ * @param section Section identifier.
153
+ * @param locale Locale identifier.
154
+ * @returns Localized slug for the section.
155
+ */
156
+ getSectionRoute(section: Section, locale: Language): string;
157
+ /**
158
+ * Returns the localized URL for a section, including locale prefix.
159
+ *
160
+ * @param section Section identifier.
161
+ * @param locale Locale identifier.
162
+ * @returns URL string for the section in the given locale.
163
+ */
164
+ getSectionURL(section: Section, locale: Language): string;
165
+ /**
166
+ * Returns the localized URL for a content entry inside a section.
167
+ *
168
+ * @param section Section identifier.
169
+ * @param locale Locale identifier.
170
+ * @param slug Entry slug.
171
+ * @returns Full URL for the entry.
172
+ */
173
+ getEntryURL(section: Section, locale: Language, slug: string): string;
174
+ }
175
+ /**
176
+ * Constructs a nominal SectionRoutes value from a raw SectionDictionary.
177
+ *
178
+ * This function enforces the domain invariants for localized section routes:
179
+ * - each (locale, slug) pair must be unique across all sections
180
+ * - the resulting value is branded as 'SectionRoutes'
181
+ *
182
+ * If any invariant is violated, an error is thrown and the SectionRoutes value
183
+ * is not constructed.
184
+ *
185
+ * @template Section - The section keys (e.g., 'blog', 'docs').
186
+ * @template Language - The language codes (e.g., 'es', 'en').
187
+ * @param routes Raw dictionary of localized slugs per section.
188
+ * @returns A branded SectionRoutes value.
189
+ */
190
+ declare function createSectionRoutes<Section extends string, Language extends string>(routes: SectionDictionary<Section, Language, string>): SectionRoutes<Section, Language>;
191
+ //#endregion
192
+ //#region src/section/translation-index.d.ts
193
+ /**
194
+ * Represents a localized entry in the translation system.
195
+ * @template E - The type of the content (e.g., { title: string, body: string }).
196
+ * @template C - The section of the application (e.g., 'blog', 'docs').
197
+ * @template L - The language code (e.g., 'es', 'en').
198
+ */
199
+ interface LocalizedEntry<TEntry, L extends string> {
200
+ cleanId: string;
201
+ translationKey: string;
202
+ locale: L;
203
+ draft: boolean;
204
+ original: TEntry;
205
+ }
206
+ /**
207
+ * Represents a translation index that groups localized entries by their translation key and locale.
208
+ * @template L - The type of the language code (e.g., 'es', 'en').
209
+ * @template E - The type of the content (e.g., { title: string, body: string }).
210
+ * @template C - The section of the application (e.g., 'blog', 'docs').
211
+ */
212
+ type TranslationIndex<L extends string, TEntry> = Record<string, Partial<Record<L, LocalizedEntry<TEntry, L>>>>;
213
+ /**
214
+ * Builds a translation index from an array of localized entries.
215
+ * The index groups entries by their translation key and locale.
216
+ * If duplicate entries for the same translation key and locale are found, an error is thrown.
217
+ * @template E - The type of the content (e.g., { title: string, body: string }).
218
+ * @template C - The section of the application (e.g., 'blog', 'docs').
219
+ * @template L - The language code (e.g., 'es', 'en').
220
+ * @param entries Entries to index
221
+ * @returns The translation index, grouped by translation key and locale
222
+ * @throws Error if duplicate entries for the same translation key and locale are found
223
+ */
224
+ declare function createTranslationIndex<L extends string, TEntry>(entries: readonly LocalizedEntry<TEntry, L>[]): TranslationIndex<L, TEntry>;
225
+ //#endregion
226
+ //#region src/section/entry-adapter.d.ts
227
+ interface GenericCollectionEntry<C extends string, TData> {
228
+ id: string;
229
+ collection: C;
230
+ data: TData;
231
+ }
232
+ declare function resolveTranslationKey<T extends object>(data: T, cleanId: string): string;
233
+ /**
234
+ * Converts a collection entry into a localized entry.
235
+ *
236
+ * Extracts the locale and clean ID from the entry ID and resolves the
237
+ * translation key used to group translations of the same content.
238
+ *
239
+ * @param entry Collection entry to adapt.
240
+ * @param locales Supported locales used to parse the entry ID.
241
+ * @returns The corresponding localized entry.
242
+ */
243
+ declare function adaptToLocalizedEntry<C extends string, T extends object, L extends string, TEntry extends GenericCollectionEntry<C, T>>(entry: TEntry, locales: Locales<L>): LocalizedEntry<TEntry, L>;
244
+ //#endregion
245
+ //#region src/section/detail-path.d.ts
246
+ type DetailPath<C extends string, L extends string, TEntry extends GenericCollectionEntry<C, object>> = {
247
+ params: {
248
+ locale: L;
249
+ section: string;
250
+ id: string;
251
+ };
252
+ props: {
253
+ entry: LocalizedEntry<TEntry, L>;
254
+ section: C;
255
+ siblings: TranslationIndex<L, TEntry>[string];
256
+ };
257
+ };
258
+ /**
259
+ * Generates the static path definitions required to build localized detail pages
260
+ * for all entries across all configured sections.
261
+ *
262
+ * For each section defined in `routes`, this function:
263
+ * 1. Fetches the collection entries.
264
+ * 2. Adapts them into localized entries.
265
+ * 3. Groups translations by translation key.
266
+ * 4. Produces one path per localized entry, including its translation siblings.
267
+ *
268
+ * The resulting paths can be consumed by static site generators to create
269
+ * localized detail pages with access to the current entry and all of its
270
+ * translations.
271
+ *
272
+ * @template TEntry Entry type returned by the collection loader.
273
+ * @template E Entry data type.
274
+ * @template C Section identifiers (for example: `'blog' | 'docs'`).
275
+ * @template L Locale identifiers (for example: `'en' | 'es'`).
276
+ *
277
+ * @param routes Localized section routes used to resolve URL segments.
278
+ * @param fetchCollection Function that retrieves all entries belonging
279
+ * to a given section.
280
+ * @param allowedLocales Supported locales
281
+ */
282
+ declare function getStaticPathsEntries<C extends string, E extends object, L extends string, TEntry extends GenericCollectionEntry<C, E>>(routes: SectionRoutes<C, L>, fetchCollection: (collection: C) => Promise<TEntry[]>, allowedLocales: Locales<L>): Promise<DetailPath<C, L, TEntry>[]>;
283
+ //#endregion
284
+ //#region src/section/details-links.d.ts
285
+ /**
286
+ * Creates translation links for all supported locales.
287
+ *
288
+ * Existing translations are returned as `available` links. Missing
289
+ * translations are represented as `missing` links. The output preserves
290
+ * the order defined by `locales.all`.
291
+ *
292
+ * @param siblings Available translations indexed by locale.
293
+ * @param sectionRoutes Routes used to build localized URLs.
294
+ * @param locales Supported locales.
295
+ * @returns One translation link per supported locale.
296
+ */
297
+ declare function createDetailTranslationLinks<C extends string, L extends string, TEntry extends GenericCollectionEntry<C, object>>(siblings: Partial<Record<L, LocalizedEntry<TEntry, L>>>, sectionRoutes: SectionRoutes<C, L>, locales: Locales<L>): TranslationLink<L>[];
298
+ //#endregion
299
+ export { AccessibleTranslationLink, AvailableLink, DetailPath, DraftLink, GenericCollectionEntry, Locales, LocalizedEntry, MissingLink, SectionDictionary, SectionRoutes, StandalonePageEntry, StandalonePageIndex, TranslationIndex, TranslationLink, adaptToLocalizedEntry, availableLink, createDetailTranslationLinks, createLocales, createSectionRoutes, createStandalonePageLinks, createTranslationIndex, draftLink, extractCleanId, getStaticPathsEntries, isAccessible, isAvailable, isDraft, isMissing, missingLink, resolveDefaultAccessibleLink, resolveTranslationKey };
package/dist/index.mjs ADDED
@@ -0,0 +1,304 @@
1
+ //#region src/core/locale.ts
2
+ function isLocale(locales, lang) {
3
+ return locales.includes(lang);
4
+ }
5
+ function createLocales(locales) {
6
+ return {
7
+ all: locales,
8
+ fromString(lang) {
9
+ if (!lang) throw new TypeError(`Invalid language: ${lang}`);
10
+ if (isLocale(locales, lang)) return lang;
11
+ throw new TypeError(`Invalid language: ${lang}`);
12
+ },
13
+ isValid(lang) {
14
+ return isLocale(locales, lang);
15
+ }
16
+ };
17
+ }
18
+ //#endregion
19
+ //#region src/core/translationLink.ts
20
+ /**
21
+ * Creates an available translation link.
22
+ */
23
+ function availableLink(href, lang) {
24
+ return {
25
+ type: "available",
26
+ href,
27
+ locale: lang
28
+ };
29
+ }
30
+ /**
31
+ * Creates a missing translation link.
32
+ */
33
+ function missingLink(lang) {
34
+ return {
35
+ type: "missing",
36
+ href: null,
37
+ locale: lang
38
+ };
39
+ }
40
+ /**
41
+ * Creates a draft translation link.
42
+ */
43
+ function draftLink(href, lang) {
44
+ return {
45
+ type: "draft",
46
+ href,
47
+ locale: lang
48
+ };
49
+ }
50
+ /** Returns whether the link is accessible. */
51
+ function isAccessible(link) {
52
+ return link.type === "available" || link.type === "draft";
53
+ }
54
+ /** Returns whether the link is available. */
55
+ function isAvailable(link) {
56
+ return link.type === "available";
57
+ }
58
+ /** Returns whether the link is a draft. */
59
+ function isDraft(link) {
60
+ return link.type === "draft";
61
+ }
62
+ /** Returns whether the link is missing. */
63
+ function isMissing(link) {
64
+ return link.type === "missing";
65
+ }
66
+ /**
67
+ * Resolves the default accessible translation link from a collection of links.
68
+ *
69
+ * Selection priority:
70
+ * 1. An `available` link matching `defaultLang`.
71
+ * 2. The first `available` link.
72
+ * 3. A `draft` link matching `defaultLang`.
73
+ * 4. The first `draft` link.
74
+ *
75
+ * @template L Type representing the supported locales.
76
+ * @param links Translation links to evaluate.
77
+ * @param defaultLang Preferred locale to prioritize during selection.
78
+ * @returns The selected accessible translation link.
79
+ *
80
+ * @throws {Error} If `links` is empty or if no accessible link exists.
81
+ */
82
+ function resolveDefaultAccessibleLink(links, defaultLang) {
83
+ if (!links || links.length === 0) throw new Error("resolveDefaultAccessibleLink: unexpected empty links array");
84
+ const defaultAny = links.find((l) => l.locale === defaultLang);
85
+ if (defaultAny && isAvailable(defaultAny)) return defaultAny;
86
+ const firstAvailable = links.find(isAvailable);
87
+ if (firstAvailable) return firstAvailable;
88
+ if (defaultAny && isDraft(defaultAny)) return defaultAny;
89
+ const firstDraft = links.find(isDraft);
90
+ if (firstDraft) return firstDraft;
91
+ throw new Error("resolveDefaultAccessibleLink: expected at least one accessible link");
92
+ }
93
+ //#endregion
94
+ //#region src/core/extract-id.ts
95
+ /**
96
+ * Extracts the locale and cleanId from an entryId of the form "es/my-post".
97
+ * Locale validation is delegated to the Locales value object.
98
+ *
99
+ * @param entryId Raw entry identifier (e.g., "es/my-post")
100
+ * @param locales Locales value object created via createLocales()
101
+ * @returns An object containing the validated locale and cleanId
102
+ * @throws Error if the entryId is malformed or the locale is invalid
103
+ */
104
+ function extractCleanId(entryId, locales) {
105
+ if (!entryId) throw new Error("entryId cannot be empty");
106
+ const firstSlash = entryId.indexOf("/");
107
+ if (firstSlash <= 0) throw new Error(`Invalid entryId "${entryId}" — missing locale prefix`);
108
+ const rawLocale = entryId.slice(0, firstSlash);
109
+ if (!locales.isValid(rawLocale)) throw new Error(`Invalid entryId "${entryId}". Unknown locale prefix "${rawLocale}". Expected one of: ${locales.all.join(", ")}.`);
110
+ return {
111
+ locale: rawLocale,
112
+ id: entryId.slice(firstSlash + 1)
113
+ };
114
+ }
115
+ //#endregion
116
+ //#region src/core/standalone.ts
117
+ /**
118
+ * Creates translation links for a standalone page.
119
+ *
120
+ * Validates that:
121
+ * - the translation key is indexed
122
+ * - the current route belongs to the indexed translation group
123
+ */
124
+ function createStandalonePageLinks(path, translationKey, index, locales) {
125
+ const { locale: currentLocale, id: currentRoute } = extractCleanId(path, locales);
126
+ const group = index[translationKey];
127
+ if (!group) throw new Error(`Standalone page '${translationKey}' is not indexed.`);
128
+ const currentEntry = group[currentLocale];
129
+ if (!currentEntry) throw new Error(`Standalone page '${translationKey}' has no entry for locale '${currentLocale}'.`);
130
+ if (currentEntry.route !== currentRoute) throw new Error(`Route '${path}' does not belong to standalone page '${translationKey}'.`);
131
+ return locales.all.map((locale) => {
132
+ const entry = group[locale];
133
+ if (!entry) return missingLink(locale);
134
+ const href = `/${locale}/${entry.route}`;
135
+ if (entry.draft) return draftLink(href, locale);
136
+ return availableLink(href, locale);
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/section/routes.ts
141
+ /**
142
+ * Constructs a nominal SectionRoutes value from a raw SectionDictionary.
143
+ *
144
+ * This function enforces the domain invariants for localized section routes:
145
+ * - each (locale, slug) pair must be unique across all sections
146
+ * - the resulting value is branded as 'SectionRoutes'
147
+ *
148
+ * If any invariant is violated, an error is thrown and the SectionRoutes value
149
+ * is not constructed.
150
+ *
151
+ * @template Section - The section keys (e.g., 'blog', 'docs').
152
+ * @template Language - The language codes (e.g., 'es', 'en').
153
+ * @param routes Raw dictionary of localized slugs per section.
154
+ * @returns A branded SectionRoutes value.
155
+ */
156
+ function createSectionRoutes(routes) {
157
+ const seen = /* @__PURE__ */ new Map();
158
+ for (const section of Object.keys(routes)) {
159
+ const localized = routes[section];
160
+ for (const locale of Object.keys(localized)) {
161
+ const slug = localized[locale];
162
+ const key = `${locale}:${slug}`;
163
+ if (seen.has(key)) {
164
+ const other = seen.get(key);
165
+ throw new Error(`Duplicated route for locale "${locale}" and slug "${slug}" between sections "${other}" and "${section}".`);
166
+ }
167
+ seen.set(key, section);
168
+ }
169
+ }
170
+ const getSections = () => Object.keys(routes);
171
+ const getSectionRoute = (section, locale) => routes[section][locale];
172
+ const getSectionURL = (section, locale) => `/${locale}/${getSectionRoute(section, locale)}`;
173
+ const getEntryURL = (section, locale, slug) => `${getSectionURL(section, locale)}/${slug}`;
174
+ return {
175
+ routes,
176
+ getSections,
177
+ getSectionRoute,
178
+ getSectionURL,
179
+ getEntryURL
180
+ };
181
+ }
182
+ //#endregion
183
+ //#region src/section/translation-index.ts
184
+ /**
185
+ * Builds a translation index from an array of localized entries.
186
+ * The index groups entries by their translation key and locale.
187
+ * If duplicate entries for the same translation key and locale are found, an error is thrown.
188
+ * @template E - The type of the content (e.g., { title: string, body: string }).
189
+ * @template C - The section of the application (e.g., 'blog', 'docs').
190
+ * @template L - The language code (e.g., 'es', 'en').
191
+ * @param entries Entries to index
192
+ * @returns The translation index, grouped by translation key and locale
193
+ * @throws Error if duplicate entries for the same translation key and locale are found
194
+ */
195
+ function createTranslationIndex(entries) {
196
+ const map = /* @__PURE__ */ new Map();
197
+ for (const entry of entries) {
198
+ const key = entry.translationKey;
199
+ const locale = entry.locale;
200
+ let group = map.get(key);
201
+ if (!group) {
202
+ group = {};
203
+ map.set(key, group);
204
+ } else if (locale in group) throw new Error(`Duplicate translation for key "${key}" and locale "${locale}"`);
205
+ group[locale] = entry;
206
+ }
207
+ return Object.fromEntries(map);
208
+ }
209
+ //#endregion
210
+ //#region src/section/entry-adapter.ts
211
+ function resolveTranslationKey(data, cleanId) {
212
+ return "translationKey" in data && typeof data.translationKey === "string" ? data.translationKey : cleanId;
213
+ }
214
+ /**
215
+ * Converts a collection entry into a localized entry.
216
+ *
217
+ * Extracts the locale and clean ID from the entry ID and resolves the
218
+ * translation key used to group translations of the same content.
219
+ *
220
+ * @param entry Collection entry to adapt.
221
+ * @param locales Supported locales used to parse the entry ID.
222
+ * @returns The corresponding localized entry.
223
+ */
224
+ function adaptToLocalizedEntry(entry, locales) {
225
+ const { locale, id: cleanId } = extractCleanId(entry.id, locales);
226
+ const draft = "draft" in entry.data && entry.data.draft === true;
227
+ return {
228
+ cleanId,
229
+ locale,
230
+ translationKey: resolveTranslationKey(entry.data, cleanId),
231
+ draft,
232
+ original: entry
233
+ };
234
+ }
235
+ //#endregion
236
+ //#region src/section/detail-path.ts
237
+ /**
238
+ * Generates the static path definitions required to build localized detail pages
239
+ * for all entries across all configured sections.
240
+ *
241
+ * For each section defined in `routes`, this function:
242
+ * 1. Fetches the collection entries.
243
+ * 2. Adapts them into localized entries.
244
+ * 3. Groups translations by translation key.
245
+ * 4. Produces one path per localized entry, including its translation siblings.
246
+ *
247
+ * The resulting paths can be consumed by static site generators to create
248
+ * localized detail pages with access to the current entry and all of its
249
+ * translations.
250
+ *
251
+ * @template TEntry Entry type returned by the collection loader.
252
+ * @template E Entry data type.
253
+ * @template C Section identifiers (for example: `'blog' | 'docs'`).
254
+ * @template L Locale identifiers (for example: `'en' | 'es'`).
255
+ *
256
+ * @param routes Localized section routes used to resolve URL segments.
257
+ * @param fetchCollection Function that retrieves all entries belonging
258
+ * to a given section.
259
+ * @param allowedLocales Supported locales
260
+ */
261
+ async function getStaticPathsEntries(routes, fetchCollection, allowedLocales) {
262
+ const allPaths = [];
263
+ for (const sectionKey of routes.getSections()) {
264
+ const localizedEntries = (await fetchCollection(sectionKey)).map((entry) => adaptToLocalizedEntry(entry, allowedLocales));
265
+ const index = createTranslationIndex(localizedEntries);
266
+ for (const localized of localizedEntries) allPaths.push({
267
+ params: {
268
+ locale: localized.locale,
269
+ section: routes.getSectionRoute(sectionKey, localized.locale),
270
+ id: localized.cleanId
271
+ },
272
+ props: {
273
+ entry: localized,
274
+ section: sectionKey,
275
+ siblings: index[localized.translationKey]
276
+ }
277
+ });
278
+ }
279
+ return allPaths;
280
+ }
281
+ //#endregion
282
+ //#region src/section/details-links.ts
283
+ /**
284
+ * Creates translation links for all supported locales.
285
+ *
286
+ * Existing translations are returned as `available` links. Missing
287
+ * translations are represented as `missing` links. The output preserves
288
+ * the order defined by `locales.all`.
289
+ *
290
+ * @param siblings Available translations indexed by locale.
291
+ * @param sectionRoutes Routes used to build localized URLs.
292
+ * @param locales Supported locales.
293
+ * @returns One translation link per supported locale.
294
+ */
295
+ function createDetailTranslationLinks(siblings, sectionRoutes, locales) {
296
+ return locales.all.map((locale) => {
297
+ const sibling = siblings[locale];
298
+ if (!sibling) return missingLink(locale);
299
+ const href = sectionRoutes.getEntryURL(sibling.original.collection, locale, sibling.cleanId);
300
+ return sibling.draft ? draftLink(href, locale) : availableLink(href, locale);
301
+ });
302
+ }
303
+ //#endregion
304
+ export { adaptToLocalizedEntry, availableLink, createDetailTranslationLinks, createLocales, createSectionRoutes, createStandalonePageLinks, createTranslationIndex, draftLink, extractCleanId, getStaticPathsEntries, isAccessible, isAvailable, isDraft, isMissing, missingLink, resolveDefaultAccessibleLink, resolveTranslationKey };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@secorto/i18n",
3
+ "version": "0.0.4",
4
+ "private": false,
5
+ "sideEffects": false,
6
+ "description": "Framework-agnostic i18n primitive",
7
+ "keywords": [
8
+ "steps",
9
+ "playwright",
10
+ "testing"
11
+ ],
12
+ "homepage": "https://github.com/secorto/secorto_web/blob/master/packages/i18n#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/secorto/secorto_web/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/secorto/secorto_web.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Sergio Carlos Orozco Torres",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./src/index.ts",
26
+ "import": "./dist/index.mjs",
27
+ "default": "./dist/index.mjs"
28
+ },
29
+ "./core": {
30
+ "types": "./src/core/index.ts",
31
+ "import": "./dist/core/index.mjs",
32
+ "default": "./dist/core/index.mjs"
33
+ }
34
+ },
35
+ "main": "./dist/index.mjs",
36
+ "types": "./src/index.ts",
37
+ "directories": {
38
+ "test": "tests"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsdown",
46
+ "clean": "rimraf dist",
47
+ "test": "vitest --run",
48
+ "test:unit": "vitest",
49
+ "test:unit:watch": "vitest --watch"
50
+ },
51
+ "devDependencies": {
52
+ "vitest": "^4.1.10"
53
+ },
54
+ "tsdown": {
55
+ "entry": [
56
+ "src/index.ts"
57
+ ]
58
+ }
59
+ }