refactor: introduce trString for frontend (#38741)

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
wxiaoguang
2026-08-03 04:40:02 +00:00
committed by GitHub
co-authored by silverwind delvh
parent cb4c65f035
commit c2ebe3724f
12 changed files with 51 additions and 21 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
import WorkflowGraph from './WorkflowGraph.vue';
import type {ActionRunViewStore} from "./ActionRunView.ts";
import {computed, onBeforeUnmount, onMounted, toRefs} from "vue";
import type {ActionRunViewStore} from './ActionRunView.ts';
import {computed, onBeforeUnmount, onMounted, toRefs} from 'vue';
import {trString} from '../modules/i18n.ts';
defineOptions({
name: 'ActionRunSummaryView',
@@ -41,7 +42,7 @@ const triggerUser = computed(() => {
const triggerLabel = computed(() => {
if (isRerun.value) return locale.rerunTriggered;
return locale.triggeredVia.replace('%s', run.value.triggerEvent);
return trString(locale.triggeredVia, run.value.triggerEvent);
});
const artifactsDisplay = computed(() => props.artifactCount > 0 ? String(props.artifactCount) : '');
+2 -1
View File
@@ -9,6 +9,7 @@ import ActionRunJobView from './ActionRunJobView.vue';
import type {ActionsJob, ActionsRunAttempt} from '../modules/gitea-actions.ts';
import {buildJobsByParentJobID, createActionRunViewStore} from './ActionRunView.ts';
import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts';
import {trString} from '../modules/i18n.ts';
defineOptions({
name: 'RepoActionView',
@@ -115,7 +116,7 @@ function approveRun() {
}
async function deleteArtifact(name: string) {
if (!window.confirm(locale.confirmDeleteArtifact.replace('%s', name))) return;
if (!window.confirm(trString(locale.confirmDeleteArtifact, name))) return;
await DELETE(buildArtifactLink(name));
await store.forceReloadCurrentRun();
}
@@ -5,6 +5,7 @@ import {showErrorToast} from '../modules/toast.ts';
import {GET} from '../modules/fetch.ts';
import {pathEscapeSegments} from '../utils/url.ts';
import type {GitRefType} from '../types.ts';
import {trString} from '../modules/i18n.ts';
type ListItem = {
selected: boolean;
@@ -121,6 +122,7 @@ export default defineComponent({
},
methods: {
trString,
selectItem(item: ListItem) {
this.menuVisible = false;
if (this.refFormActionTemplate) {
@@ -261,11 +263,11 @@ export default defineComponent({
<div class="item" v-if="showCreateNewRef" :class="{active: activeItemIndex === filteredItems.length}" :ref="'listItem' + filteredItems.length" @click="createNewRef()">
<div v-if="selectedTab === 'tags'">
<svg-icon name="octicon-tag" class="tw-mr-1"/>
<span v-text="textCreateTag.replace('%s', searchTerm)"/>
<span v-text="trString(textCreateTag, searchTerm)"/>
</div>
<div v-else>
<svg-icon name="octicon-git-branch" class="tw-mr-1"/>
<span v-text="textCreateBranch.replace('%s', searchTerm)"/>
<span v-text="trString(textCreateBranch, searchTerm)"/>
</div>
<div class="tw-text-xs">
{{ textCreateRefFrom.replace('%s', currentRefShortName) }}
+2 -2
View File
@@ -4,7 +4,7 @@ import {SvgIcon} from '../svg.ts';
import ActionStatusIcon from './ActionStatusIcon.vue';
import {localUserSettings} from '../modules/user-settings.ts';
import {isPlainClick} from '../utils/dom.ts';
import {trN} from '../modules/i18n.ts';
import {trN, trString} from '../modules/i18n.ts';
import {debounce} from '../utils/func.ts';
import type {ActionsJob} from '../modules/gitea-actions.ts';
import type {ActionRunViewStore} from './ActionRunView.ts';
@@ -113,7 +113,7 @@ const successRateLabel = computed(() => {
const graphStats = computed(() => [
trN(props.jobs.length, props.locale.graphJobsCount1, props.locale.graphJobsCountN),
trN(edges.value.length, props.locale.graphDependenciesCount1, props.locale.graphDependenciesCountN),
props.locale.graphSuccessRate.replace('%s', successRateLabel.value),
trString(props.locale.graphSuccessRate, successRateLabel.value),
].join(' • '));
const minScale = 0.3;
+2 -1
View File
@@ -1,6 +1,7 @@
import {createApp} from 'vue';
import {translateMonth, translateDay} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
import {trString} from '../modules/i18n.ts';
type HeatmapResponse = {
heatmapData: Array<[number, number]>; // [[1617235200, 2]] = [unix timestamp, count]
@@ -29,7 +30,7 @@ export async function initHeatmap() {
});
const totalFormatted = totalContributions.toLocaleString();
const textTotalContributions = el.getAttribute('data-locale-total-contributions')!.replace('%s', totalFormatted);
const textTotalContributions = trString(el.getAttribute('data-locale-total-contributions')!, totalFormatted);
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
const locale = {
+3 -3
View File
@@ -1,6 +1,7 @@
import {diffTreeStore, diffTreeStoreSetViewed} from '../modules/diff-file.ts';
import {setFileFolding} from './file-fold.ts';
import {POST} from '../modules/fetch.ts';
import {trString} from '../modules/i18n.ts';
const {pageData} = window.config;
// it is undefined on most pages, fortunately, when it is accessed by the related functions, it exists
@@ -15,9 +16,8 @@ function refreshViewedFilesSummary() {
const viewedFilesProgress = document.querySelector('#viewed-files-summary')!;
viewedFilesProgress.setAttribute('value', String(prReview.numberOfViewedFiles));
const summaryLabel = document.querySelector<HTMLElement>('#viewed-files-summary-label')!;
summaryLabel.textContent = summaryLabel.getAttribute('data-text-changed-template')!
.replace('%[1]d', String(prReview.numberOfViewedFiles))
.replace('%[2]d', String(prReview.numberOfFiles));
const trText = summaryLabel.getAttribute('data-text-changed-template')!;
summaryLabel.textContent = trString(trText, prReview.numberOfViewedFiles, prReview.numberOfFiles);
}
// Initializes a listener for viewed-file checkboxes
+2 -1
View File
@@ -1,5 +1,6 @@
import {toggleElem} from '../utils/dom.ts';
import {showFomanticModal} from '../modules/fomantic/modal.ts';
import {trString} from '../modules/i18n.ts';
export function initRepoBranchButton() {
initRepoCreateBranchButton();
@@ -36,7 +37,7 @@ function initRepoRenameBranchButton() {
toggleElem(warn, el.getAttribute('data-is-default-branch') === 'true');
const text = modal.querySelector('[data-rename-branch-to]')!;
text.textContent = text.getAttribute('data-rename-branch-to')!.replace('%s', oldBranchName);
text.textContent = trString(text.getAttribute('data-rename-branch-to')!, oldBranchName);
});
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import type {FomanticInitFunction} from '../../types.ts';
import {generateElemId, queryElems} from '../../utils/dom.ts';
import {trString} from '../i18n.ts';
const ariaPatchKey = '_giteaAriaPatchDropdown';
const fomanticDropdownFn = $.fn.dropdown;
@@ -63,7 +64,7 @@ function updateSelectionLabel(label: HTMLElement) {
const deleteIcon = label.querySelector('.delete.icon');
if (deleteIcon) {
deleteIcon.setAttribute('aria-hidden', 'false');
deleteIcon.setAttribute('aria-label', window.config.i18n.remove_label_str.replace('%s', label.getAttribute('data-value')!));
deleteIcon.setAttribute('aria-label', trString(window.config.i18n.remove_label_str, label.getAttribute('data-value')!));
deleteIcon.setAttribute('role', 'button');
}
}
+8 -1
View File
@@ -1,4 +1,4 @@
import {trN} from './i18n.ts';
import {trN, trString} from './i18n.ts';
test('trN', () => {
expect(trN(0, '%d job', '%d jobs', {lang: 'en-US'})).toEqual('0 jobs');
@@ -8,3 +8,10 @@ test('trN', () => {
// languages without a distinct singular always use the plural form
expect(trN(1, '%d job', '%d jobs', {lang: 'zh-CN'})).toEqual('1 jobs');
});
test('trString', () => {
expect(trString('from %s to %d', 'main', 12)).toBe('from main to 12');
expect(trString('from %[1]s to %[2]d', 'main', 12)).toBe('from main to 12');
expect(trString('from %s to %[2]d', 'main', 12)).toBe('from main to 12');
expect(trString('from %s to %[3]d', 'main', 12)).toBe('from main to %[3]d');
});
+17 -2
View File
@@ -2,6 +2,21 @@ import {getCurrentLocale} from '../utils.ts';
/** frontend `Locale.TrN`: pick the `_1` or `_n` form for `count` and interpolate `%d` */
export function trN(count: number, form1: string, formN: string, {lang = getCurrentLocale()}: {lang?: string} = {}): string {
const form = new Intl.PluralRules(lang).select(count) === 'one' ? form1 : formN;
return form.replace('%d', String(count));
const text = new Intl.PluralRules(lang).select(count) === 'one' ? form1 : formN;
return trString(text, String(count));
}
/**
* Fills variables in a translated string (like TrString in Golang)
* Only `%s`, `%d` or one-based position like `%[1]s` are supported.
*/
export function trString(s: string, ...args: Array<string | number>) {
// the same behavior (almost) as backend TrString
let curIdx = 0;
return s.replace(/%%|%(?:\[([1-9]\d*)\])?([sd])/g, (match, indexed: string) => {
if (match === '%%') return '%';
const argIndex = indexed ? Number(indexed) - 1 : curIdx++;
if (argIndex < 0 || argIndex >= args.length) return match;
return String(args[argIndex]);
});
}