Skip to content

Commit

Permalink
Add VIntl module and extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
brawaru committed May 29, 2023
1 parent 273a692 commit eac3e2c
Show file tree
Hide file tree
Showing 13 changed files with 735 additions and 10 deletions.
18 changes: 18 additions & 0 deletions composables/compact-number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/compact-number'
import { IntlController } from '@vintl/vintl/controller'

const formatters = new WeakMap<IntlController<any>, Formatter>()

export function useCompactNumber(): Formatter {
const vintl = useVIntl()

let formatter = formatters.get(vintl)

if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
}

return formatter
}
18 changes: 18 additions & 0 deletions composables/how-ago.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/how-ago'
import { IntlController } from '@vintl/vintl/controller'

const formatters = new WeakMap<IntlController<any>, Formatter>()

export function useRelativeTime(): Formatter {
const vintl = useVIntl()

let formatter = formatters.get(vintl)

if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
}

return formatter
}
7 changes: 7 additions & 0 deletions crowdin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
project_id: 518556
preserve_hierarchy: true

files:
- source: /locales/en-US/*
dest: /%original_file_name%
translation: /locales/%locale%/%original_file_name%
17 changes: 17 additions & 0 deletions locales/en-US/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"frog": {
"message": "You've been frogged! 🐸"
},
"frog.altText": {
"message": "A photorealistic painting of a frog labyrinth"
},
"frog.froggedPeople": {
"message": "{count, plural, one {{count} more person} other {{count} more people}} were also frogged!"
},
"frog.sinceOpened": {
"message": "This page was opened {ago}"
},
"frog.title": {
"message": "Frog"
}
}
3 changes: 3 additions & 0 deletions locales/en-US/languages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"en-US": "American English"
}
6 changes: 6 additions & 0 deletions locales/en-US/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"displayName": {
"description": "The name of the language in dialect form (e.g. Français canadien for French spoken in Canada, not French (Canada))",
"message": "American English"
}
}
91 changes: 90 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { promises as fs } from 'fs'
import { pathToFileURL } from 'node:url'
import svgLoader from 'vite-svg-loader'
import { resolve } from 'pathe'
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 STAGING_ARIADNE_URL = 'https://staging-ariadne.modrinth.com/v1/'
Expand Down Expand Up @@ -39,6 +42,14 @@ const meta = {
'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 ENABLED_LOCALES: string[] = []

export default defineNuxtConfig({
app: {
head: {
Expand Down Expand Up @@ -170,6 +181,75 @@ export default defineNuxtConfig({
})
)
},
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 (!ENABLED_LOCALES.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 cnDataImport = resolveCompactNumberDataImport(tag)
if (cnDataImport != null) {
;(locale.additionalImports ??= []).push({
from: cnDataImport,
resolve: false,
})
}
}
},
},
runtimeConfig: {
apiBaseUrl: process.env.BASE_URL ?? getApiUrl(),
Expand All @@ -190,6 +270,15 @@ export default defineNuxtConfig({
strict: true,
typeCheck: true,
},
modules: ['@vintl/nuxt'],
vintl: {
defaultLocale: 'en-US',
storage: 'cookie',
parserless: 'only-prod',
},
nitro: {
moduleSideEffects: ['@vintl/compact-number/locale-data'],
},
})

function getApiUrl() {
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@
"postinstall": "nuxi prepare",
"lint:js": "eslint . --ext .js,.vue,.ts",
"lint": "npm run lint:js && prettier --check .",
"fix": "eslint . --fix --ext .js,.vue,.ts && prettier --write ."
"fix": "eslint . --fix --ext .js,.vue,.ts && prettier --write .",
"intl:extract": "formatjs extract \"{,components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file locales/en-US/index.json --format crowdin"
},
"devDependencies": {
"@formatjs/cli": "^6.1.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/node": "^20.1.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vintl/compact-number": "^2.0.3",
"@vintl/how-ago": "^2.0.0",
"@vintl/nuxt": "^1.2.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"glob": "^10.2.6",
"nuxt": "^3.4.2",
"prettier": "^2.8.3",
"sass": "^1.58.0",
Expand Down
4 changes: 2 additions & 2 deletions pages/[type]/[id]/gallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
@click.stop=""
@click.stop
/>

<div class="floating" @click.stop="">
<div class="floating" @click.stop>
<div class="text">
<h2 v-if="expandedGalleryItem.title">
{{ expandedGalleryItem.title }}
Expand Down
60 changes: 54 additions & 6 deletions pages/frog.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,59 @@
<script setup lang="ts">
const vintl = useVIntl()
const { formatMessage } = vintl
const messages = defineMessages({
frogTitle: {
id: 'frog.title',
defaultMessage: 'Frog',
},
frogDescription: {
id: 'frog',
defaultMessage: "You've been frogged! 🐸",
},
frogAltText: {
id: 'frog.altText',
defaultMessage: 'A photorealistic painting of a frog labyrinth',
},
frogSinceOpened: {
id: 'frog.sinceOpened',
defaultMessage: 'This page was opened {ago}',
},
frogFroggedPeople: {
id: 'frog.froggedPeople',
defaultMessage:
'{count, plural, one {{count} more person} other {{count} more people}} were also frogged!',
},
})
const formatCompactNumber = useCompactNumber()
const formatRelativeTime = useRelativeTime()
const pageOpen = useState('frogPageOpen', () => Date.now())
const peopleFrogged = useState('frogPeopleFrogged', () => Math.round(Math.random() * 100_000_000))
const peopleFroggedCount = computed(() => formatCompactNumber(peopleFrogged.value))
let interval: ReturnType<typeof setTimeout>
const formattedOpenedCounter = ref(formatRelativeTime(Date.now()))
onMounted(() => {
interval = setInterval(() => {
formattedOpenedCounter.value = formatRelativeTime(pageOpen.value)
}, 1000)
})
onUnmounted(() => clearInterval(interval))
</script>

<template>
<div class="card">
<h1>Frog</h1>
<p>You've been frogged! 🐸</p>
<img
src="https://cdn.modrinth.com/frog.png"
alt="a photorealistic painting of a frog labyrinth"
/>
<h1>{{ formatMessage(messages.frogTitle) }}</h1>
<p>{{ formatMessage(messages.frogDescription) }}</p>
<img src="https://cdn.modrinth.com/frog.png" :alt="formatMessage(messages.frogAltText)" />
<p>{{ formatMessage(messages.frogSinceOpened, { ago: formattedOpenedCounter }) }}</p>
<p>{{ formatMessage(messages.frogFroggedPeople, { count: peopleFroggedCount }) }}</p>
</div>
</template>

Expand Down
Loading

0 comments on commit eac3e2c

Please sign in to comment.