05언어팩을 붙이는 t() 함수
키 카드가 컨베이어를 타고 t() 상자로 흘러 들어가면 현재 언어의 문구로 바뀌어 나오고, 언어 레버가 ko·en·ja 를 오가며 같은 키의 결과를 갈아 끼운다.
쓰임언어팩 파일을 화면에 연결할 때, 키 오타로 빈 문구가 나올 때
t('키') 8줄언어 전환키 오타 검출

<main class="stage is-demo">
<div class="pipe">
<div class="node node--key"><span class="node__tag">key</span><span class="node__txt">cta.start</span></div>
<span class="wire wire--in"></span>
<div class="node node--fn"><span class="node__txt">t()</span></div>
<span class="wire wire--out"></span>
<div class="node node--out"><span class="node__tag">ko</span><span class="node__val">시작하기</span></div>
</div>
<div class="belt" aria-hidden="true"></div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 변환 흐름 한 주기
$easing: $ease-out;
$color: $subject-blue; // t() 상자
@include stage($stage-ink);
.stage { display: grid; place-items: center; align-content: center; row-gap: $sp-4; }
.pipe {
display: flex;
align-items: center;
column-gap: $sp-2;
width: min(450px, 94vw);
}
.node {
position: relative;
border-radius: $r-card;
min-height: clamp(44px, 13vw, 64px);
flex: 1;
display: grid;
place-items: center;
align-content: center;
row-gap: $sp-1;
padding: $sp-1;
}
.node__tag {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(8px, 1.9vw, 10.5px);
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
opacity: 1; // .62 는 크림 면 위 파랑/빨강 태그를 2.38:1 로 깨뜨렸다 — AA 4.5
}
.node--key { background: $subject-cream; color: $subject-ink; }
.node--key .node__tag { color: $subject-blue-deep; } // 밝은 면 위 파란 글자 — 5.98:1
.node--key .node__txt { color: $subject-ink; }
.node--fn { background: $color; box-shadow: $shadow-raised; flex: .8; }
.node--fn .node__txt {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(14px, 3.4vw, 20px);
font-weight: 700;
color: #fff;
}
.node--out { background: $subject-cream; }
.node--out .node__tag { color: $error; } // 크림 면 위 5.13:1 — 통과
.node__val {
font-size: clamp(13px, 3.2vw, 19px);
font-weight: 800;
color: $subject-ink;
}
.node__txt {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(10px, 2.5vw, 14.5px);
font-weight: 700;
}
.wire {
flex: .5;
height: 4px;
border-radius: $r-pill;
background: repeating-linear-gradient(90deg,
$subject-cream 0 10px,
rgba($subject-cream, .18) 10px 20px);
}
.belt {
width: min(450px, 94vw);
height: 10px;
border-radius: $r-pill;
background: repeating-linear-gradient(90deg,
rgba($subject-cream, .35) 0 14px,
rgba($subject-cream, .06) 14px 28px);
}
// 키가 들어가 t() 를 지나 문구로 나오는 흐름
.stage.is-demo .node--key { animation: key-pulse $duration $easing infinite; }
.stage.is-demo .wire--in { animation: wire-flow $duration linear infinite; }
.stage.is-demo .node--fn { animation: fn-glow $duration $easing infinite; }
.stage.is-demo .wire--out { animation: wire-flow $duration linear infinite; animation-delay: .34s; }
.stage.is-demo .node--out { animation: out-pop $duration $ease-pop infinite; }
.stage.is-demo .belt { animation: belt-run $dur-base linear infinite; }
@keyframes key-pulse {
0%, 4% { transform: scale(1); }
12% { transform: scale(1.06); }
20%, 100%{ transform: scale(1); }
}
@keyframes wire-flow {
0%, 8% { opacity: .25; background-position: 0 0; }
12% { opacity: 1; }
34% { opacity: 1; background-position: 40px 0; }
40%, 100%{ opacity: .25; background-position: 40px 0; }
}
@keyframes fn-glow {
0%, 26% { transform: scale(1); box-shadow: $shadow-press; }
38% { transform: scale(1.08); box-shadow: 0 0 0 8px rgba($subject-blue, .35), $shadow-raised; }
52%, 100%{ transform: scale(1); box-shadow: $shadow-press; }
}
@keyframes out-pop {
0%, 44% { transform: scale(.6); opacity: 0; }
56% { transform: scale(1.12); opacity: 1; }
62%, 94% { transform: scale(1); opacity: 1; }
100% { transform: scale(.7); opacity: 0; }
}
@keyframes belt-run {
0% { background-position: 0 0; }
100% { background-position: 28px 0; }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .node--key,
.stage.is-demo .wire--in,
.stage.is-demo .node--fn,
.stage.is-demo .wire--out,
.stage.is-demo .node--out,
.stage.is-demo .belt { animation: none; }
.node--out { opacity: 1; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
display: grid;
place-items: center;
align-content: center;
row-gap: 16px;
}
.pipe {
display: flex;
align-items: center;
column-gap: 8px;
width: min(450px, 94vw);
}
.node {
position: relative;
border-radius: 16px;
min-height: clamp(44px, 13vw, 64px);
flex: 1;
display: grid;
place-items: center;
align-content: center;
row-gap: 4px;
padding: 4px;
}
.node__tag {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(8px, 1.9vw, 10.5px);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 1;
}
.node--key {
background: #fff7e6;
color: #17141a;
}
.node--key .node__tag {
color: #0a4fe8;
}
.node--key .node__txt {
color: #17141a;
}
.node--fn {
background: #2f6df6;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
flex: 0.8;
}
.node--fn .node__txt {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(14px, 3.4vw, 20px);
font-weight: 700;
color: #fff;
}
.node--out {
background: #fff7e6;
}
.node--out .node__tag {
color: #c2361a;
}
.node__val {
font-size: clamp(13px, 3.2vw, 19px);
font-weight: 800;
color: #17141a;
}
.node__txt {
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: clamp(10px, 2.5vw, 14.5px);
font-weight: 700;
}
.wire {
flex: 0.5;
height: 4px;
border-radius: 999px;
background: repeating-linear-gradient(90deg, #fff7e6 0 10px, rgba(255, 247, 230, 0.18) 10px 20px);
}
.belt {
width: min(450px, 94vw);
height: 10px;
border-radius: 999px;
background: repeating-linear-gradient(90deg, rgba(255, 247, 230, 0.35) 0 14px, rgba(255, 247, 230, 0.06) 14px 28px);
}
.stage.is-demo .node--key {
animation: key-pulse 2s ease-out infinite;
}
.stage.is-demo .wire--in {
animation: wire-flow 2s linear infinite;
}
.stage.is-demo .node--fn {
animation: fn-glow 2s ease-out infinite;
}
.stage.is-demo .wire--out {
animation: wire-flow 2s linear infinite;
animation-delay: 0.34s;
}
.stage.is-demo .node--out {
animation: out-pop 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.stage.is-demo .belt {
animation: belt-run 300ms linear infinite;
}
@keyframes key-pulse {
0%, 4% {
transform: scale(1);
}
12% {
transform: scale(1.06);
}
20%, 100% {
transform: scale(1);
}
}
@keyframes wire-flow {
0%, 8% {
opacity: 0.25;
background-position: 0 0;
}
12% {
opacity: 1;
}
34% {
opacity: 1;
background-position: 40px 0;
}
40%, 100% {
opacity: 0.25;
background-position: 40px 0;
}
}
@keyframes fn-glow {
0%, 26% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
38% {
transform: scale(1.08);
box-shadow: 0 0 0 8px rgba(47, 109, 246, 0.35), 0 14px 24px rgba(23, 20, 26, 0.32);
}
52%, 100% {
transform: scale(1);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
}
@keyframes out-pop {
0%, 44% {
transform: scale(0.6);
opacity: 0;
}
56% {
transform: scale(1.12);
opacity: 1;
}
62%, 94% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.7);
opacity: 0;
}
}
@keyframes belt-run {
0% {
background-position: 0 0;
}
100% {
background-position: 28px 0;
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .node--key,
.stage.is-demo .wire--in,
.stage.is-demo .node--fn,
.stage.is-demo .wire--out,
.stage.is-demo .node--out,
.stage.is-demo .belt {
animation: none;
}
.node--out {
opacity: 1;
}
}import type { CSSProperties } from "react";
import styles from "./TFunctionFlow.module.scss";
export type TFunctionFlowProps = {
duration?: number;
easing?: string;
color?: string;
keyName?: string;
output?: string;
};
export default function TFunctionFlow({
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
keyName = "cta.start",
output = "시작하기",
}: TFunctionFlowProps) {
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
return (
<div className={styles.stage} style={vars} aria-label="t() 함수 변환 흐름 데모" role="img">
<div className={styles.pipe}>
<div className={`${styles.node} ${styles.nodeKey}`}>
<span className={styles.tag}>key</span>
<span className={styles.keyTxt}>{keyName}</span>
</div>
<span className={`${styles.wire} ${styles.wireIn}`} />
<div className={`${styles.node} ${styles.nodeFn}`}>
<span className={styles.fnTxt}>t()</span>
</div>
<span className={`${styles.wire} ${styles.wireOut}`} />
<div className={`${styles.node} ${styles.nodeOut}`}>
<span className={styles.tagOut}>ko</span>
<span className={styles.val}>{output}</span>
</div>
</div>
<div className={styles.belt} aria-hidden="true" />
</div>
);
}05 언어팩을 붙이는 t() 함수 — 측정 누적면적 14.826% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 홈페이지 다국어 처리 검수 9가지 — 언어팩 키 누락·번역 누락 잡기
이 데모가 속한 글
홈페이지 다국어 처리 검수 9가지 — 언어팩 키 누락·번역 누락 잡기홈페이지 다국어 처리는 번역문을 언어팩 파일에 넣었다고 끝나지 않는다. 키가 한쪽에만 있으면 영어판 버튼이 빈 채로 남고, 자리표시자가 어긋나면 이름이 빠진 인사말이 나간다. 여기 아홉 가지 검사를 브라우저 콘솔에 이 글의 데모 전부 보기


