Lazy load locales (#1362)

Closes #1345 

`index.js` size reduction: `538.71 KiB` -> `382.74 KiB`

Also sets correct html `lang` attribute.
This commit is contained in:
Lukas 2022-12-29 13:41:59 +01:00 committed by GitHub
parent 7c9644c887
commit 72df167d2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 39 deletions

View file

@ -16,8 +16,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -26,30 +26,26 @@ import PipelineFeedSidebar from '~/components/pipeline-feed/PipelineFeedSidebar.
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
export default defineComponent({ const route = useRoute();
name: 'App', const apiClient = useApiClient();
const { notify } = useNotifications();
const i18n = useI18n();
components: { // eslint-disable-next-line promise/prefer-await-to-callbacks
Navbar, apiClient.setErrorHandler((err) => {
PipelineFeedSidebar, notify({ title: err.message || i18n.t('unknown_error'), type: 'error' });
},
setup() {
const route = useRoute();
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
// eslint-disable-next-line promise/prefer-await-to-callbacks
apiClient.setErrorHandler((err) => {
notifications.notify({ title: err.message || i18n.t('unknown_error'), type: 'error' });
});
const blank = computed(() => route.meta.blank);
return { blank };
},
}); });
const blank = computed(() => route.meta.blank);
const { locale } = useI18n();
watch(
locale,
() => {
document.documentElement.setAttribute('lang', locale.value);
},
{ immediate: true },
);
</script> </script>
<style scoped> <style scoped>

View file

@ -1,14 +1,31 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment import { nextTick } from 'vue';
// @ts-ignore
import messages from '@intlify/vite-plugin-vue-i18n/messages';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import { getUserLanguage } from '~/utils/locale'; import { getUserLanguage } from '~/utils/locale';
const userLanguage = getUserLanguage();
const fallbackLocale = 'en';
export const i18n = createI18n({ export const i18n = createI18n({
locale: getUserLanguage(), locale: userLanguage,
legacy: false, legacy: false,
globalInjection: true, globalInjection: true,
fallbackLocale: 'en', fallbackLocale,
messages,
}); });
export const loadLocaleMessages = async (locale: string) => {
const { default: messages } = await import(`~/assets/locales/${locale}.json`);
i18n.global.setLocaleMessage(locale, messages);
return nextTick();
};
export const setI18nLanguage = async (lang: string): Promise<void> => {
if (!i18n.global.availableLocales.includes(lang)) {
await loadLocaleMessages(lang);
}
i18n.global.locale.value = lang;
};
loadLocaleMessages(fallbackLocale);
loadLocaleMessages(userLanguage);

View file

@ -77,7 +77,7 @@ export default defineComponent({
const { doSubmit: activateRepo, isLoading: isActivatingRepo } = useAsyncAction(async (repo: Repo) => { const { doSubmit: activateRepo, isLoading: isActivatingRepo } = useAsyncAction(async (repo: Repo) => {
repoToActivate.value = repo; repoToActivate.value = repo;
await apiClient.activateRepo(repo.owner, repo.name); await apiClient.activateRepo(repo.owner, repo.name);
notifications.notify({ title: i18n.t('repo.enabled.success'), type: 'success' }); notifications.notify({ title: i18n.t('repo.enable.success'), type: 'success' });
repoToActivate.value = undefined; repoToActivate.value = undefined;
await router.push({ name: 'repo', params: { repoName: repo.name, repoOwner: repo.owner } }); await router.push({ name: 'repo', params: { repoName: repo.name, repoOwner: repo.owner } });
}); });

View file

@ -35,6 +35,7 @@
import { useLocalStorage } from '@vueuse/core'; import { useLocalStorage } from '@vueuse/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import TimeAgo from 'javascript-time-ago'; import TimeAgo from 'javascript-time-ago';
import { SUPPORTED_LOCALES } from 'virtual:vue-i18n-supported-locales';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -42,8 +43,9 @@ import Button from '~/components/atomic/Button.vue';
import SelectField from '~/components/form/SelectField.vue'; import SelectField from '~/components/form/SelectField.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { setI18nLanguage } from '~/compositions/useI18n';
const { t, availableLocales, locale } = useI18n(); const { t, locale } = useI18n();
const apiClient = useApiClient(); const apiClient = useApiClient();
const token = ref<string | undefined>(); const token = ref<string | undefined>();
@ -70,17 +72,17 @@ const usageWithCli = `# ${t('user.shell_setup_before')}\nwoodpecker info`;
const cliDownload = 'https://github.com/woodpecker-ci/woodpecker/releases'; const cliDownload = 'https://github.com/woodpecker-ci/woodpecker/releases';
const localeOptions = computed(() => const localeOptions = computed(() =>
availableLocales.map((availableLocale) => ({ SUPPORTED_LOCALES.map((supportedLocale) => ({
value: availableLocale, value: supportedLocale,
text: new Intl.DisplayNames(availableLocale, { type: 'language' }).of(availableLocale) || availableLocale, text: new Intl.DisplayNames(supportedLocale, { type: 'language' }).of(supportedLocale) || supportedLocale,
})), })),
); );
const storedLocale = useLocalStorage('woodpecker:locale', locale.value); const storedLocale = useLocalStorage('woodpecker:locale', locale.value);
const selectedLocale = computed<string>({ const selectedLocale = computed<string>({
set(_selectedLocale) { async set(_selectedLocale) {
await setI18nLanguage(_selectedLocale);
storedLocale.value = _selectedLocale; storedLocale.value = _selectedLocale;
locale.value = _selectedLocale;
dayjs.locale(_selectedLocale); dayjs.locale(_selectedLocale);
TimeAgo.setDefaultLocale(_selectedLocale); TimeAgo.setDefaultLocale(_selectedLocale);
}, },

View file

@ -1 +1,5 @@
// / <reference types="vite/client" /> // / <reference types="vite/client" />
declare module 'virtual:vue-i18n-supported-locales' {
export const SUPPORTED_LOCALES: string[];
}

View file

@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import vueI18n from '@intlify/vite-plugin-vue-i18n'; import vueI18n from '@intlify/vite-plugin-vue-i18n';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { readdirSync } from 'fs';
import path from 'path'; import path from 'path';
import IconsResolver from 'unplugin-icons/resolver'; import IconsResolver from 'unplugin-icons/resolver';
import Icons from 'unplugin-icons/vite'; import Icons from 'unplugin-icons/vite';
@ -30,11 +31,33 @@ export default defineConfig({
vueI18n({ vueI18n({
include: path.resolve(__dirname, 'src/assets/locales/**'), include: path.resolve(__dirname, 'src/assets/locales/**'),
}), }),
(() => {
const virtualModuleId = 'virtual:vue-i18n-supported-locales';
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
const filenames = readdirSync('src/assets/locales/').map((filename) => filename.replace('.json', ''));
return {
name: 'vue-i18n-supported-locales',
// eslint-disable-next-line consistent-return
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
// eslint-disable-next-line consistent-return
load(id) {
if (id === resolvedVirtualModuleId) {
return `export const SUPPORTED_LOCALES = ${JSON.stringify(filenames)}`;
}
},
};
})(),
WindiCSS(), WindiCSS(),
Icons(), Icons({}),
svgLoader(), svgLoader(),
Components({ Components({
resolvers: IconsResolver(), resolvers: [IconsResolver()],
}), }),
woodpeckerInfoPlugin(), woodpeckerInfoPlugin(),
prismjs({ prismjs({