Skip to content

Commit cfaf001

Browse files
committed
feat: improve texts
1 parent 6e6a02b commit cfaf001

20 files changed

Lines changed: 423 additions & 142 deletions

bitext/src/lib/components/editor/GlossInputRow.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
<script lang="ts">
22
import { Label } from 'flowbite-svelte';
33
import type { Token } from '$lib/domain/tokens.js';
4+
import { settingsStore } from '$lib/state/settings.svelte.js';
5+
import { resolveVisualizationFontCss } from '$lib/fonts/visualization-font.js';
6+
import { defaultGlossFontSizePx } from '$lib/serialization/schema.js';
47
58
const inputClass =
6-
'block w-full rounded-none border border-gray-300 bg-gray-50 p-2 text-xs text-gray-900 placeholder-gray-500 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-primary-500 dark:focus:ring-primary-500';
9+
'block w-full rounded-none border border-gray-300 bg-gray-50 p-2 text-gray-900 placeholder-gray-500 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-primary-500 dark:focus:ring-primary-500';
10+
11+
const glossFont = $derived(resolveVisualizationFontCss(settingsStore.settings, 'gloss'));
12+
const glossSizePx = $derived(defaultGlossFontSizePx(settingsStore.settings));
713
814
let {
915
tokens,
@@ -26,6 +32,8 @@
2632
class={inputClass}
2733
placeholder=" "
2834
value={t.gloss ?? ''}
35+
style:font-family={glossFont}
36+
style:font-size="{glossSizePx}px"
2937
oninput={(e) => onGloss(t.id, (e.currentTarget as HTMLInputElement).value)}
3038
/>
3139
</div>

bitext/src/lib/components/preview/AlignmentPreview.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
const bg = $derived(settingsStore.settings.background);
1616
const fontSource = $derived(resolveVisualizationFontCss(settingsStore.settings, 'source'));
1717
const fontTarget = $derived(resolveVisualizationFontCss(settingsStore.settings, 'target'));
18+
const fontGloss = $derived(resolveVisualizationFontCss(settingsStore.settings, 'gloss'));
1819
/** Explicit subscription — plain `projectStore.links` in children may not invalidate UI. */
1920
const links = $derived(projectStore.links);
2021
@@ -47,7 +48,7 @@
4748
<div
4849
data-gloss-row="source"
4950
class="preview-gloss-wrap"
50-
style:font-family={fontSource}
51+
style:font-family={fontGloss}
5152
style:margin-bottom="{glossGap}px"
5253
>
5354
<GlossRow tokens={projectStore.sourceTokens} side="source" />
@@ -78,7 +79,7 @@
7879
/>
7980
</div>
8081
{#if showTargetGloss}
81-
<div data-gloss-row="target" class="preview-gloss-wrap" style:font-family={fontTarget}>
82+
<div data-gloss-row="target" class="preview-gloss-wrap" style:font-family={fontGloss}>
8283
<GlossRow tokens={projectStore.targetTokens} side="target" />
8384
</div>
8485
{/if}

bitext/src/lib/components/preview/AlignmentSvg.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
}
100100
void settingsStore.settings.lineStyle;
101101
void settingsStore.settings.glossLineGapPx;
102+
void settingsStore.settings.sourceTextSizePx;
103+
void settingsStore.settings.targetTextSizePx;
104+
void settingsStore.settings.glossFontFamily;
105+
void settingsStore.settings.glossFontSource;
102106
void projectStore.sourceTokens;
103107
void projectStore.targetTokens;
104108
/** Two rAFs: wait for style/layout flush after font or DOM updates. */

bitext/src/lib/components/preview/GlossRow.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import type { Token } from '$lib/domain/tokens.js';
33
import { primaryLinkForToken } from '$lib/domain/alignment.js';
44
import { settingsStore } from '$lib/state/settings.svelte.js';
5+
import { defaultGlossFontSizePx } from '$lib/serialization/schema.js';
56
import { projectStore } from '$lib/state/project.svelte.js';
67
78
let { tokens, side }: { tokens: Token[]; side: 'source' | 'target' } = $props();
89
910
const gap = $derived(settingsStore.settings.gapWordPx);
10-
const sz = $derived(Math.max(12, settingsStore.settings.textSizePx * 0.75));
11+
const sz = $derived(defaultGlossFontSizePx(settingsStore.settings));
1112
const links = $derived(projectStore.links);
1213
const colorByLink = $derived(settingsStore.settings.colorTokensByLink);
1314

bitext/src/lib/components/preview/PreviewFontLoader.svelte

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,39 @@
88
99
const googleUrls = $derived(visualizationGoogleFontUrls(s));
1010
11-
let loadedCustom = $state<{ source?: string; target?: string }>({});
11+
let loadedCustom = $state<{ source?: string; target?: string; gloss?: string }>({});
1212
1313
$effect(() => {
1414
if (!browser) return;
1515
void s.sourceCustomFontName;
1616
void s.targetCustomFontName;
17+
void s.glossCustomFontName;
1718
void s.sourceFontSource;
1819
void s.targetFontSource;
20+
void s.glossFontSource;
1921
20-
async function ensure(side: 'source' | 'target', name: string | undefined) {
22+
async function ensure(side: 'source' | 'target' | 'gloss', name: string | undefined) {
2123
if (!name) return;
22-
const key = side === 'source' ? loadedCustom.source : loadedCustom.target;
24+
const key =
25+
side === 'source'
26+
? loadedCustom.source
27+
: side === 'target'
28+
? loadedCustom.target
29+
: loadedCustom.gloss;
2330
if (key === name) return;
2431
const blob = await loadCustomFontBlob(name);
2532
if (!blob) return;
2633
const ff = new FontFace(name, await blob.arrayBuffer());
2734
await ff.load();
2835
document.fonts.add(ff);
2936
if (side === 'source') loadedCustom = { ...loadedCustom, source: name };
30-
else loadedCustom = { ...loadedCustom, target: name };
37+
else if (side === 'target') loadedCustom = { ...loadedCustom, target: name };
38+
else loadedCustom = { ...loadedCustom, gloss: name };
3139
}
3240
3341
if (s.sourceFontSource === 'custom') void ensure('source', s.sourceCustomFontName);
3442
if (s.targetFontSource === 'custom') void ensure('target', s.targetCustomFontName);
43+
if (s.glossFontSource === 'custom') void ensure('gloss', s.glossCustomFontName);
3544
});
3645
</script>
3746

bitext/src/lib/components/preview/TokenView.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
2828
let hovering = $state(false);
2929
30-
const sz = $derived(settingsStore.settings.textSizePx);
30+
const sz = $derived(
31+
side === 'source'
32+
? settingsStore.settings.sourceTextSizePx
33+
: settingsStore.settings.targetTextSizePx
34+
);
3135
const palette = $derived(settingsStore.settings.palette);
3236
3337
const links = $derived(projectStore.links);
Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,62 @@
11
<script lang="ts">
2-
import { DEFAULT_DESCRIPTION, SITE_NAME } from '$lib/seo/metadata.js';
3-
4-
let { origin }: { origin: string } = $props();
5-
6-
const software = $derived({
7-
'@context': 'https://schema.org',
8-
'@type': 'SoftwareApplication',
9-
name: SITE_NAME,
10-
applicationCategory: 'EducationalApplication',
11-
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
12-
description: DEFAULT_DESCRIPTION,
13-
url: origin + '/'
14-
});
15-
16-
const faq = $derived({
2+
/**
3+
* FAQPage JSON-LD for the visible FAQ block in `SeoSections.svelte`.
4+
*
5+
* The `SoftwareApplication` schema used to live here too, but Google's docs require a real
6+
* `aggregateRating` or `review` on that type — with no legitimate review data we were shipping
7+
* invalid markup that Google ignores. We will add it back when there is something to rate.
8+
*
9+
* FAQ rich results are effectively restricted to well-known health/government sites since 2023
10+
* (https://developers.google.com/search/blog/2023/08/howto-faq-changes) so this markup is kept
11+
* mainly for semantic coverage and ordinary snippet quality. The answer text here mirrors the
12+
* visible FAQ copy on the page, which Google requires for any FAQPage markup.
13+
*/
14+
const faq = {
1715
'@context': 'https://schema.org',
1816
'@type': 'FAQPage',
1917
mainEntity: [
2018
{
2119
'@type': 'Question',
22-
name: 'Is this a machine aligner?',
20+
name: 'What is this kind of tool called?',
21+
acceptedAnswer: {
22+
'@type': 'Answer',
23+
text: 'The formal name is a word alignment visualizer. In everyday language people also call it a word-by-word translation tool, an interlinear-style visualizer, or simply a way to see which words match in a translation.'
24+
}
25+
},
26+
{
27+
'@type': 'Question',
28+
name: 'Does it handle reordered translations?',
29+
acceptedAnswer: {
30+
'@type': 'Answer',
31+
text: 'Yes. Curved connectors can cross each other freely, so sentences where the target language puts the verb, subject, or modifier in a different position still look clean. That is one of the main reasons this visualization is useful for language learning.'
32+
}
33+
},
34+
{
35+
'@type': 'Question',
36+
name: 'Can I align phrases, not just single words?',
37+
acceptedAnswer: {
38+
'@type': 'Answer',
39+
text: 'Yes. Select several words on one side before clicking the matching word or phrase on the other. The tool supports one-to-one, one-to-many, and many-to-many links, which often reflects real translations better than a strict one-word mapping.'
40+
}
41+
},
42+
{
43+
'@type': 'Question',
44+
name: 'Is this a full machine translator?',
2345
acceptedAnswer: {
2446
'@type': 'Answer',
25-
text: 'No — you align manually by clicking for full control.'
47+
text: 'No. You provide both sentences — the tool does not translate them for you. The value is in the visualization and the manual control over which words count as matches.'
2648
}
2749
},
2850
{
2951
'@type': 'Question',
30-
name: 'Can I export for print or slides?',
52+
name: 'Can I export the alignment as an image?',
3153
acceptedAnswer: {
3254
'@type': 'Answer',
33-
text: 'YesPNG, SVG, PDF, and HTML exports are supported.'
55+
text: 'Yes. PNG, SVG, PDF, and a self-contained HTML file are all supported, along with a shareable link that encodes both sentences, every connector, and your visual settings.'
3456
}
3557
}
3658
]
37-
});
59+
};
3860
3961
function safeJsonLd(obj: object): string {
4062
return (
@@ -49,6 +71,5 @@
4971
<!-- JSON-LD is trusted (serialized from app constants); @html avoids nested script parsing issues -->
5072
<!-- eslint-disable svelte/no-at-html-tags -->
5173
<svelte:head>
52-
{@html safeJsonLd(software)}
5374
{@html safeJsonLd(faq)}
5475
</svelte:head>

bitext/src/lib/components/seo/SeoIntro.svelte

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@
66
What this tool does
77
</h2>
88
<p>
9-
This <strong class="font-medium text-gray-900 dark:text-white"
10-
>word-by-word translation visualizer</strong
11-
>
12-
helps you build clean, shareable graphics that show how a
13-
<strong class="font-medium text-gray-900 dark:text-white">bilingual sentence alignment</strong>
14-
works between a source line and a target line. It is useful for language learners, conlangers, and
15-
linguists who need a fast
16-
<strong class="font-medium text-gray-900 dark:text-white"
17-
>translation alignment visualizer</strong
18-
> for lessons, articles, or social posts.
9+
This page is a free word-by-word translation visualizer: a small utility for showing which words
10+
in a sentence correspond to which words in its translation. Type the source on one line and the
11+
translation on the other, click a word in the preview, then click its match on the other side,
12+
and a connector is drawn between them. Linguists call this a word alignment. Most people just
13+
want to see which words match without learning the term.
1914
</p>
2015
</section>

bitext/src/lib/components/seo/SeoSections.svelte

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,104 @@
33
aria-labelledby="seo-sections"
44
>
55
<h2 id="seo-sections" class="sr-only font-heading">Guides</h2>
6+
67
<h3 class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
7-
What is word-by-word translation?
8+
Why a visual beats plain text
89
</h3>
910
<p>
10-
Word-by-word translation lines up each source word with the part of the translation that
11-
corresponds to it. Unlike a free translation, this view makes morphemes and multi-word units
12-
visible — especially when you add an <strong class="font-medium text-gray-900 dark:text-white"
13-
>interlinear gloss</strong
14-
> row.
11+
A plain word-by-word translation under each line breaks down the moment the translation changes
12+
word order, uses one word where the source uses three, or collapses several source words into a
13+
single morpheme. Curved connectors solve all three cases: lines can cross, fan out from one word
14+
to several, or converge from several words onto one. That is why a bilingual sentence alignment
15+
is easier to read than an interlinear row when you care about structure, not only meaning.
1516
</p>
17+
1618
<h3 class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
17-
How to align words between languages
19+
Great for language learners and teachers
1820
</h3>
1921
<p>
20-
Type your <strong class="font-medium text-gray-900 dark:text-white">source sentence</strong> and
21-
<strong class="font-medium text-gray-900 dark:text-white">target sentence</strong>, click a word
22-
in the preview, then click the matching word on the other line — a connector is drawn right
23-
away. You can link a word to multiple words on the other side. Click any connector to remove it.
24-
A
25-
<strong class="font-medium text-gray-900 dark:text-white"
26-
>bilingual sentence alignment tool</strong
27-
> like this keeps textbook examples legible at a glance.
22+
Learners see at a glance why a translation says what it does — which source word became which
23+
target word, which words are dropped, and where the target language moves things around.
24+
Teachers can build handouts, slides, or flashcards by exporting PNG or SVG and embed the
25+
finished diagram into a lesson without retyping anything.
2826
</p>
27+
2928
<h3 class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
30-
Interlinear gloss explanation
29+
Great for conlangs, glosses, and linguistics posts
3130
</h3>
3231
<p>
33-
A gloss is a short label (often grammatical or lexical) aligned with each word. With gloss
34-
enabled in settings, source glosses appear above the source line and target glosses below the
35-
target line whenever that side has at least one gloss filled — ideal for an
36-
<strong class="font-medium text-gray-900 dark:text-white">interlinear gloss tool</strong> workflow.
32+
For conlangers, the visualizer is a lightweight way to show how a constructed language maps onto
33+
English in a specific example. Turn on the
34+
<strong class="font-medium text-gray-900 dark:text-white">interlinear gloss</strong> rows to label
35+
morphemes above and below the sentences, use the alternative token separators to split agglutinating
36+
forms into their parts, and export the result for a blog post, a forum thread, or a conlang community
37+
share.
38+
</p>
39+
40+
<h3 class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
41+
Word alignment vs interlinear translation
42+
</h3>
43+
<p>
44+
Interlinear translators place a translation directly under each source word. That display is
45+
compact and great for reading, but it hides reordering — if the translation swaps word order,
46+
the row underneath lies about which word corresponds to which. Word alignment keeps both
47+
sentences on their own line and draws connectors between them, so reorderings, splits, and
48+
merges are obvious.
49+
</p>
50+
51+
<h3 class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
52+
Word alignment vs parallel text
53+
</h3>
54+
<p>
55+
Parallel text usually means side-by-side bilingual reading — two columns, or a source paragraph
56+
next to a translated paragraph, meant for studying in long form. This tool is a different job:
57+
one sentence pair at a time, with explicit connectors showing which tokens correspond.
58+
</p>
59+
60+
<h3 id="faq" class="font-heading mt-6 text-xl font-semibold text-gray-900 dark:text-white">
61+
Questions people ask
62+
</h3>
63+
64+
<h4 class="font-heading mt-4 text-base font-semibold text-gray-900 dark:text-white">
65+
What is this kind of tool called?
66+
</h4>
67+
<p>
68+
The formal name is a word alignment visualizer. In everyday language people also call it a
69+
word-by-word translation tool, an interlinear-style visualizer, or simply a way to see which
70+
words match in a translation.
71+
</p>
72+
73+
<h4 class="font-heading mt-4 text-base font-semibold text-gray-900 dark:text-white">
74+
Does it handle reordered translations?
75+
</h4>
76+
<p>
77+
Yes. Curved connectors can cross each other freely, so sentences where the target language puts
78+
the verb, subject, or modifier in a different position still look clean. That is one of the main
79+
reasons this visualization is useful for language learning.
80+
</p>
81+
82+
<h4 class="font-heading mt-4 text-base font-semibold text-gray-900 dark:text-white">
83+
Can I align phrases, not just single words?
84+
</h4>
85+
<p>
86+
Yes. Select several words on one side before clicking the matching word or phrase on the other.
87+
The tool supports one-to-one, one-to-many, and many-to-many links, which often reflects real
88+
translations better than a strict one-word mapping.
89+
</p>
90+
91+
<h4 class="font-heading mt-4 text-base font-semibold text-gray-900 dark:text-white">
92+
Is this a full machine translator?
93+
</h4>
94+
<p>
95+
No. You provide both sentences — the tool does not translate them for you. The value is in the
96+
visualization and the manual control over which words count as matches.
97+
</p>
98+
99+
<h4 class="font-heading mt-4 text-base font-semibold text-gray-900 dark:text-white">
100+
Can I export the alignment as an image?
101+
</h4>
102+
<p>
103+
Yes. PNG, SVG, PDF, and a self-contained HTML file are all supported, along with a shareable
104+
link that encodes both sentences, every connector, and your visual settings.
37105
</p>
38106
</section>

0 commit comments

Comments
 (0)