Skip to content

Commit

Permalink
Add support for translations using VIntl
Browse files Browse the repository at this point in the history
Adds support for translations using VIntl module as a peer dependency.

Peer dependencies are dependencies that have to be provided by the
consumer, which means that `@vintl/vintl` has to be installed and set up
in Theseus and any other consumers.

Translations for Omorphia are done in Omorphia to prevent duplicate
work, and the consumer has to import the locale files like they do for
with the styles already. Those locale files have to be registered in
VIntl plugin and processed using `@vintl/unplugin`.

`@vintl/unplugin` is a Unplugin (which means it works with Rollup, Vite,
and even Webpack) that takes in locale files generated by
`@formatjs/cli` utility and transforms them into the proper ES modules.
This allows consumers to get rid of the `intl-messageformat-parser`,
which has a huge toll on the bundle size otherwise, at the cost of
inability to parse custom messages.

To give an example of how this can be approached, docs have been updated
to support changing the language used in examples. Knossos is already
doing all this automatically by using `@vintl/nuxt`.

`@formatjs/cli` allows to define messages directly and the code and then
automatically extract them into the locale file. Collocation makes it
easier to maintain the messages and see them right in the code.

Translations are done using Crowdin, so Crowdin needs to be connected to
this repository, just like it was connected to Knossos [^1]. This commit
also introduces a config that allows to do just that.

[^1]: modrinth/knossos#738 (comment)
  • Loading branch information
brawaru committed Oct 24, 2023
1 parent 91f79d2 commit 8d9e937
Show file tree
Hide file tree
Showing 12 changed files with 697 additions and 44 deletions.
8 changes: 8 additions & 0 deletions crowdin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
project_id: 518556
preserve_hierarchy: true
commit_message: '[ci skip]'

files:
- source: /locales/en-US/*
dest: /%original_file_name%
translation: /locales/%locale%/%original_file_name%
39 changes: 38 additions & 1 deletion docs/.vitepress/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { resolve } from 'path'
import { resolve, basename } from 'path'
import svgLoader from 'vite-svg-loader'
import eslintPlugin from 'vite-plugin-eslint'
import { icuMessages } from '@vintl/unplugin/vite'
import virtual from '@rollup/plugin-virtual'
import { globSync } from 'glob'

/** @type {import('vitepress').SiteConfig} */
export default {
title: 'Omorphia',
description: 'A components library used for Modrinth.',
Expand Down Expand Up @@ -75,11 +79,44 @@ export default {
},
}),
eslintPlugin(),
icuMessages({
filter(id) {
// console.log('filter', id)
return id.endsWith('.json?messages')
},
pluginsWrapping: true,
}),
virtual({
'@modrinth/omorphia-dev/locales/index.js': (() => {
const localeDirs = globSync('../../locales/*', { cwd: __dirname, absolute: true })
let fileContents = ''
fileContents += 'export const localeDefinitions = Object.create(null);\n'
for (const localeDir of localeDirs) {
const tag = basename(localeDir)
fileContents += `localeDefinitions[${JSON.stringify(tag)}] = {\n`
fileContents += '\tasync importFunction() {\n'
fileContents += `\t\tconst messages = Object.create(null);\n`
for (const filePath of globSync('*', { cwd: localeDir, absolute: true })) {
const fileName = basename(filePath)
if (fileName === 'index.json') {
fileContents += `\t\tObject.assign(messages, await import(${JSON.stringify(
`${filePath}?messages`
)}).then((mod) => mod['default']));\n`
}
}
fileContents += '\t\treturn { messages }\n'
fileContents += '\t},\n'
fileContents += '}\n'
}
return fileContents
})(),
}),
],
resolve: {
alias: {
'@': resolve(__dirname, '../../lib'),
omorphia: resolve(__dirname, '../../lib'),
'@formatjs/icu-messageformat-parser': '@formatjs/icu-messageformat-parser/lib/no-parser',
},
dedupe: ['vue'],
},
Expand Down
15 changes: 15 additions & 0 deletions docs/.vitepress/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference types="vite/client" />

declare module '@modrinth/omorphia-dev/locales/index.js' {
interface LocaleExport {
messages: Record<string, any[]>
}

interface LocaleDefinition {
importFunction(): Promise<LocaleExport>
}

const localeDefinitions: Partial<Record<string, LocaleDefinition>>

export { localeDefinitions }
}
73 changes: 73 additions & 0 deletions docs/.vitepress/theme/LanguageSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useVIntl } from '@vintl/vintl'
import { DropdownSelect } from 'omorphia'
import { computed, ref } from 'vue'
const { $locales, $config, changeLocale } = useVIntl()
const getLocaleDisplayName = (() => {
const cache = new Map<string, Intl.DisplayNames>()
return function getLocaleDisplayName(locale: string) {
let displayNames = cache.get(locale)
if (displayNames == null) {
displayNames = new Intl.DisplayNames(locale, {
type: 'language',
languageDisplay: 'standard',
})
cache.set(locale, displayNames)
}
return displayNames.of(locale)
}
})()
const isChanging = ref(false)
const currentLocale = computed({
get() {
return $config.locale
},
async set(value) {
if (isChanging.value) return
try {
isChanging.value = true
await changeLocale(value)
} finally {
isChanging.value = false
}
},
})
</script>
<template>
<div class="LanguageSwitcher">
<h2 class="title">Playground language</h2>

<DropdownSelect
class="locale-dropdown"
name="locale"
v-model="currentLocale"
placeholder="Change language"
:disabled="isChanging"
:options="Array.from($locales).map(([{ tag }]) => tag)"
:display-name="(locale: string) => getLocaleDisplayName(locale)"
/>
</div>
</template>
<style scoped>
.LanguageSwitcher {
padding-block: 18px;
border-bottom: 1px solid var(--vp-c-divider);
}
.LanguageSwitcher .title {
font-weight: 700;
font-size: 14px;
color: var(--vp-c-text-1);
}
.LanguageSwitcher .locale-dropdown {
width: 200px;
font-size: 14px;
}
</style>
38 changes: 37 additions & 1 deletion docs/.vitepress/theme/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
import DefaultTheme from 'vitepress/theme'
import { localeDefinitions } from '@modrinth/omorphia-dev/locales/index.js'
import { createPlugin } from '@vintl/vintl/plugin'
import Omorphia from 'omorphia'
import DefaultTheme from 'vitepress/theme'
import { createVNode } from 'vue'
import DemoContainer from './DemoContainer.vue'
import LanguageSwitcher from './LanguageSwitcher.vue'

import './compat.scss'

/** @type {import('vitepress').Theme} */
export default {
...DefaultTheme,
enhanceApp(ctx) {
ctx.app.use(Omorphia)
ctx.app.component('DemoContainer', DemoContainer)
ctx.app.use(
createPlugin({
controllerOpts: {
locales: Object.keys(localeDefinitions).map((tag) => ({ tag })),
listen: {
async localeload(event) {
const locale = event.locale.tag
if (!Object.hasOwn(localeDefinitions, locale)) {
throw new Error(`Unknown locale: ${locale}`)
}

try {
const { messages } = await localeDefinitions[locale].importFunction()
event.addMessages(messages)
} catch (err) {
console.error(`Failed to load locale: ${locale}`, err)
}
},
},
defaultMessageOrder: ['locale', 'descriptor'],
},
globalMixin: false,
})
)
},
Layout() {
return createVNode(DefaultTheme.Layout, null, {
'sidebar-nav-before'() {
return createVNode(LanguageSwitcher)
},
})
},
}
8 changes: 7 additions & 1 deletion lib/components/base/CopyCode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
<button class="code" :class="{ copied }" :title="formatMessage(copiedMessage)" @click="copyText">
<span>{{ text }}</span>
<CheckIcon v-if="copied" />
<ClipboardCopyIcon v-else />
Expand All @@ -8,6 +8,12 @@

<script setup>
import { CheckIcon, ClipboardCopyIcon } from '@'
import { useVIntl, defineMessage } from '@vintl/vintl'
const copiedMessage = defineMessage({
id: 'omorphia.component.copy.action.copy',
defaultMessage: 'Copy code to clipboard',
})
const { formatMessage } = useVIntl()
</script>

<script>
Expand Down
5 changes: 5 additions & 0 deletions locales/en-US/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"omorphia.component.copy.action.copy": {
"defaultMessage": "Copy code to clipboard"
}
}
5 changes: 5 additions & 0 deletions locales/uk/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"omorphia.component.copy.action.copy": {
"defaultMessage": "Скопіювати код до буфера обміну"
}
}
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"type": "module",
"version": "0.6.2",
"files": [
"dist"
"dist",
"locales"
],
"main": "./dist/omorphia.umd.cjs",
"module": "./dist/omorphia.js",
Expand All @@ -13,6 +14,9 @@
"import": "./dist/omorphia.js",
"require": "./dist/omorphia.cjs"
},
"./locales/*.json": {
"import": "./locales/*.json"
},
"./dist/style.css": "./dist/style.css"
},
"scripts": {
Expand All @@ -22,7 +26,8 @@
"fix": "eslint --fix --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue . && prettier --write .",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"intl:extract": "formatjs extract \"lib/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"lib/**/*.d.ts\" --out-file locales/en-US/index.json --preserve-whitespace"
},
"dependencies": {
"@codemirror/commands": "^6.3.0",
Expand All @@ -42,13 +47,18 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@formatjs/cli": "^6.2.1",
"@rollup/plugin-virtual": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vintl/unplugin": "^1.5.1",
"@vintl/vintl": "^4.3.0",
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.14.1",
"glob": "^10.3.10",
"postcss": "^8.4.24",
"postcss-prefix-selector": "^1.16.0",
"prettier": "^2.8.8",
Expand Down
Loading

0 comments on commit 8d9e937

Please sign in to comment.