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" -} -``` + + Translation status + -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('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