diff --git a/docs/docs/92-development/07-translations.md b/docs/docs/92-development/07-translations.md
index 9b39c40a0..6ddd9f1eb 100644
--- a/docs/docs/92-development/07-translations.md
+++ b/docs/docs/92-development/07-translations.md
@@ -1,17 +1,9 @@
# Translations
-Woodpecker uses [Vue I18n](https://vue-i18n.intlify.dev/) as translation library, thus you can easily translate the web UI into your language. Therefore, copy the file `web/src/assets/locales/en.json` to the same path with your language's code and `.json` as name.
-Then, translate content of this file, but only the values:
+To translate the web UI into your language, we have [our own Weblate instance](https://translate.woodpecker-ci.org/). Please register there and translate Woodpecker into your language. **We won't accept PRs changing any language except English.**
-```json
-{
- "dont_translate": "Only translate this text"
-}
-```
+
+
+
-To add support for time formatting, import the language into two files:
-
-1. `web/src/compositions/useDate.ts`: Just add a line like `import 'dayjs/locale/en';` to the first block of `import` statements and replace `en` with your language's code.
-2. `web/src/utils/timeAgo.ts`: Add a line like `import en from 'javascript-time-ago/locale/en.json';` to the other `import`-statements and replace both `en`s with your language's code. Then, add the line `TimeAgo.addDefaultLocale(en);` to the other lines of them, and replace `en` with your language's code.
-
-Then, the web UI should be available in your language. You should open a pull request to our repository to get your changes into the next release.
+Woodpecker uses [Vue I18n](https://vue-i18n.intlify.dev/) as translation library.
diff --git a/web/.gitignore b/web/.gitignore
index d451ff16c..f16bcb46c 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -3,3 +3,4 @@ node_modules
dist
dist-ssr
*.local
+src/assets/timeAgoLocales
diff --git a/web/src/components/admin/settings/AdminAgentsTab.vue b/web/src/components/admin/settings/AdminAgentsTab.vue
index f5074d6a4..ac94261d2 100644
--- a/web/src/components/admin/settings/AdminAgentsTab.vue
+++ b/web/src/components/admin/settings/AdminAgentsTab.vue
@@ -140,11 +140,12 @@ import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
+import useTimeAgo from '~/compositions/useTimeAgo';
import { Agent } from '~/lib/api/types';
-import timeAgo from '~/utils/timeAgo';
const apiClient = useApiClient();
const notifications = useNotifications();
+const timeAgo = useTimeAgo();
const { t } = useI18n();
const selectedAgent = ref>();
diff --git a/web/src/components/pipeline-feed/PipelineFeedItem.vue b/web/src/components/pipeline-feed/PipelineFeedItem.vue
index 321394c5a..2e162f63f 100644
--- a/web/src/components/pipeline-feed/PipelineFeedItem.vue
+++ b/web/src/components/pipeline-feed/PipelineFeedItem.vue
@@ -10,7 +10,7 @@
{{ since }}
{{ $t('created') }} {{ created }}{{ $t('repo.pipeline.created') }} {{ created }}
diff --git a/web/src/compositions/useDate.ts b/web/src/compositions/useDate.ts
index 5827c00ac..5c0bddb87 100644
--- a/web/src/compositions/useDate.ts
+++ b/web/src/compositions/useDate.ts
@@ -1,19 +1,12 @@
-import 'dayjs/locale/en';
-import 'dayjs/locale/lv';
-import 'dayjs/locale/de';
-
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { useI18n } from 'vue-i18n';
-import { getUserLanguage } from '~/utils/locale';
-
dayjs.extend(timezone);
dayjs.extend(utc);
dayjs.extend(advancedFormat);
-dayjs.locale(getUserLanguage());
export function useDate() {
function toLocaleString(date: Date) {
diff --git a/web/src/compositions/useI18n.ts b/web/src/compositions/useI18n.ts
index ce99e68b9..c3bc1fb3f 100644
--- a/web/src/compositions/useI18n.ts
+++ b/web/src/compositions/useI18n.ts
@@ -3,6 +3,8 @@ import { createI18n } from 'vue-i18n';
import { getUserLanguage } from '~/utils/locale';
+import { loadTimeAgoLocale } from './useTimeAgo';
+
const userLanguage = getUserLanguage();
const fallbackLocale = 'en';
export const i18n = createI18n({
@@ -17,6 +19,8 @@ export const loadLocaleMessages = async (locale: string) => {
i18n.global.setLocaleMessage(locale, messages);
+ loadTimeAgoLocale(locale);
+
return nextTick();
};
diff --git a/web/src/compositions/usePipeline.ts b/web/src/compositions/usePipeline.ts
index 2724e69e1..1f44bc9a7 100644
--- a/web/src/compositions/usePipeline.ts
+++ b/web/src/compositions/usePipeline.ts
@@ -6,7 +6,8 @@ import { useElapsedTime } from '~/compositions/useElapsedTime';
import { Pipeline } from '~/lib/api/types';
import { prettyDuration } from '~/utils/duration';
import { convertEmojis } from '~/utils/emoji';
-import timeAgo from '~/utils/timeAgo';
+
+import useTimeAgo from './useTimeAgo';
const { toLocaleString } = useDate();
@@ -27,6 +28,7 @@ export default (pipeline: Ref) => {
const { time: sinceElapsed } = useElapsedTime(sinceUnderOneHour, sinceRaw);
const i18n = useI18n();
+ const timeAgo = useTimeAgo();
const since = computed(() => {
if (sinceRaw.value === 0) {
return i18n.t('time.not_started');
diff --git a/web/src/compositions/useTimeAgo.ts b/web/src/compositions/useTimeAgo.ts
new file mode 100644
index 000000000..2c5b67a7e
--- /dev/null
+++ b/web/src/compositions/useTimeAgo.ts
@@ -0,0 +1,17 @@
+import TimeAgo from 'javascript-time-ago';
+import en from 'javascript-time-ago/locale/en.json';
+
+import { getUserLanguage } from '~/utils/locale';
+
+TimeAgo.addDefaultLocale(en);
+
+const addedLocales = ['en'];
+
+export default () => new TimeAgo(getUserLanguage());
+export async function loadTimeAgoLocale(locale: string) {
+ if (!addedLocales.includes(locale)) {
+ const { default: timeAgoLocale } = await import(`~/assets/timeAgoLocales/${locale}.js`);
+ TimeAgo.addLocale(timeAgoLocale);
+ addedLocales.push(locale);
+ }
+}
diff --git a/web/src/utils/timeAgo.ts b/web/src/utils/timeAgo.ts
deleted file mode 100644
index 6724b3fec..000000000
--- a/web/src/utils/timeAgo.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import TimeAgo from 'javascript-time-ago';
-import de from 'javascript-time-ago/locale/de.json';
-import en from 'javascript-time-ago/locale/en.json';
-import lv from 'javascript-time-ago/locale/lv.json';
-
-import { getUserLanguage } from '~/utils/locale';
-
-TimeAgo.addDefaultLocale(en);
-TimeAgo.addLocale(de);
-TimeAgo.addLocale(lv);
-
-const timeAgo = new TimeAgo(getUserLanguage());
-
-export default timeAgo;
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 34b5bdbc6..ff89989c8 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,7 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import vue from '@vitejs/plugin-vue';
-import { readdirSync } from 'fs';
+import { copyFile, existsSync, mkdirSync, readdirSync } from 'fs';
import path from 'path';
import IconsResolver from 'unplugin-icons/resolver';
import Icons from 'unplugin-icons/vite';
@@ -38,6 +38,39 @@ export default defineConfig({
const filenames = readdirSync('src/assets/locales/').map((filename) => filename.replace('.json', ''));
+ if (!existsSync('src/assets/timeAgoLocales')) {
+ mkdirSync('src/assets/timeAgoLocales');
+ }
+
+ filenames.forEach((name) => {
+ // copy timeAgo language
+ if (name === 'zh-Hans') {
+ // zh-Hans is called zh in javascript-time-ago, so we need to rename this
+ copyFile(
+ 'node_modules/javascript-time-ago/locale/zh.json.js',
+ 'src/assets/timeAgoLocales/zh-Hans.js',
+ // eslint-disable-next-line promise/prefer-await-to-callbacks
+ (err) => {
+ if (err) {
+ throw err;
+ }
+ },
+ );
+ } else if (name !== 'en') {
+ // English is always directly loaded (compiled by Vite) and thus not copied
+ copyFile(
+ `node_modules/javascript-time-ago/locale/${name}.json.js`,
+ `src/assets/timeAgoLocales/${name}.js`,
+ // eslint-disable-next-line promise/prefer-await-to-callbacks
+ (err) => {
+ if (err) {
+ throw err;
+ }
+ },
+ );
+ }
+ });
+
return {
name: 'vue-i18n-supported-locales',
// eslint-disable-next-line consistent-return