Skip to content

Commit 17d6163

Browse files
committed
Adds i18n support for UI labels and placeholders
Improves localization by translating UI text, labels, and placeholders throughout the site using new translation keys and i18n utilities. Enhances accessibility for non-English users and standardizes text rendering via the translation function. Refines component structure and styling for better readability and maintainability.
1 parent c2868fd commit 17d6163

12 files changed

Lines changed: 200 additions & 80 deletions

File tree

packages/web/src/lib/components/matches/MatchScore.svelte

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@
7979
$: blueOPR1 = teamOprMap?.[blueTeamNums[0] ?? -1] ?? null;
8080
$: blueOPR2 = teamOprMap?.[blueTeamNums[1] ?? -1] ?? null;
8181
82-
$: showPrediction =
83-
!match.scores &&
82+
$: hasOpr =
8483
!!teamOprMap &&
85-
[redOPR1, redOPR2, blueOPR1, blueOPR2].every(
84+
[redOPR1, redOPR2, blueOPR1, blueOPR2].some(
8685
(val) => typeof val == "number" && !Number.isNaN(val)
8786
);
87+
88+
$: safeRedOPR1 = typeof redOPR1 == "number" && !Number.isNaN(redOPR1) ? redOPR1 : 0;
89+
$: safeRedOPR2 = typeof redOPR2 == "number" && !Number.isNaN(redOPR2) ? redOPR2 : 0;
90+
$: safeBlueOPR1 = typeof blueOPR1 == "number" && !Number.isNaN(blueOPR1) ? blueOPR1 : 0;
91+
$: safeBlueOPR2 = typeof blueOPR2 == "number" && !Number.isNaN(blueOPR2) ? blueOPR2 : 0;
92+
93+
$: showPrediction = hasOpr;
8894
</script>
8995

9096
<td
@@ -103,39 +109,54 @@
103109
</div>
104110
<div class="score">
105111
{#if match.scores == undefined}
106-
<div class="upcoming">
112+
<div class="score-row time-row">
107113
<span class="time">{prettyPrintTimeString(match.scheduledStartTime, timeZone)}</span>
108-
{#if showPrediction}
109-
<MatchPrediction {redOPR1} {redOPR2} {blueOPR1} {blueOPR2} />
110-
{/if}
111114
</div>
112115
{:else if "red" in match.scores}
113-
<div class="left" class:winner={winner == Alliance.Red} class:tie={winner == "Tie"}>
114-
<!-- // Help: Season Specific -->
115-
{#if match.season == Season.Decode && match.tournamentLevel == TournamentLevel.Quals}
116-
<div class="dots red">
117-
{#each new Array(rps[0] + 3 * +(winner == Alliance.Red) + +(winner == "Tie")) as _, i}
118-
<div class="dot" style="right: calc({i} * var(--dot-stride))" />
119-
{/each}
120-
</div>
121-
{/if}
122-
123-
{match.scores.red.totalPoints}
124-
</div>
125-
<div class="minus">-</div>
126-
<div class="right" class:winner={winner == Alliance.Blue} class:tie={winner == "Tie"}>
127-
{match.scores.blue.totalPoints}
128-
129-
{#if match.season == Season.Decode && match.tournamentLevel == TournamentLevel.Quals}
130-
<div class="dots blue">
131-
{#each new Array(rps[1] + 3 * +(winner == Alliance.Blue) + +(winner == "Tie")) as _, i}
132-
<div class="dot" style="left: calc({i} * var(--dot-stride))" />
133-
{/each}
134-
</div>
135-
{/if}
116+
<div class="score-row score-values">
117+
<div class="left" class:winner={winner == Alliance.Red} class:tie={winner == "Tie"}>
118+
<!-- // Help: Season Specific -->
119+
{#if match.season == Season.Decode && match.tournamentLevel == TournamentLevel.Quals}
120+
<div class="dots red">
121+
{#each new Array(rps[0] + 3 * +(winner == Alliance.Red) + +(winner == "Tie")) as _, i}
122+
<div class="dot" style="right: calc({i} * var(--dot-stride))" />
123+
{/each}
124+
</div>
125+
{/if}
126+
127+
{match.scores.red.totalPoints}
128+
</div>
129+
<div class="minus">-</div>
130+
<div
131+
class="right"
132+
class:winner={winner == Alliance.Blue}
133+
class:tie={winner == "Tie"}
134+
>
135+
{match.scores.blue.totalPoints}
136+
137+
{#if match.season == Season.Decode && match.tournamentLevel == TournamentLevel.Quals}
138+
<div class="dots blue">
139+
{#each new Array(rps[1] + 3 * +(winner == Alliance.Blue) + +(winner == "Tie")) as _, i}
140+
<div class="dot" style="left: calc({i} * var(--dot-stride))" />
141+
{/each}
142+
</div>
143+
{/if}
144+
</div>
136145
</div>
137146
{:else}
138-
<b>{match.scores.totalPoints}</b>
147+
<div class="score-row score-values">
148+
<b>{match.scores.totalPoints}</b>
149+
</div>
150+
{/if}
151+
{#if showPrediction}
152+
<div class="prediction-row">
153+
<MatchPrediction
154+
redOPR1={safeRedOPR1}
155+
redOPR2={safeRedOPR2}
156+
blueOPR1={safeBlueOPR1}
157+
blueOPR2={safeBlueOPR2}
158+
/>
159+
</div>
139160
{/if}
140161
</div>
141162
</td>
@@ -225,45 +246,49 @@
225246
}
226247
227248
.score {
249+
display: flex;
250+
flex-direction: column;
251+
align-items: center;
252+
gap: var(--sm-gap);
253+
}
254+
255+
.score-row {
228256
display: flex;
229257
justify-content: space-around;
230258
gap: var(--sm-gap);
231259
align-items: center;
232-
flex-wrap: wrap;
260+
width: 100%;
261+
}
262+
263+
.score-row.time-row {
264+
justify-content: center;
233265
}
234266
235-
.score .left {
267+
.score-row .left {
236268
width: 100%;
237269
text-align: right;
238270
}
239271
240-
.score .left.winner {
272+
.score-row .left.winner {
241273
font-weight: bold;
242274
color: var(--red-team-text-color);
243275
}
244276
245-
.score .right {
277+
.score-row .right {
246278
width: 100%;
247279
text-align: left;
248280
}
249281
250-
.score .right.winner {
282+
.score-row .right.winner {
251283
font-weight: bold;
252284
color: var(--blue-team-text-color);
253285
}
254286
255-
.score .tie {
287+
.score-row .tie {
256288
font-weight: bold;
257289
color: var(--neutral-team-text-color);
258290
}
259291
260-
.upcoming {
261-
display: flex;
262-
flex-direction: column;
263-
align-items: center;
264-
gap: var(--sm-gap);
265-
}
266-
267292
.time {
268293
white-space: nowrap;
269294
}

packages/web/src/lib/components/nav/LocaleSelect.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@
5050

5151
<style>
5252
select {
53-
padding: calc(var(--sm-pad) * 0.6) var(--sm-pad);
54-
border-radius: 6px;
53+
padding: calc(var(--navbar-size) / 6) var(--lg-pad);
54+
padding-right: 30px;
55+
border-radius: 4px;
5556
border: 1px solid var(--sep-color);
56-
background: var(--theme-color);
57-
color: var(--theme-text-color);
57+
background-color: var(--fg-color);
58+
color: var(--text-color);
5859
font-size: 14px;
5960
}
6061
</style>

packages/web/src/lib/components/nav/search/Searchbar.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import SkeletonRow from "../../skeleton/SkeletonRow.svelte";
1919
import { afterNavigate, goto, preloadData } from "$app/navigation";
2020
import { focusWithinOut, watchForFocus } from "../../../util/directives";
21+
import { t } from "$lib/i18n";
2122
2223
let searchText = "";
2324
let shown = false;
@@ -141,7 +142,7 @@
141142
on:keydown={key}
142143
use:watchForFocus={{ store: focusNum, myNum: -1 }}
143144
id="searchbar"
144-
placeholder="Search for teams and events"
145+
placeholder={$t("search.placeholder", "Search for teams and events")}
145146
type="search"
146147
autocomplete="off"
147148
/>
@@ -173,7 +174,7 @@
173174
{/each}
174175
</ul>
175176
{:else if searchText != ""}
176-
<b>No Results</b>
177+
<b>{$t("search.no-results", "No Results")}</b>
177178
{/if}
178179
</div>
179180
</form>

packages/web/src/lib/components/ui/form/SearchInput.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
<script lang="ts">
22
import { faSearch, faXmark } from "@fortawesome/free-solid-svg-icons";
33
import Fa from "svelte-fa";
4+
import { t } from "$lib/i18n";
45
56
export let value: string;
6-
export let placeholder = "Search";
7+
export let placeholder: string | null = null;
78
export let name: string | null = null;
89
export let id: string | null = null;
10+
11+
$: resolvedPlaceholder = placeholder ?? $t("form.search", "Search");
912
</script>
1013

1114
<div>
1215
<span class="glass">
1316
<Fa icon={faSearch} />
1417
</span>
1518

16-
<input type="search" autocomplete="off" {placeholder} {name} {id} bind:value />
19+
<input
20+
type="search"
21+
autocomplete="off"
22+
placeholder={resolvedPlaceholder}
23+
{name}
24+
{id}
25+
bind:value
26+
/>
1727

1828
<button on:mousedown|preventDefault on:click={() => (value = "")}>
1929
<Fa icon={faXmark} />

packages/web/src/lib/i18n.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export type LocaleOption = {
1818
label: string;
1919
};
2020

21+
type Translator = (key: string, fallback?: string) => string;
22+
2123
const LOCALE_LABELS: Record<SupportedLocale, string> = {
2224
en: "English",
2325
"zh-Hans": "Chinese (Simplified)",
@@ -62,6 +64,28 @@ function normalizeLocale(value: string | undefined): SupportedLocale {
6264
return resolveSupportedLocale(value);
6365
}
6466

67+
function translateFromDictionary(
68+
currentLocale: string | undefined,
69+
key: string,
70+
fallback: string
71+
): string {
72+
if (!currentLocale) return fallback;
73+
if (currentLocale.toLowerCase().startsWith("en")) return fallback;
74+
let normalizedLocale = resolveSupportedLocale(currentLocale);
75+
let dictionary = DICTIONARIES[normalizedLocale];
76+
return dictionary?.[key] ?? fallback;
77+
}
78+
79+
export const t = derived(
80+
locale,
81+
($locale): Translator =>
82+
(key: string, fallback?: string) => {
83+
let text = fallback ?? key;
84+
if (!text.trim()) return text;
85+
return translateFromDictionary($locale, key, text);
86+
}
87+
);
88+
6589
export function initLocale(params?: {
6690
locale?: string;
6791
detectedLocale?: string;
@@ -125,7 +149,5 @@ export function loadLocaleCookie() {
125149

126150
export async function translateText(locale: string, key: string, text: string): Promise<string> {
127151
if (!text.trim()) return text;
128-
let normalizedLocale = resolveSupportedLocale(locale);
129-
let dictionary = DICTIONARIES[normalizedLocale];
130-
return dictionary?.[key] ?? text;
152+
return translateFromDictionary(locale, key, text);
131153
}

packages/web/src/lib/locales/en.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,27 @@
1313
"nav.github": "GitHub",
1414
"nav.discord": "Discord",
1515
"nav.email": "Email",
16-
"nav.status": "Status"
16+
"nav.status": "Status",
17+
"home.tagline.prefix": "A new way to track and scout",
18+
"home.tagline.suffix": "Tech Challenge",
19+
"home.active-teams": "Active Teams",
20+
"home.matches-played": "Matches Played",
21+
"home.todays-events": "Today's Events",
22+
"home.no-events": "There are no events scheduled for today.",
23+
"home.world-record": "World Record",
24+
"home.wr.tooltip": "Top score in a FIRST sponsored event.",
25+
"home.wr.tooltip-no-penalty": "Top no penalty score in a FIRST sponsored event.",
26+
"form.search": "Search",
27+
"form.regions": "Regions",
28+
"form.season": "Season",
29+
"form.event-types": "Event Types",
30+
"form.date-range": "Date Range",
31+
"search.placeholder": "Search for teams and events",
32+
"search.no-results": "No Results",
33+
"filters.try-modifying": "Try modifying your filters.",
34+
"teams.title": "Teams",
35+
"teams.no-matching": "No matching teams.",
36+
"events.title": "Events",
37+
"events.week": "Week",
38+
"events.no-matching": "No matching events."
1739
}

packages/web/src/lib/locales/zh-Hans.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,27 @@
1313
"nav.github": "GitHub",
1414
"nav.discord": "Discord",
1515
"nav.email": "邮箱",
16-
"nav.status": "状态"
16+
"nav.status": "状态",
17+
"home.tagline.prefix": "一种全新的方式来追踪并侦察",
18+
"home.tagline.suffix": "科技挑战赛",
19+
"home.active-teams": "活跃队伍",
20+
"home.matches-played": "已进行的比赛",
21+
"home.todays-events": "今日赛事",
22+
"home.no-events": "今天没有安排赛事。",
23+
"home.world-record": "世界纪录",
24+
"home.wr.tooltip": "FIRST 官方赛事中的最高得分。",
25+
"home.wr.tooltip-no-penalty": "FIRST 官方赛事中的最高无罚分得分。",
26+
"form.search": "搜索",
27+
"form.regions": "地区",
28+
"form.season": "赛季",
29+
"form.event-types": "赛事类型",
30+
"form.date-range": "日期范围",
31+
"search.placeholder": "搜索队伍和赛事",
32+
"search.no-results": "无结果",
33+
"filters.try-modifying": "请尝试修改筛选条件。",
34+
"teams.title": "队伍",
35+
"teams.no-matching": "没有匹配的队伍。",
36+
"events.title": "赛事",
37+
"events.week": "",
38+
"events.no-matching": "没有匹配的赛事。"
1739
}

0 commit comments

Comments
 (0)