Migration to new Rust-based engine (#401)

* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
vcoppeandvcoppe authored and GitHub committed 2026-10-31 18:08:22 +01:00
1 parent 6a0d434371
commit 8fe7d8d64d
484 files changed
+53949 -41152

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import { writable } from 'svelte/store';
type Dictionary = {
[key: string]: string | Dictionary;
};
function getDateFormatter(locale: string) {
return new Intl.DateTimeFormat(locale, {
dateStyle: 'medium',
timeStyle: 'medium',
});
}
class Locale {
private _store = writable(this);
private _lang = $state('');
private _isLoadingInitial = $state(true);
private _isLoading = $state(true);
private dictionary: Dictionary = $state({});
private _t = $derived((key: string, fallback?: string) => {
const keys = key.split('.');
let value: string | Dictionary = this.dictionary;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return fallback || key;
}
}
return typeof value === 'string' ? value : key;
});
private _df = $derived(getDateFormatter(this._lang === '' ? 'en' : this._lang));
public set lang(lang: string) {
this._lang = lang;
if (this._lang !== '') {
if (!this._isLoading) {
this._isLoading = true;
}
import(`../../locales/${this._lang}.json`).then((module) => {
this.dictionary = module.default;
this._store.set(this);
if (this._isLoadingInitial) {
this._isLoadingInitial = false;
}
if (this._isLoading) {
this._isLoading = false;
}
});
}
}
public get lang() {
return this._lang;
}
public get isLoading() {
return this._isLoading;
}
public get isLoadingInitial() {
return this._isLoadingInitial;
}
public get _() {
return this._t;
}
public get df() {
return this._df;
}
public subscribe(run: (value: Locale) => void, invalidate?: (value?: Locale) => void) {
return this._store.subscribe(run, invalidate);
}
}
export const i18n = new Locale();
+84
View File
@@ -0,0 +1,84 @@
// The keys of the translations that the code asks for have to exist, which nothing else checks:
// a missing key shows as the key itself.
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { languages } from '$lib/i18n/languages';
const src = fileURLToPath(new URL('../..', import.meta.url));
function sources(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
return ['locales', 'docs'].includes(entry.name) ? [] : sources(path);
}
return /\.(ts|svelte)$/.test(entry.name) && !entry.name.endsWith('.test.ts') ? [path] : [];
});
}
/** The value at a key (`a.b.c`) of a dictionary, `undefined` if there is none. */
function valueAt(dictionary: object, key: string): unknown {
let value: unknown = dictionary;
for (const part of key.split('.')) {
if (value === null || typeof value !== 'object' || !(part in value)) {
return undefined;
}
value = (value as Record<string, unknown>)[part];
}
return value;
}
function has(dictionary: object, key: string): boolean {
return typeof valueAt(dictionary, key) === 'string';
}
function keysOf(dictionary: object, prefix = ''): string[] {
return Object.entries(dictionary).flatMap(([key, value]) =>
typeof value === 'object' && value !== null
? keysOf(value, `${prefix}${key}.`)
: [`${prefix}${key}`]
);
}
const english = JSON.parse(readFileSync(join(src, 'locales/en.json'), 'utf-8'));
describe('translations', () => {
it('exist in english for every key that the code asks for', () => {
const missing: string[] = [];
let checked = 0;
for (const file of sources(src)) {
const code = readFileSync(file, 'utf-8');
for (const match of code.matchAll(/i18n\._\(\s*(['"])([a-z0-9_.-]+)\1\s*[,)]/gi)) {
checked++;
if (!has(english, match[2])) {
missing.push(`${match[2]} (${file.replace(src, '')})`);
}
}
}
// the pattern does find the keys
assert.ok(checked > 100, `${checked}`);
assert.deepEqual(missing, []);
});
it('have no empty text in english', () => {
const empty = keysOf(english).filter((key) => {
const value = valueAt(english, key);
return typeof value === 'string' && value.trim() === '';
});
assert.deepEqual(empty, []);
});
it('are valid JSON for every language of the site, with only keys that english has', () => {
const known = new Set(keysOf(english));
for (const language of Object.keys(languages)) {
const dictionary = JSON.parse(
readFileSync(join(src, `locales/${language}.json`), 'utf-8')
);
const unknown = keysOf(dictionary).filter((key) => !known.has(key));
assert.deepEqual(unknown, [], language);
}
});
});
+15
View File
@@ -0,0 +1,15 @@
export const languages: Record<string, string> = {
ca: 'Català',
cs: 'Čeština',
en: 'English',
es: 'Español',
eu: 'Euskara',
de: 'Deutsch',
fr: 'Français',
it: 'Italiano',
nl: 'Nederlands',
'pt-BR': 'Português (Brasil)',
tr: 'Türkçe',
uk: 'Українська',
zh: '简体中文',
};