Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/lib/components/organisms/Footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@
{ label: 'Good first issues', href: 'https://github.com/langx/langx/contribute' },
{ label: 'Releases', href: 'https://github.com/langx/langx/releases' },
{ label: 'Docs', href: 'https://docs.langx.io' },
{ label: 'Word lists', href: '/tools/most-common-words' },
{ label: 'Status', href: 'https://status.langx.io' },
{ label: 'Branding', href: 'https://github.com/langx/branding' }
]
},
{
title: 'Tools',
links: [
{ label: 'All tools', href: '/tools' },
{ label: 'Word lists', href: '/tools/most-common-words' },
{ label: 'Say it in any language', href: '/tools/say' },
{ label: 'Vocabulary test', href: '/tools/vocabulary-test' },
{ label: 'Alphabets', href: '/tools/alphabet' },
{ label: 'Languages that overlap', href: '/tools/similar' }
]
},
{
title: 'Community',
links: [
Expand Down Expand Up @@ -171,7 +181,11 @@

.groups {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
// Was a fixed four, which left the fifth group stranded alone on a second
// row. Five will not fit across the space the brand column leaves — the
// labels here are longer than "Legal" — so three and two, which reads as
// a layout rather than an accident.
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-lg) var(--space-md);

@include for-phone-only {
Expand Down
167 changes: 167 additions & 0 deletions src/lib/components/organisms/OpenSource.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script lang="ts">
import { onMount } from 'svelte';
import Button from '$lib/components/atoms/Button.svelte';
import GithubIcon from '$lib/icons/socials/github.svelte';

/**
* The open-source chapter. Every line here is a fact somebody can check in a
* minute, which is the only kind of claim this section is allowed to make:
* the licence file, the repository, the self-hosting guide.
*
* Deliberately says nothing about tracking or analytics. The app ships a
* product-analytics SDK, so a "no trackers" line here would be false — see
* the note in PRODUCT.md.
*/
const repo = 'langx/langx';
let stars = -1;

onMount(async () => {
try {
const res = await fetch(`https://api.github.com/repos/${repo}`);
if (!res.ok) return;
stars = (await res.json()).stargazers_count ?? -1;
} catch {
// Rate-limited or offline. The count is decoration; the facts are not.
}
});

const facts = [
{ big: 'BSD-3', small: 'licensed — use it, fork it, sell it' },
{ big: 'Every line', small: 'of the app and the API, in the open' },
{ big: 'Self-hostable', small: 'run the whole thing on your own server' }
];
</script>

<section class="os">
<div class="container">
<h2>open source. all of it.</h2>
<p class="lede">
Not a public mirror, not an SDK, not the client with the interesting half missing. The app you
install and the server it talks to are the same code you can read, change and run yourself.
</p>

<dl class="facts">
{#each facts as f}
<div>
<dt>{f.big}</dt>
<dd>{f.small}</dd>
</div>
{/each}
</dl>

<div class="actions">
<Button href="https://github.com/{repo}" variant="secondary" size="md">
<span class="gh"><GithubIcon /></span>
Read the source
{#if stars > 0}<span class="stars">{stars.toLocaleString('en-US')}</span>{/if}
</Button>
<a class="ghost" href="https://github.com/langx/langx/contribute">Good first issues</a>
</div>
</div>
</section>

<style lang="scss">
@import '$lib/scss/breakpoints.scss';

.os {
border-top: 1px solid var(--color--border);
padding: var(--space-3xl) 0;

@include for-phone-only {
padding: var(--space-2xl) 0;
}
}

// The chapter voice: lowercase, ending in a full stop. Ink rather than blue,
// because there is no phone beside it.
h2 {
font-family: var(--font--title);
font-weight: 800;
font-size: clamp(2rem, 1.4rem + 2.4vw, 3rem);
line-height: 1.05;
letter-spacing: -0.02em;
max-width: 16ch;
}

.lede {
font-size: 1.125rem;
line-height: 1.6;
color: var(--color--text-shade);
max-width: 52ch;
margin-top: var(--space-sm);
}

// Hairlines and figures, not cards — the same device the tools pages use.
.facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-top: 1px solid var(--color--border);
border-bottom: 1px solid var(--color--border);
margin: var(--space-xl) 0 var(--space-lg);

> div {
padding: var(--space-md) var(--space-md) var(--space-md) 0;

+ div {
border-left: 1px solid var(--color--border);
padding-left: var(--space-md);
}
}

dt {
font-family: var(--font--title);
font-weight: 800;
font-size: clamp(1.375rem, 1.1rem + 1.1vw, 1.875rem);
line-height: 1.1;
letter-spacing: -0.02em;
}

dd {
margin-top: var(--space-2xs);
font-size: 0.8125rem;
line-height: 1.4;
color: var(--color--text-quiet);
max-width: 24ch;
}

@include for-phone-only {
grid-template-columns: 1fr;

> div + div {
border-left: 0;
padding-left: 0;
border-top: 1px solid var(--color--border);
}
}
}

.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-sm);
}

.gh {
display: inline-flex;
width: 18px;
height: 18px;
}

.stars {
font-variant-numeric: tabular-nums;
color: var(--color--text-quiet);
}

.ghost {
color: var(--color--accent);
font-size: 0.9375rem;
font-weight: 600;
padding: 0 10px;
}

// No scroll reveal here, deliberately. The other chapters fade in because
// what they hold is a demonstration; this one holds claims about the licence
// and the source, and a claim that depends on an IntersectionObserver firing
// is a claim that can fail to appear at all.
</style>
2 changes: 2 additions & 0 deletions src/routes/(site)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import LanguageMarquee from '$lib/components/organisms/LanguageMarquee.svelte';
import Story from '$lib/components/organisms/Story.svelte';
import Anywhere from '$lib/components/organisms/Anywhere.svelte';
import OpenSource from '$lib/components/organisms/OpenSource.svelte';
import Testimonials from '$lib/components/organisms/Testimonials.svelte';
import Faq from '$lib/components/organisms/FAQ.svelte';
import FinalCta from '$lib/components/organisms/FinalCta.svelte';
Expand All @@ -16,6 +17,7 @@
<LanguageMarquee />
<Story />
<Anywhere />
<OpenSource />
<Testimonials />
<Faq />
<FinalCta />
Expand Down
138 changes: 138 additions & 0 deletions src/routes/(site)/tools/vocabulary-test/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script lang="ts">
import Seo from '$lib/components/atoms/Seo.svelte';
import ScriptDisc from '$lib/components/atoms/ScriptDisc.svelte';
import PageHeader from '$lib/components/organisms/PageHeader.svelte';
import { WORD_LISTS } from '$lib/data/most-common-words';

const nf = new Intl.NumberFormat('en-US');
const POPULAR = ['es', 'fr', 'de', 'it', 'pt', 'ru', 'tr', 'zh', 'ko', 'ar', 'nl', 'pl'];
const popular = POPULAR.map((c) => WORD_LISTS.find((l) => l.code === c)).filter(
Boolean
) as typeof WORD_LISTS;
</script>

<Seo
title="Vocabulary tests"
path="/tools/vocabulary-test"
description="Find out roughly how many words you know, in any of {WORD_LISTS.length} languages. Forty-two words, two minutes, no account."
/>

<div class="container">
<PageHeader
title="How many words do you know?"
lede="Forty-two words drawn from across the whole frequency range — the commonest through to the
rare. Mark the ones you know and see roughly how much of the language that covers. Two minutes, no
account, and the result is a link you can keep."
/>

<section class="block">
<h2>Start here</h2>
<ul class="popular" role="list">
{#each popular as l}
<li>
<a href="/tools/vocabulary-test/{l.slug}">
<ScriptDisc nativeName={l.nativeName} code={l.code} size={44} />
<span class="names">
<span class="native" lang={l.code}>{l.nativeName}</span>
<span class="english">{l.name}</span>
</span>
</a>
</li>
{/each}
</ul>
</section>

<section class="block">
<h2>All {WORD_LISTS.length} languages</h2>
<ul class="rows" role="list">
{#each WORD_LISTS as l}
<li>
<a href="/tools/vocabulary-test/{l.slug}">
<ScriptDisc nativeName={l.nativeName} code={l.code} size={30} />
<span class="lang">{l.name}</span>
<span class="n tabular">{nf.format(l.count)} words</span>
</a>
</li>
{/each}
</ul>
</section>
</div>

<style lang="scss">
@import '$lib/scss/breakpoints.scss';

.block {
padding: var(--space-xl) 0 0;

h2 {
font-family: var(--font--title);
font-weight: 800;
font-size: clamp(1.75rem, 1.3rem + 1.6vw, 2.375rem);
letter-spacing: -0.015em;
margin-bottom: var(--space-sm);
}
}

.popular {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 0 var(--space-lg);
border-top: 1px solid var(--color--border);

li {
border-bottom: 1px solid var(--color--border);
}

a {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: 14px 0;
color: var(--color--text);
}

.names {
display: flex;
flex-direction: column;
min-width: 0;
}

.native {
font-family: var(--font--title);
font-weight: 800;
font-size: 1.125rem;
}

.english {
font-size: 0.8125rem;
color: var(--color--text-quiet);
}
}

.rows {
border-top: 1px solid var(--color--border);

li {
border-bottom: 1px solid var(--color--border);
}

a {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 13px 0;
color: var(--color--text);
}
}

.lang {
font-family: var(--font--title);
font-weight: 800;
}

.n {
margin-left: auto;
color: var(--color--text-quiet);
font-size: 0.8125rem;
}
</style>