mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 20:19:39 +00:00
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:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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: '简体中文',
|
||||
};
|
||||
Reference in new issue
Block a user