mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f4c6046a | ||
|
|
5ec3922985 | ||
|
|
9e17135e83 | ||
|
|
22cc04531d | ||
|
|
71ec457bb4 | ||
|
|
c2eabcef76 | ||
|
|
ae19100175 | ||
|
|
624ab8eeff | ||
|
|
35430bd3c9 | ||
|
|
3c8ac0e647 | ||
|
|
6b70b2819d | ||
|
|
fd0593c2cc | ||
|
|
efa1f4c20a | ||
|
|
926f5696d9 | ||
|
|
0186b77441 | ||
|
|
b28daf9b30 | ||
|
|
1c1fefcf8b | ||
|
|
e583146a6c | ||
|
|
d8d7573729 | ||
|
|
109f601f5f | ||
|
|
8c65609ed9 | ||
|
|
9a847e07d9 | ||
|
|
8791367c7b | ||
|
|
809d905221 | ||
|
|
63e0ffeb65 | ||
|
|
be398c7912 | ||
|
|
063ba7c71b | ||
|
|
5a4dd1289c | ||
|
|
6abf092907 | ||
|
|
5bde5f0108 | ||
|
|
f84d0f3d7d | ||
|
|
c9b930defa | ||
|
|
bd7c494b93 | ||
|
|
7d7ea6b864 | ||
|
|
91cdcaec84 | ||
|
|
d3a1cd30a3 | ||
|
|
ff3a4778ef | ||
|
|
3dd22ce937 | ||
|
|
e988df8978 | ||
|
|
6094b7792b | ||
|
|
9e42c63628 | ||
|
|
efdcdb988e | ||
|
|
92c90127b8 | ||
|
|
5786f3c0d4 | ||
|
|
6f57e4a704 | ||
|
|
95fdfbb51d | ||
|
|
ce471bc4e3 | ||
|
|
39fd25bda6 | ||
|
|
512b80184b | ||
|
|
473014349b | ||
|
|
114d04ac77 | ||
|
|
a08c222774 | ||
|
|
08cfcebded | ||
|
|
cdc373986c | ||
|
|
9d939c1b1b | ||
|
|
2007377bcf | ||
|
|
6479891b2d | ||
|
|
ec6744774d | ||
|
|
70a4cc57b0 | ||
|
|
f8f4494527 | ||
|
|
fe75e7ca94 | ||
|
|
a9fdc595f5 | ||
|
|
eee372b21e | ||
|
|
220a9cbf00 | ||
|
|
81c23b0b8d | ||
|
|
2b5a59b33d | ||
|
|
d8dabcf62e | ||
|
|
02281aa6c7 | ||
|
|
ef16a887d2 | ||
|
|
880cb1972d | ||
|
|
979f2bdedd | ||
|
|
7cb833df1e | ||
|
|
682133f893 | ||
|
|
9bff4db4c1 | ||
|
|
70c4dfab73 | ||
|
|
e1c501caad | ||
|
|
b883a37aeb | ||
|
|
951f36381f | ||
|
|
ae71089e74 | ||
|
|
e45e0fdde9 | ||
|
|
db449d439a |
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
|
||||
@@ -19,7 +19,7 @@ const activeLocale = computed({
|
||||
<div>
|
||||
<label>
|
||||
Locale:
|
||||
<select v-model="activeLocale" :disabled="switching.value">
|
||||
<select v-model="activeLocale" :disabled="switching">
|
||||
<option v-for="locale in vintl.availableLocales" :key="locale.tag">
|
||||
{{ locale.tag }}
|
||||
</option>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
VIntl for Nuxt provides variety of options to configure the plugin and integration.
|
||||
|
||||
::alert
|
||||
All options are validated using Zod, and only when launching Nuxt in dev or build modes.
|
||||
Some options will be checked when launching Nuxt in dev or build modes.
|
||||
::
|
||||
|
||||
## `ModuleOptions`
|
||||
@@ -12,15 +12,17 @@ Represents the VIntl module options.
|
||||
|
||||
**Properties**
|
||||
|
||||
| Property | Default | Description |
|
||||
| :---------------------- | :------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultLocale` | | A string that represents the BCP 47 code of the locale to use by default. |
|
||||
| `resolveDir` | `'.'` | An optional string that represents a directory where all the imports associated with the locales are being resolved. If it's relative, then it will be resolved against the Nuxt `srcDir` directory. If this property is not set, modules will be resolved from the Nuxt `srcDir`. |
|
||||
| `locales` | | An array of [`LocaleDescriptor`] objects that represent all the configured locales. |
|
||||
| `storage` | | An optional string that represents either a built-in storage name (`localStorage` / `cookie`), or a node import specifier that exports a custom storage adapter as the default export. |
|
||||
| `broadcastLocaleChange` | `true` | An optional boolean value that determines whether to use [`BroadcastChannel`] (if available) to synchronize the locale changes between the tabs on the same origin. |
|
||||
| `hostLanguageParam` | `'hl'` | An optional name of the query parameter used to override the user language (including automatically detected one). It is used to generate SEO meta tags. Using `null` will disable them. |
|
||||
| `parserless` | `{ enabled: false }` | An optional [`ParserlessOptions`] object or [`ParserlessMode`] which configures the parserless mode. |
|
||||
| Property | Default | Description |
|
||||
| :---------------------- | :-----: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultLocale` | | A string that represents the BCP 47 code of the locale to use by default. |
|
||||
| `resolveDir` | `'.'` | An optional string that represents a directory where all the imports associated with the locales are being resolved. If it's relative, then it will be resolved against the Nuxt `srcDir` directory. If this property is not set, modules will be resolved from the Nuxt `srcDir`. |
|
||||
| `locales` | | An array of [`LocaleDescriptor`] objects that represent all the configured locales. |
|
||||
| `storage` | | An optional string that represents either a built-in storage name (`localStorage` / `cookie`), or a node import specifier that exports a custom storage adapter as the default export. |
|
||||
| `broadcastLocaleChange` | `true` | An optional boolean value that determines whether to use [`BroadcastChannel`] (if available) to synchronize the locale changes between the tabs on the same origin. |
|
||||
| `hostLanguageParam` | `'hl'` | **Deprecated.** An optional name of the query parameter used to override the user language (including automatically detected one). It is used to generate SEO meta tags. Using `null` will disable them. |
|
||||
| `parserless` | `<...>` | An optional [`ParserlessOptions`] object or [`ParserlessMode`] which configures the parserless mode. |
|
||||
| `seo` | `<...>` | An optional [`SEOOptions`] object which configures automatic head tags to improve discoverability of other languages by search engines. |
|
||||
| `onParseError` | | An optional [`ParsingErrorHandlingOption`] value that defines how to handle errors that occur when transforming message files. If not defined, the errors are not handled and thrown during the building, preventing it from succeeding. |
|
||||
|
||||
[`BroadcastChannel`]: https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
||||
[`ParserlessOptions`]: #parserlessoptions
|
||||
@@ -131,9 +133,9 @@ Represents an import source for the messages file. It is similar to the [`Import
|
||||
|
||||
Represents options for the parserless mode.
|
||||
|
||||
| Property | Description |
|
||||
| :-------- | :----------------------------------------------------------------------- |
|
||||
| `enabled` | A `ParserlessMode` value representing the status of the parserless mode. |
|
||||
| Property | Default | Description |
|
||||
| :-------- | :-----: | :----------------------------------------------------------------------- |
|
||||
| `enabled` | `false` | A `ParserlessMode` value representing the status of the parserless mode. |
|
||||
|
||||
## `ParserlessMode`
|
||||
|
||||
@@ -144,3 +146,39 @@ A string or boolean value representing the status of the parserless mode.
|
||||
| `always` | `true` | Parserless mode is always enabled, regardless of the mode in which Nuxt is running. |
|
||||
| `only-prod` | | Parserless mode is only enabled when building Nuxt app for production. |
|
||||
| `never` | `false` | Parserless mode is never enabled, the parser is always present in the runtime. |
|
||||
|
||||
## `SEOOptions`
|
||||
|
||||
[`SEOOptions`]: #seooptions
|
||||
|
||||
Represents options for the automatic head tags.
|
||||
|
||||
By default VIntl will add `<link>` tags for every language to make search engines aware of other languages, you can disable this by setting `enabled` to `false`.
|
||||
|
||||
| Property | Default | Description |
|
||||
| :-------------------------- | :-----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `enabled` | `true` | A boolean value representing whether VIntl should automatically add `<link>` tags to `<head>` of the site. These tags make search engines aware that your site is provided in different languages. |
|
||||
| `hostLanguageParameter`: | `hl` | A string value representing the query parameter that VIntl would use to configure the language of the site. |
|
||||
| `baseURL` | | **Recommended.** An optional string value representing the base URL of the site. If not set, VIntl will try to guess the URL based on inital request URL or the current `window.location`. |
|
||||
| `defaultLocaleHasParameter` | `true` | A boolean value representing whether the default locale should have a host language parameter. Disabling this may make URLs of visitors cleaner, however it is not correct, because lack of host language instructs VIntl to use user's language (either the one they've used before or browser's one). |
|
||||
| `xDefaultHreflang` | `true` | A boolean value representing whether the `x-default` hreflang is present. |
|
||||
|
||||
## `ParsingErrorHandlingOption`
|
||||
|
||||
[`ParsingErrorHandlingOption`]: #parsingerrorhandlingoption
|
||||
|
||||
Represents a value for the message pasing error handling option.
|
||||
|
||||
It can be one of the following:
|
||||
|
||||
- A string containing a name of the strategy for handling the errors. The following strategies are available:
|
||||
|
||||
| Name | Strategy |
|
||||
| :----------------------- | :-------------------------------------------------------------------------------- |
|
||||
| `use-message-as-literal` | Use the raw message contents as a literal. |
|
||||
| `use-id-as-literal` | Use the message ID as a literal. |
|
||||
| `use-empty-literal` | Use empty string as a literal. |
|
||||
| `log-and-skip` | Log the error to console and don't include the message into the transformed file. |
|
||||
| `skip` | Skip including the message into the transformed file. |
|
||||
|
||||
- An function that takes in a context for the error (that includes error itself) and optionally returns a fallback AST (not returning anything skips including the message into the transformed file).
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
import peonyOverrides from '@vintl-dev/eslint-config-peony/overrides'
|
||||
|
||||
export default withNuxt().append(peonyOverrides)
|
||||
@@ -1,6 +1,6 @@
|
||||
export default defineNuxtConfig({
|
||||
extends: '@nuxt-themes/docus',
|
||||
modules: ['@vintl/nuxt'],
|
||||
extends: ['@nuxt-themes/docus'],
|
||||
modules: ['@vintl/nuxt', '@nuxt/eslint'],
|
||||
vintl: {
|
||||
defaultLocale: 'en-US',
|
||||
locales: [
|
||||
|
||||
+10
-8
@@ -13,15 +13,17 @@
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.1.2",
|
||||
"@nuxt-themes/docus": "^1.12.3",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt-themes/docus": "^1.15.0",
|
||||
"@nuxt/eslint": "^0.3.13",
|
||||
"@nuxt/eslint-config": "^0.3.13",
|
||||
"@nuxt/ui-templates": "^1.3.4",
|
||||
"@vintl-dev/eslint-config-peony": "workspace:^",
|
||||
"@vintl/nuxt": "workspace:^",
|
||||
"eslint": "^8.42.0",
|
||||
"nuxi": "^3.5.3",
|
||||
"nuxt": "^3.5.3",
|
||||
"theme-colors": "^0.0.5",
|
||||
"vue": "^3.3.4"
|
||||
"eslint": "^9.6.0",
|
||||
"nuxi": "^3.12.0",
|
||||
"nuxt": "^3.12.2",
|
||||
"theme-colors": "^0.1.0",
|
||||
"vue": "^3.4.31"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
if (import.meta.client && typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'vintl', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
value: nuxtApp.$i18n,
|
||||
})
|
||||
console.log('~'.repeat(80))
|
||||
console.log('%cHey there! 👋', 'font-size: 1.2rem')
|
||||
console.log(
|
||||
'VIntl controller can be used here through a global `vintl` variable.',
|
||||
)
|
||||
console.log(
|
||||
'Have playing with it! And if you break anything, just refresh the page :P',
|
||||
)
|
||||
if (!import.meta.dev) {
|
||||
console.log(
|
||||
'⚠️ This is a production environment. Message parsing is unavailable.',
|
||||
)
|
||||
}
|
||||
console.log('~'.repeat(80))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<users>
|
||||
<user>9491DAF6E17F64A987F923EC70F0BAEE</user>
|
||||
</users>
|
||||
@@ -0,0 +1,5 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
import peonyOverrides from '@vintl-dev/eslint-config-peony/overrides'
|
||||
|
||||
export default withNuxt().append(peonyOverrides)
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"broken-message": {
|
||||
"message": "This is {broken, }"
|
||||
},
|
||||
"example": {
|
||||
"message": "Example"
|
||||
},
|
||||
@@ -8,6 +11,9 @@
|
||||
"index.other-page": {
|
||||
"message": "To the other page"
|
||||
},
|
||||
"index.use-automatic": {
|
||||
"message": "Use automatic"
|
||||
},
|
||||
"language": {
|
||||
"message": "Language"
|
||||
},
|
||||
@@ -17,6 +23,9 @@
|
||||
"other-page.back": {
|
||||
"message": "Go back"
|
||||
},
|
||||
"other-page.description": {
|
||||
"message": "You must've clicked me a thousand times"
|
||||
},
|
||||
"other-page.title": {
|
||||
"message": "Hello from the other page!"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@vintl/nuxt'],
|
||||
modules: ['@vintl/nuxt', '@nuxt/eslint'],
|
||||
vintl: {
|
||||
defaultLocale: 'en-US',
|
||||
locales: [
|
||||
@@ -16,6 +16,11 @@ export default defineNuxtConfig({
|
||||
format: 'crowdin',
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
static: {
|
||||
iso: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'uk',
|
||||
@@ -30,5 +35,23 @@ export default defineNuxtConfig({
|
||||
storage: 'cookie',
|
||||
broadcastLocaleChange: true,
|
||||
parserless: 'always',
|
||||
seo: {
|
||||
xDefaultHreflang: false,
|
||||
defaultLocaleHasParameter: false,
|
||||
},
|
||||
onParseError: 'log-and-skip',
|
||||
},
|
||||
typescript: {
|
||||
tsConfig: {
|
||||
compilerOptions: {
|
||||
moduleResolution: 'bundler',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
src: './plugins/test.ts',
|
||||
order: -100,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -10,15 +10,16 @@
|
||||
"generate": "nuxi generate",
|
||||
"preview": "nuxi preview",
|
||||
"lint": "eslint --cache .",
|
||||
"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.json --format simple",
|
||||
"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.json --format crowdin",
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^0.3.13",
|
||||
"@vintl-dev/eslint-config-peony": "workspace:^",
|
||||
"@vintl-dev/pg-messages": "workspace:^",
|
||||
"@vintl/nuxt": "workspace:^",
|
||||
"eslint": "^8.42.0",
|
||||
"nuxi": "^3.5.3",
|
||||
"nuxt": "^3.5.3"
|
||||
"eslint": "^9.6.0",
|
||||
"nuxi": "^3.12.0",
|
||||
"nuxt": "^3.12.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,47 +23,83 @@ const messages = defineMessages({
|
||||
id: 'index.other-page',
|
||||
defaultMessage: 'To the other page',
|
||||
},
|
||||
useAutomatic: {
|
||||
id: 'index.use-automatic',
|
||||
defaultMessage: 'Use automatic',
|
||||
},
|
||||
brokenMessage: {
|
||||
id: 'broken-message',
|
||||
defaultMessage: 'This is {broken, }',
|
||||
},
|
||||
} as const)
|
||||
|
||||
const vintl = useVIntl()
|
||||
const { formatMessage } = vintl
|
||||
|
||||
const changing = ref(false)
|
||||
|
||||
async function changeLocale(locale: string) {
|
||||
if (changing.value) return
|
||||
changing.value = true
|
||||
try {
|
||||
await vintl.changeLocale(locale)
|
||||
} catch (err) {
|
||||
console.error('Error changing locale', err)
|
||||
} finally {
|
||||
changing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
const currentLocale = computed({
|
||||
get() {
|
||||
return vintl.locale
|
||||
},
|
||||
set() {
|
||||
// ignore
|
||||
set(value) {
|
||||
changeLocale(value).then(noop, noop)
|
||||
},
|
||||
})
|
||||
|
||||
async function onLocaleChange(e: Event) {
|
||||
await vintl.changeLocale(
|
||||
(e as Event & { target: HTMLSelectElement }).target.value,
|
||||
)
|
||||
}
|
||||
const useAutomatic = computed({
|
||||
get() {
|
||||
return vintl.automatic
|
||||
},
|
||||
set(value) {
|
||||
changeLocale(value ? 'auto' : vintl.locale).then(noop, noop)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<h3>{{ formatMessage(messages.options) }}</h3>
|
||||
<label for="language-select">
|
||||
{{ formatMessage(messages.language) }}
|
||||
</label>
|
||||
<select
|
||||
id="language-select"
|
||||
v-model="currentLocale"
|
||||
@change="onLocaleChange"
|
||||
>
|
||||
<option
|
||||
v-for="locale in [...$nuxt.$i18n.$locales.value.keys()]"
|
||||
:key="locale.tag"
|
||||
:value="locale.tag"
|
||||
>
|
||||
{{ locale.tag }}
|
||||
</option>
|
||||
</select>
|
||||
<div>
|
||||
<label>
|
||||
<input v-model="useAutomatic" type="checkbox" />
|
||||
{{ formatMessage(messages.useAutomatic) }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{ formatMessage(messages.language) }}
|
||||
<select
|
||||
id="language-select"
|
||||
v-model="currentLocale"
|
||||
:disabled="vintl.automatic"
|
||||
>
|
||||
<option
|
||||
v-for="locale in [...$i18n.$locales.value.keys()]"
|
||||
:key="locale.tag"
|
||||
:value="locale.tag"
|
||||
>
|
||||
{{ locale.tag }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('i18n:extendLocale', ({ event }) => {
|
||||
console.log('[vintl] loading locale', event.locale.tag)
|
||||
})
|
||||
|
||||
nuxtApp.hook('i18n:ready', () => {
|
||||
console.log('[vintl] ready')
|
||||
})
|
||||
})
|
||||
+3
-3
@@ -7,8 +7,8 @@
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.1",
|
||||
"turbo": "^1.10.1"
|
||||
"@changesets/cli": "^2.27.6",
|
||||
"turbo": "^2.0.6"
|
||||
},
|
||||
"scripts": {
|
||||
"pg:prepare": "turbo run --no-daemon @vintl-dev/pg#dev:prepare",
|
||||
@@ -36,5 +36,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@8.6.1"
|
||||
"packageManager": "pnpm@9.4.0"
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"extends": ["./nuxt-eslint-config.cjs"],
|
||||
"rules": {
|
||||
"space-before-function-paren": "off",
|
||||
"comma-dangle": "off",
|
||||
"arrow-parens": ["error", "always"],
|
||||
"curly": ["error", "multi-line"],
|
||||
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
"error",
|
||||
{ "functions": false, "classes": false, "variables": false }
|
||||
],
|
||||
"no-redeclare": "off",
|
||||
"@typescript-eslint/no-redeclare": [
|
||||
"error",
|
||||
{ "builtinGlobals": false, "ignoreDeclarationMerge": true }
|
||||
],
|
||||
"no-void": ["error", { "allowAsStatement": true }],
|
||||
"require-await": "off",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ "fixStyle": "inline-type-imports" }
|
||||
],
|
||||
"@typescript-eslint/no-extra-semi": "off",
|
||||
"indent": "off",
|
||||
"no-useless-constructor": "off",
|
||||
"@typescript-eslint/no-useless-constructor": "warn",
|
||||
"brace-style": "off",
|
||||
"vue/singleline-html-element-content-newline": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"extraFileExtensions": [".vue"]
|
||||
},
|
||||
"ignorePatterns": ["node_modules", ".turbo"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { FlatConfigComposer } from 'eslint-flat-config-utils'
|
||||
|
||||
declare const config: FlatConfigComposer
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
||||
import overrides from './overrides.mjs'
|
||||
|
||||
export default createConfigForNuxt().append(overrides)
|
||||
@@ -1,84 +0,0 @@
|
||||
// MIT License
|
||||
// Copyright (c) 2019-2022 Nuxt Project
|
||||
// https://github.com/nuxt/eslint-config/blob/main/LICENSE
|
||||
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
/** @type {import('eslint').ESLint.ConfigData} */
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
parser: {
|
||||
js: 'espree',
|
||||
jsx: 'espree',
|
||||
|
||||
ts: '@typescript-eslint/parser',
|
||||
tsx: '@typescript-eslint/parser',
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
env: { node: true },
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.mts', '*.cts', '*.vue'],
|
||||
rules: {
|
||||
// The core 'no-unused-vars' rules (in the eslint:recommended ruleset)
|
||||
// does not work with type definitions.
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Include typescript eslint rules in *.vue files
|
||||
// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/eslint-recommended.ts
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'constructor-super': 'off', // ts(2335) & ts(2377)
|
||||
'getter-return': 'off', // ts(2378)
|
||||
'no-const-assign': 'off', // ts(2588)
|
||||
'no-dupe-args': 'off', // ts(2300)
|
||||
'no-dupe-class-members': 'off', // ts(2393) & ts(2300)
|
||||
'no-dupe-keys': 'off', // ts(1117)
|
||||
'no-func-assign': 'off', // ts(2539)
|
||||
'no-import-assign': 'off', // ts(2539) & ts(2540)
|
||||
'no-new-symbol': 'off', // ts(7009)
|
||||
'no-obj-calls': 'off', // ts(2349)
|
||||
'no-redeclare': 'off', // ts(2451)
|
||||
'no-setter-return': 'off', // ts(2408)
|
||||
'no-this-before-super': 'off', // ts(2376)
|
||||
'no-undef': 'off', // ts(2304)
|
||||
'no-unreachable': 'off', // ts(7027)
|
||||
'no-unsafe-negation': 'off', // ts(2365) & ts(2360) & ts(2358)
|
||||
'no-var': 'error', // ts transpiles let/const to var, so no need for vars any more
|
||||
'prefer-const': 'error', // ts provides better types with const
|
||||
'prefer-rest-params': 'error', // ts provides better types with rest args over arguments
|
||||
'prefer-spread': 'error', // ts transpiles spread to apply, so no need for manual apply
|
||||
'valid-typeof': 'off', // ts(2367)
|
||||
},
|
||||
},
|
||||
{
|
||||
// These pages are not used directly by users so they can have one-word names.
|
||||
files: [
|
||||
'**/pages/**/*.{js,ts,vue}',
|
||||
'**/layouts/**/*.{js,ts,vue}',
|
||||
'**/app.{js,ts,vue}',
|
||||
'**/error.{js,ts,vue}',
|
||||
],
|
||||
rules: { 'vue/multi-word-component-names': 'off' },
|
||||
},
|
||||
{
|
||||
// Pages and layouts are required to have a single root element if transitions are enabled.
|
||||
files: ['**/pages/**/*.{js,ts,vue}', '**/layouts/**/*.{js,ts,vue}'],
|
||||
rules: { 'vue/no-multiple-template-root': 'error' },
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Linter } from 'eslint'
|
||||
|
||||
declare const overrides: Linter.FlatConfig<Linter.RulesRecord>
|
||||
|
||||
export default overrides
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineFlatConfig } from 'eslint-flat-config-utils'
|
||||
|
||||
export default defineFlatConfig({
|
||||
name: 'vintl/peony',
|
||||
rules: {
|
||||
// Ma'am, this is SFCs (also conflicts with Prettier)
|
||||
'vue/html-self-closing': 'off',
|
||||
// Gladly we know how to use dynamic delete, tyvm
|
||||
'@typescript-eslint/no-dynamic-delete': 'off',
|
||||
},
|
||||
})
|
||||
@@ -18,22 +18,21 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./config.json"
|
||||
"import": "./eslint.config.mjs"
|
||||
},
|
||||
"./overrides": {
|
||||
"import": "./overrides.mjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"config.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
||||
"@typescript-eslint/parser": "^5.59.9",
|
||||
"eslint-plugin-vue": "^9.14.1",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.40.0",
|
||||
"eslint": "^8.42.0"
|
||||
"eslint": "^9.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/eslint-config": "^0.3.13",
|
||||
"eslint-flat-config-utils": "^0.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,135 @@
|
||||
# @vintl/nuxt
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- efdcdb9: Replaced Zod validation with a much simpler normalization and assertion 🚀
|
||||
|
||||
This brings a huge TypeScript performance boost for both maintainers and users of the module.
|
||||
|
||||
Developers using the module will also be happy to discover that each option is now documented (they always were, but documentation was erased from Zod inferred types by TypeScript) and each type used in options can be exported if you need to programatically extend the options.
|
||||
|
||||
Better yet? Common mistakes are still checked and pretty printed!
|
||||
|
||||
See the full explanation for decision to switch from Zod in the commit message.
|
||||
|
||||
🐞 If you stumble upon a bug caused by this switch, please [report them on issue tracker](https://github.com/vintl-dev/nuxt/issues).
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9e42c63: Fixed a typo in message about message that cannot be parsed
|
||||
|
||||
## 1.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 114d04a: Switched to a slightly more compatible `#app` augmentation
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9d939c1: Switched to using module ID markers
|
||||
|
||||
Previously VIntl for Nuxt kept track of files that were used as locale messages and matched module IDs based on them. This, however, was very unreliable in certain environments.
|
||||
|
||||
With this change VIntl for Nuxt now uses ‘markers’, URL parameters added to the end of imported module IDs. This allows it to very quickly test whether a specific module is imported through a message file and should be processed by the VIntl unplugin.
|
||||
|
||||
## 1.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6479891: VIntl updated to the latest version
|
||||
|
||||
Latest version of VIntl fixes missing type declarations for global properties, like `$t`, `$fmt` and `$i18n`.
|
||||
|
||||
We might add automatic handling for disabling of `controllerOpts.globalMixin` in the future releases. You can follow the status update in [#74](https://github.com/vintl-dev/nuxt/issues/74).
|
||||
|
||||
For now, if you disable global mixin, use the below augmentation to disable these unusable properties:
|
||||
|
||||
```ts
|
||||
declare global {
|
||||
namespace VueIntlController {
|
||||
interface Options {
|
||||
globalMixin: false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a9fdc59: Add support for parsing the messages in the error handlers
|
||||
|
||||
With [update to `@vintl/unplugin`](https://github.com/vintl-dev/unplugin/releases/tag/v1.5.0), it's now possible to use `parseMessage` from the error handler's context to parse custom messages or re-parse the current message with any modified options.
|
||||
|
||||
## 1.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 81c23b0: Support for parsing error handling
|
||||
|
||||
`@vintl/nuxt` can now handle errors when parsing message files containing broken messages in a configurable manner.
|
||||
|
||||
You can configure how errors are handled using `onParseError` module option, which can either be a name of the built-in strategy, or a function that takes in context and optionally provides fallback message AST.
|
||||
|
||||
Check out documentation for more information.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2b5a59b: Unpin FormatJS dependencies
|
||||
|
||||
We proveviously had `@formatjs/intl` pinned because of the broken ESM migration, however this has been fixed for quite some time already, so the pinned version has been to changed to a range, which ensures that `@formatjs/intl` can be updated with latest bug fixes and improvements until the next major version.
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 979f2bd: Add more SEO options
|
||||
|
||||
Two options have been added to the SEO configuration:
|
||||
|
||||
- `defaultLocaleHasParameter` allows to disable the host language parameter for the default locale.
|
||||
|
||||
This is not recommended and not correct, because lack of the host language parameter instructs VIntl to use user's language (either the one they've used before or browser's one). However, it may be useful in some cases.
|
||||
|
||||
- `xDefaultHreflang` allows to disable the `x-default` hreflang in case it's not useful to have (e.g. you disable the automatic language detection), although this is quite rare.
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e1c501c: Improve alternate links generation
|
||||
|
||||
Generation of `alternate` `hreflang` links has been improved to properly use absolute URLs.
|
||||
|
||||
Since there's no good way to know the URL of the site, there's now also `seo.baseURL` option which allows to set site's proper domain.
|
||||
|
||||
`hostLanguageParam` option has been deperecated in favour of `seo.hostLanguageParameter`, which functions almost the same way, except it does not accept `null`. If you want to disable generation of such tags or handle them yourself, you can set `seo.enabled` to `false`.
|
||||
|
||||
See [Configuration](https://vintl-nuxt.netlify.app/introduction/configuration#seooptions) documentation page for more details.
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 951f363: Add graceful fallback for unknown locales in storage
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae71089: Use automatic locale if none returned by the storage
|
||||
|
||||
Fixed a bug where automatic locale would not be restored from the storage properly on the next page reload, despite it being saved properly.
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- db449d4: Fix missing options in the newer Nuxt versions
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -12,6 +12,14 @@ export default defineBuildConfig({
|
||||
entry.ext = 'js'
|
||||
}
|
||||
}
|
||||
|
||||
ctx.options.entries.push({
|
||||
builder: 'rollup',
|
||||
name: 'options',
|
||||
input: './src/options/index.ts',
|
||||
declaration: 'compatible',
|
||||
outDir: './dist',
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @ts-check
|
||||
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import config from '@vintl-dev/eslint-config-peony'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
export default config.append([
|
||||
{
|
||||
name: 'vintl/nuxt/module',
|
||||
files: ['./src/**/*.ts', './types/*.d.ts'],
|
||||
ignores: ['./src/runtime/**/*.ts', './dist'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: resolve(__dirname, 'tsconfig.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vintl/nuxt/build',
|
||||
files: ['./build.config.ts'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: resolve(__dirname, 'tsconfig.build.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vintl/nuxt/runtime',
|
||||
files: [
|
||||
'./stubs/*.{d.ts,js}',
|
||||
'./src/runtime/**/*.{ts,mts}',
|
||||
'./types/*.d.ts',
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: resolve(__dirname, 'tsconfig.runtime.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vintl/nuxt",
|
||||
"version": "1.2.2",
|
||||
"version": "1.9.0",
|
||||
"description": "Nuxt Module for VIntl integration",
|
||||
"keywords": [
|
||||
"i18n",
|
||||
@@ -26,6 +26,10 @@
|
||||
"import": "./dist/module.mjs",
|
||||
"require": "./dist/module.cjs"
|
||||
},
|
||||
"./options": {
|
||||
"types": "./dist/options.d.mts",
|
||||
"import": "./dist/options.mjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"module": "./dist/module.mjs",
|
||||
@@ -34,33 +38,35 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "nuxt-module-build",
|
||||
"build": "nuxt-module-build build",
|
||||
"lint": "eslint --cache .",
|
||||
"prepack": "pnpm run -s lint && pnpm run -s build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/module-builder": "^0.4.0",
|
||||
"@nuxt/schema": "^3.5.3",
|
||||
"@types/hash-sum": "^1.0.0",
|
||||
"@types/node": "^18.16.16",
|
||||
"@unhead/vue": "^1.1.27",
|
||||
"@nuxt/module-builder": "^0.8.1",
|
||||
"@nuxt/schema": "^3.12.2",
|
||||
"@types/hash-sum": "^1.0.2",
|
||||
"@types/node": "^18.19.39",
|
||||
"@unhead/vue": "^1.9.14",
|
||||
"@vintl-dev/eslint-config-peony": "workspace:^",
|
||||
"hookable": "^5.5.3",
|
||||
"prettier-plugin-jsdoc": "^0.4.2",
|
||||
"typescript": "^5.1.3"
|
||||
"prettier-plugin-jsdoc": "^1.3.0",
|
||||
"typescript": "^5.5.2",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl": "2.7.2",
|
||||
"@nuxt/kit": "^3.5.3",
|
||||
"@vintl/unplugin": "^1.2.4",
|
||||
"@vintl/vintl": "^4.2.1",
|
||||
"@formatjs/intl": "^2.10.4",
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"@nuxt/kit": "^3.12.2",
|
||||
"@vintl/unplugin": "^2.0.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"astring": "^1.8.6",
|
||||
"consola": "^3.1.0",
|
||||
"consola": "^3.2.3",
|
||||
"hash-sum": "^2.0.0",
|
||||
"import-meta-resolve": "^3.0.0",
|
||||
"pathe": "^1.1.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"pathe": "^1.1.2",
|
||||
"picocolors": "^1.0.1",
|
||||
"slash": "^5.1.0",
|
||||
"zod": "^3.21.4"
|
||||
"ufo": "^1.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,25 @@ import {
|
||||
import { generate as generateOptions } from './options-gen.js'
|
||||
import { createDirResolver } from './utils/resolvers.js'
|
||||
import { purgeEntryResources } from './manifest-clean.js'
|
||||
import { type z as t, ZodError } from 'zod'
|
||||
import { moduleOptionsSchema } from './schemas/index.js'
|
||||
import { PluginOptionsBank } from './parser-bank.js'
|
||||
import { formatZodError } from './utils/zod-error.js'
|
||||
import { OptionsError } from './errors.js'
|
||||
import { consola } from 'consola'
|
||||
import {
|
||||
normalizeModuleOptions,
|
||||
assertModuleOptionsValid,
|
||||
type ModuleOptions,
|
||||
} from './options/index.ts'
|
||||
import { getErrorSummary } from './utils/error.ts'
|
||||
import { OptionsError } from './errors.ts'
|
||||
import { indent } from './utils/strings.ts'
|
||||
import fmt from 'picocolors'
|
||||
import type { ResolvedNuxtTemplate } from '@nuxt/schema'
|
||||
|
||||
/** Path to the options file relative to `buildDir`. */
|
||||
const optionsFilePath = 'i18n/options.mjs'
|
||||
|
||||
type _InputModuleOptions = typeof moduleOptionsSchema
|
||||
export type { ModuleOptions }
|
||||
|
||||
export interface InputModuleOptions extends t.input<_InputModuleOptions> {}
|
||||
|
||||
export default defineNuxtModule<InputModuleOptions>({
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
meta: {
|
||||
name: '@vintl/nuxt',
|
||||
configKey: 'vintl',
|
||||
@@ -51,25 +55,23 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
addPlugin(resolveInRuntime('./plugin.js').path)
|
||||
|
||||
if (!nuxt.options._prepare) {
|
||||
let options: t.output<typeof moduleOptionsSchema>
|
||||
const options = normalizeModuleOptions(inputOptions)
|
||||
|
||||
try {
|
||||
options = moduleOptionsSchema.parse(inputOptions)
|
||||
assertModuleOptionsValid(options)
|
||||
} catch (err) {
|
||||
if (err instanceof ZodError) {
|
||||
consola.error(
|
||||
formatZodError(err, {
|
||||
initialMessage: 'Invalid Nuxt VIntl configuration',
|
||||
rootPropertyName: 'vintl',
|
||||
}),
|
||||
)
|
||||
let msg = fmt.red(
|
||||
'Your VIntl for Nuxt module options have failed correctness assertion',
|
||||
)
|
||||
|
||||
throw new OptionsError(
|
||||
'You have errors in your Nuxt VIntl configuration, check console log for more information',
|
||||
)
|
||||
}
|
||||
msg += `\n${indent(getErrorSummary(err), 2)}`
|
||||
|
||||
throw err
|
||||
consola.error(msg.trim())
|
||||
|
||||
throw new OptionsError(
|
||||
'You have errors in your Nuxt VIntl configuration, check console log for more information',
|
||||
{ cause: err },
|
||||
)
|
||||
}
|
||||
|
||||
const parserlessModeEnabled = (() => {
|
||||
@@ -83,7 +85,7 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
|
||||
const pluginOptionsBank = new PluginOptionsBank()
|
||||
|
||||
const optionsFile = addTemplate({
|
||||
const optionsFile: ResolvedNuxtTemplate = addTemplate({
|
||||
filename: optionsFilePath,
|
||||
write: true,
|
||||
getContents() {
|
||||
@@ -96,7 +98,7 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
return resolveInResDir(specifier).relativeTo(optionsFile.dst)
|
||||
},
|
||||
registerMessagesFile(file, importPath) {
|
||||
pluginOptionsBank.registerFile(
|
||||
return pluginOptionsBank.registerFile(
|
||||
file,
|
||||
resolvePath(dirname(optionsFile.dst), importPath),
|
||||
)
|
||||
@@ -124,11 +126,31 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
|
||||
const { icuMessages } = await import('@vintl/unplugin')
|
||||
|
||||
const onParseError = (() => {
|
||||
let { onParseError } = options
|
||||
|
||||
if (onParseError === 'log-and-skip') {
|
||||
onParseError = (ctx) => {
|
||||
consola.warn(
|
||||
`[vintl] Cannot parse the message "${
|
||||
ctx.messageId
|
||||
}" in "${relativizePath(
|
||||
nuxt.options.srcDir,
|
||||
ctx.moduleId,
|
||||
)}". It will be skipped.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return onParseError
|
||||
})()
|
||||
|
||||
extendWebpackConfig((cfg) => {
|
||||
const plugins = (cfg.plugins ??= [])
|
||||
plugins.push(
|
||||
...pluginOptionsBank
|
||||
.createOptions({
|
||||
onParseError,
|
||||
output: {
|
||||
type: parserlessModeEnabled ? 'ast' : 'raw',
|
||||
},
|
||||
@@ -143,6 +165,7 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
...pluginOptionsBank
|
||||
.createOptions({
|
||||
pluginsWrapping: true,
|
||||
onParseError,
|
||||
output: {
|
||||
type: parserlessModeEnabled ? 'ast' : 'raw',
|
||||
},
|
||||
@@ -199,7 +222,5 @@ export default defineNuxtModule<InputModuleOptions>({
|
||||
})
|
||||
|
||||
export interface ModuleHooks {
|
||||
'vintl:extendOptions'(
|
||||
options: t.input<typeof moduleOptionsSchema>,
|
||||
): Promise<void> | void
|
||||
'vintl:extendOptions'(options: ModuleOptions): Promise<void> | void
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import hash from 'hash-sum'
|
||||
import { generate as fromAST } from 'astring'
|
||||
import type { z as t } from 'zod'
|
||||
import type {
|
||||
messagesImportSourceSchema,
|
||||
moduleOptionsSchema,
|
||||
} from './schemas/index.js'
|
||||
NormalizedMessagesImportSource,
|
||||
NormalizedModuleOptions,
|
||||
} from './options/index.ts'
|
||||
|
||||
// You probably opened this file and shocked to find out the whole AST tree
|
||||
// generation here. Yeah, I guess this is a bit too much, but this actually
|
||||
@@ -155,7 +154,10 @@ class VariableDeclaration implements Node {
|
||||
class ExportSpecifier implements Node {
|
||||
public readonly type = 'ExportSpecifier'
|
||||
|
||||
constructor(public local: Identifier, public exported: Identifier = local) {}
|
||||
constructor(
|
||||
public local: Identifier,
|
||||
public exported: Identifier = local,
|
||||
) {}
|
||||
}
|
||||
|
||||
class ExportNamedDeclaration {
|
||||
@@ -623,11 +625,12 @@ interface GeneratorContext {
|
||||
*
|
||||
* @param file Input message file options.
|
||||
* @param importPath Resolved path.
|
||||
* @returns Marked import path.
|
||||
*/
|
||||
registerMessagesFile(
|
||||
file: t.output<typeof messagesImportSourceSchema>,
|
||||
file: NormalizedMessagesImportSource,
|
||||
importPath: string,
|
||||
): void
|
||||
): string
|
||||
|
||||
/**
|
||||
* Resolves a module from the runtime directory.
|
||||
@@ -650,7 +653,7 @@ interface GeneratorContext {
|
||||
* to a special file and read at runtime to retrieve the options.
|
||||
*/
|
||||
export function generate(
|
||||
opts: t.output<typeof moduleOptionsSchema>,
|
||||
opts: NormalizedModuleOptions,
|
||||
{
|
||||
resolve,
|
||||
resolveRuntimeModule,
|
||||
@@ -739,18 +742,6 @@ export function generate(
|
||||
]),
|
||||
)
|
||||
|
||||
const files: t.output<typeof messagesImportSourceSchema>[] = []
|
||||
|
||||
if (locale.file != null) {
|
||||
files.push(locale.file)
|
||||
}
|
||||
|
||||
if (locale.files != null) {
|
||||
for (const file of locale.files) {
|
||||
files.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
// if (isDefaultLocale) {
|
||||
// // import locale<hash> from "<locale path>"
|
||||
// imports.push(
|
||||
@@ -778,10 +769,12 @@ export function generate(
|
||||
// )
|
||||
// }
|
||||
|
||||
for (const messageFile of files) {
|
||||
for (const messageFile of locale.files) {
|
||||
const { from: importPath, name: importKey } = messageFile
|
||||
const resolvedPath = resolve(messageFile.from)
|
||||
registerMessagesFile(messageFile, resolvedPath)
|
||||
const resolvedPath = registerMessagesFile(
|
||||
messageFile,
|
||||
resolve(messageFile.from),
|
||||
)
|
||||
|
||||
if (isDefaultLocale) {
|
||||
// if import key is 'default' then:
|
||||
@@ -1020,8 +1013,8 @@ export function generate(
|
||||
new ExportNamedDeclaration().setDeclaration(
|
||||
new VariableDeclaration('const', [
|
||||
new VariableDeclarator(
|
||||
new Identifier('hostLanguageParam'),
|
||||
new Literal(opts.hostLanguageParam),
|
||||
new Identifier('seo'),
|
||||
new Literal(null).setRaw(JSON.stringify(opts.seo)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Describes an import in a form of a tuple containing module ID and optionally
|
||||
* the export name.
|
||||
*
|
||||
* The optional export name defaults to `'default'`.
|
||||
*
|
||||
* @example
|
||||
* // import { en_us } from '@acme/site-translations'
|
||||
* const americanEnglish: ImportSourceTuple = [
|
||||
* '@acme/site-translations',
|
||||
* 'en_us',
|
||||
* ]
|
||||
*/
|
||||
export type ImportSourceTuple = readonly [moduleId: string, name?: string]
|
||||
|
||||
export function isImportSourceTuple(
|
||||
input: unknown,
|
||||
): input is ImportSourceTuple {
|
||||
return (
|
||||
Array.isArray(input) &&
|
||||
typeof input[0] === 'string' &&
|
||||
(input.length === 1 || (input.length === 2 && typeof input[1] === 'string'))
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeImportSourceTuple(input: ImportSourceTuple) {
|
||||
const [moduleId, name = 'default'] = input
|
||||
return [moduleId, name] as const
|
||||
}
|
||||
|
||||
export type NormalizedImportSourceTuple = ReturnType<
|
||||
typeof normalizeImportSourceTuple
|
||||
>
|
||||
|
||||
/**
|
||||
* Describes an import in a form of an object containing module ID, and
|
||||
* optionally the export name and parameter that sets whether or not the
|
||||
* consumer of the import should try to resolve the module ID to a path or use
|
||||
* it as is.
|
||||
*
|
||||
* @example
|
||||
* // import { en_us } from '@acme/site-translations'
|
||||
* const americanEnglish: ImportSourceObject = {
|
||||
* from: '@acme/site-translations',
|
||||
* name: 'en_us',
|
||||
* resolve: false,
|
||||
* }
|
||||
*/
|
||||
export type ImportSourceObject = Readonly<{
|
||||
/** Module ID to import from. */
|
||||
from: string
|
||||
|
||||
/**
|
||||
* Name of the export to import.
|
||||
*
|
||||
* @default 'default'
|
||||
*/
|
||||
name?: string
|
||||
|
||||
/**
|
||||
* Whether to resolve the module ID to a path.
|
||||
*
|
||||
* You most likely want to avoid resolving any IDs coming from modules.
|
||||
*
|
||||
* @default true // '@acme/site-translations' => '../../node_modules/@acme/sit...'
|
||||
*/
|
||||
resolve?: boolean
|
||||
}>
|
||||
|
||||
export function isImportSourceObject(
|
||||
input: unknown,
|
||||
): input is ImportSourceObject {
|
||||
return (
|
||||
typeof input === 'object' &&
|
||||
input !== null &&
|
||||
'from' in input &&
|
||||
typeof input.from === 'string' &&
|
||||
(!('name' in input) ||
|
||||
input.name == null ||
|
||||
typeof input.name === 'string') &&
|
||||
(!('resolve' in input) ||
|
||||
input.resolve == null ||
|
||||
typeof input.resolve === 'boolean')
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeImportSourceObject(input: ImportSourceObject) {
|
||||
const { from, name, resolve } = input
|
||||
return {
|
||||
from,
|
||||
name: name ?? 'default',
|
||||
resolve: resolve ?? true,
|
||||
} as const
|
||||
}
|
||||
|
||||
export type NormalizedImportSourceObject = ReturnType<
|
||||
typeof normalizeImportSourceObject
|
||||
>
|
||||
|
||||
/**
|
||||
* Describes an import in a form of a string containing module ID.
|
||||
*
|
||||
* @example
|
||||
* // import mod from '@acme/site-translations/en-US'
|
||||
* const americanEnglish: ImportSourceString =
|
||||
* '@acme/site-translations/en-US'
|
||||
*/
|
||||
export type ImportSourceString = string
|
||||
|
||||
/**
|
||||
* Describes an import in either a string, a tuple or an object form.
|
||||
*
|
||||
* See {@link ImportSourceString}, {@link ImportSourceTuple} and
|
||||
* {@link ImportSourceObject} for expected inputs for each form.
|
||||
*/
|
||||
export type ImportSource =
|
||||
| ImportSourceString
|
||||
| ImportSourceTuple
|
||||
| ImportSourceObject
|
||||
|
||||
export function normalizeImportSource(input: ImportSource) {
|
||||
if (typeof input === 'string') {
|
||||
return normalizeImportSourceObject({ from: input })
|
||||
}
|
||||
|
||||
if (isImportSourceTuple(input)) {
|
||||
const [from, name] = normalizeImportSourceTuple(input)
|
||||
return normalizeImportSourceObject({ from, name })
|
||||
}
|
||||
|
||||
if (isImportSourceObject(input)) {
|
||||
return normalizeImportSourceObject(input)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be an import source',
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an unspecified import in a form of an object.
|
||||
*
|
||||
* @example
|
||||
* // import '@acme/intl-extension/locale-data/en-US'
|
||||
* const americanEnglishData: UnspecifiedImportSourceObject = {
|
||||
* from: '@acme/intl-extension/locale-data/en-US',
|
||||
* resolve: false,
|
||||
* }
|
||||
*/
|
||||
export type UnspecifiedImportSourceObject = Omit<ImportSourceObject, 'name'>
|
||||
|
||||
export function isUnspecifiedImportSourceObject(
|
||||
input: unknown,
|
||||
): input is UnspecifiedImportSourceObject {
|
||||
return (
|
||||
input != null &&
|
||||
typeof input === 'object' &&
|
||||
'from' in input &&
|
||||
typeof input.from === 'string' &&
|
||||
(!('resolve' in input) ||
|
||||
input.resolve == null ||
|
||||
typeof input.resolve === 'boolean')
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeUnspecifiedImportSourceObject(
|
||||
input: UnspecifiedImportSourceObject,
|
||||
) {
|
||||
const { from, resolve } = input
|
||||
return { from, resolve: resolve ?? true } as const
|
||||
}
|
||||
|
||||
export type NormalizedUnspecifiedImportSourceObject = ReturnType<
|
||||
typeof normalizeUnspecifiedImportSourceObject
|
||||
>
|
||||
|
||||
export type UnspecifiedImportSource = ImportSourceString | ImportSourceObject
|
||||
|
||||
export function normalizeUnspecifiedImportSource(
|
||||
input: UnspecifiedImportSource,
|
||||
) {
|
||||
if (typeof input === 'string') {
|
||||
return normalizeUnspecifiedImportSourceObject({ from: input })
|
||||
}
|
||||
|
||||
if (isUnspecifiedImportSourceObject(input)) {
|
||||
return normalizeUnspecifiedImportSourceObject(input)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be an unspecified import source',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './imports.ts'
|
||||
export * from './language-tag.ts'
|
||||
export * from './locale-descriptor.ts'
|
||||
export * from './message-imports.ts'
|
||||
export * from './module-options.ts'
|
||||
export * from './parserless.ts'
|
||||
export * from './seo.ts'
|
||||
@@ -0,0 +1,19 @@
|
||||
declare const languageTagMarker: unique symbol
|
||||
|
||||
export type LanguageTagMarker = typeof languageTagMarker
|
||||
|
||||
export type LanguageTag = string & { [K in LanguageTagMarker]: true }
|
||||
|
||||
export function assertLanguageTagValid(
|
||||
input: unknown,
|
||||
): asserts input is LanguageTag {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error('Language tag must be a string')
|
||||
}
|
||||
|
||||
try {
|
||||
new Intl.Locale(input)
|
||||
} catch {
|
||||
throw new Error('Language tag must be a valid BCP 47 language tag')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
normalizeUnspecifiedImportSource,
|
||||
type ImportSource,
|
||||
type NormalizedUnspecifiedImportSourceObject,
|
||||
type UnspecifiedImportSource,
|
||||
type NormalizedImportSourceObject,
|
||||
normalizeImportSource,
|
||||
} from './imports.ts'
|
||||
import { assertLanguageTagValid } from './language-tag.ts'
|
||||
import {
|
||||
normalizeMessagesImportSource,
|
||||
type MessagesImportSource,
|
||||
type NormalizedMessagesImportSource,
|
||||
} from './message-imports.ts'
|
||||
|
||||
type Literal = string | number | boolean | null | undefined
|
||||
|
||||
type JSONValue = Literal | { [key: string]: JSONValue } | JSONValue[]
|
||||
|
||||
/** Represents static locale metadata used by VIntl. */
|
||||
export type LocaleStaticMeta = {
|
||||
/**
|
||||
* Hreflang attribute value.
|
||||
*
|
||||
* If there's a case where BCP 47 language does not align with `hreflang`
|
||||
* specification, which requires an ISO 639-1 language code and optionally,
|
||||
* ISO 3166-1 code of the country preceded by a dash character, this property
|
||||
* can be used to override the value.
|
||||
*
|
||||
* @example
|
||||
* 'en-US'
|
||||
*/
|
||||
iso?: string
|
||||
}
|
||||
|
||||
/** Represents locale metadata. */
|
||||
export interface LocaleMeta {
|
||||
/** Static properties used by VIntl. */
|
||||
static?: LocaleStaticMeta
|
||||
[key: string]: JSONValue
|
||||
}
|
||||
|
||||
export interface LocaleDescriptor {
|
||||
/** BCP 47 language tag associated with the locale. */
|
||||
tag: string
|
||||
|
||||
/** An import source of the first messages file to load. */
|
||||
file?: MessagesImportSource
|
||||
|
||||
/** An array of import sources of the messages file to load. */
|
||||
files?: MessagesImportSource[]
|
||||
|
||||
/**
|
||||
* An array of import sources for side-effect only imports (like polyfill
|
||||
* data).
|
||||
*/
|
||||
additionalImports?: UnspecifiedImportSource[]
|
||||
|
||||
/**
|
||||
* A record of resource imports where keys are resource names and values are
|
||||
* import sources.
|
||||
*
|
||||
* Unlike additional imports, which are only used for side effects, these
|
||||
* imports will be mapped to the provided key at runtime.
|
||||
*
|
||||
* This is useful for larger language-associated documents, which would
|
||||
* otherwise create a frustrating experience for translators were these
|
||||
* documents left as regular strings in the messages file. However, this is
|
||||
* not limited to just documents, but anything that can be imported.
|
||||
*/
|
||||
resources?: Record<string, ImportSource>
|
||||
|
||||
/**
|
||||
* Custom metadata for the locale that is always accessible at runtime, even
|
||||
* if the locale is not loaded.
|
||||
*
|
||||
* Use it for data like the translations coverage percentage, or display
|
||||
* names. It must be JSON serialisable.
|
||||
*/
|
||||
meta?: LocaleMeta
|
||||
}
|
||||
|
||||
export function normalizeLocaleDescriptor(input: LocaleDescriptor) {
|
||||
const files: NormalizedMessagesImportSource[] = []
|
||||
|
||||
if (input.file != null) {
|
||||
files.push(normalizeMessagesImportSource(input.file))
|
||||
}
|
||||
|
||||
if (input.files != null) {
|
||||
for (const source of input.files) {
|
||||
files.push(normalizeMessagesImportSource(source))
|
||||
}
|
||||
}
|
||||
|
||||
const additionalImports: NormalizedUnspecifiedImportSourceObject[] = []
|
||||
|
||||
if (input.additionalImports != null) {
|
||||
for (const source of input.additionalImports) {
|
||||
additionalImports.push(normalizeUnspecifiedImportSource(source))
|
||||
}
|
||||
}
|
||||
|
||||
let resources: Record<string, NormalizedImportSourceObject> | undefined
|
||||
if (input.resources != null) {
|
||||
resources = Object.create(null)
|
||||
for (const [name, source] of Object.entries(input.resources)) {
|
||||
resources![name] = normalizeImportSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
const { tag, meta } = input
|
||||
|
||||
return {
|
||||
tag,
|
||||
files,
|
||||
additionalImports,
|
||||
resources,
|
||||
meta,
|
||||
} as const
|
||||
}
|
||||
|
||||
export type NormalizedLocaleDescriptor = ReturnType<
|
||||
typeof normalizeLocaleDescriptor
|
||||
>
|
||||
|
||||
export function assertLocaleDescriptorValid(input: NormalizedLocaleDescriptor) {
|
||||
const errors: unknown[] = []
|
||||
try {
|
||||
assertLanguageTagValid(input.tag)
|
||||
} catch (err) {
|
||||
errors.push(err)
|
||||
}
|
||||
|
||||
if (input.files.length === 0) {
|
||||
errors.push(new RangeError('Locale descriptor is missing any source files'))
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.stringify(input.meta)
|
||||
} catch (err) {
|
||||
errors.push(
|
||||
new RangeError(
|
||||
'Locale descriptor contains meta that cannot be serialized to JSON',
|
||||
{ cause: err },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (input?.meta?.static?.iso === 'x-default') {
|
||||
errors.push(new RangeError('Locale descriptor uses x-default as ISO code'))
|
||||
}
|
||||
|
||||
if (errors.length !== 0) {
|
||||
throw new AggregateError(
|
||||
errors,
|
||||
'Locale descriptor has failed one or more assertions',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
normalizeImportSourceObject,
|
||||
isImportSourceTuple,
|
||||
isImportSourceObject,
|
||||
type ImportSourceObject,
|
||||
type ImportSourceTuple,
|
||||
type ImportSourceString,
|
||||
} from './imports.ts'
|
||||
|
||||
/**
|
||||
* An optional name of the messages formatter.
|
||||
*
|
||||
* @example
|
||||
* const crowdinFormatter: MessagesImportFormatterName = 'crowdin'
|
||||
*
|
||||
* @default 'default'
|
||||
*/
|
||||
export type MessagesImportFormatterName = string | undefined | null
|
||||
|
||||
export function isMessagesImportFormatterName(
|
||||
input: unknown,
|
||||
): input is MessagesImportFormatterName {
|
||||
return input == null || typeof input === 'string'
|
||||
}
|
||||
|
||||
export function normalizeMessagesImportFormatterName(
|
||||
input: MessagesImportFormatterName,
|
||||
) {
|
||||
return input ?? 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a function that receives parsed messages file content and returns
|
||||
* a map for creation of the parsed message bundle.
|
||||
*
|
||||
* @param input Parsed contents of the messages file.
|
||||
* @returns A record where message IDs are used as properties and messages
|
||||
* themselves as values.
|
||||
*/
|
||||
export type MessagesImportFormatterFunction<I> = (
|
||||
input: I,
|
||||
) => Record<string, string>
|
||||
|
||||
export type MessagesImportFormatter<I> =
|
||||
| MessagesImportFormatterName
|
||||
| MessagesImportFormatterFunction<I>
|
||||
|
||||
export function normalizeMessagesImportFormatter<I>(
|
||||
input: MessagesImportFormatter<I>,
|
||||
) {
|
||||
if (isMessagesImportFormatterName(input)) {
|
||||
return normalizeMessagesImportFormatterName(input)
|
||||
}
|
||||
|
||||
if (typeof input === 'function') {
|
||||
return input
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be a messages import formatter',
|
||||
)
|
||||
}
|
||||
|
||||
export type NormalizedMessagesImportFormatter = ReturnType<
|
||||
typeof normalizeMessagesImportFormatter
|
||||
>
|
||||
|
||||
/**
|
||||
* Represents a function that receives messages file content and its module ID
|
||||
* and parses it into a value that is later used by the formatter.
|
||||
*
|
||||
* @example
|
||||
* const jsonParser: MessagesImportParser<unknown> = (code) =>
|
||||
* JSON.parse(code)
|
||||
*
|
||||
* @param code Messages file contents to parse.
|
||||
* @param moduleId Module ID of the messages file.
|
||||
* @returns A value later used by the formatter.
|
||||
*/
|
||||
export type MessagesImportParser<R> = (code: string, moduleId: string) => R
|
||||
|
||||
export interface MessagesImportOptions<V = unknown> {
|
||||
/**
|
||||
* A function that receives file contents and produces an output that will be
|
||||
* used by the formatter.
|
||||
*/
|
||||
parser?: MessagesImportParser<V>
|
||||
|
||||
/**
|
||||
* Either a function or a name of the built-in formatted which formats the
|
||||
* parsed output into a record with message IDs as properties and messages
|
||||
* themselves as values.
|
||||
*/
|
||||
format?: MessagesImportFormatter<V>
|
||||
}
|
||||
|
||||
export function isMessagesImportOptions(
|
||||
input: unknown,
|
||||
): input is MessagesImportOptions<unknown> {
|
||||
return (
|
||||
input != null &&
|
||||
typeof input === 'object' &&
|
||||
(!('parser' in input) ||
|
||||
input.parser == null ||
|
||||
typeof input.parser === 'function') &&
|
||||
(!('format' in input) ||
|
||||
input.format == null ||
|
||||
typeof input.format === 'string' ||
|
||||
typeof input.format === 'function')
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeMessagesImportOptions<V = unknown>(
|
||||
input: MessagesImportOptions<V>,
|
||||
) {
|
||||
return {
|
||||
format: normalizeMessagesImportFormatter(input.format),
|
||||
parser: input.parser,
|
||||
} as const
|
||||
}
|
||||
|
||||
export type NormalizedMessagesImportOptions<V = unknown> = ReturnType<
|
||||
typeof normalizeMessagesImportOptions<V>
|
||||
>
|
||||
|
||||
export type MessagesImportSourceObject<V = unknown> = Omit<
|
||||
ImportSourceObject,
|
||||
'resolve'
|
||||
> &
|
||||
MessagesImportOptions<V>
|
||||
|
||||
export function isMessagesImportSourceObject(
|
||||
input: unknown,
|
||||
): input is MessagesImportSourceObject {
|
||||
let filteredInput
|
||||
if (typeof input === 'object' && input != null) {
|
||||
const { resolve: _ignored, ...rest } = input as Record<string, unknown>
|
||||
filteredInput = rest
|
||||
}
|
||||
return (
|
||||
isImportSourceObject(filteredInput) &&
|
||||
isMessagesImportOptions(filteredInput)
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeMessagesImportSourceObject<V>(
|
||||
input: MessagesImportSourceObject<V>,
|
||||
) {
|
||||
const { from, name } = normalizeImportSourceObject(input)
|
||||
return {
|
||||
from,
|
||||
name,
|
||||
...normalizeMessagesImportOptions(input),
|
||||
} as const
|
||||
}
|
||||
|
||||
/** Describes an import source in a form of a string, a tuple, or an object. */
|
||||
export type MessagesImportSource =
|
||||
| ImportSourceString
|
||||
| ImportSourceTuple
|
||||
| MessagesImportSourceObject
|
||||
|
||||
export function normalizeMessagesImportSource(input: MessagesImportSource) {
|
||||
if (typeof input === 'string') {
|
||||
return normalizeMessagesImportSourceObject({ from: input })
|
||||
}
|
||||
|
||||
if (isImportSourceTuple(input)) {
|
||||
const [from, name] = input
|
||||
return normalizeMessagesImportSourceObject({ from, name })
|
||||
}
|
||||
|
||||
if (isMessagesImportSourceObject(input)) {
|
||||
return normalizeMessagesImportSourceObject(input)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be a messages import source',
|
||||
)
|
||||
}
|
||||
|
||||
export type NormalizedMessagesImportSource = ReturnType<
|
||||
typeof normalizeMessagesImportSource
|
||||
>
|
||||
@@ -0,0 +1,261 @@
|
||||
import {
|
||||
normalizeLocaleDescriptor,
|
||||
type LocaleDescriptor,
|
||||
assertLocaleDescriptorValid,
|
||||
} from './locale-descriptor.ts'
|
||||
import { normalizeSEOOptions, type SEOOptions } from './seo.ts'
|
||||
import {
|
||||
normalizeParserlessModeInput,
|
||||
type ParserlessModeOptions,
|
||||
type ParserlessModeValue,
|
||||
} from './parserless.ts'
|
||||
import {
|
||||
type ParseErrorHandlingOption,
|
||||
isParseErrorHandlingOption,
|
||||
} from './parse-errors.ts'
|
||||
import {
|
||||
specialErrorMessage,
|
||||
type ErrorWithSpecialMessage,
|
||||
} from '../utils/error.ts'
|
||||
|
||||
/** Represents either a name of the built-in storage option or a module ID. */
|
||||
export type StorageOptionValue =
|
||||
| 'localStorage'
|
||||
| 'cookie'
|
||||
| (string & Record<never, never>)
|
||||
|
||||
export interface ModuleOptions {
|
||||
/**
|
||||
* BCP 47 language tag of the locale to use by default.
|
||||
*
|
||||
* @default 'en-US'
|
||||
*/
|
||||
defaultLocale?: string
|
||||
|
||||
/**
|
||||
* A directory from where all the locale imports are being resolved.
|
||||
*
|
||||
* If this contains a relative path, that path will be resolved against the
|
||||
* Nuxt's `srcDir` directory.
|
||||
*
|
||||
* @default '.' // Modules will be resolved from the Nuxt'
|
||||
*/
|
||||
resolveDir?: string
|
||||
|
||||
/**
|
||||
* An array of locale definitions.
|
||||
*
|
||||
* At least one locale must be defined, and none should be duplicated.
|
||||
*
|
||||
* @example
|
||||
* ;({
|
||||
* tag: 'en-US',
|
||||
* files: [
|
||||
* {
|
||||
* from: '@acne/site-translations',
|
||||
* name: 'en_us',
|
||||
* resolve: false,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
*/
|
||||
locales: LocaleDescriptor[]
|
||||
|
||||
/* Either a string containing a built-in storage option or a module ID to import. */
|
||||
storage?: StorageOptionValue
|
||||
|
||||
/**
|
||||
* Whether to use `BroadcastChannel` (if it's available) to synchronise the
|
||||
* locale between multiple open tabs on the same origin.
|
||||
*
|
||||
* @default true // Broadcasts locale changes to other open tabs.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
||||
*/
|
||||
broadcastLocaleChange?: boolean
|
||||
|
||||
/**
|
||||
* Name of the host language URL parameter, used to override the default or
|
||||
* user language. Set to `null` to disable, the SEO tags generation will be
|
||||
* disabled as well.
|
||||
*
|
||||
* @deprecated Use `seo` options instead.
|
||||
* @default 'hl' // ?hl=de to use German (if it is defined).
|
||||
*/
|
||||
hostLanguageParam?: string | null
|
||||
|
||||
/** SEO configuration options. */
|
||||
seo?: SEOOptions
|
||||
|
||||
/**
|
||||
* Configuration options for the parserless mode.
|
||||
*
|
||||
* @default { enabled: 'only-prod' } // Enabled only in production
|
||||
*/
|
||||
parserless?: ParserlessModeValue | ParserlessModeOptions
|
||||
|
||||
onParseError?: ParseErrorHandlingOption
|
||||
}
|
||||
|
||||
export function normalizeModuleOptions(input: ModuleOptions) {
|
||||
const {
|
||||
defaultLocale,
|
||||
resolveDir,
|
||||
locales,
|
||||
parserless,
|
||||
broadcastLocaleChange,
|
||||
storage,
|
||||
seo,
|
||||
onParseError,
|
||||
} = input
|
||||
|
||||
let seoResult
|
||||
if (seo === undefined) {
|
||||
let { hostLanguageParam } = input
|
||||
|
||||
if (hostLanguageParam === undefined) {
|
||||
hostLanguageParam = 'hl'
|
||||
}
|
||||
|
||||
seoResult = normalizeSEOOptions({
|
||||
enabled: hostLanguageParam != null,
|
||||
hostLanguageParameter: hostLanguageParam ?? undefined,
|
||||
})
|
||||
} else {
|
||||
seoResult = normalizeSEOOptions(seo)
|
||||
}
|
||||
|
||||
return {
|
||||
defaultLocale: defaultLocale ?? 'en-US',
|
||||
resolveDir: resolveDir ?? '.',
|
||||
locales: locales.map((locale) => normalizeLocaleDescriptor(locale)),
|
||||
broadcastLocaleChange: broadcastLocaleChange ?? false,
|
||||
seo: seoResult,
|
||||
storage,
|
||||
parserless: normalizeParserlessModeInput(
|
||||
parserless ?? { enabled: 'only-prod' },
|
||||
),
|
||||
onParseError,
|
||||
}
|
||||
}
|
||||
|
||||
export type NormalizedModuleOptions = ReturnType<typeof normalizeModuleOptions>
|
||||
|
||||
interface IssuesMap {
|
||||
/** Duplicate indexes. */
|
||||
tags: Map<string, number[]>
|
||||
|
||||
/** Duplicate hreflangs. */
|
||||
hreflangs: Map<string, number[]>
|
||||
}
|
||||
|
||||
export class DuplicateLocalesError
|
||||
extends RangeError
|
||||
implements ErrorWithSpecialMessage
|
||||
{
|
||||
public constructor(public readonly issues: IssuesMap) {
|
||||
super('Your module options contain duplicate locale tags or hreflangs')
|
||||
}
|
||||
|
||||
[specialErrorMessage]() {
|
||||
let { message } = this
|
||||
|
||||
let duplicateTags: string | undefined
|
||||
|
||||
for (const [tag, indexes] of this.issues.tags.entries()) {
|
||||
if (indexes.length < 2) continue
|
||||
if (duplicateTags == null) duplicateTags = 'Duplicate tags:'
|
||||
duplicateTags += `\n- Locale tag "${tag}" is defined several times at indexes `
|
||||
duplicateTags += indexes.join(', ')
|
||||
}
|
||||
|
||||
if (duplicateTags != null) message += `\n${duplicateTags}`
|
||||
|
||||
let duplicateHreflangs: string | undefined
|
||||
|
||||
for (const [hreflang, indexes] of this.issues.hreflangs.entries()) {
|
||||
if (indexes.length < 2) continue
|
||||
if (duplicateHreflangs == null) {
|
||||
duplicateHreflangs = 'Duplicate hreflangs:'
|
||||
}
|
||||
duplicateHreflangs += `\n- Hreflang "${hreflang}" is defined several times at indexes `
|
||||
duplicateHreflangs += indexes.join(', ')
|
||||
}
|
||||
|
||||
if (duplicateTags != null) message += '\n'
|
||||
if (duplicateHreflangs != null) message += `\n${duplicateHreflangs}`
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
export function assertModuleOptionsValid(options: NormalizedModuleOptions) {
|
||||
const errors: unknown[] = []
|
||||
|
||||
const { locales, onParseError } = options
|
||||
|
||||
if (locales.length === 0) {
|
||||
errors.push(
|
||||
new RangeError('Module options are missing any locale definitions'),
|
||||
)
|
||||
}
|
||||
|
||||
const localeTagIndexes = new Map<string, number[]>()
|
||||
const hrefLangIndexes = new Map<string, number[]>()
|
||||
|
||||
let hasDuplicates = false
|
||||
|
||||
for (let i = 0, l = locales.length; i < l; i++) {
|
||||
const locale = locales[i]
|
||||
|
||||
try {
|
||||
assertLocaleDescriptorValid(locale)
|
||||
} catch (cause) {
|
||||
errors.push(
|
||||
new RangeError(`Assertion failed for locale definition at index ${i}`, {
|
||||
cause,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const prevIndexes = localeTagIndexes.get(locale.tag)
|
||||
if (prevIndexes == null) {
|
||||
localeTagIndexes.set(locale.tag, [i])
|
||||
} else {
|
||||
prevIndexes.push(i)
|
||||
hasDuplicates = true
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const hrefLang = locale.meta?.static?.iso ?? locale.tag
|
||||
const prevIndexes = hrefLangIndexes.get(hrefLang)
|
||||
if (prevIndexes == null) {
|
||||
hrefLangIndexes.set(hrefLang, [i])
|
||||
} else {
|
||||
prevIndexes.push(i)
|
||||
hasDuplicates = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDuplicates) {
|
||||
errors.push(
|
||||
new DuplicateLocalesError({
|
||||
tags: localeTagIndexes,
|
||||
hreflangs: hrefLangIndexes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (onParseError != null && !isParseErrorHandlingOption(onParseError)) {
|
||||
errors.push(new RangeError('Value for `onParseError` is invalid'))
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(
|
||||
errors,
|
||||
'Module options have failed one or more assertions',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ParseErrorHandlingOption as RegularParseErrorHandlingOption } from '@vintl/unplugin'
|
||||
|
||||
export type ParseErrorHandlingOption =
|
||||
| RegularParseErrorHandlingOption
|
||||
| 'log-and-skip'
|
||||
|
||||
export function isParseErrorHandlingOption(
|
||||
input: unknown,
|
||||
): input is ParseErrorHandlingOption {
|
||||
return (
|
||||
typeof input === 'function' ||
|
||||
input === 'use-message-as-literal' ||
|
||||
input === 'use-id-as-literal' ||
|
||||
input === 'use-empty-literal' ||
|
||||
input === 'skip' ||
|
||||
input === 'log-and-skip'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Represents a condition of a parserless mode.
|
||||
*
|
||||
* - `always` - parserless mode is always enabled, even in development.
|
||||
* - `only-prod` - parserless mode is only enabled when compiling production
|
||||
* build.
|
||||
* - `never` - parserless mode is never enabled.
|
||||
*/
|
||||
export type ParserlessMode = 'always' | 'only-prod' | 'never'
|
||||
|
||||
export function isParserlessMode(input: unknown): input is ParserlessMode {
|
||||
return input === 'always' || input === 'only-prod' || input === 'never'
|
||||
}
|
||||
|
||||
export type ParserlessModeValue = boolean | ParserlessMode
|
||||
|
||||
export function isParserlessModeValue(
|
||||
input: unknown,
|
||||
): input is ParserlessModeValue {
|
||||
return typeof input === 'boolean' || isParserlessMode(input)
|
||||
}
|
||||
|
||||
export function normalizeParserlessModeValue(input: ParserlessModeValue) {
|
||||
if (typeof input === 'boolean') {
|
||||
return input ? 'always' : 'never'
|
||||
}
|
||||
|
||||
if (typeof input === 'string') {
|
||||
return input
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be a parserless mode value',
|
||||
)
|
||||
}
|
||||
|
||||
export interface ParserlessModeOptions {
|
||||
/**
|
||||
* Whether the parserless mode is enabled or under which condition.
|
||||
*
|
||||
* @default false // Parserless mode is never enabled.
|
||||
*/
|
||||
enabled?: ParserlessModeValue
|
||||
}
|
||||
|
||||
export function isParserlessModeOptions(
|
||||
input: unknown,
|
||||
): input is ParserlessModeOptions {
|
||||
return (
|
||||
input != null &&
|
||||
typeof input === 'object' &&
|
||||
(!('enabled' in input) ||
|
||||
input.enabled === undefined ||
|
||||
isParserlessModeValue(input.enabled))
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeParserlessModeOptions(input: ParserlessModeOptions) {
|
||||
return {
|
||||
enabled: normalizeParserlessModeValue(input.enabled ?? false),
|
||||
} as const
|
||||
}
|
||||
|
||||
export type ParserlessModeInput = ParserlessModeValue | ParserlessModeOptions
|
||||
|
||||
export function normalizeParserlessModeInput(input: ParserlessModeInput) {
|
||||
if (isParserlessModeOptions(input)) {
|
||||
return normalizeParserlessModeOptions(input)
|
||||
}
|
||||
|
||||
if (isParserlessModeValue(input)) {
|
||||
return normalizeParserlessModeOptions({ enabled: input })
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Cannot normalize an input that does not appear to be a parserless mode input',
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export interface SEOOptions {
|
||||
/**
|
||||
* Whether the SEO features are enabled.
|
||||
*
|
||||
* @default true // Seo features are enabled.
|
||||
*/
|
||||
enabled?: boolean
|
||||
|
||||
/**
|
||||
* Name of the host language URL parameter, used to override the default or
|
||||
* user language.
|
||||
*
|
||||
* @default 'hl' // ?hl=de in the URL will force German (if it is defined).
|
||||
*/
|
||||
hostLanguageParameter?: string
|
||||
|
||||
/**
|
||||
* Base URL at which the site will be available. This is required to generate
|
||||
* absolute hreflang links. Without this, VIntl will try to guess the URL
|
||||
* based on the server request URL / current `window.location`.
|
||||
*/
|
||||
baseURL?: string
|
||||
|
||||
/**
|
||||
* Whether to use the host language parameter for the default locale.
|
||||
*
|
||||
* This option helps to keep URLs for your default language clean, however it
|
||||
* is not correct, and is not recommended, because the lack of the host
|
||||
* language parameter tells VIntl to use the user locale (whether it's the one
|
||||
* they've chosen or their browser's one).
|
||||
*
|
||||
* @default true // All hreflang links have the host language parameter, including the default one.
|
||||
*/
|
||||
defaultLocaleHasParameter?: boolean
|
||||
|
||||
/**
|
||||
* Whether to enable `x-default` hreflang.
|
||||
*
|
||||
* Disabling this is not recommended.
|
||||
*
|
||||
* @default true // There is a x-default hreflang.
|
||||
*/
|
||||
xDefaultHreflang?: boolean
|
||||
}
|
||||
|
||||
export function normalizeSEOOptions(input: SEOOptions) {
|
||||
return {
|
||||
enabled: input.enabled ?? true,
|
||||
hostLanguageParameter: input.hostLanguageParameter ?? 'hl',
|
||||
baseURL: input.baseURL,
|
||||
defaultLocaleHasParameter: input.defaultLocaleHasParameter ?? true,
|
||||
xDefaultHreflang: input.xDefaultHreflang ?? true,
|
||||
} as const
|
||||
}
|
||||
|
||||
export type NormalizedSEOOptions = ReturnType<typeof normalizeSEOOptions>
|
||||
@@ -2,19 +2,45 @@
|
||||
// we track every message file import, we need a special class that create
|
||||
// unique plugin instances.
|
||||
import type { PluginOptions } from '@vintl/unplugin'
|
||||
import type { z as t } from 'zod'
|
||||
import type {
|
||||
messagesImportOptionsSchema,
|
||||
messagesImportSourceSchema,
|
||||
} from './schemas/messages-imports.js'
|
||||
NormalizedMessagesImportOptions,
|
||||
NormalizedMessagesImportSource,
|
||||
} from './options'
|
||||
|
||||
// we have two unique properties: 1) file structure format 2) file parser
|
||||
|
||||
type UniqueOptions = t.output<typeof messagesImportOptionsSchema>
|
||||
type UniqueOptions = NormalizedMessagesImportOptions
|
||||
|
||||
class Marker {
|
||||
private readonly regExp: RegExp
|
||||
|
||||
public constructor(public readonly marker: string) {
|
||||
this.regExp = new RegExp(`(\\?|&)${marker}(&|$)`)
|
||||
}
|
||||
|
||||
public apply(id: string) {
|
||||
return `${id}${id.includes('?') ? '&' : '?'}${this.marker}`
|
||||
}
|
||||
|
||||
public matches(id: string) {
|
||||
return this.regExp.test(id)
|
||||
}
|
||||
}
|
||||
|
||||
class MarkedFileSet extends Set<string> {
|
||||
public readonly marker: Marker
|
||||
|
||||
constructor(marker: Marker, iterable?: Iterable<string> | null | undefined) {
|
||||
super(iterable)
|
||||
this.marker = marker
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginOptionsBank {
|
||||
// let the uniqueOptions be a map of unique options to a list of files that have the same options
|
||||
private readonly filesByOptions: Map<UniqueOptions, Set<string>> = new Map()
|
||||
private readonly filesByOptions = new Map<UniqueOptions, MarkedFileSet>()
|
||||
|
||||
private incrementingMarker = 0
|
||||
|
||||
private findOrCreateFileSet(optionsVariation: UniqueOptions) {
|
||||
for (const [knownOptionsVariation, files] of this.filesByOptions) {
|
||||
@@ -31,16 +57,19 @@ export class PluginOptionsBank {
|
||||
parser: optionsVariation.parser,
|
||||
}
|
||||
|
||||
const fileSet = new Set<string>()
|
||||
const marker = new Marker(`icu-messages-${this.incrementingMarker++}`)
|
||||
|
||||
const fileSet = new MarkedFileSet(marker)
|
||||
this.filesByOptions.set(filteredOptions, fileSet)
|
||||
return fileSet
|
||||
}
|
||||
|
||||
public registerFile(
|
||||
file: t.output<typeof messagesImportSourceSchema>,
|
||||
file: NormalizedMessagesImportSource,
|
||||
resolvedPath: string,
|
||||
) {
|
||||
this.findOrCreateFileSet(file).add(resolvedPath)
|
||||
const { marker } = this.findOrCreateFileSet(file).add(resolvedPath)
|
||||
return marker.apply(resolvedPath)
|
||||
}
|
||||
|
||||
public createOptions<T extends { name: string }>(
|
||||
@@ -49,7 +78,7 @@ export class PluginOptionsBank {
|
||||
const options: PluginOptions<T>[] = []
|
||||
|
||||
for (const [{ format, parser }, files] of this.filesByOptions) {
|
||||
const filter = (id: string) => files.has(id)
|
||||
const filter = (id: string) => files.marker.matches(id)
|
||||
|
||||
options.push({
|
||||
...baseOptions,
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { IntlController } from '@vintl/vintl/controller'
|
||||
import type { AfterLocaleChangeEvent } from '@vintl/vintl/events'
|
||||
import { isOfType } from './utils/type-checks.js'
|
||||
|
||||
export function setupBroadcasting(controller: IntlController<any>) {
|
||||
if (!process.client) return
|
||||
// eslint-disable-next-rule
|
||||
export function setupBroadcasting<T>(controller: IntlController<T>) {
|
||||
if (!import.meta.client) return
|
||||
|
||||
if (typeof BroadcastChannel !== 'function') {
|
||||
console.warn(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useHead } from '#imports'
|
||||
import type { SEOOptions } from '@vintl/nuxt-runtime/options'
|
||||
import type { IntlController } from '@vintl/vintl/controller'
|
||||
import { useRouter as _useRouter, useRequestURL } from 'nuxt/app'
|
||||
import { joinURL } from 'ufo'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'nuxt/app'
|
||||
import type { LocationQuery, useRouter as _useRouter$type } from 'vue-router'
|
||||
|
||||
type LocationQuery = ReturnType<
|
||||
typeof useRouter
|
||||
>['currentRoute']['value']['query']
|
||||
const useRouter = _useRouter as typeof _useRouter$type
|
||||
|
||||
// I wish there was a better way to do this T_T
|
||||
function getSeachParams(query: LocationQuery) {
|
||||
@@ -35,11 +36,29 @@ type LangHrefLink = {
|
||||
href: string
|
||||
}
|
||||
|
||||
export function initHead(controller: IntlController<any>, hlParam: string) {
|
||||
type HeadOptions = Omit<SEOOptions, 'enabled'>
|
||||
|
||||
export function initHead<T>(
|
||||
controller: IntlController<T>,
|
||||
options: HeadOptions,
|
||||
) {
|
||||
const router = useRouter()
|
||||
|
||||
const currentRoute = computed(() => router.currentRoute.value)
|
||||
|
||||
const requestURL = useRequestURL()
|
||||
|
||||
const normalizeURL = (url: string) => {
|
||||
try {
|
||||
return options.baseURL == null
|
||||
? new URL(url, requestURL).toString()
|
||||
: joinURL(options.baseURL, url)
|
||||
} catch (err) {
|
||||
console.error(`[vintl] cannot normalize url: ${String(err)}`)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: () => controller.$config.locale,
|
||||
@@ -51,37 +70,43 @@ export function initHead(controller: IntlController<any>, hlParam: string) {
|
||||
const query = String(getSeachParams(route.query))
|
||||
const { path } = route
|
||||
|
||||
let defaultEntry: LangHrefLink
|
||||
const hrefLangs: Map<string, LangHrefLink> = new Map()
|
||||
|
||||
{
|
||||
if (options.xDefaultHreflang) {
|
||||
const sp = new URLSearchParams(query)
|
||||
sp.delete(hlParam)
|
||||
defaultEntry = {
|
||||
sp.delete(options.hostLanguageParameter)
|
||||
hrefLangs.set('x-default', {
|
||||
// key: `hreflang-default`,
|
||||
rel: 'alternate',
|
||||
hreflang: 'x-default',
|
||||
href: withQueryParams(path, sp.toString()),
|
||||
}
|
||||
href: normalizeURL(withQueryParams(path, sp.toString())),
|
||||
})
|
||||
}
|
||||
|
||||
const hrefLangs: Map<string, LangHrefLink> = new Map()
|
||||
for (const locale of controller.availableLocales) {
|
||||
const hrefLang = locale.meta?.static?.iso ?? locale.tag
|
||||
|
||||
if (hrefLangs.has(hrefLang)) continue
|
||||
|
||||
const sp = new URLSearchParams(query)
|
||||
sp.set(hlParam, locale.tag)
|
||||
if (
|
||||
controller.defaultLocale === locale.tag &&
|
||||
!options.defaultLocaleHasParameter
|
||||
) {
|
||||
sp.delete(options.hostLanguageParameter)
|
||||
} else {
|
||||
sp.set(options.hostLanguageParameter, locale.tag)
|
||||
}
|
||||
|
||||
hrefLangs.set(hrefLang, {
|
||||
// key: `hreflang-${locale.tag}`,
|
||||
rel: 'alternate',
|
||||
hreflang: hrefLang,
|
||||
href: withQueryParams(path, sp.toString()),
|
||||
href: normalizeURL(withQueryParams(path, sp.toString())),
|
||||
})
|
||||
}
|
||||
|
||||
return [defaultEntry, ...hrefLangs.values()]
|
||||
return Array.from(hrefLangs.values())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
localeDefinitions,
|
||||
storageAdapterFactory,
|
||||
broadcastLocaleChange,
|
||||
hostLanguageParam,
|
||||
parserless,
|
||||
seo as seoOptions,
|
||||
} from '@vintl/nuxt-runtime/options'
|
||||
import type { LocaleDescriptor, MessageValueType } from '@vintl/vintl'
|
||||
import type { IntlController } from '@vintl/vintl/controller'
|
||||
@@ -21,6 +21,7 @@ import { useAcceptLanguageHeader } from '@vintl/vintl/sources/header'
|
||||
import { defineNuxtPlugin } from 'nuxt/app'
|
||||
import { syncCaller } from './utils/hookable.js'
|
||||
import { initHead } from './head.js'
|
||||
import { match as matchLocales } from '@formatjs/intl-localematcher'
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const locales: LocaleDescriptor[] = Object.entries(localeDefinitions).map(
|
||||
@@ -35,14 +36,14 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
try {
|
||||
locale = (await storage.read()) ?? undefined
|
||||
} catch (err) {
|
||||
if (process.dev) {
|
||||
if (import.meta.dev) {
|
||||
console.error('[@vintl/nuxt] Cannot read last used locale', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hostLanguageParam != null) {
|
||||
let hlLocale = nuxtApp._route.query[hostLanguageParam]
|
||||
if (seoOptions.enabled) {
|
||||
let hlLocale = nuxtApp._route.query[seoOptions.hostLanguageParameter]
|
||||
|
||||
if (Array.isArray(hlLocale)) {
|
||||
hlLocale = hlLocale[0]
|
||||
@@ -53,12 +54,23 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (locale != null) {
|
||||
const match = matchLocales(
|
||||
[locale],
|
||||
locales.map(({ tag }) => tag),
|
||||
'en-x-placeholder',
|
||||
)
|
||||
|
||||
locale = match == 'en-x-placeholder' ? undefined : match
|
||||
}
|
||||
|
||||
const plugin = createPlugin<MessageValueType>({
|
||||
injectInto: [nuxtApp],
|
||||
controllerOpts: {
|
||||
defaultLocale,
|
||||
locales,
|
||||
locale,
|
||||
usePreferredLocale: locale == null,
|
||||
listen: {
|
||||
error(event) {
|
||||
nuxtApp.hooks.callHookWith(syncCaller, 'i18n:error', {
|
||||
@@ -100,7 +112,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
try {
|
||||
await storage?.save(event.automatic ? null : event.locale.tag)
|
||||
} catch (_err) {
|
||||
if (process.dev) {
|
||||
if (import.meta.dev) {
|
||||
console.error(
|
||||
'[@vintl/nuxt] Cannot save last used locale',
|
||||
event.locale.tag,
|
||||
@@ -115,7 +127,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
},
|
||||
},
|
||||
preferredLocaleSources: [
|
||||
process.server
|
||||
import.meta.server
|
||||
? (() => {
|
||||
const acceptLanguage =
|
||||
nuxtApp.ssrContext?.event.node.req.headers['accept-language']
|
||||
@@ -139,8 +151,8 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
setupBroadcasting(controller)
|
||||
}
|
||||
|
||||
if (hostLanguageParam != null) {
|
||||
nuxtApp.hook('vue:setup', () => initHead(controller, hostLanguageParam!))
|
||||
if (seoOptions.enabled) {
|
||||
nuxtApp.hook('vue:setup', () => initHead(controller, seoOptions))
|
||||
}
|
||||
|
||||
await nuxtApp.callHook('i18n:ready', controller)
|
||||
@@ -151,7 +163,7 @@ type EventContext<E> = {
|
||||
controller: IntlController<MessageValueType>
|
||||
}
|
||||
|
||||
declare module 'nuxt/app' {
|
||||
declare module '#app' {
|
||||
interface NuxtApp
|
||||
extends InjectedProperties<VueIntlController.MessageValueTypes> {}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { HookCallback } from 'hookable'
|
||||
* @param hooks An array of hooks to call.
|
||||
* @param arguments_ Arguments to call hooks with.
|
||||
*/
|
||||
export function syncCaller(hooks: HookCallback[], arguments_?: any[]): void {
|
||||
export function syncCaller(
|
||||
hooks: HookCallback[],
|
||||
arguments_?: unknown[],
|
||||
): void {
|
||||
for (const hook of hooks) hook(arguments_)
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
import { tSwitch } from '../utils/zod-utils.js'
|
||||
|
||||
/** A schema for an object containing import source and custom export name. */
|
||||
export const importSourceObjectSchema = t.object({
|
||||
/** Module source relative to the `options.resolveDir`. */
|
||||
from: t.string({
|
||||
invalid_type_error: 'Import source must be a string',
|
||||
}),
|
||||
|
||||
/** Custom export name. */
|
||||
name: t
|
||||
.string({
|
||||
invalid_type_error: 'Export name must be a string',
|
||||
})
|
||||
.default('default'),
|
||||
|
||||
/** Whether to resolve the import to a path or import as is. */
|
||||
resolve: t.boolean().default(true),
|
||||
})
|
||||
|
||||
/** A schema for the tuple consisting of an import path and a custom export name. */
|
||||
export const importSourceTupleSchema = t.tuple([
|
||||
t
|
||||
.string({
|
||||
required_error: 'A path must be provided',
|
||||
invalid_type_error: 'Import path must be a string',
|
||||
})
|
||||
.describe(
|
||||
'Import path from where the export with the name provided as a second element of the tuple is imported',
|
||||
),
|
||||
t
|
||||
.string({
|
||||
required_error: 'A custom export name must be provided',
|
||||
invalid_type_error: 'Custom export name must be a string',
|
||||
})
|
||||
.describe('Custom export name')
|
||||
.default('default'),
|
||||
])
|
||||
|
||||
/**
|
||||
* A schema for a union of import source path or a tuple with import source path
|
||||
* and a custom export name.
|
||||
*/
|
||||
export const importSourceSchema = tSwitch((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return t
|
||||
.string()
|
||||
.transform((from) => ({ from }))
|
||||
.pipe(importSourceObjectSchema)
|
||||
} else if (Array.isArray(value)) {
|
||||
return importSourceTupleSchema
|
||||
.transform(([from, name]) => ({ from, name }))
|
||||
.pipe(importSourceObjectSchema)
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return importSourceObjectSchema
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* A schema for an import source without any specifiers (side-effect only
|
||||
* import).
|
||||
*/
|
||||
export const unspecifiedImportSourceObjectSchema =
|
||||
importSourceObjectSchema.omit({ name: true })
|
||||
|
||||
/**
|
||||
* A schema for an import source without any specifiers (side-effect only
|
||||
* import) in two form: a string or an object.
|
||||
*/
|
||||
export const unspecifiedImportSourceSchema = tSwitch((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return t
|
||||
.string()
|
||||
.transform((from) => ({ from }))
|
||||
.pipe(unspecifiedImportSourceObjectSchema)
|
||||
} else if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
return unspecifiedImportSourceObjectSchema
|
||||
}
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './imports.js'
|
||||
export * from './messages-imports.js'
|
||||
export * from './language-tag.js'
|
||||
export * from './json.js'
|
||||
export * from './locales.js'
|
||||
export * from './module-options.js'
|
||||
@@ -1,51 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
import { tSwitch } from '../utils/zod-utils.js'
|
||||
|
||||
/**
|
||||
* Checks if provided value is an acceptable JSON literal.
|
||||
*
|
||||
* @param value The value to check.
|
||||
* @returns `true` if the value is a JSON literal, `false` otherwise.
|
||||
*/
|
||||
function isLiteral(value: unknown): value is string | number | boolean | null {
|
||||
return (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value === null
|
||||
)
|
||||
}
|
||||
|
||||
const literalSchema = tSwitch((value) => {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return t.string()
|
||||
case 'number':
|
||||
return t.number()
|
||||
case 'boolean':
|
||||
return t.boolean()
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case null:
|
||||
return t.null()
|
||||
case undefined:
|
||||
return t.undefined()
|
||||
}
|
||||
})
|
||||
|
||||
type Literal = t.infer<typeof literalSchema>
|
||||
|
||||
type JSONValue = Literal | { [key: string]: JSONValue } | JSONValue[]
|
||||
|
||||
export const jsonValueSchema: t.ZodType<JSONValue> = t.lazy(() =>
|
||||
tSwitch((value) => {
|
||||
if (isLiteral(value)) {
|
||||
return literalSchema
|
||||
} else if (Array.isArray(value)) {
|
||||
return t.array(jsonValueSchema)
|
||||
} else if (typeof value === 'object') {
|
||||
return t.record(jsonValueSchema)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
|
||||
export const languageTagSchema = t
|
||||
.string({
|
||||
description: 'BCP 47 language tag',
|
||||
})
|
||||
.nonempty()
|
||||
.refine(
|
||||
(value) => {
|
||||
try {
|
||||
new Intl.Locale(value)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Language tag must be a valid BCP 47 language tag (Intl.Locale instantiation failed)',
|
||||
},
|
||||
)
|
||||
@@ -1,102 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
import { importSourceSchema, unspecifiedImportSourceSchema } from './imports.js'
|
||||
import { jsonValueSchema } from './json.js'
|
||||
import { languageTagSchema } from './language-tag.js'
|
||||
import { messagesImportSourceSchema } from './messages-imports.js'
|
||||
|
||||
export const localeMetaSchema = t
|
||||
.object({
|
||||
/** Properties that are used by VIntl. */
|
||||
static: t
|
||||
.object({
|
||||
/**
|
||||
* If there's a case where BCP 47 language tag wouldn't match `hreflang`
|
||||
* specification, which is ISO 639-1 for language code and optionally,
|
||||
* dash and ISO 3166-1 code of the country, this value must be
|
||||
* specified.
|
||||
*
|
||||
* @see [Google Search Central — Tell Google about localized versions of your page § Language codes](https://developers.google.com/search/docs/specialty/international/localized-versions#language-codes)
|
||||
*/
|
||||
iso: t
|
||||
.string()
|
||||
.refine((v) => v !== 'x-default', 'Must not be x-default')
|
||||
.optional(), // how about to go crazy and validate it lol
|
||||
})
|
||||
.describe('Static properties used by VIntl library')
|
||||
.optional(),
|
||||
})
|
||||
.catchall(jsonValueSchema.optional())
|
||||
.refine(
|
||||
(value) => {
|
||||
try {
|
||||
JSON.stringify(value)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
{ message: 'Locale meta must be JSON serialisable' },
|
||||
)
|
||||
|
||||
export const localeDescriptorSchema = t
|
||||
.object({
|
||||
/**
|
||||
* BCP 47 language of the locale.
|
||||
*
|
||||
* @see https://www.w3.org/International/questions/qa-choosing-language-tags W3 guide on choosing the language tags.
|
||||
*/
|
||||
tag: languageTagSchema,
|
||||
|
||||
/**
|
||||
* A source of import for the messages file, which is resolved against the
|
||||
* {@link Options.resolveDir}. If {@link files} option specified together with
|
||||
* this option, this messages file will be imported before any other file
|
||||
* defined in {@link files}.
|
||||
*/
|
||||
file: messagesImportSourceSchema.optional(),
|
||||
|
||||
/**
|
||||
* BCP 47 language tag of the locale.
|
||||
*
|
||||
* @see https://www.w3.org/International/questions/qa-choosing-language-tags W3 guide on choosing the language tags.
|
||||
*/
|
||||
files: t.array(messagesImportSourceSchema).optional(),
|
||||
|
||||
/** List of additional side-effect only imports (like polyfill data). */
|
||||
additionalImports: t
|
||||
.array(unspecifiedImportSourceSchema)
|
||||
.describe(
|
||||
'List of additional side-effect only imports (like polyfill data)',
|
||||
)
|
||||
.optional(),
|
||||
|
||||
/**
|
||||
* An array of mapped resource imports.
|
||||
*
|
||||
* Unlike additional imports, which are used for side effects, these imports
|
||||
* will be mapped to provided key in runtime.
|
||||
*
|
||||
* This is useful for language-associated documents, which would otherwise
|
||||
* create frustrating experience for translators be they left as regular
|
||||
* strings in the messages file.
|
||||
*/
|
||||
resources: t.record(importSourceSchema).optional(),
|
||||
|
||||
/**
|
||||
* Custom meta for the locale that is always accessible even when the locale
|
||||
* is not loaded.
|
||||
*
|
||||
* It can be used to store data like translation coverage percentage, or
|
||||
* display names. Keep in mind, it needs to be JSON serialisable.
|
||||
*/
|
||||
meta: localeMetaSchema.optional(),
|
||||
})
|
||||
.refine(
|
||||
(value) => {
|
||||
return value.file != null || value.files != null
|
||||
},
|
||||
{
|
||||
message:
|
||||
'LocaleDescriptor must contain at least one of `file` or `files`',
|
||||
},
|
||||
)
|
||||
@@ -1,107 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
import { tSwitch } from '../utils/zod-utils.js'
|
||||
import { importSourceObjectSchema, importSourceTupleSchema } from './imports.js'
|
||||
|
||||
export const messagesImportFormatterName = t.string({
|
||||
invalid_type_error: 'Format must be a string',
|
||||
})
|
||||
|
||||
export const messagesImportFormatterFunction = t.function(
|
||||
t.tuple([t.unknown()]),
|
||||
t.record(t.string()),
|
||||
)
|
||||
|
||||
/** A schema for the locale import options. */
|
||||
export const messagesImportOptionsSchema = t.object({
|
||||
/** Format of the messages file, either CLI name or . */
|
||||
format: tSwitch((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return messagesImportFormatterName
|
||||
} else if (typeof value === 'function') {
|
||||
return messagesImportFormatterFunction
|
||||
}
|
||||
}).default('default'),
|
||||
|
||||
/**
|
||||
* A function that parses the imported file into a JS object later used by the
|
||||
* formatter.
|
||||
*/
|
||||
parser: t
|
||||
.function(
|
||||
t.tuple([t.string().describe('Code'), t.string().describe('Module ID')]),
|
||||
t.any().describe('Output used by the formatter'),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const messagesImportSourceObjectSchema = importSourceObjectSchema
|
||||
.omit({ resolve: true })
|
||||
.merge(messagesImportOptionsSchema)
|
||||
|
||||
// const _messagesImportSourceSchema = t.union([
|
||||
// t.string(),
|
||||
// importSourceWithSpecifierSchema,
|
||||
// messagesImportSourceObjectSchema,
|
||||
// ])
|
||||
|
||||
// /** A schema for the locale import source. */
|
||||
// export const messagesImportSourceSchema = t.any().transform((value, ctx) => {
|
||||
// if (Array.isArray(value)) {
|
||||
// const r = messagesImportSourceObjectSchema.safeParse({
|
||||
// source: value[0],
|
||||
// name: value[1],
|
||||
// })
|
||||
// if (r.success) {
|
||||
// return r.data
|
||||
// } else {
|
||||
// r.error.issues.forEach((i) => ctx.addIssue(i))
|
||||
// return undefined as never
|
||||
// }
|
||||
// } else if (typeof value === 'string') {
|
||||
// const r = messagesImportSourceObjectSchema.safeParse({
|
||||
// source: value,
|
||||
// })
|
||||
// if (r.success) {
|
||||
// return r.data
|
||||
// } else {
|
||||
// r.error.issues.forEach((i) => ctx.addIssue(i))
|
||||
// return undefined as never
|
||||
// }
|
||||
// } else if (typeof value === 'object' && value != null) {
|
||||
// const r = messagesImportSourceObjectSchema.safeParse(value)
|
||||
// if (r.success) {
|
||||
// return r.data
|
||||
// } else {
|
||||
// r.error.issues.forEach((i) => ctx.addIssue(i))
|
||||
// return undefined as never
|
||||
// }
|
||||
// }
|
||||
|
||||
// {
|
||||
// const r = t.never().safeParse(value)
|
||||
// if (r.success) {
|
||||
// return r.data
|
||||
// } else {
|
||||
// r.error.issues.forEach((i) => ctx.addIssue(i))
|
||||
// return undefined as never
|
||||
// }
|
||||
// }
|
||||
// }) as unknown as typeof _messagesImportSourceSchema
|
||||
|
||||
export const messagesImportSourceSchema = tSwitch((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return importSourceTupleSchema
|
||||
.transform((v) => ({ from: v[0], name: v[1] }))
|
||||
.pipe(messagesImportSourceObjectSchema)
|
||||
} else if (typeof value === 'string') {
|
||||
return t
|
||||
.string()
|
||||
.transform((v) => ({ from: v }))
|
||||
.pipe(messagesImportSourceObjectSchema)
|
||||
} else if (typeof value === 'object' && value != null) {
|
||||
return messagesImportSourceObjectSchema
|
||||
}
|
||||
})
|
||||
|
||||
// type In = t.input<typeof messagesImportSourceSchema>
|
||||
// type Out = t.output<typeof messagesImportSourceSchema>
|
||||
@@ -1,202 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
import { tSwitch } from '../utils/zod-utils.js'
|
||||
import { languageTagSchema } from './language-tag.js'
|
||||
import { localeDescriptorSchema } from './locales.js'
|
||||
|
||||
const parselessModeEnumSchema = t.enum(['always', 'only-prod', 'never'])
|
||||
|
||||
export const parselessModeSchema = tSwitch((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return parselessModeEnumSchema
|
||||
} else if (typeof value === 'boolean') {
|
||||
return t
|
||||
.boolean()
|
||||
.transform((value) => (value ? 'always' : 'never'))
|
||||
.pipe(parselessModeEnumSchema)
|
||||
}
|
||||
})
|
||||
|
||||
export const parselessOptionsSchema = t
|
||||
.object({
|
||||
/** Specifies whether the parseless mode is enabled. */
|
||||
enabled: parselessModeSchema.optional().default(false),
|
||||
})
|
||||
.describe('Options for the parseless mode')
|
||||
|
||||
export const storageValueSchema = t.custom<
|
||||
'localStorage' | 'cookie' | (string & Record<never, never>)
|
||||
>((value) => {
|
||||
return t.string().safeParse(value).success
|
||||
}, 'Storage name must be a string')
|
||||
|
||||
export const moduleOptionsSchema = t
|
||||
.object({
|
||||
/**
|
||||
* BCP 47 code of the locale to use by default.
|
||||
*
|
||||
* @default 'en-US' // American English will be used as a default locale.
|
||||
*/
|
||||
defaultLocale: languageTagSchema.default('en-US'),
|
||||
|
||||
/**
|
||||
* A directory where all the imports associated with the locales are being
|
||||
* resolved.
|
||||
*
|
||||
* If it's relative, then it will be resolved against the Nuxt `srcDir`
|
||||
* directory.
|
||||
*
|
||||
* @default '.' // Modules will be resolved from Nuxt srcDir.
|
||||
*/
|
||||
resolveDir: t
|
||||
.string()
|
||||
.describe('Directory where all the imports are being resolved')
|
||||
.default('.'),
|
||||
|
||||
/** An array of all the configured locales. */
|
||||
locales: t
|
||||
.array(localeDescriptorSchema)
|
||||
.min(1, {
|
||||
message: 'Must define at least one locale',
|
||||
})
|
||||
.superRefine((locales, ctx) => {
|
||||
const localeTagIndexes = new Map<string, number[]>()
|
||||
const hrefLangIndexes = new Map<string, number[]>()
|
||||
// const conflicts = new Map<
|
||||
// number,
|
||||
// {
|
||||
// tags: number[]
|
||||
// hreflangs: number[]
|
||||
// }
|
||||
// >()
|
||||
|
||||
// const mapConflicts = (localeIndex: number) => {
|
||||
// let ret = conflicts.get(localeIndex)
|
||||
// if (ret == null) {
|
||||
// ret = {
|
||||
// tags: [],
|
||||
// hreflangs: [],
|
||||
// }
|
||||
// conflicts.set(localeIndex, ret)
|
||||
// }
|
||||
// return ret
|
||||
// }
|
||||
|
||||
// const uniquePush = <T>(arr: T[], value: T) => {
|
||||
// if (arr.includes(value)) {
|
||||
// return
|
||||
// }
|
||||
// arr.push(value)
|
||||
// }
|
||||
|
||||
for (let i = 0, l = locales.length; i < l; i++) {
|
||||
const locale = locales[i]!
|
||||
|
||||
{
|
||||
const prevIndexes = localeTagIndexes.get(locale.tag)
|
||||
if (prevIndexes === undefined) {
|
||||
localeTagIndexes.set(locale.tag, [i])
|
||||
} else {
|
||||
prevIndexes.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const hrefLang = locale.meta?.static?.iso ?? locale.tag
|
||||
const prevIndexes = hrefLangIndexes.get(hrefLang)
|
||||
if (prevIndexes === undefined) {
|
||||
hrefLangIndexes.set(hrefLang, [i])
|
||||
} else {
|
||||
prevIndexes.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tag, indexes] of localeTagIndexes) {
|
||||
if (indexes.length < 2) continue
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
params: {
|
||||
type: 'language_tag_conflicts',
|
||||
tag,
|
||||
indexes,
|
||||
},
|
||||
message: `Language "${tag}" is defined multiple times at indexes ${indexes.join(
|
||||
', ',
|
||||
)}`,
|
||||
})
|
||||
}
|
||||
|
||||
for (const [hrefLang, indexes] of hrefLangIndexes) {
|
||||
if (indexes.length < 2) continue
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
params: {
|
||||
type: 'hreflang_conflicts',
|
||||
hrefLang,
|
||||
indexes,
|
||||
},
|
||||
message: `Multiple occurances of hreflang "${hrefLang}" at indexes ${indexes.join(
|
||||
', ',
|
||||
)}`,
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Either a built-in storage name (localStorage / cookie), node module name
|
||||
* or path to a file that exports an adapter as default export.
|
||||
*/
|
||||
storage: storageValueSchema.optional(),
|
||||
|
||||
/**
|
||||
* Whether to use `BroadcastChannel` (if available) to synchronise locale
|
||||
* change between multiple tabs on the same origin.
|
||||
*
|
||||
* @default true // Broadcasts locale changes to other tabs.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
||||
*/
|
||||
broadcastLocaleChange: t
|
||||
.boolean()
|
||||
.describe(
|
||||
'A boolean value that specifies whether the `BroadcastChannel` is used to synchronise locale changes between multiple tabs on the same origin.',
|
||||
)
|
||||
.default(true),
|
||||
|
||||
/**
|
||||
* Name of the host language URL parameter, used to override the default or
|
||||
* user language. `null` to disable. If this value is `null`, the SEO tags
|
||||
* generation will be disabled.
|
||||
*
|
||||
* @default 'hl' // For example, ?hl=en to use English locale (if defined).
|
||||
*/
|
||||
hostLanguageParam: t
|
||||
.string()
|
||||
.describe(
|
||||
'Name of the host language URL parameter, used to override the default or user language.',
|
||||
)
|
||||
.nullable()
|
||||
.default('hl'),
|
||||
|
||||
/**
|
||||
* Configuration options for the parserless mode, a mode in which the parser
|
||||
* is removed from runtime and all strings are processed during the initial
|
||||
* build, which can have drastic bundle size reduction and performance
|
||||
* improvements. The tradeoff, however, is that no messages can be parsed
|
||||
* anymore, they all will have to be processed beforehand.
|
||||
*
|
||||
* Parserless mode is still an experimental feature and will continue to
|
||||
* improve with time.
|
||||
*/
|
||||
parserless: tSwitch((value) => {
|
||||
if (typeof value === 'string') {
|
||||
return parselessModeSchema
|
||||
.transform((value) => ({ enabled: value }))
|
||||
.pipe(parselessOptionsSchema)
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return parselessOptionsSchema
|
||||
}
|
||||
}).default('only-prod'),
|
||||
})
|
||||
.refine((value) => {
|
||||
return value.locales.some((locale) => locale.tag === value.defaultLocale)
|
||||
}, 'Default locale must be defined')
|
||||
@@ -0,0 +1,112 @@
|
||||
import fmt from 'picocolors'
|
||||
import { indent } from './strings.ts'
|
||||
|
||||
function getObjectName(object: unknown) {
|
||||
if (object == null) return `<${String(object)}>`
|
||||
|
||||
if (typeof object === 'function') {
|
||||
return object.name === '' ? '(anonymous)' : object.name
|
||||
}
|
||||
|
||||
const constructor = Object.getPrototypeOf(object)?.constructor
|
||||
|
||||
if (typeof constructor === 'function') return constructor.name
|
||||
|
||||
return '<unknown>'
|
||||
}
|
||||
|
||||
export const specialErrorMessage = Symbol('specialErrorMessage')
|
||||
|
||||
export interface ErrorWithSpecialMessage {
|
||||
[specialErrorMessage]: string | (() => string)
|
||||
}
|
||||
|
||||
function hasSpecialMessage<T extends Error>(
|
||||
error: T,
|
||||
): error is T & ErrorWithSpecialMessage {
|
||||
return (
|
||||
specialErrorMessage in error &&
|
||||
(typeof error[specialErrorMessage] === 'function' ||
|
||||
typeof error[specialErrorMessage] === 'string')
|
||||
)
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (hasSpecialMessage(error)) {
|
||||
if (typeof error[specialErrorMessage] === 'function') {
|
||||
return String(error[specialErrorMessage]())
|
||||
} else {
|
||||
return error[specialErrorMessage]
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message.length === 0) {
|
||||
return fmt.gray('<no message>')
|
||||
}
|
||||
|
||||
return error.message
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
function getCause(error: unknown) {
|
||||
if (error instanceof Error) return error.cause
|
||||
}
|
||||
|
||||
function getCausationSummary(error: unknown) {
|
||||
const visitedCauses = new Set<unknown>()
|
||||
|
||||
let currentCause = getCause(error)
|
||||
|
||||
if (currentCause == null) return ''
|
||||
|
||||
const summaries: string[] = []
|
||||
|
||||
while (currentCause != null) {
|
||||
if (visitedCauses.has(currentCause)) break
|
||||
visitedCauses.add(currentCause)
|
||||
|
||||
summaries.push(
|
||||
`... Caused by ${indent(getErrorSummary(currentCause), 4).trim()}`,
|
||||
)
|
||||
|
||||
currentCause = getCause(currentCause)
|
||||
}
|
||||
|
||||
return summaries.join('\n')
|
||||
}
|
||||
|
||||
function getAggregatedSummary(error: AggregateError) {
|
||||
if (error.errors.length === 0) return ''
|
||||
|
||||
const summaries: string[] = []
|
||||
|
||||
for (const aggregatedError of error.errors) {
|
||||
summaries.push(`- ${indent(getErrorSummary(aggregatedError), 2).trim()}`)
|
||||
}
|
||||
|
||||
return `${fmt.yellow('Aggregated errors:')}\n${summaries.join('\n')}`
|
||||
}
|
||||
|
||||
export function getErrorSummary(error: unknown) {
|
||||
let summary = `${fmt.red(getObjectName(error))}: `
|
||||
summary += indent(getErrorMessage(error), 2).trim()
|
||||
|
||||
if (error instanceof AggregateError) {
|
||||
const aggregatedSummary = getAggregatedSummary(error)
|
||||
if (aggregatedSummary.length !== 0) {
|
||||
summary += `\n${indent(aggregatedSummary, 2)}`
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const causationSummary = getCausationSummary(error)
|
||||
if (causationSummary.length >= 0) {
|
||||
summary += `\n${indent(causationSummary, 2)}`
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
*/
|
||||
export function isFunction<T>(
|
||||
value: T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): value is T extends (...args: any[]) => any ? T : never {
|
||||
return typeof value === 'function'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export function indent(value: string, padding: number | string = 0) {
|
||||
if (value.length === 0) return ''
|
||||
|
||||
let pad: string
|
||||
if (typeof padding === 'number') {
|
||||
pad = ' '.repeat(padding)
|
||||
} else {
|
||||
pad = String(padding)
|
||||
}
|
||||
|
||||
const paddedValue = value.replace(/(\r?\n|\r)/g, (match) => `${match}${pad}`)
|
||||
|
||||
return `${pad}${paddedValue}`
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export type ValueOf<T> = T | (() => T | Promise<T>)
|
||||
*/
|
||||
export function retrieveValue<T>(
|
||||
value: ValueOf<T>,
|
||||
): Promise<T extends (...args: any[]) => any ? ReturnType<T> | T : T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<T extends (...args: any[]) => infer R ? R | T : T> {
|
||||
return isFunction(value) ? value() : value
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import type { ZodError } from 'zod'
|
||||
import pico from 'picocolors'
|
||||
|
||||
function isEqualArray<T>(a: T[], b: T[]) {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple boolean flag.
|
||||
*
|
||||
* @returns Object with getter to retrieve the value of the flag and toggle it.
|
||||
*/
|
||||
function flag() {
|
||||
let value = false
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return value
|
||||
},
|
||||
toggleIf(v = false) {
|
||||
if (v) value = true
|
||||
|
||||
return this
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class PathedMap<T> extends Map<PropertyKey[], T> {
|
||||
public get(path: PropertyKey[]) {
|
||||
if (super.has(path)) return super.get(path)
|
||||
|
||||
for (const [key, value] of this.entries()) {
|
||||
if (isEqualArray(key, path)) return value
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
public set(path: PropertyKey[], value: T) {
|
||||
this.delete(path)
|
||||
super.set(path, value)
|
||||
return this
|
||||
}
|
||||
|
||||
public delete(path: PropertyKey[]) {
|
||||
const deletions = flag()
|
||||
deletions.toggleIf(super.delete(path))
|
||||
|
||||
for (const [key] of this.entries()) {
|
||||
if (isEqualArray(key, path)) {
|
||||
deletions.toggleIf(super.delete(key))
|
||||
}
|
||||
}
|
||||
|
||||
return deletions.value
|
||||
}
|
||||
|
||||
public has(path: PropertyKey[]) {
|
||||
if (super.has(path)) return true
|
||||
|
||||
for (const key of this.keys()) {
|
||||
if (isEqualArray(key, path)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public getOrSet(path: PropertyKey[], initializer: () => T) {
|
||||
let v: T | undefined = this.get(path)
|
||||
if (v !== undefined) return v
|
||||
v = initializer()
|
||||
super.set(path, v)
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Property name to best of our ability. In actuality property keys rules are
|
||||
* far more complex.
|
||||
*/
|
||||
const propertyKeyRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
||||
|
||||
/**
|
||||
* Converts property name to a readable form.
|
||||
*
|
||||
* @example
|
||||
* toPropertyName('hello') // => 'hello'
|
||||
* toPropretyName('dashed-key') // => '["dashed-key"]'
|
||||
* toPropertyName(Symbol('test')) // => '[Symbol(test)]'
|
||||
* toPropertyName(0) // => '[0]'
|
||||
*
|
||||
* @param prop Original property key.
|
||||
* @returns Property key in readable form.
|
||||
*/
|
||||
function toPropertyName(
|
||||
prop: PropertyKey,
|
||||
): readonly [name: string, wrapped: boolean] {
|
||||
if (
|
||||
typeof prop === 'symbol' ||
|
||||
typeof prop === 'number' ||
|
||||
!Number.isNaN(Number(prop))
|
||||
) {
|
||||
return [`[${String(prop)}]`, true]
|
||||
}
|
||||
|
||||
return propertyKeyRegex.test(prop)
|
||||
? [prop, false]
|
||||
: [`[${JSON.stringify(prop)}]`, true]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a standard property prefix for bullet points.
|
||||
*
|
||||
* @example
|
||||
* toPropertyPrefix('hello') // => '.hello: '
|
||||
* toPropertyPrefix('dashed-key') // => '.["dashed-key"]: '
|
||||
* toPropertyPrefix(Symbol('test')) // => '.[Symbol(test)]: '
|
||||
* toPropertyPrefix(0) // => '.[0]: '
|
||||
* toPropertyPrefix(undefined) // => ''
|
||||
*
|
||||
* @param prop Original property key or `undefined` for empty prefix.
|
||||
*/
|
||||
function toPropertyPrefix(prop: PropertyKey | undefined) {
|
||||
if (prop === undefined) return ''
|
||||
return `${pico.cyan(toPropertyName(prop)[0])}: `
|
||||
}
|
||||
|
||||
export function formatZodError<T>(
|
||||
e: ZodError<T>,
|
||||
{
|
||||
initialMessage = 'Validation error',
|
||||
rootPropertyName = '<root>',
|
||||
}: {
|
||||
/**
|
||||
* Initial message displayed right at the top.
|
||||
*
|
||||
* @default 'Validation error'
|
||||
*/
|
||||
initialMessage?: string
|
||||
/**
|
||||
* Name of the root property.
|
||||
*
|
||||
* @default '<root>'
|
||||
*/
|
||||
rootPropertyName?: string
|
||||
},
|
||||
) {
|
||||
const allBullets = new PathedMap<Set<string>>()
|
||||
const createSet = () => new Set<string>()
|
||||
|
||||
for (const issue of e.issues) {
|
||||
const prop = issue.path.slice(-1)[0]
|
||||
const parentPath = issue.path.slice(0, -1)
|
||||
const msg = issue.message
|
||||
|
||||
let bullet = issue.fatal === true ? pico.red('×') : pico.yellow('▲')
|
||||
bullet += ` ${toPropertyPrefix(prop)}${msg}`
|
||||
|
||||
allBullets.getOrSet(parentPath, createSet).add(bullet)
|
||||
}
|
||||
|
||||
let message = pico.bold(pico.red(initialMessage))
|
||||
|
||||
for (const [path, bullets] of allBullets.entries()) {
|
||||
const jointPath = path.reduce<string>((v, prop) => {
|
||||
const [name, wrapped] = toPropertyName(prop)
|
||||
return v + (wrapped ? name : `.${name}`)
|
||||
}, rootPropertyName)
|
||||
|
||||
let jointBullets = ''
|
||||
{
|
||||
const bulletsIt = bullets.values()
|
||||
let bullet = bulletsIt.next()
|
||||
while (!bullet.done) {
|
||||
jointBullets += ` ${bullet.value}`
|
||||
bullet = bulletsIt.next()
|
||||
if (!bullet.done) jointBullets += '\n'
|
||||
}
|
||||
}
|
||||
|
||||
message += `\n\n${pico.bold(pico.cyan(jointPath))}\n${jointBullets}`
|
||||
}
|
||||
|
||||
return message.endsWith('\n') ? message.slice(0, -1) : message
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { z as t } from 'zod'
|
||||
|
||||
/**
|
||||
* Schema based on the input value.
|
||||
*
|
||||
* @param match A function that takes an input value and returns a schema.
|
||||
* @param params Optional parameters for the returned interceptor schema, as
|
||||
* well as `no_match_error`, which is reported as an issue when matcher method
|
||||
* does not return any schema.
|
||||
* @returns An interceptor schema that mimics all schemas returned by
|
||||
* `switcher`, as they were in union.
|
||||
* @unstable This is a dirty hack.
|
||||
*/
|
||||
export function tSwitch<R extends t.ZodTypeAny>(
|
||||
match: (value: unknown) => R | undefined,
|
||||
params?: t.RawCreateParams & { no_match_error?: string },
|
||||
): t.ZodEffects<R> {
|
||||
return t.unknown(params).transform((value, ctx) => {
|
||||
const schema = match(value)
|
||||
|
||||
if (schema == null) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
params: { code: 'no_match' },
|
||||
message: params?.no_match_error ?? 'Invalid input',
|
||||
})
|
||||
|
||||
return undefined as never
|
||||
}
|
||||
|
||||
const r = schema.safeParse(value)
|
||||
|
||||
if (r.success) {
|
||||
return r.data
|
||||
} else {
|
||||
r.error.issues.forEach((i) => ctx.addIssue(i))
|
||||
return undefined as never
|
||||
}
|
||||
}) as t.ZodEffects<t.infer<R>, t.output<R>, t.input<R>>
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from 'nuxt/app'
|
||||
@@ -3,13 +3,17 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
"module": "ESNext",
|
||||
"module": "NodeNext",
|
||||
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "NodeNext",
|
||||
|
||||
"esModuleInterop": true,
|
||||
|
||||
"types": ["node"]
|
||||
"types": ["node"],
|
||||
|
||||
"allowJs": true,
|
||||
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["./build.config.ts"]
|
||||
"include": ["./build.config.ts", "./eslint.config.mjs"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
"types": ["./types/nuxt"],
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
"module": "ESNext",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
|
||||
"types": ["./types/nuxt"],
|
||||
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"#imports": ["./stubs/imports.js"]
|
||||
"#imports": ["./stubs/imports.js"],
|
||||
"#app": ["./stubs/app.js"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
+33
@@ -64,4 +64,37 @@ declare module '@vintl/nuxt-runtime/options' {
|
||||
* descriptor.
|
||||
*/
|
||||
export const parserless: boolean
|
||||
|
||||
export interface SEOOptions {
|
||||
/**
|
||||
* Whether SEO features are enabled.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled: boolean
|
||||
|
||||
/**
|
||||
* Name of the host language URL parameter, used to override the default or
|
||||
* user language.
|
||||
*
|
||||
* @default 'hl' // For example, ?hl=en to use English locale (if defined).
|
||||
*/
|
||||
hostLanguageParameter: string
|
||||
|
||||
/**
|
||||
* Base URL at which the site will be available. This is required to
|
||||
* generate absolute hreflang links. Without this, VIntl must try to guess
|
||||
* the URL based on the server request URL / current `window.location`.
|
||||
*/
|
||||
baseURL?: string
|
||||
|
||||
/** Whether to use the host language parameter for the default locale. */
|
||||
defaultLocaleHasParameter: boolean
|
||||
|
||||
/** Whether to enable `x-default` hreflang. */
|
||||
xDefaultHreflang: boolean
|
||||
}
|
||||
|
||||
/** SEO options. */
|
||||
export const seo: SEOOptions
|
||||
}
|
||||
|
||||
Generated
+12423
-6675
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -3,7 +3,7 @@
|
||||
"extends": [
|
||||
"config:base",
|
||||
"group:allNonMajor",
|
||||
":semanticCommitTypeAll(chore)"
|
||||
":semanticCommitsDisabled"
|
||||
],
|
||||
"meteor": {
|
||||
"enabled": false
|
||||
|
||||
+84
-81
@@ -1,81 +1,84 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"@vintl/nuxt#build": {
|
||||
"outputs": ["dist"]
|
||||
},
|
||||
"@vintl/nuxt#lint": {
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"@vintl-dev/pg#dev:prepare": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"inputs": ["./package.json", "./nuxt.config.ts"],
|
||||
"outputs": [".nuxt"]
|
||||
},
|
||||
"@vintl-dev/pg#build": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".output", ".nuxt", ".vercel"]
|
||||
},
|
||||
"@vintl-dev/pg#dev": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".nuxt"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/pg#start": {
|
||||
"dependsOn": ["build"],
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
},
|
||||
"@vintl-dev/pg#lint": {
|
||||
"dependsOn": ["dev:prepare"],
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"@vintl-dev/docs#dev:prepare": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"inputs": [
|
||||
"./package.json",
|
||||
"./nuxt.config.ts",
|
||||
"./app.config.ts",
|
||||
"./tokens.config.ts"
|
||||
],
|
||||
"outputs": [".nuxt"]
|
||||
},
|
||||
"@vintl-dev/docs#build": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".output", ".nuxt", ".vercel"]
|
||||
},
|
||||
"@vintl-dev/docs#dev": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".nuxt"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/docs#start": {
|
||||
"dependsOn": ["build"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/docs#lint": {
|
||||
"dependsOn": ["dev:prepare"],
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"//#prepare": {
|
||||
"dependsOn": ["@vintl-dev/pg#dev:prepare", "@vintl-dev/docs#dev:prepare"]
|
||||
},
|
||||
"//#build": {
|
||||
"dependsOn": [
|
||||
"@vintl/nuxt#build",
|
||||
"@vintl-dev/pg#build",
|
||||
"@vintl-dev/docs#build"
|
||||
]
|
||||
},
|
||||
"//#lint": {
|
||||
"dependsOn": [
|
||||
"@vintl/nuxt#lint",
|
||||
"@vintl-dev/pg#lint",
|
||||
"@vintl-dev/docs#lint"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"@vintl/nuxt#build": {
|
||||
"outputs": ["dist"]
|
||||
},
|
||||
"@vintl/nuxt#lint": {
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"@vintl-dev/pg#intl:extract": {
|
||||
"outputs": ["./locales/en-US.json"]
|
||||
},
|
||||
"@vintl-dev/pg#dev:prepare": {
|
||||
"dependsOn": ["@vintl/nuxt#build", "@vintl-dev/pg#intl:extract"],
|
||||
"inputs": ["./package.json", "./nuxt.config.ts"],
|
||||
"outputs": [".nuxt"]
|
||||
},
|
||||
"@vintl-dev/pg#build": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".output", ".nuxt", ".vercel"]
|
||||
},
|
||||
"@vintl-dev/pg#dev": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".nuxt"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/pg#start": {
|
||||
"dependsOn": ["build"],
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
},
|
||||
"@vintl-dev/pg#lint": {
|
||||
"dependsOn": ["dev:prepare"],
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"@vintl-dev/docs#dev:prepare": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"inputs": [
|
||||
"./package.json",
|
||||
"./nuxt.config.ts",
|
||||
"./app.config.ts",
|
||||
"./tokens.config.ts"
|
||||
],
|
||||
"outputs": [".nuxt"]
|
||||
},
|
||||
"@vintl-dev/docs#build": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".output", ".nuxt", ".vercel"]
|
||||
},
|
||||
"@vintl-dev/docs#dev": {
|
||||
"dependsOn": ["@vintl/nuxt#build"],
|
||||
"outputs": [".nuxt"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/docs#start": {
|
||||
"dependsOn": ["build"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@vintl-dev/docs#lint": {
|
||||
"dependsOn": ["dev:prepare"],
|
||||
"outputs": [".eslintcache"]
|
||||
},
|
||||
"//#prepare": {
|
||||
"dependsOn": ["@vintl-dev/pg#dev:prepare", "@vintl-dev/docs#dev:prepare"]
|
||||
},
|
||||
"//#build": {
|
||||
"dependsOn": [
|
||||
"@vintl/nuxt#build",
|
||||
"@vintl-dev/pg#build",
|
||||
"@vintl-dev/docs#build"
|
||||
]
|
||||
},
|
||||
"//#lint": {
|
||||
"dependsOn": [
|
||||
"@vintl/nuxt#lint",
|
||||
"@vintl-dev/pg#lint",
|
||||
"@vintl-dev/docs#lint"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user