Files
AstralRinth/nuxt.config.ts
Sasha Sorokin 467b0fa988 Add the language setting page (#1210)
* Add initial language picker prototype

* Heap o' improvements and Pirate tongue

* Move .visually-hidden to shared utils and add copyright notice

* Add a little space before categories names

* Simplify search to input focus logic

* Remove larger font size and padding from the search field

* Some refactors

* Braw's descent into madness

Thanks web development!

In seriousness though, tried to make the list more accessible. Making it
fully accessible feels like unbearable task, so at least that.

* Litol refactoring

* Extract new strings and remove old ones

* Update @vintl/nuxt to 1.3.0

This fixes the bug where default locale won't be saved.

* A buncha refactorings and cleanup

* Scuttle the Pirate lingo

'Twas employed 'ere for testin' purposes, but fear not, for it shall be
returnin' in the days to come. Should ye require it fer testin', simply
roll back this here commit.

* Clean languages source file

* Change "US" to "United States"

I think it would make distinguishing two languages simpler as now
there's more than one letter of difference (US/UK vs United States/
United Kingdom).
2023-08-21 13:26:39 -04:00

355 lines
10 KiB
TypeScript

import { promises as fs } from 'fs'
import { pathToFileURL } from 'node:url'
import svgLoader from 'vite-svg-loader'
import { resolve, basename } from 'pathe'
import { defineNuxtConfig } from 'nuxt/config'
import { $fetch } from 'ofetch'
import { globIterate } from 'glob'
import { match as matchLocale } from '@formatjs/intl-localematcher'
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
const preloadedFonts = [
'inter/Inter-Regular.woff2',
'inter/Inter-Medium.woff2',
'inter/Inter-SemiBold.woff2',
'inter/Inter-Bold.woff2',
]
const favicons = {
'(prefers-color-scheme:no-preference)': '/favicon-light.ico',
'(prefers-color-scheme:light)': '/favicon-light.ico',
'(prefers-color-scheme:dark)': '/favicon.ico',
}
const meta = {
description:
'Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
publisher: 'Rinth, Inc.',
'apple-mobile-web-app-title': 'Modrinth',
'theme-color': '#1bd96a',
'color-scheme': 'dark light',
// OpenGraph
'og:title': 'Modrinth',
'og:site_name': 'Modrinth',
'og:description': 'An open source modding platform',
'og:type': 'website',
'og:url': 'https://modrinth.com',
'og:image': 'https://cdn.modrinth.com/modrinth-new.png?',
// Twitter
'twitter:card': 'summary',
'twitter:site': '@modrinth',
}
/**
* Tags of locales that are auto-discovered besides the default locale.
*
* Preferably only the locales that reach a certain threshold of complete
* translations would be included in this array.
*/
const enabledLocales: string[] = []
/**
* Overrides for the categories of the certain locales.
*/
const localesCategoriesOverrides: Partial<Record<string, 'fun' | 'experimental'>> = {
'en-x-pirate': 'fun',
'en-x-updown': 'fun',
'en-x-lolcat': 'fun',
'en-x-uwu': 'fun',
'ru-x-bandit': 'fun',
}
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: {
lang: 'en',
},
title: 'Modrinth',
meta: Object.entries(meta).map(([name, content]): object => {
return { name, content }
}),
link: [
// The type is necessary because the linter can't always compare this very nested/complex type on itself
...preloadedFonts.map((font): object => {
return {
rel: 'preload',
href: `https://cdn-raw.modrinth.com/fonts/${font}?v=3.19`,
as: 'font',
type: 'font/woff2',
crossorigin: 'anonymous',
}
}),
...Object.entries(favicons).map(([media, href]): object => {
return { rel: 'icon', type: 'image/x-icon', href, media }
}),
{
rel: 'search',
type: 'application/opensearchdescription+xml',
href: '/opensearch.xml',
title: 'Modrinth mods',
},
],
},
},
vite: {
plugins: [
svgLoader({
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
}),
],
},
hooks: {
async 'build:before'() {
// 30 minutes
const TTL = 30 * 60 * 1000
let state: {
lastGenerated?: string
apiUrl?: string
categories?: any[]
loaders?: any[]
gameVersions?: any[]
donationPlatforms?: any[]
reportTypes?: any[]
} = {}
let homePageProjects: any[] = []
try {
state = JSON.parse(await fs.readFile('./generated/state.json', 'utf8'))
homePageProjects = JSON.parse(await fs.readFile('./generated/homepage.json', 'utf8'))
} catch {
// File doesn't exist, create folder
await fs.mkdir('./generated', { recursive: true })
}
const API_URL = getApiUrl()
if (
// Skip regeneration if within TTL...
state.lastGenerated &&
new Date(state.lastGenerated).getTime() + TTL > new Date().getTime() &&
// ...but only if the API URL is the same
state.apiUrl === API_URL &&
homePageProjects.length !== 0
) {
return
}
state.lastGenerated = new Date().toISOString()
state.apiUrl = API_URL
const headers = {
headers: {
'user-agent': 'Knossos generator (support@modrinth.com)',
},
}
const [categories, loaders, gameVersions, donationPlatforms, reportTypes, projects] =
await Promise.all([
$fetch(`${API_URL}tag/category`, headers),
$fetch(`${API_URL}tag/loader`, headers),
$fetch(`${API_URL}tag/game_version`, headers),
$fetch(`${API_URL}tag/donation_platform`, headers),
$fetch(`${API_URL}tag/report_type`, headers),
$fetch(`${API_URL}projects_random?count=40`, headers),
])
state.categories = categories
state.loaders = loaders
state.gameVersions = gameVersions
state.donationPlatforms = donationPlatforms
state.reportTypes = reportTypes
homePageProjects = projects
await fs.writeFile('./generated/state.json', JSON.stringify(state))
await fs.writeFile('./generated/homepage.json', JSON.stringify(homePageProjects))
console.log('Tags generated!')
},
'pages:extend'(routes) {
routes.splice(
routes.findIndex((x) => x.name === 'search-searchProjectType'),
1
)
const types = ['mods', 'modpacks', 'plugins', 'resourcepacks', 'shaders', 'datapacks']
types.forEach((type) =>
routes.push({
name: `search-${type}`,
path: `/${type}`,
file: resolve(__dirname, 'pages/search/[searchProjectType].vue'),
children: [],
})
)
},
async 'vintl:extendOptions'(opts) {
opts.locales ??= []
const resolveCompactNumberDataImport = await (async () => {
const compactNumberLocales: string[] = []
const resolvedImports = new Map<string, string>()
for await (const localeFile of globIterate(
'node_modules/@vintl/compact-number/dist/locale-data/*.mjs',
{ ignore: '**/*.data.mjs' }
)) {
const tag = basename(localeFile, '.mjs')
compactNumberLocales.push(tag)
resolvedImports.set(tag, String(pathToFileURL(resolve(localeFile))))
}
function resolveImport(tag: string) {
const matchedTag = matchLocale([tag], compactNumberLocales, 'en-x-placeholder')
return matchedTag === 'en-x-placeholder'
? undefined
: `@vintl/compact-number/locale-data/${matchedTag}`
}
return resolveImport
})()
for await (const localeDir of globIterate('locales/*/', { posix: true })) {
const tag = basename(localeDir)
if (!enabledLocales.includes(tag) && opts.defaultLocale !== tag) continue
const locale =
opts.locales.find((locale) => locale.tag === tag) ??
opts.locales[opts.locales.push({ tag }) - 1]
for await (const localeFile of globIterate(`${localeDir}/*`, { posix: true })) {
const fileName = basename(localeFile)
if (fileName === 'index.json') {
if (locale.file == null) {
locale.file = {
from: `./${localeFile}`,
format: 'crowdin',
}
} else {
;(locale.files ??= []).push({
from: `./${localeFile}`,
format: 'crowdin',
})
}
} else if (fileName === 'meta.json') {
/** @type {Record<string, { message: string }>} */
const meta = await fs.readFile(localeFile, 'utf8').then((date) => JSON.parse(date))
locale.meta ??= {}
for (const key in meta) {
locale.meta[key] = meta[key].message
}
} else {
;(locale.resources ??= {})[fileName] = `./${localeFile}`
}
}
const categoryOverride = localesCategoriesOverrides[tag]
if (categoryOverride != null) {
;(locale.meta ??= {}).category = categoryOverride
}
const cnDataImport = resolveCompactNumberDataImport(tag)
if (cnDataImport != null) {
;(locale.additionalImports ??= []).push({
from: cnDataImport,
resolve: false,
})
}
}
},
},
runtimeConfig: {
// @ts-ignore
apiBaseUrl: process.env.BASE_URL ?? globalThis.BASE_URL ?? getApiUrl(),
// @ts-ignore
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
public: {
apiBaseUrl: getApiUrl(),
siteUrl: getDomain(),
owner: process.env.VERCEL_GIT_REPO_OWNER || 'modrinth',
slug: process.env.VERCEL_GIT_REPO_SLUG || 'knossos',
branch:
process.env.VERCEL_GIT_COMMIT_REF ||
process.env.CF_PAGES_BRANCH ||
// @ts-ignore
globalThis.CF_PAGES_BRANCH ||
'master',
hash:
process.env.VERCEL_GIT_COMMIT_SHA ||
process.env.CF_PAGES_COMMIT_SHA ||
// @ts-ignore
globalThis.CF_PAGES_COMMIT_SHA ||
'unknown',
},
},
typescript: {
shim: false,
strict: true,
typeCheck: true,
tsConfig: {
compilerOptions: {
moduleResolution: 'bundler',
allowImportingTsExtensions: true,
},
},
},
modules: ['@vintl/nuxt', '@nuxtjs/turnstile'],
vintl: {
defaultLocale: 'en-US',
storage: 'cookie',
parserless: 'only-prod',
},
turnstile: {
siteKey: '0x4AAAAAAAHWfmKCm7cUG869',
},
nitro: {
moduleSideEffects: ['@vintl/compact-number/locale-data'],
},
devtools: {
enabled: true,
},
})
function getApiUrl() {
// @ts-ignore
return process.env.BROWSER_BASE_URL ?? globalThis.BROWSER_BASE_URL ?? STAGING_API_URL
}
function getDomain() {
if (process.env.NODE_ENV === 'production') {
if (process.env.SITE_URL) {
return process.env.SITE_URL
}
// @ts-ignore
else if (process.env.CF_PAGES_URL || globalThis.CF_PAGES_URL) {
// @ts-ignore
return process.env.CF_PAGES_URL ?? globalThis.CF_PAGES_URL
} else if (process.env.HEROKU_APP_NAME) {
return `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
} else if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`
} else if (getApiUrl() === STAGING_API_URL) {
return 'https://staging.modrinth.com'
} else {
return 'https://modrinth.com'
}
} else {
return 'http://localhost:3000'
}
}