Skip to content

Commit

Permalink
Merge pull request #21 from chris-kruining/feature/add-localization
Browse files Browse the repository at this point in the history
Feature/add localization
  • Loading branch information
chris-kruining authored Jan 6, 2025
2 parents ce7a300 + 1f9aad7 commit 7d6a022
Show file tree
Hide file tree
Showing 21 changed files with 398 additions and 81 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"name": "calque",
"dependencies": {
"@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/storage": "^4.2.1",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.2",
"@solidjs/start": "^1.0.10",
"dexie": "^4.0.10",
"flag-icons": "^7.2.3",
"iterator-helpers-polyfill": "^3.0.1",
"sitemap": "^8.0.0",
"solid-icons": "^1.1.0",
Expand Down
9 changes: 6 additions & 3 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { MetaProvider } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
import { ThemeProvider } from "./components/colorschemepicker";
import { I18nProvider } from "./features/i18n";
import "./app.css";

export default function App() {
return (
<Router
root={props => (
<MetaProvider>
<ThemeProvider>
<Suspense>{props.children}</Suspense>
<I18nProvider>
<Suspense>{props.children}</Suspense>
</I18nProvider>
</ThemeProvider>
</MetaProvider>
</ MetaProvider>
)}
>
<FileRoutes />
Expand Down
23 changes: 1 addition & 22 deletions src/components/colorschemepicker.module.css
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
.picker {
display: flex;
flex-flow: row;
align-items: center;
background-color: inherit;
border: 1px solid transparent;
border-radius: var(--radii-m);
padding: var(--padding-s);

& select {
flex: 1 1 auto;
border: none;
background-color: inherit;
border-radius: var(--radii-m);

&:focus {
outline: none;
}
}

&:has(:focus-visible) {
border-color: var(--info);
}
grid-template-columns: auto 1fr;
}

.hue {
Expand Down
34 changes: 16 additions & 18 deletions src/components/colorschemepicker.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Component, createContext, createEffect, createResource, For, ParentComponent, Show, Suspense, useContext } from "solid-js";
import css from './colorschemepicker.module.css';
import { CgDarkMode } from "solid-icons/cg";
import { Component, createContext, createEffect, createResource, Match, ParentComponent, Show, Suspense, Switch, useContext } from "solid-js";
import { action, query, useAction } from "@solidjs/router";
import { useSession } from "vinxi/http";
import { createStore } from "solid-js/store";
import { ComboBox } from "./combobox";
import { WiMoonAltFull, WiMoonAltNew, WiMoonAltFirstQuarter } from "solid-icons/wi";
import css from './colorschemepicker.module.css';

export enum ColorScheme {
Auto = 'light dark',
Light = 'light',
Dark = 'dark',
}

const colorSchemes = Object.entries(ColorScheme) as readonly [keyof typeof ColorScheme, ColorScheme][];
const colorSchemes: Record<ColorScheme, keyof typeof ColorScheme> = Object.fromEntries(Object.entries(ColorScheme).map(([k, v]) => [v, k] as const)) as any;

export interface State {
colorScheme: ColorScheme;
Expand Down Expand Up @@ -88,20 +89,17 @@ export const ColorSchemePicker: Component = (props) => {
const { theme, setColorScheme, setHue } = useStore();

return <>
<label class={css.picker} aria-label="Color scheme picker">
<CgDarkMode />

<select name="color-scheme-picker" onInput={(e) => {
if (e.target.value !== theme.colorScheme) {
const nextValue = (e.target.value ?? ColorScheme.Auto) as ColorScheme;

setColorScheme(nextValue);
}
}}>
<For each={colorSchemes}>{
([label, value]) => <option value={value} selected={value === theme.colorScheme}>{label}</option>
}</For>
</select>
<label aria-label="Color scheme picker">
<ComboBox id="color-scheme-picker" class={css.picker} value={theme.colorScheme} setValue={(next) => setColorScheme(next())} values={colorSchemes}>{
(k, v) => <>
<Switch>
<Match when={k === ColorScheme.Auto}><WiMoonAltFirstQuarter /></Match>
<Match when={k === ColorScheme.Light}><WiMoonAltNew /></Match>
<Match when={k === ColorScheme.Dark}><WiMoonAltFull /></Match>
</Switch>
{v}
</>
}</ComboBox>
</label>

<label class={css.hue} aria-label="Hue slider">
Expand Down
76 changes: 76 additions & 0 deletions src/components/combobox/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.box {
display: contents;
inline-size: max-content;

&:has(> :popover-open) > .button {
background-color: var(--surface-500);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}

.button {
display: grid;
grid-template-columns: inherit;
place-items: center start;

inline-size: max-content;

padding: var(--padding-m);
background-color: transparent;
border: none;
border-radius: var(--radii-m);
font-size: 1rem;

cursor: pointer;
}

.dialog {
display: none;
grid-template-columns: inherit;

inset-inline-start: anchor(start);
inset-block-start: anchor(end);
position-try-fallbacks: flip-inline;

inline-size: anchor-size(self-inline);
background-color: var(--surface-500);
padding: var(--padding-m);
border: none;
box-shadow: var(--shadow-2);

&:popover-open {
display: grid;
}

& > header {
display: grid;
grid-column: 1 / -1;

gap: var(--padding-s);
}

& > main {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
row-gap: var(--padding-s);
}
}

.option {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
place-items: center start;

border-radius: var(--radii-m);
padding: var(--padding-s);
margin-inline: calc(-1 * var(--padding-s));

cursor: pointer;

&.selected {
background-color: oklch(from var(--info) l c h / .1);
}
}
67 changes: 67 additions & 0 deletions src/components/combobox/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createMemo, createSignal, For, JSX, Setter, createEffect, Show } from "solid-js";
import css from './index.module.css';

interface ComboBoxProps<T, K extends string> {
id: string;
class?: string;
value: K;
setValue?: Setter<K>;
values: Record<K, T>;
open?: boolean;
children: (key: K, value: T) => JSX.Element;
filter?: (query: string, key: K, value: T) => boolean;
}

export function ComboBox<T, K extends string>(props: ComboBoxProps<T, K>) {
const [dialog, setDialog] = createSignal<HTMLDialogElement>();
const [value, setValue] = createSignal<K>(props.value);
const [open, setOpen] = createSignal<boolean>(props.open ?? false);
const [query, setQuery] = createSignal<string>('');

const values = createMemo(() => {
let entries = Object.entries<T>(props.values) as [K, T][];
const filter = props.filter;
const q = query();

if (filter) {
entries = entries.filter(([k, v]) => filter(q, k, v));
}

return entries;
});

createEffect(() => {
props.setValue?.(() => value());
});

createEffect(() => {
dialog()?.[open() ? 'showPopover' : 'hidePopover']();
});

return <section class={`${css.box} ${props.class}`}>
<button id={`${props.id}_button`} popoverTarget={`${props.id}_dialog`} class={css.button}>
{props.children(value(), props.values[value()])}
</button>

<dialog ref={setDialog} id={`${props.id}_dialog`} anchor={`${props.id}_button`} popover class={css.dialog} onToggle={e => setOpen(e.newState === 'open')}>
<Show when={props.filter !== undefined}>
<header>
<input value={query()} onInput={e => setQuery(e.target.value)} />
</header>
</Show>

<main>
<For each={values()}>{
([k, v]) => {
const selected = createMemo(() => value() === k);

return <span class={`${css.option} ${selected() ? css.selected : ''}`} onpointerdown={() => {
setValue(() => k);
dialog()?.hidePopover();
}}>{props.children(k, v)}</span>;
}
}</For>
</main>
</dialog>
</section>;
}
4 changes: 2 additions & 2 deletions src/components/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface TableApi<T extends Record<string, any>> {
readonly rows: Accessor<DataSet<T>>;
readonly columns: Accessor<Column<T>[]>;
selectAll(): void;
clear(): void;
clearSelection(): void;
}

interface TableContextType<T extends Record<string, any>> {
Expand Down Expand Up @@ -123,7 +123,7 @@ function Api<T extends Record<string, any>>(props: { api: undefined | ((api: Tab
selectAll() {
selectionContext.selectAll();
},
clear() {
clearSelection() {
selectionContext.clear();
},
};
Expand Down
14 changes: 12 additions & 2 deletions src/features/file/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { debounce, Mutation } from "~/utilities";
import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table";
import { SelectionItem } from "../selectable";
import { useI18n } from "../i18n";
import css from "./grid.module.css"

export type Entry = { key: string } & { [lang: string]: string };
Expand All @@ -11,7 +12,8 @@ export interface GridApi {
readonly selection: Accessor<SelectionItem<number, Entry>[]>;
remove(indices: number[]): void;
addKey(key: string): void;
// addLocale(locale: string): void;
selectAll(): void;
clearSelection(): void;
};

const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
Expand All @@ -28,12 +30,14 @@ const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
}

export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any }) {
const { t } = useI18n();

const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } }));
const locales = createMemo(() => props.locales);
const columns = createMemo<Column<Entry>[]>(() => [
{
id: 'key',
label: 'Key',
label: t('feature.file.grid.key'),
renderer: ({ value }) => value.split('.').at(-1),
},
...locales().map<Column<Entry>>(lang => ({
Expand Down Expand Up @@ -68,6 +72,12 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
addKey(key) {
r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) });
},
selectAll() {
api()?.selectAll();
},
clearSelection() {
api()?.clearSelection();
},
});
});

Expand Down
9 changes: 9 additions & 0 deletions src/features/i18n/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Locale } from "./context";

import Flag_en_GB from 'flag-icons/flags/4x3/gb.svg';
import Flag_nl_NL from 'flag-icons/flags/4x3/nl.svg';

export const locales: Record<Locale, { label: string, flag: any }> = {
'en-GB': { label: 'English', flag: Flag_en_GB },
'nl-NL': { label: 'Nederlands', flag: Flag_nl_NL },
} as const;
Loading

0 comments on commit 7d6a022

Please sign in to comment.