www/src/utils/i18n.ts
2025-05-15 16:22:26 +12:00

155 lines
4.9 KiB
TypeScript

import type { GetStaticPaths } from 'astro'
import { CONSTANT } from '~/constants'
import UI_EN from '~/i18n/en/translation.json'
/**
* Represents the available locales in the application
* @typedef {string} Locale
*/
export type Locale = (typeof locales)[number]
/**
* Generates a localized path by prefixing the locale if necessary
* @param {Locale} [locale] - The current locale
* @returns {function(string): string} A function that transforms paths based on the locale
*/
export const getPath = (locale?: Locale) => (path: string) => {
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
}
return path
}
/**
* Extracts the current locale from Astro's params, defaulting to the default locale
* @param {Object} Astro - Astro's context object
* @param {Object} [Astro.params] - Routing parameters
* @param {string} [Astro.params.locale] - The current locale parameter
* @returns {Locale} The determined locale
*/
export const getLocale = (Astro: { params?: { locale?: string } }) => {
if (Astro.params?.locale) {
return Astro.params.locale as Locale
}
return CONSTANT.I18N.DEFAULT_LOCALE as Locale
}
/**
* List of all supported locales
* @type {Locale[]}
*/
export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
/**
* List of locales excluding the default locale
* @type {Locale[]}
*/
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE)
/**
* Retrieves locales other than the default locale
* @returns {Locale[]} Array of non-default locales
*/
export const getOtherLocales = () => otherLocales
/**
* Type definition for UI translations based on the English translation
* @typedef {Object} UI
*/
export type UI = typeof UI_EN
/**
* Mapping of locales to their UI translation objects
* @type {Object.<Locale, UI>}
*/
export const ui = { en: UI_EN }
/**
* Retrieves UI translations for a given locale, merging with default translations
* @param {Locale} [locale] - The target locale for translations
* @returns {UI} Merged UI translations
*/
export const getUI = (locale?: Locale | string): UI => {
const validLocale = locales.includes(locale as Locale) ? locale : CONSTANT.I18N.DEFAULT_LOCALE
const defaultUI = ui[CONSTANT.I18N.DEFAULT_LOCALE]
const localeUI = ui[validLocale as Locale]
/**
* Recursively merges two objects, with the override object taking precedence
* @template T
* @param {T} defaultObj - The default object to merge from
* @param {Partial<T>} overrideObj - The object to merge over the default
* @returns {T} The deeply merged object
*/
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
// Handle non-object cases
if (typeof defaultObj !== 'object' || defaultObj === null) {
return (overrideObj ?? defaultObj) as T
}
if (typeof overrideObj !== 'object' || overrideObj === null) {
return (overrideObj ?? defaultObj) as T
}
// Create a new object or array based on the default object's type
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
// Merge properties from the default object
for (const key of Object.keys(defaultObj) as Array<keyof T>) {
const defaultValue = defaultObj[key]
const overrideValue = overrideObj[key]
// Recursively merge nested objects
if (
defaultValue !== null &&
overrideValue !== null &&
typeof defaultValue === 'object' &&
typeof overrideValue === 'object'
) {
// Type assertion to handle nested merging
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>)
} else if (overrideValue !== undefined) {
// Override with the new value if it exists
;(result as Record<keyof T, unknown>)[key] = overrideValue
}
}
// Add any new properties from overrideObj
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
if (!(key in defaultObj)) {
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
}
}
return result as T
}
return deepMerge(defaultUI, localeUI)
}
/**
* Generates static paths for internationalization
* @type {GetStaticPaths}
* @returns {Array} An array of static paths for different locales
*/
export const getStaticPaths = (() => {
return [
{
params: { locale: undefined },
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
},
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(({ value }) => ({
params: { locale: value },
props: {
locale: value,
},
})),
]
}) satisfies GetStaticPaths
/**
* Retrieves all available locales, including both default and non-default
* @returns {Locale[]} Combined array of all locales
*/
export const getLocales = () => {
return [...locales, ...otherLocales]
}