Add internationalization support with astro-i18next integration

- Implemented astro-i18next for multi-language support, including English, Dutch, and Italian.
- Configured default locale and language fallback settings.
- Defined routes for localized content in the configuration.
- Updated package.json and package-lock.json to include new dependencies for i18next and related plugins.
This commit is contained in:
becarta
2025-05-23 15:10:00 +02:00
parent 8a3507dce0
commit 3168826fa8
581 changed files with 88691 additions and 494 deletions

4
node_modules/i18next-fs-backend/lib/extname.js generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export default (filename) => {
if (filename.indexOf('.') < 0) return undefined
return `.${filename.split('.').pop()}`
}

1426
node_modules/i18next-fs-backend/lib/formats/json5.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1652
node_modules/i18next-fs-backend/lib/formats/jsonc.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

3851
node_modules/i18next-fs-backend/lib/formats/yaml.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

172
node_modules/i18next-fs-backend/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,172 @@
import { defaults, debounce, getPath, setPath, pushPath } from './utils.js'
import { readFile, readFileSync } from './readFile.js'
import { writeFile, removeFile } from './writeFile.js'
const getDefaults = () => {
return {
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/{{lng}}/{{ns}}.missing.json',
ident: 2,
parse: JSON.parse,
stringify: JSON.stringify
// expirationTime: 60 * 60 * 1000
}
}
class Backend {
constructor (services, options = {}, allOptions = {}) {
this.services = services
this.options = options
this.allOptions = allOptions
this.type = 'backend'
this.init(services, options, allOptions)
}
init (services, options = {}, allOptions = {}) {
this.services = services
this.options = defaults(options, this.options || {}, getDefaults())
this.allOptions = allOptions
this.queuedWrites = {}
this.debouncedWrite = debounce(this.write, 250)
}
read (language, namespace, callback) {
let loadPath = this.options.loadPath
if (typeof this.options.loadPath === 'function') {
loadPath = this.options.loadPath(language, namespace)
}
const filename = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace })
if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) {
try {
const { data, stat } = readFileSync(filename, this.options)
const timestamp = stat && stat.mtime && stat.mtime.getTime()
if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) {
this.removeFile(language, namespace)
return callback(new Error('File expired!'), false) // no retry
}
callback(null, data, timestamp)
} catch (err) {
callback(err, false) // no retry
}
return
}
readFile(filename, this.options)
.then(({ data, stat }) => {
const timestamp = stat && stat.mtime && stat.mtime.getTime()
if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) {
this.removeFile(language, namespace)
return callback(new Error('File expired!'), false) // no retry
}
callback(null, data, timestamp)
})
.catch((err) => callback(err, false)) // no retry
}
create (languages, namespace, key, fallbackValue, callback) {
if (typeof callback !== 'function') callback = () => {}
if (typeof languages === 'string') languages = [languages]
let todo = languages.length
const done = () => {
if (!--todo) callback()
}
languages.forEach((lng) => {
// eslint-disable-next-line no-useless-call
this.queue.call(this, lng, namespace, key, fallbackValue, done)
})
}
// this way i18next-fs-backend can be used as cache layer in combination with i18next-chained-backend
save (language, namespace, data, callback) {
if (!callback) callback = () => {}
const keys = Object.keys(data)
let todo = keys.length
const done = () => {
if (!--todo) callback()
}
keys.forEach((key) => {
// eslint-disable-next-line no-useless-call
this.queue.call(this, language, namespace, key, data[key], done)
})
}
removeFile (language, namespace) {
let addPath = this.options.addPath
if (typeof this.options.addPath === 'function') {
addPath = this.options.addPath(language, namespace)
}
const filename = this.services.interpolator.interpolate(addPath, { lng: language, ns: namespace })
removeFile(filename, this.options)
.then(() => {})
.catch(() => {})
}
write () {
for (const lng in this.queuedWrites) {
const namespaces = this.queuedWrites[lng]
if (lng !== 'locks') {
for (const ns in namespaces) {
this.writeFile(lng, ns)
}
}
}
}
writeFile (lng, namespace) {
const lock = getPath(this.queuedWrites, ['locks', lng, namespace])
if (lock) return
let addPath = this.options.addPath
if (typeof this.options.addPath === 'function') {
addPath = this.options.addPath(lng, namespace)
}
const filename = this.services.interpolator.interpolate(addPath, { lng, ns: namespace })
const missings = getPath(this.queuedWrites, [lng, namespace])
setPath(this.queuedWrites, [lng, namespace], [])
if (missings.length) {
// lock
setPath(this.queuedWrites, ['locks', lng, namespace], true)
const proceed = ({ data }) => {
missings.forEach((missing) => {
const path = this.allOptions.keySeparator === false ? [missing.key] : (missing.key.split(this.allOptions.keySeparator || '.'))
try {
setPath(data, path, missing.fallbackValue)
} catch (e) {
if (path.length < 2 || !e.message || (e.message.indexOf('Cannot create property') < 0)) throw e
setPath(data, [missing.key], missing.fallbackValue)
}
})
const proceedWrite = () => {
// unlock
setPath(this.queuedWrites, ['locks', lng, namespace], false)
missings.forEach((missing) => {
if (missing.callback) missing.callback()
})
// rerun
this.debouncedWrite()
}
writeFile(filename, data, this.options)
.then(proceedWrite)
.catch(proceedWrite)
}
readFile(filename, this.options).then(proceed).catch(() => proceed({ data: {} }))
}
}
queue (lng, namespace, key, fallbackValue, callback) {
pushPath(this.queuedWrites, [lng, namespace], { key, fallbackValue: fallbackValue || '', callback })
this.debouncedWrite()
}
}
Backend.type = 'backend'
export default Backend

146
node_modules/i18next-fs-backend/lib/readFile.js generated vendored Normal file
View File

@@ -0,0 +1,146 @@
import JSON5 from './formats/json5.js'
import { parse as parseJSONC } from './formats/jsonc.js'
import jsYaml from './formats/yaml.js'
import extname from './extname.js'
const isDeno = typeof Deno !== 'undefined'
const isBun = typeof Bun !== 'undefined'
const YAML = typeof jsYaml !== 'undefined' && jsYaml.load ? jsYaml : undefined
const fs = (!isDeno/* && !isBun */) ? (await import('node:fs')).default : undefined
// eslint-disable-next-line no-eval
const evalAlias = eval
const readFileInNodeSync = (filename) => {
const data = fs.readFileSync(filename, 'utf8')
let stat
try {
stat = fs.statSync(filename)
} catch (e) {}
return { data, stat }
}
const readFileInNode = (filename) => {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) return reject(err)
fs.stat(filename, (err, stat) => {
if (err) return resolve({ data })
return resolve({ data, stat })
})
})
})
}
const readFileInDenoSync = (filename) => {
const decoder = new TextDecoder('utf-8')
// eslint-disable-next-line no-undef
const d = Deno.readFileSync(filename)
const data = decoder.decode(d)
let stat
try {
// eslint-disable-next-line no-undef
stat = Deno.statSync(filename)
} catch (e) {}
return { data, stat }
}
const readFileInDeno = (filename) => {
return new Promise((resolve, reject) => {
const decoder = new TextDecoder('utf-8')
// eslint-disable-next-line no-undef
Deno.readFile(filename).then((d) => {
const data = decoder.decode(d)
// eslint-disable-next-line no-undef
Deno.stat(filename).then((stat) => resolve({ data, stat })).catch(() => resolve({ data }))
}).catch(reject)
})
}
const readFileInBunSync = readFileInNodeSync
const readFileInBun = readFileInNode
// const readFileInBun = async (filename) => {
// const f = Bun.file(filename)
// const data = await f.text()
// return { data } // Bun has no stat interface yet
// }
const replaceLast = (str, find, replace) => {
const index = str.lastIndexOf(find)
if (index > -1) {
return str.substring(0, index) + replace + str.substring(index + find.length)
}
return str.toString()
}
const parseData = (extension, data, options) => {
data = data.replace(/^\uFEFF/, '')
let result = {}
switch (extension) {
case '.js':
case '.ts':
if (typeof module === 'undefined') {
if (data.indexOf('exports') > -1) { // just to try...
data = `(${replaceLast(data.substring(data.indexOf('=') + 1), '};', '')})`
} else if (data.indexOf('export default ') > -1) { // just to try...
data = `(${replaceLast(data.substring(data.indexOf('export default ') + 15), '};', '')})`
}
}
result = evalAlias(data)
break
case '.json5':
result = JSON5.parse(data)
break
case '.jsonc':
result = parseJSONC(data)
break
case '.yml':
case '.yaml':
result = YAML.load(data)
break
default:
result = options.parse(data)
}
return result
}
// const resolvePath = (filename) => {
// return !path.isAbsolute(filename) && typeof process !== 'undefined' && process.cwd && !fs.existsSync(filename) ? path.join(process.cwd(), filename) : filename
// }
export function readFileSync (filename, options) {
const ext = extname(filename)
let data, stat
if (isBun) {
const ret = readFileInBunSync(filename)
data = ret.data
stat = ret.stat
} else if (isDeno) {
const ret = readFileInDenoSync(filename)
data = ret.data
stat = ret.stat
} else {
const ret = readFileInNodeSync(filename)
data = ret.data
stat = ret.stat
}
return { data: parseData(ext, data, options), stat }
}
export function readFile (filename, options = { parse: JSON.parse }) {
const ext = extname(filename)
// if (['.js', '.ts'].indexOf(ext) > -1) {
// return import(resolvePath(filename)).then((imp) => {
// return { data: (imp && imp.default) || imp }
// })
// }
const fn = isBun ? readFileInBun : isDeno ? readFileInDeno : readFileInNode
return new Promise((resolve, reject) => {
fn(filename).then(({ data, stat }) => {
try {
const ret = parseData(ext, data, options)
resolve({ data: ret, stat })
} catch (err) {
err.message = 'error parsing ' + filename + ': ' + err.message
reject(err)
}
}).catch(reject)
})
}

70
node_modules/i18next-fs-backend/lib/utils.js generated vendored Normal file
View File

@@ -0,0 +1,70 @@
const arr = []
const each = arr.forEach
const slice = arr.slice
export function defaults (obj) {
each.call(slice.call(arguments, 1), (source) => {
if (source) {
for (const prop in source) {
if (obj[prop] === undefined) obj[prop] = source[prop]
}
}
})
return obj
}
export function debounce (func, wait, immediate) {
let timeout
return function () {
const context = this
const args = arguments
const later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
function getLastOfPath (object, path, Empty) {
function cleanKey (key) {
return (key && key.indexOf('###') > -1) ? key.replace(/###/g, '.') : key
}
const stack = (typeof path !== 'string') ? [].concat(path) : path.split('.')
while (stack.length > 1) {
if (!object) return {}
const key = cleanKey(stack.shift())
if (!object[key] && Empty) object[key] = new Empty()
object = object[key]
}
if (!object) return {}
return {
obj: object,
k: cleanKey(stack.shift())
}
}
export function setPath (object, path, newValue) {
const { obj, k } = getLastOfPath(object, path, Object)
if (Array.isArray(obj) && isNaN(k)) throw new Error(`Cannot create property "${k}" here since object is an array`)
obj[k] = newValue
}
export function pushPath (object, path, newValue, concat) {
const { obj, k } = getLastOfPath(object, path, Object)
obj[k] = obj[k] || []
if (concat) obj[k] = obj[k].concat(newValue)
if (!concat) obj[k].push(newValue)
}
export function getPath (object, path) {
const { obj, k } = getLastOfPath(object, path)
if (!obj) return undefined
return obj[k]
}

170
node_modules/i18next-fs-backend/lib/writeFile.js generated vendored Normal file
View File

@@ -0,0 +1,170 @@
import JSON5 from './formats/json5.js'
import jsYaml from './formats/yaml.js'
import extname from './extname.js'
const isDeno = typeof Deno !== 'undefined'
const isBun = typeof Bun !== 'undefined'
const YAML = typeof jsYaml !== 'undefined' && jsYaml.load ? jsYaml : undefined
const fs = (!isDeno/* && !isBun */) ? (await import('node:fs')).default : undefined
function dirname (path) {
if (path.length === 0) return '.'
let code = path.charCodeAt(0)
const hasRoot = code === 47
let end = -1
let matchedSlash = true
for (let i = path.length - 1; i >= 1; --i) {
code = path.charCodeAt(i)
if (code === 47) {
if (!matchedSlash) {
end = i
break
}
} else {
// We saw the first non-path separator
matchedSlash = false
}
}
if (end === -1) return hasRoot ? '/' : '.'
if (hasRoot && end === 1) return '//'
return path.slice(0, end)
}
const writeFileInNodeSync = (filename, payload) => {
try {
fs.mkdirSync(dirname(filename), { recursive: true })
} catch (err) {}
return fs.writeFileSync(filename, payload, 'utf8')
}
const writeFileInNode = (filename, payload) => {
return new Promise((resolve, reject) => {
fs.mkdir(dirname(filename), { recursive: true }, () => {
fs.writeFile(filename, payload, 'utf8', (err, data) => err ? reject(err) : resolve(data))
})
})
}
const removeFileInNodeSync = (filename) => {
return fs.unlinkSync(filename)
}
const removeFileInNode = (filename) => {
return new Promise((resolve, reject) => fs.unlink(filename, (err) => err ? reject(err) : resolve()))
}
const writeFileInDenoSync = (filename, payload) => {
const encoder = new TextEncoder()
const data = encoder.encode(payload)
try {
// eslint-disable-next-line no-undef
Deno.mkdirSync(dirname(filename), { recursive: true })
} catch (err) {}
// eslint-disable-next-line no-undef
Deno.writeFileSync(filename, data)
}
const writeFileInDeno = (filename, payload) => {
const encoder = new TextEncoder()
const data = encoder.encode(payload)
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-undef
Deno.mkdir(dirname(filename), { recursive: true }).then(() => {
// eslint-disable-next-line no-undef
Deno.writeFile(filename, data).then(resolve, reject)
}).catch(() => {
// eslint-disable-next-line no-undef
Deno.writeFile(filename, data).then(resolve, reject)
})
})
}
const removeFileInDenoSync = (filename) => {
// eslint-disable-next-line no-undef
Deno.removeSync(filename)
}
const removeFileInDeno = (filename) => {
// eslint-disable-next-line no-undef
return Deno.remove(filename)
}
const writeFileInBunSync = writeFileInNodeSync // not yet a specific Bun interface
const writeFileInBun = writeFileInNode
// Bun.write generates some error warnings yet...
// const writeFileInBun = (filename, payload) => Bun.write(filename, payload)
const removeFileInBunSync = removeFileInNodeSync // not yet a specific Bun interface
const removeFileInBun = removeFileInNode // not yet a specific Bun interface
const stringifyData = (extension, data, options) => {
let result = ''
switch (extension) {
case '.js':
case '.ts':
if (typeof module === 'undefined') {
result = `export default ${options.stringify(data, null, options.ident)}`
} else {
result = `module.exports = ${options.stringify(data, null, options.ident)}`
}
break
case '.json5':
result = JSON5.stringify(data, null, options.ident)
break
case '.yml':
case '.yaml':
result = YAML.dump(data, { ident: options.indent })
break
default:
result = options.stringify(data, null, options.ident)
}
return result
}
export function writeFileSync (filename, payload, options) {
const ext = extname(filename)
let data
try {
data = stringifyData(ext, payload, options)
} catch (err) {
err.message = 'error stringifying ' + filename + ': ' + err.message
throw err
}
if (isBun) {
return writeFileInBunSync(filename, data)
} else if (isDeno) {
return writeFileInDenoSync(filename, data)
} else {
return writeFileInNodeSync(filename, data)
}
}
export function writeFile (filename, payload, options = { stringify: JSON.stringify, ident: 2 }) {
const ext = extname(filename)
let data
try {
data = stringifyData(ext, payload, options)
} catch (err) {
err.message = 'error stringifying ' + filename + ': ' + err.message
throw err
}
const fn = isBun ? writeFileInBun : isDeno ? writeFileInDeno : writeFileInNode
return fn(filename, data)
}
export function removeFileSync (filename) {
if (isBun) {
return removeFileInBunSync(filename)
} else if (isDeno) {
return removeFileInDenoSync(filename)
} else {
return removeFileInNodeSync(filename)
}
}
export function removeFile (filename) {
const fn = isBun ? removeFileInBun : isDeno ? removeFileInDeno : removeFileInNode
return fn(filename)
}