fix(locales): Replace hardcoded strings (#37788)

The Workflow Dependencies graph in the Actions run details view had
hard-coded English strings.
Also in projects view and contributors view I found some hard-coded
strings.
  
The other items in the issue #37787 (Summary / All jobs / Run Details /
Workflow file / Triggered via / Total duration) were already wired
through ctx.Locale.Tr; their translations just need to land in the
non-English locale_*.json files via the translation pipeline.



Fixes #37787

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
This commit is contained in:
Nicolas
2026-05-30 01:50:55 +02:00
committed by GitHub
parent d07a42e777
commit a342206a21
11 changed files with 74 additions and 20 deletions

View File

@@ -59,6 +59,7 @@ onBeforeUnmount(() => {
:jobs="run.jobs"
:run-link="run.link"
:workflow-id="run.workflowID"
:locale="locale"
/>
</div>
</template>

View File

@@ -270,7 +270,7 @@ export default defineComponent({
plugins: {
title: {
display: type === 'main',
text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
text: this.locale.chartZoomHint,
position: 'top',
align: 'center',
},

View File

@@ -4,6 +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 {debounce} from 'throttle-debounce';
import type {ActionsJob, ActionsStatus} from '../modules/gitea-actions.ts';
import type {ActionRunViewStore} from './ActionRunView.ts';
@@ -43,6 +44,7 @@ const props = defineProps<{
jobs: ActionsJob[];
runLink: string;
workflowId: string;
locale: Record<string, string>;
}>()
const settingKeyStates = 'actions-graph-states';
@@ -344,6 +346,12 @@ const graphMetrics = 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', graphMetrics.value.successRate),
].join(' • '))
const nodeHeight = 52;
const verticalSpacing = 90;
const margin = 40;
@@ -543,27 +551,22 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
<template>
<div class="workflow-graph" v-if="jobs.length > 0">
<div class="graph-header">
<h4 class="graph-title">Workflow Dependencies</h4>
<div class="graph-stats">
{{ jobs.length }} jobs {{ edges.length }} dependencies
<span v-if="graphMetrics">
<span class="graph-metrics">{{ graphMetrics.successRate }} success</span>
</span>
</div>
<h4 class="graph-title">{{ locale.workflowDependencies }}</h4>
<div class="graph-stats">{{ graphStats }}</div>
<div class="flex-text-block">
<button
type="button"
@click="zoomIn"
class="ui compact tiny icon button"
:disabled="!canZoomIn"
:title="canZoomIn ? 'Zoom in (Ctrl/Cmd + scroll on graph)' : 'Already at 100% zoom'"
:title="canZoomIn ? locale.graphZoomIn : locale.graphZoomMax"
>
<SvgIcon name="octicon-zoom-in" :size="12"/>
</button>
<button type="button" @click="resetView" class="ui compact tiny icon button" title="Reset view">
<button type="button" @click="resetView" class="ui compact tiny icon button" :title="locale.graphResetView">
<SvgIcon name="octicon-sync" :size="12"/>
</button>
<button type="button" @click="zoomOut" class="ui compact tiny icon button" title="Zoom out (Ctrl/Cmd + scroll on graph)">
<button type="button" @click="zoomOut" class="ui compact tiny icon button" :title="locale.graphZoomOut">
<SvgIcon name="octicon-zoom-out" :size="12"/>
</button>
</div>
@@ -701,11 +704,6 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
white-space: nowrap;
}
.graph-metrics {
color: var(--color-primary);
font-weight: var(--font-weight-medium);
}
.graph-container {
flex: 1;
overflow: hidden;

View File

@@ -20,6 +20,7 @@ export async function initRepoContributors() {
loadingTitle: el.getAttribute('data-locale-loading-title'),
loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
loadingInfo: el.getAttribute('data-locale-loading-info'),
chartZoomHint: el.getAttribute('data-locale-chart-zoom-hint'),
},
});
View.mount(el);

View File

@@ -53,6 +53,16 @@ export function initRepositoryActionView() {
logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
workflowFile: el.getAttribute('data-locale-workflow-file'),
runDetails: el.getAttribute('data-locale-run-details'),
workflowDependencies: el.getAttribute('data-locale-workflow-dependencies'),
graphJobsCount1: el.getAttribute('data-locale-graph-jobs-count-1'),
graphJobsCountN: el.getAttribute('data-locale-graph-jobs-count-n'),
graphDependenciesCount1: el.getAttribute('data-locale-graph-dependencies-count-1'),
graphDependenciesCountN: el.getAttribute('data-locale-graph-dependencies-count-n'),
graphSuccessRate: el.getAttribute('data-locale-graph-success-rate'),
graphZoomIn: el.getAttribute('data-locale-graph-zoom-in'),
graphZoomMax: el.getAttribute('data-locale-graph-zoom-max'),
graphZoomOut: el.getAttribute('data-locale-graph-zoom-out'),
graphResetView: el.getAttribute('data-locale-graph-reset-view'),
},
});
view.mount(el);

View File

@@ -0,0 +1,15 @@
import {trN} from './i18n.ts';
import {getCurrentLocale} from '../utils.ts';
vi.mock('../utils.ts', () => ({getCurrentLocale: vi.fn()}));
test('trN', () => {
vi.mocked(getCurrentLocale).mockReturnValue('en-US');
expect(trN(0, '%d job', '%d jobs')).toEqual('0 jobs');
expect(trN(1, '%d job', '%d jobs')).toEqual('1 job');
expect(trN(2, '%d job', '%d jobs')).toEqual('2 jobs');
expect(trN(1000, '%d job', '%d jobs')).toEqual('1000 jobs');
// languages without a distinct singular always use the plural form
vi.mocked(getCurrentLocale).mockReturnValue('zh-CN');
expect(trN(1, '%d job', '%d jobs')).toEqual('1 jobs');
});

View File

@@ -0,0 +1,7 @@
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): string {
const form = new Intl.PluralRules(getCurrentLocale()).select(count) === 'one' ? form1 : formN;
return form.replace('%d', String(count));
}