diff --git a/client/.gitignore b/client/.gitignore
index 2530b230e..b6eaff298 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -6,3 +6,7 @@ playwright/.cache/
# vite-plugin-pwa dev output (devOptions.enabled)
dev-dist/
+
+# The native apps' address screen (npm run build:native), copied into
+# android/ and ios/ by `cap sync`
+native-dist/
diff --git a/client/android/app/build.gradle b/client/android/app/build.gradle
index 375ba6fe6..969a2a6c0 100644
--- a/client/android/app/build.gradle
+++ b/client/android/app/build.gradle
@@ -35,6 +35,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
+ implementation "androidx.browser:browser:$androidxBrowserVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
diff --git a/client/android/app/capacitor.build.gradle b/client/android/app/capacitor.build.gradle
index 262141409..e4738501f 100644
--- a/client/android/app/capacitor.build.gradle
+++ b/client/android/app/capacitor.build.gradle
@@ -10,23 +10,11 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
- implementation project(':capacitor-browser')
- implementation project(':capacitor-calendar')
- implementation project(':capacitor-camera')
- implementation project(':capacitor-file-transfer')
implementation project(':capacitor-filesystem')
- implementation project(':capacitor-geolocation')
- implementation project(':capacitor-haptics')
- implementation project(':capacitor-inappbrowser')
implementation project(':capacitor-keyboard')
- implementation project(':capacitor-local-notifications')
- implementation project(':capacitor-network')
- implementation project(':capacitor-preferences')
- implementation project(':capacitor-push-notifications')
implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
- implementation project(':capacitor-toast')
}
diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml
index 68156fea0..2bc68148b 100644
--- a/client/android/app/src/main/AndroidManifest.xml
+++ b/client/android/app/src/main/AndroidManifest.xml
@@ -24,16 +24,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
diff --git a/client/android/app/src/main/java/com/liketrek/trek/MainActivity.java b/client/android/app/src/main/java/com/liketrek/trek/MainActivity.java
index e75665e31..a5d32a1b8 100644
--- a/client/android/app/src/main/java/com/liketrek/trek/MainActivity.java
+++ b/client/android/app/src/main/java/com/liketrek/trek/MainActivity.java
@@ -1,5 +1,21 @@
package com.liketrek.trek;
+import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
-public class MainActivity extends BridgeActivity {}
+public class MainActivity extends BridgeActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ registerPlugin(TrekShellPlugin.class);
+ // With a server picked, the WebView loads that server instead of the
+ // bundled address screen, so the UI arrives in the version the server
+ // runs and on its own origin. BridgeActivity reads `config` while it
+ // builds the bridge inside super.onCreate().
+ String server = TrekServerStore.getUrl(this);
+ if (server != null) {
+ config = TrekServerStore.configFor(this, server);
+ }
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/client/android/app/src/main/java/com/liketrek/trek/TrekServerStore.java b/client/android/app/src/main/java/com/liketrek/trek/TrekServerStore.java
new file mode 100644
index 000000000..9781015a0
--- /dev/null
+++ b/client/android/app/src/main/java/com/liketrek/trek/TrekServerStore.java
@@ -0,0 +1,79 @@
+package com.liketrek.trek;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import com.getcapacitor.CapConfig;
+import com.getcapacitor.Logger;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** The server the user picked, and the Capacitor configuration that loads it. */
+final class TrekServerStore {
+
+ private static final String PREFS = "trek_shell";
+ private static final String KEY_URL = "server_url";
+ private static final String CONFIG_DIR = "trek-server-config";
+
+ private TrekServerStore() {}
+
+ static String getUrl(Context context) {
+ return prefs(context).getString(KEY_URL, null);
+ }
+
+ static void setUrl(Context context, String url) {
+ SharedPreferences.Editor editor = prefs(context).edit();
+ if (url == null) {
+ editor.remove(KEY_URL);
+ } else {
+ editor.putString(KEY_URL, url);
+ }
+ editor.commit();
+ }
+
+ /**
+ * The bundled capacitor.config.json with server.url set. Capacitor only
+ * takes the server URL from a config file, so this writes a copy into the
+ * app's private storage and loads that. Falls back to the bundled config,
+ * which shows the address screen, if the copy cannot be written.
+ */
+ static CapConfig configFor(Context context, String url) {
+ try {
+ JSONObject json = new JSONObject(readAsset(context, "capacitor.config.json"));
+ JSONObject server = json.optJSONObject("server");
+ if (server == null) {
+ server = new JSONObject();
+ json.put("server", server);
+ }
+ server.put("url", url);
+
+ File dir = new File(context.getFilesDir(), CONFIG_DIR);
+ if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("Cannot create " + dir);
+ try (FileOutputStream out = new FileOutputStream(new File(dir, "capacitor.config.json"))) {
+ out.write(json.toString().getBytes(StandardCharsets.UTF_8));
+ }
+ return CapConfig.loadFromFile(context, dir.getAbsolutePath());
+ } catch (IOException | JSONException ex) {
+ Logger.error("TrekShell", "Could not point the app at " + url, ex);
+ return CapConfig.loadDefault(context);
+ }
+ }
+
+ private static String readAsset(Context context, String name) throws IOException {
+ try (InputStream in = context.getAssets().open(name); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
+ return out.toString("UTF-8");
+ }
+ }
+
+ private static SharedPreferences prefs(Context context) {
+ return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+ }
+}
diff --git a/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java b/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java
new file mode 100644
index 000000000..bf8e2e13a
--- /dev/null
+++ b/client/android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java
@@ -0,0 +1,148 @@
+package com.liketrek.trek;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.browser.customtabs.CustomTabsIntent;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import org.json.JSONObject;
+
+/** The native half of client/src/native/trekShell.ts. */
+@CapacitorPlugin(name = "TrekShell")
+public class TrekShellPlugin extends Plugin {
+
+ /** How long to wait after returning from the browser for the redirect to land. */
+ private static final long CANCEL_GRACE_MS = 600;
+
+ private String authCallId;
+ private String authScheme;
+ private boolean authBrowserOpen;
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ @PluginMethod
+ public void getServer(PluginCall call) {
+ String url = TrekServerStore.getUrl(getContext());
+ JSObject result = new JSObject();
+ result.put("url", url == null ? JSONObject.NULL : url);
+ call.resolve(result);
+ }
+
+ @PluginMethod
+ public void setServer(PluginCall call) {
+ String url = call.getString("url");
+ Uri parsed = url == null ? null : Uri.parse(url);
+ if (parsed == null || parsed.getHost() == null || !("https".equals(parsed.getScheme()) || "http".equals(parsed.getScheme()))) {
+ call.reject("A http(s) URL is required");
+ return;
+ }
+ TrekServerStore.setUrl(getContext(), url);
+ call.resolve();
+ restart();
+ }
+
+ @PluginMethod
+ public void resetServer(PluginCall call) {
+ TrekServerStore.setUrl(getContext(), null);
+ call.resolve();
+ restart();
+ }
+
+ @PluginMethod
+ public void reload(PluginCall call) {
+ call.resolve();
+ restart();
+ }
+
+ /** The server URL is part of the bridge's configuration, so it takes a new activity. */
+ private void restart() {
+ getActivity().runOnUiThread(() -> getActivity().recreate());
+ }
+
+ @PluginMethod
+ public void authenticate(PluginCall call) {
+ String url = call.getString("url");
+ String scheme = call.getString("callbackScheme");
+ if (url == null || scheme == null) {
+ call.reject("url and callbackScheme are required");
+ return;
+ }
+ rejectPendingAuth("Superseded by a new sign-in");
+ call.setKeepAlive(true);
+ getBridge().saveCall(call);
+ authCallId = call.getCallbackId();
+ authScheme = scheme;
+ authBrowserOpen = true;
+ // A Custom Tab shares the browser's cookies, so a running SSO session at
+ // the identity provider carries over. The redirect back to the app's
+ // scheme arrives through the intent-filter in AndroidManifest.xml.
+ new CustomTabsIntent.Builder().build().launchUrl(getActivity(), Uri.parse(url));
+ }
+
+ @Override
+ protected void handleOnNewIntent(Intent intent) {
+ super.handleOnNewIntent(intent);
+ Uri data = intent.getData();
+ PluginCall call = pendingAuthCall();
+ if (call == null || data == null || !authScheme.equals(data.getScheme())) return;
+ JSObject result = new JSObject();
+ result.put("url", data.toString());
+ call.resolve(result);
+ finishAuth(call);
+ }
+
+ /**
+ * Back in the app without a redirect means the user closed the browser.
+ * onNewIntent runs before onResume when the redirect did arrive, so the
+ * grace period only covers devices that deliver the two out of order.
+ */
+ @Override
+ protected void handleOnResume() {
+ super.handleOnResume();
+ if (!authBrowserOpen) return;
+ authBrowserOpen = false;
+ handler.postDelayed(() -> {
+ PluginCall call = pendingAuthCall();
+ if (call == null) return;
+ call.reject("Sign-in cancelled", "CANCELLED");
+ finishAuth(call);
+ }, CANCEL_GRACE_MS);
+ }
+
+ private PluginCall pendingAuthCall() {
+ return authCallId == null ? null : getBridge().getSavedCall(authCallId);
+ }
+
+ private void rejectPendingAuth(String message) {
+ PluginCall call = pendingAuthCall();
+ if (call == null) return;
+ call.reject(message, "CANCELLED");
+ finishAuth(call);
+ }
+
+ private void finishAuth(PluginCall call) {
+ getBridge().releaseCall(call);
+ authCallId = null;
+ authScheme = null;
+ authBrowserOpen = false;
+ }
+
+ /** iOS only: Android keeps the service worker, which already starts TREK offline. */
+ @PluginMethod
+ public void refreshOfflineCopy(PluginCall call) {
+ JSObject result = new JSObject();
+ result.put("saved", false);
+ call.resolve(result);
+ }
+
+ @PluginMethod
+ public void openOfflineCopy(PluginCall call) {
+ JSObject result = new JSObject();
+ result.put("opened", false);
+ call.resolve(result);
+ }
+}
diff --git a/client/android/app/src/main/res/values/strings.xml b/client/android/app/src/main/res/values/strings.xml
index ba2fc8823..0b80e5533 100644
--- a/client/android/app/src/main/res/values/strings.xml
+++ b/client/android/app/src/main/res/values/strings.xml
@@ -4,8 +4,4 @@
TREK - Plan your trips!
com.liketrek.trek
com.liketrek.trek
-
- trek_default
diff --git a/client/android/capacitor.settings.gradle b/client/android/capacitor.settings.gradle
index c22006c5e..8b55e3e59 100644
--- a/client/android/capacitor.settings.gradle
+++ b/client/android/capacitor.settings.gradle
@@ -5,45 +5,12 @@ project(':capacitor-android').projectDir = new File('../../node_modules/@capacit
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android')
-include ':capacitor-browser'
-project(':capacitor-browser').projectDir = new File('../../node_modules/@capacitor/browser/android')
-
-include ':capacitor-calendar'
-project(':capacitor-calendar').projectDir = new File('../../node_modules/@capacitor/calendar/android')
-
-include ':capacitor-camera'
-project(':capacitor-camera').projectDir = new File('../../node_modules/@capacitor/camera/android')
-
-include ':capacitor-file-transfer'
-project(':capacitor-file-transfer').projectDir = new File('../../node_modules/@capacitor/file-transfer/android')
-
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android')
-include ':capacitor-geolocation'
-project(':capacitor-geolocation').projectDir = new File('../../node_modules/@capacitor/geolocation/android')
-
-include ':capacitor-haptics'
-project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android')
-
-include ':capacitor-inappbrowser'
-project(':capacitor-inappbrowser').projectDir = new File('../../node_modules/@capacitor/inappbrowser/android')
-
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android')
-include ':capacitor-local-notifications'
-project(':capacitor-local-notifications').projectDir = new File('../../node_modules/@capacitor/local-notifications/android')
-
-include ':capacitor-network'
-project(':capacitor-network').projectDir = new File('../../node_modules/@capacitor/network/android')
-
-include ':capacitor-preferences'
-project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android')
-
-include ':capacitor-push-notifications'
-project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/android')
-
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../../node_modules/@capacitor/share/android')
@@ -52,6 +19,3 @@ project(':capacitor-splash-screen').projectDir = new File('../../node_modules/@c
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capacitor/status-bar/android')
-
-include ':capacitor-toast'
-project(':capacitor-toast').projectDir = new File('../../node_modules/@capacitor/toast/android')
diff --git a/client/android/google-services.json.example b/client/android/google-services.json.example
deleted file mode 100644
index 5c4e9b003..000000000
--- a/client/android/google-services.json.example
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "_comment": [
- "Template only — this file is not read by the build.",
- "",
- "To enable Android push notifications:",
- " 1. Create a Firebase project at https://console.firebase.google.com",
- " 2. Add an Android app with package name com.liketrek.trek",
- " 3. Download the generated google-services.json",
- " 4. Place it at client/android/app/google-services.json (gitignored)",
- "",
- "Without it, app/build.gradle skips the google-services plugin and the app",
- "still builds and runs — PushNotifications.register() simply fails at runtime.",
- "",
- "The server that SENDS the notifications needs a service-account key from the",
- "SAME Firebase project. See docs for the self-hosting implications."
- ],
- "project_info": {
- "project_number": "000000000000",
- "project_id": "your-firebase-project-id",
- "storage_bucket": "your-firebase-project-id.appspot.com"
- },
- "client": [
- {
- "client_info": {
- "mobilesdk_app_id": "1:000000000000:android:0000000000000000000000",
- "android_client_info": {
- "package_name": "com.liketrek.trek"
- }
- },
- "oauth_client": [],
- "api_key": [
- {
- "current_key": "REPLACE_WITH_YOUR_ANDROID_API_KEY"
- }
- ],
- "services": {
- "appinvite_service": {
- "other_platform_oauth_client": []
- }
- }
- }
- ],
- "configuration_version": "1"
-}
diff --git a/client/android/variables.gradle b/client/android/variables.gradle
index 199310493..980b88bd5 100644
--- a/client/android/variables.gradle
+++ b/client/android/variables.gradle
@@ -1,10 +1,5 @@
ext {
- // 26, not Capacitor's default 24: @capacitor/inappbrowser requires a minimum
- // target of 26. This drops Android 7.0/7.1 (~1% of devices and no longer
- // receiving security updates). Removing @capacitor/inappbrowser — its system-
- // browser mode overlaps @capacitor/browser almost exactly — would allow 24
- // again.
- minSdkVersion = 26
+ minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
@@ -18,7 +13,6 @@ ext {
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
- // Consumed by @capacitor/push-notifications. Pinned rather than left to the
- // plugin's default so an FCM SDK bump is a visible, reviewable change here.
- firebaseMessagingVersion = '25.0.1'
+ // Custom Tabs for SSO and passkey sign-in (TrekShellPlugin.authenticate).
+ androidxBrowserVersion = '1.8.0'
}
\ No newline at end of file
diff --git a/client/capacitor.config.ts b/client/capacitor.config.ts
index 4e4e6ca88..fd51389cf 100644
--- a/client/capacitor.config.ts
+++ b/client/capacitor.config.ts
@@ -1,5 +1,3 @@
-///
-///
///
///
import type { CapacitorConfig } from '@capacitor/cli';
@@ -8,40 +6,33 @@ import { KeyboardResize } from '@capacitor/keyboard';
const config: CapacitorConfig = {
appId: 'com.liketrek.trek',
appName: 'TREK - Plan your trips!',
- webDir: 'dist',
- // TREK is self-hosted: the server the app talks to is whatever the user
- // enters, so every request is cross-origin. Both schemes stay https so the
- // webview origin is a secure context (crypto.subtle, WebAuthn, service
- // workers, IndexedDB persistence all require it).
+ // The app ships only the address screen. Once a server is picked, the native
+ // side points the WebView at it (TrekBridgeViewController.swift,
+ // MainActivity.java) and the UI comes from that server, always in the version
+ // the server runs. Session cookie, uploads, /ws and plugins work as they do in
+ // a browser, because the WebView is on the server's own origin.
+ webDir: 'native-dist',
server: {
+ // Android serves the address screen from https://localhost so it is a
+ // secure context. iOS keeps its default capacitor:// scheme: WKWebView
+ // handles https itself and Capacitor silently drops an iosScheme of https.
androidScheme: 'https',
- iosScheme: 'https',
+ // Shown, from the app bundle, whenever the server fails to load. The page
+ // knows a server is remembered and offers to retry or pick another one;
+ // on iOS it first tries the stored offline copy of the server's start page.
+ errorPath: 'index.html',
},
plugins: {
- PushNotifications: {
- // No `badge`: TREK has no unread count the OS could keep honest, and a
- // badge that only ever grows is worse than none.
- presentationOptions: ['sound', 'banner', 'list'],
- },
- LocalNotifications: {
- // Same white-on-transparent mask as FCM uses; generated by
- // scripts/generate-native-icons.mjs. Without it Android falls back to the
- // launcher icon and renders a white blob.
- smallIcon: 'notification_icon',
- presentationOptions: ['sound', 'banner', 'list'],
- },
SplashScreen: {
// Matches in index.html, so the launch image,
// the splash and the first painted frame are the same colour and there is
// no white flash between them.
backgroundColor: '#09090b',
- launchShowDuration: 500,
- // Deliberately true. With `false` the splash stays up until JS calls
- // SplashScreen.hide(), and until that call exists a failure to boot the
- // bundle looks like a frozen app. Flip this to false — and drop
- // launchShowDuration — in the same change that adds the hide() call once
- // the React tree has mounted.
+ // The server's UI hides it once React has mounted (native/nativeApp.ts),
+ // the address screen once it has rendered. Auto-hide stays on as the
+ // ceiling, so a page that never gets there cannot leave it up for good.
launchAutoHide: true,
+ launchShowDuration: 4000,
launchFadeOutDuration: 200,
showSpinner: false,
androidScaleType: 'CENTER_CROP',
@@ -54,13 +45,7 @@ const config: CapacitorConfig = {
// Android-only workaround: with an edge-to-edge status bar the webview
// otherwise does not resize at all when the keyboard opens.
resizeOnFullScreen: true,
- // No `style`: TREK has a real appearance system, so the keyboard should
- // follow the device/app theme rather than being pinned here.
},
- // No StatusBar block on purpose. From Android 15 / Capacitor 8 the app is
- // always edge-to-edge, so `overlaysWebView` and `backgroundColor` no longer
- // do anything. The status-bar style has to follow the user's chosen scheme
- // at runtime — call StatusBar.setStyle() from applyAppearance().
},
};
diff --git a/client/eslint.config.mjs b/client/eslint.config.mjs
index fe8e629c1..e37279968 100644
--- a/client/eslint.config.mjs
+++ b/client/eslint.config.mjs
@@ -30,12 +30,12 @@ export default tseslint.config(
'public',
'test-results',
'playwright-report',
- // Capacitor native shells. `npx cap sync` copies the built bundle from
- // dist/ into both platforms, so without these ESLint lints the minified
- // output and `npm run lint` reports tens of thousands of errors that have
- // nothing to do with the source. gitignore() does not cover them: the iOS
- // and Android .gitignore files live one level down, and this config's
- // gitignore integration only reads client/.gitignore.
+ // Capacitor native shells. `npx cap sync` copies the built address screen
+ // from native-dist/ into both platforms, so without these ESLint lints the
+ // minified output. gitignore() does not cover them: the iOS and Android
+ // .gitignore files live one level down, and this config's gitignore
+ // integration only reads client/.gitignore.
+ 'native-dist',
'android/**',
'ios/**',
'e2e/**',
diff --git a/client/ios/App/App.xcodeproj/project.pbxproj b/client/ios/App/App.xcodeproj/project.pbxproj
index 45991c4af..d2dc88309 100644
--- a/client/ios/App/App.xcodeproj/project.pbxproj
+++ b/client/ios/App/App.xcodeproj/project.pbxproj
@@ -17,6 +17,8 @@
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9582B6822FE993A50072D4E8 /* SceneDelegate.swift */; };
7A1E00032C0000000000A003 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */; };
+ 7A1E00052C0000000000A005 /* TrekBridgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */; };
+ 7A1E00072C0000000000A007 /* TrekShellPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E00062C0000000000A006 /* TrekShellPlugin.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -33,6 +35,8 @@
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
7A1E00012C0000000000A001 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; };
7A1E00022C0000000000A002 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekBridgeViewController.swift; sourceTree = ""; };
+ 7A1E00062C0000000000A006 /* TrekShellPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrekShellPlugin.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -70,6 +74,8 @@
9582B6822FE993A50072D4E8 /* SceneDelegate.swift */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
+ 7A1E00042C0000000000A004 /* TrekBridgeViewController.swift */,
+ 7A1E00062C0000000000A006 /* TrekShellPlugin.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
@@ -171,6 +177,8 @@
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
9582B6832FE993A70072D4E8 /* SceneDelegate.swift in Sources */,
+ 7A1E00052C0000000000A005 /* TrekBridgeViewController.swift in Sources */,
+ 7A1E00072C0000000000A007 /* TrekShellPlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/client/ios/App/App/App.entitlements b/client/ios/App/App/App.entitlements
index 78c3cf2ae..6631ffa6f 100644
--- a/client/ios/App/App/App.entitlements
+++ b/client/ios/App/App/App.entitlements
@@ -2,20 +2,5 @@
-
- aps-environment
- development
diff --git a/client/ios/App/App/Base.lproj/Main.storyboard b/client/ios/App/App/Base.lproj/Main.storyboard
index b44df7be8..2edb91811 100644
--- a/client/ios/App/App/Base.lproj/Main.storyboard
+++ b/client/ios/App/App/Base.lproj/Main.storyboard
@@ -11,7 +11,7 @@
-
+
diff --git a/client/ios/App/App/Info.plist b/client/ios/App/App/Info.plist
index fc2f05c0c..ea4ab407b 100644
--- a/client/ios/App/App/Info.plist
+++ b/client/ios/App/App/Info.plist
@@ -51,39 +51,25 @@
logs "Local network prohibited" instead of a useful error. -->
NSLocalNetworkUsageDescription
TREK connects to the TREK server you choose, which is often running on your home or office network.
-
+ The WebView's file picker offers "Take Photo" and "Photo Library". -->
NSCameraUsageDescription
TREK uses the camera so you can photograph a place, a journey entry or a booking document and add it to your trip.
NSPhotoLibraryUsageDescription
TREK needs access to your photos so you can add them to trips, places and journey entries.
+
NSPhotoLibraryAddUsageDescription
TREK saves photos and exports you download from your trips back to your photo library.
-
+
NSLocationWhenInUseUsageDescription
TREK uses your location to centre the map on you and to find places near you.
- NSLocationAlwaysAndWhenInUseUsageDescription
- TREK uses your location to centre the map on you and to find places near you.
-
- NSCalendarsWriteOnlyAccessUsageDescription
- TREK adds your trip days and bookings to your calendar.
- NSCalendarsFullAccessUsageDescription
- TREK reads your calendar to show existing commitments alongside your trip, and adds trip events to it.
- NSCalendarsUsageDescription
- TREK adds your trip days and bookings to your calendar.
NSPrivacyAccessedAPITypes
+
NSPrivacyAccessedAPIType
NSPrivacyAccessedAPICategoryFileTimestamp
@@ -25,7 +21,8 @@
C617.1
-
+
NSPrivacyAccessedAPIType
NSPrivacyAccessedAPICategoryUserDefaults
diff --git a/client/ios/App/App/SceneDelegate.swift b/client/ios/App/App/SceneDelegate.swift
index f352e1e95..5c55e16a0 100644
--- a/client/ios/App/App/SceneDelegate.swift
+++ b/client/ios/App/App/SceneDelegate.swift
@@ -8,7 +8,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
- window?.rootViewController = CAPBridgeViewController()
+ window?.rootViewController = TrekBridgeViewController()
window?.makeKeyAndVisible()
SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions)
diff --git a/client/ios/App/App/TrekBridgeViewController.swift b/client/ios/App/App/TrekBridgeViewController.swift
new file mode 100644
index 000000000..411670442
--- /dev/null
+++ b/client/ios/App/App/TrekBridgeViewController.swift
@@ -0,0 +1,41 @@
+import UIKit
+import Capacitor
+
+/// Points the WebView at the TREK server the user picked instead of the bundled
+/// address screen. The UI then comes from that server, in the version it runs,
+/// on its own origin, so its session cookie and service calls work as they do
+/// in Safari.
+class TrekBridgeViewController: CAPBridgeViewController {
+ override func instanceDescriptor() -> InstanceDescriptor {
+ let descriptor = super.instanceDescriptor()
+ if let server = TrekServerStore.url {
+ // The trailing slash matters: Capacitor treats every URL that starts
+ // with this string as the app's own, and without it that would take
+ // in https://trek.example.com.attacker.test as well.
+ descriptor.serverURL = server.hasSuffix("/") ? server : server + "/"
+ }
+ return descriptor
+ }
+
+ override func capacitorDidLoad() {
+ bridge?.registerPluginInstance(TrekShellPlugin())
+ }
+}
+
+/// The remembered server and the offline copy of its start page.
+enum TrekServerStore {
+ private static let key = "TrekServerURL"
+
+ static var url: String? {
+ get { UserDefaults.standard.string(forKey: key) }
+ set {
+ if let newValue { UserDefaults.standard.set(newValue, forKey: key) } else { UserDefaults.standard.removeObject(forKey: key) }
+ try? FileManager.default.removeItem(at: offlineCopyURL)
+ }
+ }
+
+ static var offlineCopyURL: URL {
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
+ return support.appendingPathComponent("offline-start-page.html")
+ }
+}
diff --git a/client/ios/App/App/TrekShellPlugin.swift b/client/ios/App/App/TrekShellPlugin.swift
new file mode 100644
index 000000000..c946e5474
--- /dev/null
+++ b/client/ios/App/App/TrekShellPlugin.swift
@@ -0,0 +1,139 @@
+import AuthenticationServices
+import Capacitor
+import UIKit
+import WebKit
+
+/// The native half of client/src/native/trekShell.ts.
+@objc(TrekShellPlugin)
+public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPresentationContextProviding {
+ public let identifier = "TrekShellPlugin"
+ public let jsName = "TrekShell"
+ public let pluginMethods: [CAPPluginMethod] = [
+ CAPPluginMethod(name: "getServer", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "setServer", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "resetServer", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "authenticate", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "refreshOfflineCopy", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "openOfflineCopy", returnType: CAPPluginReturnPromise),
+ ]
+
+ private var authSession: ASWebAuthenticationSession?
+
+ @objc func getServer(_ call: CAPPluginCall) {
+ call.resolve(["url": TrekServerStore.url ?? NSNull()])
+ }
+
+ @objc func setServer(_ call: CAPPluginCall) {
+ guard let raw = call.getString("url"), let url = URL(string: raw), ["https", "http"].contains(url.scheme), url.host != nil else {
+ call.reject("A http(s) URL is required")
+ return
+ }
+ TrekServerStore.url = raw
+ call.resolve()
+ restartBridge()
+ }
+
+ @objc func resetServer(_ call: CAPPluginCall) {
+ TrekServerStore.url = nil
+ call.resolve()
+ restartBridge()
+ }
+
+ @objc func reload(_ call: CAPPluginCall) {
+ call.resolve()
+ restartBridge()
+ }
+
+ /// A new bridge rather than a navigation: the server URL is part of the
+ /// bridge's configuration and decides which origin counts as the app.
+ private func restartBridge() {
+ DispatchQueue.main.async { [weak self] in
+ guard let window = self?.bridge?.viewController?.view.window else { return }
+ window.rootViewController = TrekBridgeViewController()
+ }
+ }
+
+ // MARK: - Sign-in through the system browser
+
+ @objc func authenticate(_ call: CAPPluginCall) {
+ guard let raw = call.getString("url"), let url = URL(string: raw), let scheme = call.getString("callbackScheme") else {
+ call.reject("url and callbackScheme are required")
+ return
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let self else { return }
+ let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { [weak self] callbackURL, error in
+ self?.authSession = nil
+ if let callbackURL {
+ call.resolve(["url": callbackURL.absoluteString])
+ } else if let authError = error as? ASWebAuthenticationSessionError, authError.code == .canceledLogin {
+ call.reject("Sign-in cancelled", "CANCELLED")
+ } else {
+ call.reject(error?.localizedDescription ?? "Sign-in failed")
+ }
+ }
+ // Shares Safari's cookies, so a running SSO session at the identity
+ // provider carries over and nobody types their password twice.
+ session.prefersEphemeralWebBrowserSession = false
+ session.presentationContextProvider = self
+ self.authSession = session
+ if !session.start() {
+ self.authSession = nil
+ call.reject("Could not open the browser")
+ }
+ }
+ }
+
+ public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ bridge?.viewController?.view.window ?? ASPresentationAnchor()
+ }
+
+ // MARK: - Offline start
+
+ /// WKWebView runs no service worker for a domain that is not listed in the
+ /// app at build time, and a user's own server never is. So the app keeps
+ /// the server's start page itself, and the scripts it references stay in
+ /// WebKit's HTTP cache (the server marks them immutable).
+ @objc func refreshOfflineCopy(_ call: CAPPluginCall) {
+ guard let raw = TrekServerStore.url, let url = URL(string: raw) else {
+ call.resolve(["saved": false])
+ return
+ }
+ var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
+ request.setValue("text/html", forHTTPHeaderField: "Accept")
+ URLSession.shared.dataTask(with: request) { data, response, _ in
+ guard let data, let http = response as? HTTPURLResponse, http.statusCode == 200,
+ http.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("text/html") == true,
+ TrekServerStore.url == raw else {
+ call.resolve(["saved": false])
+ return
+ }
+ do {
+ let target = TrekServerStore.offlineCopyURL
+ try FileManager.default.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try data.write(to: target, options: .atomic)
+ call.resolve(["saved": true])
+ } catch {
+ call.resolve(["saved": false])
+ }
+ }.resume()
+ }
+
+ @objc func openOfflineCopy(_ call: CAPPluginCall) {
+ guard let raw = TrekServerStore.url, let url = URL(string: raw),
+ let html = try? Data(contentsOf: TrekServerStore.offlineCopyURL) else {
+ call.resolve(["opened": false])
+ return
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let webView = self?.bridge?.webView else {
+ call.resolve(["opened": false])
+ return
+ }
+ let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "text/html; charset=utf-8"])!
+ call.resolve(["opened": true])
+ webView.loadSimulatedRequest(URLRequest(url: url), response: response, responseData: html)
+ }
+ }
+}
diff --git a/client/ios/App/CapApp-SPM/Package.swift b/client/ios/App/CapApp-SPM/Package.swift
index 6b3c3b311..4748cf9b2 100644
--- a/client/ios/App/CapApp-SPM/Package.swift
+++ b/client/ios/App/CapApp-SPM/Package.swift
@@ -11,25 +11,13 @@ let package = Package(
targets: ["CapApp-SPM"])
],
dependencies: [
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.0"),
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.2"),
.package(name: "CapacitorApp", path: "../../../../node_modules/@capacitor/app"),
- .package(name: "CapacitorBrowser", path: "../../../../node_modules/@capacitor/browser"),
- .package(name: "CapacitorCalendar", path: "../../../../node_modules/@capacitor/calendar"),
- .package(name: "CapacitorCamera", path: "../../../../node_modules/@capacitor/camera"),
- .package(name: "CapacitorFileTransfer", path: "../../../../node_modules/@capacitor/file-transfer"),
.package(name: "CapacitorFilesystem", path: "../../../../node_modules/@capacitor/filesystem"),
- .package(name: "CapacitorGeolocation", path: "../../../../node_modules/@capacitor/geolocation"),
- .package(name: "CapacitorHaptics", path: "../../../../node_modules/@capacitor/haptics"),
- .package(name: "CapacitorInappbrowser", path: "../../../../node_modules/@capacitor/inappbrowser"),
.package(name: "CapacitorKeyboard", path: "../../../../node_modules/@capacitor/keyboard"),
- .package(name: "CapacitorLocalNotifications", path: "../../../../node_modules/@capacitor/local-notifications"),
- .package(name: "CapacitorNetwork", path: "../../../../node_modules/@capacitor/network"),
- .package(name: "CapacitorPreferences", path: "../../../../node_modules/@capacitor/preferences"),
- .package(name: "CapacitorPushNotifications", path: "../../../../node_modules/@capacitor/push-notifications"),
.package(name: "CapacitorShare", path: "../../../../node_modules/@capacitor/share"),
.package(name: "CapacitorSplashScreen", path: "../../../../node_modules/@capacitor/splash-screen"),
- .package(name: "CapacitorStatusBar", path: "../../../../node_modules/@capacitor/status-bar"),
- .package(name: "CapacitorToast", path: "../../../../node_modules/@capacitor/toast")
+ .package(name: "CapacitorStatusBar", path: "../../../../node_modules/@capacitor/status-bar")
],
targets: [
.target(
@@ -38,23 +26,11 @@ let package = Package(
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorApp", package: "CapacitorApp"),
- .product(name: "CapacitorBrowser", package: "CapacitorBrowser"),
- .product(name: "CapacitorCalendar", package: "CapacitorCalendar"),
- .product(name: "CapacitorCamera", package: "CapacitorCamera"),
- .product(name: "CapacitorFileTransfer", package: "CapacitorFileTransfer"),
.product(name: "CapacitorFilesystem", package: "CapacitorFilesystem"),
- .product(name: "CapacitorGeolocation", package: "CapacitorGeolocation"),
- .product(name: "CapacitorHaptics", package: "CapacitorHaptics"),
- .product(name: "CapacitorInappbrowser", package: "CapacitorInappbrowser"),
.product(name: "CapacitorKeyboard", package: "CapacitorKeyboard"),
- .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"),
- .product(name: "CapacitorNetwork", package: "CapacitorNetwork"),
- .product(name: "CapacitorPreferences", package: "CapacitorPreferences"),
- .product(name: "CapacitorPushNotifications", package: "CapacitorPushNotifications"),
.product(name: "CapacitorShare", package: "CapacitorShare"),
.product(name: "CapacitorSplashScreen", package: "CapacitorSplashScreen"),
- .product(name: "CapacitorStatusBar", package: "CapacitorStatusBar"),
- .product(name: "CapacitorToast", package: "CapacitorToast")
+ .product(name: "CapacitorStatusBar", package: "CapacitorStatusBar")
]
)
]
diff --git a/client/native-shell/i18n.ts b/client/native-shell/i18n.ts
new file mode 100644
index 000000000..43e9df949
--- /dev/null
+++ b/client/native-shell/i18n.ts
@@ -0,0 +1,34 @@
+import { SUPPORTED_LANGUAGE_CODES } from '@trek/shared/i18n'
+import type { TranslationStrings } from '@trek/shared/i18n'
+
+// Only the native strings of every locale, not the whole UI: this page ships
+// inside the app binary and shows three screens.
+const bundles = import.meta.glob('../../shared/src/i18n/*/native.ts', { eager: true, import: 'default' })
+
+function bundleFor(code: string): TranslationStrings | undefined {
+ return bundles[`../../shared/src/i18n/${code}/native.ts`]
+}
+
+export function pickLocale(preferred: readonly string[]): string {
+ for (const lang of preferred) {
+ const lower = lang.toLowerCase()
+ if (lower === 'pt-br') return 'br'
+ if (lower.startsWith('zh-tw') || lower.startsWith('zh-hk') || lower.startsWith('zh-hant')) return 'zh-TW'
+ if (lower.startsWith('el')) return 'gr'
+ const prefix = lower.split('-')[0]
+ const match = SUPPORTED_LANGUAGE_CODES.find((code) => code.toLowerCase() === lower || code === prefix)
+ if (match) return match
+ }
+ return 'en'
+}
+
+export function translator(code: string) {
+ const strings = bundleFor(code) ?? {}
+ const fallback = bundleFor('en') ?? {}
+ return (key: string, params: Record = {}): string => {
+ const template = strings[key] ?? fallback[key] ?? key
+ return typeof template === 'string'
+ ? template.replace(/\{(\w+)\}/g, (whole, name: string) => params[name] ?? whole)
+ : key
+ }
+}
diff --git a/client/native-shell/index.html b/client/native-shell/index.html
new file mode 100644
index 000000000..ba3f6913d
--- /dev/null
+++ b/client/native-shell/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ TREK
+
+
+
+
+
+
diff --git a/client/native-shell/main.ts b/client/native-shell/main.ts
new file mode 100644
index 000000000..3dc71ec6c
--- /dev/null
+++ b/client/native-shell/main.ts
@@ -0,0 +1,119 @@
+import { CapacitorHttp } from '@capacitor/core'
+import { SplashScreen } from '@capacitor/splash-screen'
+import { TrekShell } from '../src/native/trekShell'
+import { probeServer, type ProbeFailure, type ProbeGet } from '../src/native/serverAddress'
+import { pickLocale, translator } from './i18n'
+import logoUrl from '../public/icons/icon-white.svg'
+import './shell.css'
+
+/**
+ * The only page the app ships itself. It asks for a server on first start and
+ * stands in when the remembered one cannot be reached; everything else is the
+ * UI that server delivers.
+ */
+
+const locale = pickLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
+const t = translator(locale)
+document.documentElement.lang = locale
+document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr'
+
+const root = document.getElementById('shell')!
+
+const PROBE_TIMEOUT_MS = 8000
+
+// Native HTTP rather than fetch: the probe goes to a server of the user's
+// choosing, which has no reason to answer this page's origin with CORS.
+const get: ProbeGet = async (url) => {
+ const response = await CapacitorHttp.get({ url, connectTimeout: PROBE_TIMEOUT_MS, readTimeout: PROBE_TIMEOUT_MS })
+ return { status: response.status, data: response.data, url: response.url }
+}
+
+const FAILURE_KEYS: Record = {
+ invalid: 'native.connect.errorInvalid',
+ unreachable: 'native.connect.errorUnreachable',
+ notTrek: 'native.connect.errorNotTrek',
+ tooOld: 'native.connect.errorTooOld',
+}
+
+function el(tag: K, props: Partial = {}, ...children: (Node | string)[]) {
+ const node = Object.assign(document.createElement(tag), props)
+ node.append(...children)
+ return node
+}
+
+function header(title: string, hint: string) {
+ return [
+ el('img', { src: logoUrl, alt: '', className: 'shell-logo' }),
+ el('h1', { className: 'shell-title' }, title),
+ el('p', { className: 'shell-hint' }, hint),
+ ]
+}
+
+function showConnect(prefill = '') {
+ const input = el('input', {
+ type: 'url',
+ className: 'shell-input',
+ placeholder: t('native.connect.placeholder'),
+ value: prefill,
+ autocapitalize: 'off',
+ spellcheck: false,
+ required: true,
+ })
+ input.setAttribute('inputmode', 'url')
+ input.setAttribute('autocomplete', 'url')
+ input.setAttribute('aria-label', t('native.connect.title'))
+ const error = el('p', { className: 'shell-error', role: 'alert' })
+ const submit = el('button', { type: 'submit', className: 'shell-button' }, t('native.connect.submit'))
+ const form = el('form', { className: 'shell-form', noValidate: true }, input, submit, error)
+
+ form.addEventListener('submit', async (event) => {
+ event.preventDefault()
+ error.textContent = ''
+ input.disabled = true
+ submit.disabled = true
+ submit.textContent = t('native.connect.checking')
+ const result = await probeServer(input.value, get)
+ if (result.ok) {
+ await TrekShell.setServer({ url: result.url })
+ return
+ }
+ error.textContent = t(FAILURE_KEYS[result.reason])
+ input.disabled = false
+ submit.disabled = false
+ submit.textContent = t('native.connect.submit')
+ input.focus()
+ })
+
+ root.replaceChildren(...header(t('native.connect.title'), t('native.connect.hint')), form)
+}
+
+function showUnreachable(server: string) {
+ const retry = el('button', { type: 'button', className: 'shell-button' }, t('native.offline.retry'))
+ retry.addEventListener('click', () => {
+ retry.disabled = true
+ void TrekShell.reload()
+ })
+ const change = el('button', { type: 'button', className: 'shell-link' }, t('native.changeServer'))
+ change.addEventListener('click', () => { void TrekShell.resetServer() })
+
+ root.replaceChildren(
+ ...header(t('native.offline.title'), t('native.offline.hint', { server: new URL(server).host })),
+ el('div', { className: 'shell-form' }, retry, change),
+ )
+}
+
+async function start() {
+ const { url } = await TrekShell.getServer()
+ if (!url) {
+ showConnect()
+ } else {
+ // Only reached when loading the server failed. On iOS the stored copy of
+ // its start page brings the offline-capable UI up anyway.
+ const { opened } = await TrekShell.openOfflineCopy()
+ if (opened) return
+ showUnreachable(url)
+ }
+ await SplashScreen.hide()
+}
+
+void start()
diff --git a/client/native-shell/shell.css b/client/native-shell/shell.css
new file mode 100644
index 000000000..0b3b92173
--- /dev/null
+++ b/client/native-shell/shell.css
@@ -0,0 +1,90 @@
+/* Mirrors the app's neutral default scheme (client/src/index.css), dark first:
+ this page shares the launch screen's background and must not flash. */
+:root {
+ --bg: #09090b;
+ --surface: #18181b;
+ --edge: #27272a;
+ --ink: #fafafa;
+ --muted: #a1a1aa;
+ --accent: #e4e4e7;
+ --accent-ink: #09090b;
+ --danger: #f87171;
+ color-scheme: dark;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --bg: #ffffff;
+ --surface: #f4f4f5;
+ --edge: #e4e4e7;
+ --ink: #111827;
+ --muted: #6b7280;
+ --accent: #111827;
+ --accent-ink: #ffffff;
+ --danger: #dc2626;
+ color-scheme: light;
+ }
+ .shell-logo { filter: invert(1); }
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ min-height: 100%;
+ background: var(--bg);
+ color: var(--ink);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ -webkit-text-size-adjust: 100%;
+}
+
+.shell {
+ min-height: 100vh;
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 12px;
+ max-width: 420px;
+ margin: 0 auto;
+ padding: calc(env(safe-area-inset-top) + 24px) 24px calc(env(safe-area-inset-bottom) + 24px);
+}
+
+.shell-logo { width: 56px; height: 56px; margin-bottom: 8px; }
+.shell-title { font-size: 1.5rem; font-weight: 600; margin: 0; }
+.shell-hint { color: var(--muted); margin: 0 0 12px; line-height: 1.5; overflow-wrap: anywhere; }
+
+.shell-form { display: flex; flex-direction: column; gap: 12px; }
+
+.shell-input {
+ font: inherit;
+ font-size: 1rem;
+ padding: 14px 16px;
+ border-radius: 12px;
+ border: 1px solid var(--edge);
+ background: var(--surface);
+ color: var(--ink);
+ outline: none;
+}
+.shell-input:focus { border-color: var(--accent); }
+
+.shell-button {
+ font: inherit;
+ font-weight: 600;
+ padding: 14px 16px;
+ border: 0;
+ border-radius: 12px;
+ background: var(--accent);
+ color: var(--accent-ink);
+}
+.shell-button:disabled { opacity: 0.6; }
+
+.shell-link {
+ font: inherit;
+ padding: 10px;
+ border: 0;
+ background: none;
+ color: var(--muted);
+}
+
+.shell-error { color: var(--danger); margin: 0; min-height: 1.5em; line-height: 1.5; }
diff --git a/client/package.json b/client/package.json
index 9afe4e9f7..1bb971287 100644
--- a/client/package.json
+++ b/client/package.json
@@ -7,6 +7,8 @@
"dev": "vite",
"prebuild": "node scripts/patch-maplibre.mjs && node scripts/generate-icons.mjs",
"icons:native": "node scripts/generate-native-icons.mjs",
+ "build:native": "vite build --config vite.native.config.js",
+ "cap:sync": "npm run build:native && cap sync",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"preview": "vite preview",
@@ -34,25 +36,13 @@
"dependencies": {
"@capacitor/android": "^8.5.0",
"@capacitor/app": "^8.1.1",
- "@capacitor/browser": "^8.0.4",
- "@capacitor/calendar": "^1.0.1",
- "@capacitor/camera": "^8.2.3",
"@capacitor/core": "^8.5.0",
- "@capacitor/file-transfer": "^2.0.5",
"@capacitor/filesystem": "^8.1.3",
- "@capacitor/geolocation": "^8.2.2",
- "@capacitor/haptics": "^8.0.2",
- "@capacitor/inappbrowser": "^4.0.3",
"@capacitor/ios": "^8.5.0",
"@capacitor/keyboard": "^8.0.5",
- "@capacitor/local-notifications": "^8.3.1",
- "@capacitor/network": "^8.0.1",
- "@capacitor/preferences": "^8.0.1",
- "@capacitor/push-notifications": "^8.1.2",
"@capacitor/share": "^8.0.1",
"@capacitor/splash-screen": "^8.0.2",
"@capacitor/status-bar": "^8.0.3",
- "@capacitor/toast": "^8.0.1",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/eb-garamond": "^5.3.0",
"@fontsource/geist-sans": "^5.2.5",
diff --git a/client/src/native/serverAddress.test.ts b/client/src/native/serverAddress.test.ts
new file mode 100644
index 000000000..02e33b71e
--- /dev/null
+++ b/client/src/native/serverAddress.test.ts
@@ -0,0 +1,73 @@
+import { describe, it, expect, vi } from 'vitest'
+import { candidateUrls, probeServer, type ProbeResponse } from './serverAddress'
+
+const features = (url: string, data: unknown = { bookingImport: false, aiParsing: false, nativeApp: true }): ProbeResponse =>
+ ({ status: 200, data, url })
+
+describe('candidateUrls', () => {
+ it('tries https before http when no scheme was typed', () => {
+ expect(candidateUrls('trek.example.com')).toEqual(['https://trek.example.com', 'http://trek.example.com'])
+ expect(candidateUrls(' 192.168.1.10:3001/ ')).toEqual(['https://192.168.1.10:3001', 'http://192.168.1.10:3001'])
+ })
+
+ it('keeps an explicit scheme and path, without query, hash or trailing slashes', () => {
+ expect(candidateUrls('http://nas.local:3001/trek/?x=1#y')).toEqual(['http://nas.local:3001/trek'])
+ expect(candidateUrls('HTTPS://Trek.Example.com//')).toEqual(['https://trek.example.com'])
+ })
+
+ it('returns nothing for empty input or other schemes', () => {
+ expect(candidateUrls(' ')).toEqual([])
+ expect(candidateUrls('ftp://trek.example.com')).toEqual([])
+ expect(candidateUrls('javascript://alert(1)')).toEqual([])
+ })
+})
+
+describe('probeServer', () => {
+ it('accepts a server that reports nativeApp and follows its redirect', async () => {
+ const get = vi.fn().mockResolvedValue(features('https://trek.example.com/api/health/features'))
+ expect(await probeServer('http://trek.example.com', get)).toEqual({ ok: true, url: 'https://trek.example.com' })
+ expect(get).toHaveBeenCalledWith('http://trek.example.com/api/health/features')
+ })
+
+ it('falls back to the candidate itself when the final URL is not the probe path', async () => {
+ const get = vi.fn().mockResolvedValue(features('https://sso.example.com/login'))
+ expect(await probeServer('https://trek.example.com', get)).toEqual({ ok: true, url: 'https://trek.example.com' })
+ })
+
+ it('moves on to plain http when https cannot be reached', async () => {
+ const get = vi.fn()
+ .mockRejectedValueOnce(new Error('ECONNREFUSED'))
+ .mockResolvedValueOnce(features('http://192.168.1.10:3001/api/health/features'))
+ expect(await probeServer('192.168.1.10:3001', get)).toEqual({ ok: true, url: 'http://192.168.1.10:3001' })
+ expect(get).toHaveBeenNthCalledWith(2, 'http://192.168.1.10:3001/api/health/features')
+ })
+
+ it('names a TREK server without the native flag as too old', async () => {
+ const get = vi.fn().mockResolvedValue(features('https://old.example.com/api/health/features', { bookingImport: true, aiParsing: false }))
+ expect(await probeServer('https://old.example.com', get)).toEqual({ ok: false, reason: 'tooOld' })
+ })
+
+ it('tells a server that is not TREK apart from one that is not there', async () => {
+ const html = vi.fn().mockResolvedValue({ status: 200, data: '', url: 'https://x.example.com/api/health/features' })
+ expect(await probeServer('https://x.example.com', html)).toEqual({ ok: false, reason: 'notTrek' })
+
+ const missing = vi.fn().mockResolvedValue({ status: 404, data: {}, url: 'https://x.example.com/api/health/features' })
+ expect(await probeServer('https://x.example.com', missing)).toEqual({ ok: false, reason: 'notTrek' })
+
+ const down = vi.fn().mockRejectedValue(new Error('timeout'))
+ expect(await probeServer('x.example.com', down)).toEqual({ ok: false, reason: 'unreachable' })
+ })
+
+ it('keeps the most telling failure across candidates', async () => {
+ const get = vi.fn()
+ .mockResolvedValueOnce(features('https://nas.local/api/health/features', { bookingImport: false }))
+ .mockRejectedValueOnce(new Error('ECONNREFUSED'))
+ expect(await probeServer('nas.local', get)).toEqual({ ok: false, reason: 'tooOld' })
+ })
+
+ it('refuses input that is not an address', async () => {
+ const get = vi.fn()
+ expect(await probeServer('', get)).toEqual({ ok: false, reason: 'invalid' })
+ expect(get).not.toHaveBeenCalled()
+ })
+})
diff --git a/client/src/native/serverAddress.ts b/client/src/native/serverAddress.ts
new file mode 100644
index 000000000..aa0717182
--- /dev/null
+++ b/client/src/native/serverAddress.ts
@@ -0,0 +1,78 @@
+/**
+ * Turns what someone typed into the app's address field into a TREK server it
+ * can load, or a reason it cannot.
+ */
+
+export type ProbeFailure = 'invalid' | 'unreachable' | 'notTrek' | 'tooOld'
+export type ProbeResult =
+ | { ok: true; url: string; reason?: undefined }
+ | { ok: false; reason: ProbeFailure; url?: undefined }
+
+export interface ProbeResponse {
+ status: number
+ data: unknown
+ /** The URL after redirects. */
+ url: string
+}
+export type ProbeGet = (url: string) => Promise
+
+const FEATURES_PATH = '/api/health/features'
+
+function normalize(raw: string): string | null {
+ let parsed: URL
+ try {
+ parsed = new URL(raw)
+ } catch {
+ return null
+ }
+ if ((parsed.protocol !== 'https:' && parsed.protocol !== 'http:') || !parsed.hostname) return null
+ return parsed.origin + parsed.pathname.replace(/\/+$/, '')
+}
+
+/**
+ * Without a scheme, https comes first and plain http second: a server on the
+ * home network is often reached as a bare IP and port without TLS.
+ */
+export function candidateUrls(input: string): string[] {
+ const trimmed = input.trim()
+ if (!trimmed) return []
+ const candidates = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
+ ? [normalize(trimmed)]
+ : [normalize(`https://${trimmed}`), normalize(`http://${trimmed}`)]
+ return candidates.filter((url): url is string => url !== null)
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null
+}
+
+function judge(response: ProbeResponse): ProbeResult {
+ if (response.status !== 200 || !isRecord(response.data)) return { ok: false, reason: 'notTrek' }
+ if (response.data.nativeApp === true) {
+ // Follow a redirect (http to https, a canonical host) so the app loads the
+ // address the server actually answers on.
+ const base = response.url.endsWith(FEATURES_PATH) ? response.url.slice(0, -FEATURES_PATH.length) : null
+ return { ok: true, url: (base && normalize(base)) || '' }
+ }
+ return 'bookingImport' in response.data ? { ok: false, reason: 'tooOld' } : { ok: false, reason: 'notTrek' }
+}
+
+const FAILURE_RANK: Record = { invalid: 0, unreachable: 1, notTrek: 2, tooOld: 3 }
+
+export async function probeServer(input: string, get: ProbeGet): Promise {
+ const candidates = candidateUrls(input)
+ if (!candidates.length) return { ok: false, reason: 'invalid' }
+
+ let failure: ProbeFailure = 'unreachable'
+ for (const candidate of candidates) {
+ let verdict: ProbeResult
+ try {
+ verdict = judge(await get(candidate + FEATURES_PATH))
+ } catch {
+ continue
+ }
+ if (verdict.ok) return { ok: true, url: verdict.url || candidate }
+ if (FAILURE_RANK[verdict.reason] > FAILURE_RANK[failure]) failure = verdict.reason
+ }
+ return { ok: false, reason: failure }
+}
diff --git a/client/src/native/trekShell.ts b/client/src/native/trekShell.ts
new file mode 100644
index 000000000..0551043d1
--- /dev/null
+++ b/client/src/native/trekShell.ts
@@ -0,0 +1,36 @@
+import { registerPlugin } from '@capacitor/core'
+
+/**
+ * The app's own native plugin, implemented in
+ * ios/App/App/TrekShellPlugin.swift and
+ * android/app/src/main/java/com/liketrek/trek/TrekShellPlugin.java.
+ *
+ * The app is a frame around the UI the user's server delivers, so what the
+ * web side needs from it is small: which server that is, a way to change it,
+ * a system browser for the sign-ins a WebView cannot do, and on iOS the
+ * offline copy of the start page that stands in for the missing service
+ * worker.
+ */
+export interface TrekShellPlugin {
+ getServer(): Promise<{ url: string | null }>
+ /** Remembers the server and loads it. The current page is gone afterwards. */
+ setServer(options: { url: string }): Promise
+ /** Forgets the server and returns to the address screen. */
+ resetServer(): Promise
+ /** Loads the remembered server again, after it could not be reached. */
+ reload(): Promise
+ /**
+ * Opens `url` in the system browser and resolves with the URL it was sent
+ * back to under `callbackScheme`. Rejects with code `CANCELLED` when the
+ * user closes the browser.
+ */
+ authenticate(options: { url: string; callbackScheme: string }): Promise<{ url: string }>
+ /** iOS: stores the server's start page for a cold start without network. */
+ refreshOfflineCopy(): Promise<{ saved: boolean }>
+ /** iOS: loads that copy under the server's origin. False when there is none. */
+ openOfflineCopy(): Promise<{ opened: boolean }>
+}
+
+export const TrekShell = registerPlugin('TrekShell')
+
+export const APP_URL_SCHEME = 'com.liketrek.trek'
diff --git a/client/tsconfig.json b/client/tsconfig.json
index d3c51f17e..84641aa31 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -26,5 +26,5 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
- "include": ["src", "tests"]
+ "include": ["src", "tests", "native-shell"]
}
diff --git a/client/vite.native.config.js b/client/vite.native.config.js
new file mode 100644
index 000000000..4c39f31a5
--- /dev/null
+++ b/client/vite.native.config.js
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import { fileURLToPath } from 'url'
+
+// The address screen the native apps ship (native-shell/). Everything past it
+// is the UI of the server the user picks, so this build stays tiny and apart
+// from the main one: no PWA, no React, only the native strings of each locale.
+export default defineConfig({
+ root: fileURLToPath(new URL('./native-shell', import.meta.url)),
+ base: './',
+ publicDir: false,
+ build: {
+ outDir: fileURLToPath(new URL('./native-dist', import.meta.url)),
+ emptyOutDir: true,
+ target: ['es2020', 'safari15', 'chrome100'],
+ },
+})
diff --git a/package-lock.json b/package-lock.json
index 4ab3e602e..95c86a4d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,25 +30,13 @@
"dependencies": {
"@capacitor/android": "^8.5.0",
"@capacitor/app": "^8.1.1",
- "@capacitor/browser": "^8.0.4",
- "@capacitor/calendar": "^1.0.1",
- "@capacitor/camera": "^8.2.3",
"@capacitor/core": "^8.5.0",
- "@capacitor/file-transfer": "^2.0.5",
"@capacitor/filesystem": "^8.1.3",
- "@capacitor/geolocation": "^8.2.2",
- "@capacitor/haptics": "^8.0.2",
- "@capacitor/inappbrowser": "^4.0.3",
"@capacitor/ios": "^8.5.0",
"@capacitor/keyboard": "^8.0.5",
- "@capacitor/local-notifications": "^8.3.1",
- "@capacitor/network": "^8.0.1",
- "@capacitor/preferences": "^8.0.1",
- "@capacitor/push-notifications": "^8.1.2",
"@capacitor/share": "^8.0.1",
"@capacitor/splash-screen": "^8.0.2",
"@capacitor/status-bar": "^8.0.3",
- "@capacitor/toast": "^8.0.1",
"@fontsource/bebas-neue": "^5.3.0",
"@fontsource/eb-garamond": "^5.3.0",
"@fontsource/geist-sans": "^5.2.5",
@@ -2409,33 +2397,6 @@
"@capacitor/core": ">=8.0.0"
}
},
- "node_modules/@capacitor/browser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.4.tgz",
- "integrity": "sha512-ORZ4u/y+7aMuu6yVuk6SP529fUZyqcbnuHB1q5McpA3o2SYfVoi28iB8rsQjI2ad1qlKJd29ZUuGtspXLBDlWg==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/calendar": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@capacitor/calendar/-/calendar-1.0.1.tgz",
- "integrity": "sha512-2ogW+Jlamy6BJkZmaggB/Jx282G+o3aBPfgx3GxmjlLjl8hqcorQKFhlCiveDui6xEKoVy13AQpKhukHQ8MrCQ==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/camera": {
- "version": "8.2.4",
- "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-8.2.4.tgz",
- "integrity": "sha512-kZIeRkOvtE2dnbq76tEo3ZWKyGfzHFK0txsR56qKAmjAARTg9r86iIUcKZa/y4U1vOHH/gdQae5e3+OAF3wwPQ==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
"node_modules/@capacitor/cli": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz",
@@ -2560,18 +2521,6 @@
"tslib": "^2.1.0"
}
},
- "node_modules/@capacitor/file-transfer": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@capacitor/file-transfer/-/file-transfer-2.0.5.tgz",
- "integrity": "sha512-G2a/+NKvADNcmxMSqy6HPp+Ij3trCy0+EVNMrRWmp8ueyCNaJEi96LpfEIztv+vqK9+xLRskQ7iVl+Pj8qt4hg==",
- "license": "MIT",
- "dependencies": {
- "@capacitor/synapse": "^1.0.4"
- },
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
"node_modules/@capacitor/filesystem": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-8.1.3.tgz",
@@ -2584,36 +2533,6 @@
"@capacitor/core": ">=8.0.0"
}
},
- "node_modules/@capacitor/geolocation": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.2.tgz",
- "integrity": "sha512-X8ryw6CfL4LspJnnfVL4++DUKKZVfdUIA9JljWVRljEDIQxZQZtz6xf7qstht+ymc+le6oa5EaUChVA6KsCYlA==",
- "license": "MIT",
- "dependencies": {
- "@capacitor/synapse": "^1.0.4"
- },
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/haptics": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz",
- "integrity": "sha512-c2hZzRR5Fk1tbTvhG1jhh2XBAf3EhnIerMIb2sl7Mt41Gxx1fhBJFDa0/BI1IbY4loVepyyuqNC9820/GZuoWQ==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/inappbrowser": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@capacitor/inappbrowser/-/inappbrowser-4.0.3.tgz",
- "integrity": "sha512-Jnl6Ssp2QZByncO6QhDKzNpi3yeum1aSQM+70VMVh8TJ16HOwOrcTnf8E4+7FpT9ruSRGyhqQu1yBSrpl+wD1g==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
"node_modules/@capacitor/ios": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.5.2.tgz",
@@ -2632,42 +2551,6 @@
"@capacitor/core": ">=8.0.0"
}
},
- "node_modules/@capacitor/local-notifications": {
- "version": "8.3.1",
- "resolved": "https://registry.npmjs.org/@capacitor/local-notifications/-/local-notifications-8.3.1.tgz",
- "integrity": "sha512-F7zu/+p/mIR958YP4axOkeI5hBKRaXDyYjbNPrAil2wWfd7OxScPSTvP9t85NpQ0eULVRu0/sntoxOg6ck477A==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/network": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@capacitor/network/-/network-8.0.1.tgz",
- "integrity": "sha512-9xK/FHFmzKGanB6BdoSZOzXk8vF0OFVQSQ4PAsIrzAzLuXHryO317qy8dcHVpgxYeuZq2noI0My9z1DvVDi/9w==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/preferences": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
- "integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
- "node_modules/@capacitor/push-notifications": {
- "version": "8.1.2",
- "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.2.tgz",
- "integrity": "sha512-TQl4XxqJfNF+KbSYBFMXYYT5WXUaleVKXlByhh/8+KySMVNjp+l5OqAUoGe/sY1IAnGXEuUeCjA/WLZEVvGwrA==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
"node_modules/@capacitor/share": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.2.tgz",
@@ -2701,15 +2584,6 @@
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
"license": "ISC"
},
- "node_modules/@capacitor/toast": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@capacitor/toast/-/toast-8.0.1.tgz",
- "integrity": "sha512-0uoyftoAeFjtOMiozBo7YXEo+zcpQCAxtBONSvrljJ05SKdlb1A8OFHaV/DZfIpGt+19kzUGaEaUemLdll4wUw==",
- "license": "MIT",
- "peerDependencies": {
- "@capacitor/core": ">=8.0.0"
- }
- },
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
diff --git a/shared/src/i18n/ar/index.ts b/shared/src/i18n/ar/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/ar/index.ts
+++ b/shared/src/i18n/ar/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/ar/native.ts b/shared/src/i18n/ar/native.ts
new file mode 100644
index 000000000..f53875ca1
--- /dev/null
+++ b/shared/src/i18n/ar/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'الاتصال بـ TREK',
+ 'native.connect.hint': 'أدخل العنوان الذي تفتح به TREK في متصفحك.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'اتصال',
+ 'native.connect.checking': 'جارٍ التحقق…',
+ 'native.connect.errorInvalid': 'لا يبدو هذا عنوان ويب.',
+ 'native.connect.errorUnreachable': 'لا يستجيب أي خادم على هذا العنوان. تحقق من العنوان ومن اتصالك.',
+ 'native.connect.errorNotTrek': 'الخادم على هذا العنوان ليس TREK.',
+ 'native.connect.errorTooOld': 'خادم TREK هذا قديم جدًا بالنسبة للتطبيق. يحتاج إلى تحديث أولًا.',
+ 'native.offline.title': 'تعذّر الوصول إلى الخادم',
+ 'native.offline.hint': 'تعذّر على TREK الوصول إلى {server}. تحقق من اتصالك ثم حاول مرة أخرى.',
+ 'native.offline.retry': 'إعادة المحاولة',
+ 'native.changeServer': 'تغيير الخادم',
+ 'native.login.failed': 'تعذّر تسجيل الدخول. يرجى المحاولة مرة أخرى.',
+ 'native.handoff.title': 'المتابعة في التطبيق',
+ 'native.handoff.hint': 'لقد سجّلت الدخول باسم {name}. افتح تطبيق TREK لإكمال تسجيل الدخول هناك.',
+ 'native.handoff.open': 'فتح تطبيق TREK',
+ 'native.handoff.switchAccount': 'استخدام حساب آخر',
+ 'native.settings.server': 'الخادم',
+ 'native.settings.serverHint': 'هذا التطبيق متصل بـ {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/br/index.ts b/shared/src/i18n/br/index.ts
index 0afbf275b..cb4a48de4 100644
--- a/shared/src/i18n/br/index.ts
+++ b/shared/src/i18n/br/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/br/native.ts b/shared/src/i18n/br/native.ts
new file mode 100644
index 000000000..7bb613ac6
--- /dev/null
+++ b/shared/src/i18n/br/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Conectar ao TREK',
+ 'native.connect.hint': 'Digite o endereço em que você abre o TREK no navegador.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Conectar',
+ 'native.connect.checking': 'Verificando…',
+ 'native.connect.errorInvalid': 'Isso não parece um endereço da web.',
+ 'native.connect.errorUnreachable': 'Nenhum servidor respondeu neste endereço. Verifique o endereço e sua conexão.',
+ 'native.connect.errorNotTrek': 'O servidor neste endereço não é o TREK.',
+ 'native.connect.errorTooOld': 'Este servidor TREK é antigo demais para o app. Ele precisa ser atualizado primeiro.',
+ 'native.offline.title': 'Servidor inacessível',
+ 'native.offline.hint': 'O TREK não conseguiu acessar {server}. Verifique sua conexão e tente novamente.',
+ 'native.offline.retry': 'Tentar novamente',
+ 'native.changeServer': 'Trocar de servidor',
+ 'native.login.failed': 'Não foi possível entrar. Tente novamente.',
+ 'native.handoff.title': 'Continuar no app',
+ 'native.handoff.hint': 'Você entrou como {name}. Abra o app TREK para concluir o login por lá.',
+ 'native.handoff.open': 'Abrir o app TREK',
+ 'native.handoff.switchAccount': 'Usar outra conta',
+ 'native.settings.server': 'Servidor',
+ 'native.settings.serverHint': 'Este app está conectado a {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/ca/index.ts b/shared/src/i18n/ca/index.ts
index 99ad8c034..0efdeaec1 100644
--- a/shared/src/i18n/ca/index.ts
+++ b/shared/src/i18n/ca/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/ca/native.ts b/shared/src/i18n/ca/native.ts
new file mode 100644
index 000000000..dfa07a281
--- /dev/null
+++ b/shared/src/i18n/ca/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Connecta amb TREK',
+ 'native.connect.hint': 'Introdueix l’adreça amb què obres TREK al navegador.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Connecta',
+ 'native.connect.checking': 'S’està comprovant…',
+ 'native.connect.errorInvalid': 'Això no sembla una adreça web.',
+ 'native.connect.errorUnreachable': 'Cap servidor respon en aquesta adreça. Comprova l’adreça i la connexió.',
+ 'native.connect.errorNotTrek': 'El servidor d’aquesta adreça no és TREK.',
+ 'native.connect.errorTooOld': 'Aquest servidor de TREK és massa antic per a l’app. Primer cal actualitzar-lo.',
+ 'native.offline.title': 'No es pot accedir al servidor',
+ 'native.offline.hint': 'TREK no ha pogut connectar amb {server}. Comprova la connexió i torna-ho a provar.',
+ 'native.offline.retry': 'Torna-ho a provar',
+ 'native.changeServer': 'Canvia de servidor',
+ 'native.login.failed': 'No s’ha pogut iniciar la sessió. Torna-ho a provar.',
+ 'native.handoff.title': 'Continua a l’app',
+ 'native.handoff.hint': 'Has iniciat la sessió com a {name}. Obre l’app de TREK per acabar d’iniciar-hi la sessió.',
+ 'native.handoff.open': 'Obre l’app de TREK',
+ 'native.handoff.switchAccount': 'Fes servir un altre compte',
+ 'native.settings.server': 'Servidor',
+ 'native.settings.serverHint': 'Aquesta app està connectada a {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/cs/index.ts b/shared/src/i18n/cs/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/cs/index.ts
+++ b/shared/src/i18n/cs/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/cs/native.ts b/shared/src/i18n/cs/native.ts
new file mode 100644
index 000000000..826386836
--- /dev/null
+++ b/shared/src/i18n/cs/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Připojit k TREK',
+ 'native.connect.hint': 'Zadejte adresu, na které otevíráte TREK v prohlížeči.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Připojit',
+ 'native.connect.checking': 'Ověřování…',
+ 'native.connect.errorInvalid': 'To nevypadá jako webová adresa.',
+ 'native.connect.errorUnreachable': 'Na této adrese neodpovídá žádný server. Zkontrolujte adresu a připojení.',
+ 'native.connect.errorNotTrek': 'Server na této adrese není TREK.',
+ 'native.connect.errorTooOld': 'Tento server TREK je pro aplikaci příliš starý. Nejprve potřebuje aktualizaci.',
+ 'native.offline.title': 'Server není dostupný',
+ 'native.offline.hint': 'TREK se nepodařilo spojit s {server}. Zkontrolujte připojení a zkuste to znovu.',
+ 'native.offline.retry': 'Zkusit znovu',
+ 'native.changeServer': 'Změnit server',
+ 'native.login.failed': 'Přihlášení se nezdařilo. Zkuste to prosím znovu.',
+ 'native.handoff.title': 'Pokračovat v aplikaci',
+ 'native.handoff.hint': 'Jste přihlášeni jako {name}. Otevřete aplikaci TREK a dokončete přihlášení v ní.',
+ 'native.handoff.open': 'Otevřít aplikaci TREK',
+ 'native.handoff.switchAccount': 'Použít jiný účet',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Tato aplikace je připojena k {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/de/index.ts b/shared/src/i18n/de/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/de/index.ts
+++ b/shared/src/i18n/de/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/de/native.ts b/shared/src/i18n/de/native.ts
new file mode 100644
index 000000000..e19558553
--- /dev/null
+++ b/shared/src/i18n/de/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Mit TREK verbinden',
+ 'native.connect.hint': 'Gib die Adresse ein, unter der du TREK im Browser öffnest.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Verbinden',
+ 'native.connect.checking': 'Wird geprüft…',
+ 'native.connect.errorInvalid': 'Das sieht nicht nach einer Webadresse aus.',
+ 'native.connect.errorUnreachable': 'Unter dieser Adresse antwortet kein Server. Prüfe die Adresse und deine Verbindung.',
+ 'native.connect.errorNotTrek': 'Unter dieser Adresse läuft kein TREK.',
+ 'native.connect.errorTooOld': 'Dieser TREK-Server ist zu alt für die App. Er braucht zuerst ein Update.',
+ 'native.offline.title': 'Server nicht erreichbar',
+ 'native.offline.hint': 'TREK konnte {server} nicht erreichen. Prüfe deine Verbindung und versuche es erneut.',
+ 'native.offline.retry': 'Erneut versuchen',
+ 'native.changeServer': 'Server wechseln',
+ 'native.login.failed': 'Die Anmeldung hat nicht geklappt. Bitte versuche es erneut.',
+ 'native.handoff.title': 'Weiter in der App',
+ 'native.handoff.hint': 'Du bist als {name} angemeldet. Öffne die TREK-App, um die Anmeldung dort abzuschließen.',
+ 'native.handoff.open': 'TREK-App öffnen',
+ 'native.handoff.switchAccount': 'Anderes Konto verwenden',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Diese App ist mit {server} verbunden.',
+};
+
+export default native;
diff --git a/shared/src/i18n/en/index.ts b/shared/src/i18n/en/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/en/index.ts
+++ b/shared/src/i18n/en/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/en/native.ts b/shared/src/i18n/en/native.ts
new file mode 100644
index 000000000..7c96c9d67
--- /dev/null
+++ b/shared/src/i18n/en/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Connect to TREK',
+ 'native.connect.hint': 'Enter the address you open TREK at in your browser.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Connect',
+ 'native.connect.checking': 'Checking…',
+ 'native.connect.errorInvalid': 'That does not look like a web address.',
+ 'native.connect.errorUnreachable': 'No server answered at this address. Check the address and your connection.',
+ 'native.connect.errorNotTrek': 'The server at this address is not TREK.',
+ 'native.connect.errorTooOld': 'This TREK server is too old for the app. It needs an update first.',
+ 'native.offline.title': 'Server not reachable',
+ 'native.offline.hint': 'TREK could not reach {server}. Check your connection and try again.',
+ 'native.offline.retry': 'Try again',
+ 'native.changeServer': 'Change server',
+ 'native.login.failed': 'Signing in did not work. Please try again.',
+ 'native.handoff.title': 'Continue in the app',
+ 'native.handoff.hint': 'You are signed in as {name}. Open the TREK app to finish signing in there.',
+ 'native.handoff.open': 'Open the TREK app',
+ 'native.handoff.switchAccount': 'Use a different account',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'This app is connected to {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/es/index.ts b/shared/src/i18n/es/index.ts
index e4f631e73..85ed6e733 100644
--- a/shared/src/i18n/es/index.ts
+++ b/shared/src/i18n/es/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/es/native.ts b/shared/src/i18n/es/native.ts
new file mode 100644
index 000000000..d9d0c58b8
--- /dev/null
+++ b/shared/src/i18n/es/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Conectar con TREK',
+ 'native.connect.hint': 'Introduce la dirección con la que abres TREK en tu navegador.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Conectar',
+ 'native.connect.checking': 'Comprobando…',
+ 'native.connect.errorInvalid': 'Eso no parece una dirección web.',
+ 'native.connect.errorUnreachable': 'Ningún servidor responde en esta dirección. Revisa la dirección y tu conexión.',
+ 'native.connect.errorNotTrek': 'El servidor de esta dirección no es TREK.',
+ 'native.connect.errorTooOld': 'Este servidor de TREK es demasiado antiguo para la app. Primero necesita una actualización.',
+ 'native.offline.title': 'Servidor no disponible',
+ 'native.offline.hint': 'TREK no pudo conectar con {server}. Revisa tu conexión e inténtalo de nuevo.',
+ 'native.offline.retry': 'Reintentar',
+ 'native.changeServer': 'Cambiar de servidor',
+ 'native.login.failed': 'No se pudo iniciar sesión. Inténtalo de nuevo.',
+ 'native.handoff.title': 'Continuar en la app',
+ 'native.handoff.hint': 'Has iniciado sesión como {name}. Abre la app de TREK para terminar de iniciar sesión allí.',
+ 'native.handoff.open': 'Abrir la app de TREK',
+ 'native.handoff.switchAccount': 'Usar otra cuenta',
+ 'native.settings.server': 'Servidor',
+ 'native.settings.serverHint': 'Esta app está conectada a {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/fr/index.ts b/shared/src/i18n/fr/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/fr/index.ts
+++ b/shared/src/i18n/fr/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/fr/native.ts b/shared/src/i18n/fr/native.ts
new file mode 100644
index 000000000..a2abdba64
--- /dev/null
+++ b/shared/src/i18n/fr/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Se connecter à TREK',
+ 'native.connect.hint': 'Saisissez l’adresse à laquelle vous ouvrez TREK dans votre navigateur.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Se connecter',
+ 'native.connect.checking': 'Vérification…',
+ 'native.connect.errorInvalid': 'Cela ne ressemble pas à une adresse web.',
+ 'native.connect.errorUnreachable': 'Aucun serveur ne répond à cette adresse. Vérifiez l’adresse et votre connexion.',
+ 'native.connect.errorNotTrek': 'Le serveur à cette adresse n’est pas TREK.',
+ 'native.connect.errorTooOld': 'Ce serveur TREK est trop ancien pour l’application. Il doit d’abord être mis à jour.',
+ 'native.offline.title': 'Serveur injoignable',
+ 'native.offline.hint': 'TREK n’a pas pu joindre {server}. Vérifiez votre connexion et réessayez.',
+ 'native.offline.retry': 'Réessayer',
+ 'native.changeServer': 'Changer de serveur',
+ 'native.login.failed': 'La connexion n’a pas abouti. Veuillez réessayer.',
+ 'native.handoff.title': 'Continuer dans l’application',
+ 'native.handoff.hint': 'Vous êtes connecté en tant que {name}. Ouvrez l’application TREK pour y terminer la connexion.',
+ 'native.handoff.open': 'Ouvrir l’application TREK',
+ 'native.handoff.switchAccount': 'Utiliser un autre compte',
+ 'native.settings.server': 'Serveur',
+ 'native.settings.serverHint': 'Cette application est connectée à {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/gr/index.ts b/shared/src/i18n/gr/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/gr/index.ts
+++ b/shared/src/i18n/gr/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/gr/native.ts b/shared/src/i18n/gr/native.ts
new file mode 100644
index 000000000..6b5e45e10
--- /dev/null
+++ b/shared/src/i18n/gr/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Σύνδεση στο TREK',
+ 'native.connect.hint': 'Πληκτρολογήστε τη διεύθυνση με την οποία ανοίγετε το TREK στο πρόγραμμα περιήγησης.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Σύνδεση',
+ 'native.connect.checking': 'Έλεγχος…',
+ 'native.connect.errorInvalid': 'Αυτό δεν μοιάζει με διεύθυνση ιστού.',
+ 'native.connect.errorUnreachable': 'Κανένας διακομιστής δεν απαντά σε αυτή τη διεύθυνση. Ελέγξτε τη διεύθυνση και τη σύνδεσή σας.',
+ 'native.connect.errorNotTrek': 'Ο διακομιστής σε αυτή τη διεύθυνση δεν είναι TREK.',
+ 'native.connect.errorTooOld': 'Αυτός ο διακομιστής TREK είναι πολύ παλιός για την εφαρμογή. Χρειάζεται πρώτα ενημέρωση.',
+ 'native.offline.title': 'Ο διακομιστής δεν είναι προσβάσιμος',
+ 'native.offline.hint': 'Το TREK δεν μπόρεσε να συνδεθεί στο {server}. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά.',
+ 'native.offline.retry': 'Δοκιμάστε ξανά',
+ 'native.changeServer': 'Αλλαγή διακομιστή',
+ 'native.login.failed': 'Η σύνδεση δεν ολοκληρώθηκε. Δοκιμάστε ξανά.',
+ 'native.handoff.title': 'Συνέχεια στην εφαρμογή',
+ 'native.handoff.hint': 'Έχετε συνδεθεί ως {name}. Ανοίξτε την εφαρμογή TREK για να ολοκληρώσετε εκεί τη σύνδεση.',
+ 'native.handoff.open': 'Άνοιγμα της εφαρμογής TREK',
+ 'native.handoff.switchAccount': 'Χρήση άλλου λογαριασμού',
+ 'native.settings.server': 'Διακομιστής',
+ 'native.settings.serverHint': 'Αυτή η εφαρμογή είναι συνδεδεμένη στο {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/hu/index.ts b/shared/src/i18n/hu/index.ts
index 0afbf275b..cb4a48de4 100644
--- a/shared/src/i18n/hu/index.ts
+++ b/shared/src/i18n/hu/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/hu/native.ts b/shared/src/i18n/hu/native.ts
new file mode 100644
index 000000000..22112f0d5
--- /dev/null
+++ b/shared/src/i18n/hu/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Csatlakozás a TREK-hez',
+ 'native.connect.hint': 'Add meg azt a címet, amelyen a böngésződben megnyitod a TREK-et.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Csatlakozás',
+ 'native.connect.checking': 'Ellenőrzés…',
+ 'native.connect.errorInvalid': 'Ez nem úgy néz ki, mint egy webcím.',
+ 'native.connect.errorUnreachable': 'Ezen a címen nem válaszol szerver. Ellenőrizd a címet és a kapcsolatot.',
+ 'native.connect.errorNotTrek': 'Az ezen a címen futó szerver nem TREK.',
+ 'native.connect.errorTooOld': 'Ez a TREK-szerver túl régi az alkalmazáshoz. Előbb frissíteni kell.',
+ 'native.offline.title': 'A szerver nem érhető el',
+ 'native.offline.hint': 'A TREK nem érte el ezt: {server}. Ellenőrizd a kapcsolatot, és próbáld újra.',
+ 'native.offline.retry': 'Újra',
+ 'native.changeServer': 'Szerver módosítása',
+ 'native.login.failed': 'A bejelentkezés nem sikerült. Próbáld újra.',
+ 'native.handoff.title': 'Folytatás az alkalmazásban',
+ 'native.handoff.hint': '{name} néven vagy bejelentkezve. Nyisd meg a TREK alkalmazást, hogy ott befejezd a bejelentkezést.',
+ 'native.handoff.open': 'TREK alkalmazás megnyitása',
+ 'native.handoff.switchAccount': 'Másik fiók használata',
+ 'native.settings.server': 'Szerver',
+ 'native.settings.serverHint': 'Ez az alkalmazás ehhez csatlakozik: {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/id/index.ts b/shared/src/i18n/id/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/id/index.ts
+++ b/shared/src/i18n/id/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/id/native.ts b/shared/src/i18n/id/native.ts
new file mode 100644
index 000000000..cc5630681
--- /dev/null
+++ b/shared/src/i18n/id/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Hubungkan ke TREK',
+ 'native.connect.hint': 'Masukkan alamat yang kamu gunakan untuk membuka TREK di browser.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Hubungkan',
+ 'native.connect.checking': 'Memeriksa…',
+ 'native.connect.errorInvalid': 'Itu tidak terlihat seperti alamat web.',
+ 'native.connect.errorUnreachable': 'Tidak ada server yang merespons di alamat ini. Periksa alamat dan koneksimu.',
+ 'native.connect.errorNotTrek': 'Server di alamat ini bukan TREK.',
+ 'native.connect.errorTooOld': 'Server TREK ini terlalu lama untuk aplikasi. Server perlu diperbarui terlebih dahulu.',
+ 'native.offline.title': 'Server tidak dapat dijangkau',
+ 'native.offline.hint': 'TREK tidak dapat menjangkau {server}. Periksa koneksimu dan coba lagi.',
+ 'native.offline.retry': 'Coba lagi',
+ 'native.changeServer': 'Ganti server',
+ 'native.login.failed': 'Gagal masuk. Silakan coba lagi.',
+ 'native.handoff.title': 'Lanjutkan di aplikasi',
+ 'native.handoff.hint': 'Kamu masuk sebagai {name}. Buka aplikasi TREK untuk menyelesaikan proses masuk di sana.',
+ 'native.handoff.open': 'Buka aplikasi TREK',
+ 'native.handoff.switchAccount': 'Gunakan akun lain',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Aplikasi ini terhubung ke {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/it/index.ts b/shared/src/i18n/it/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/it/index.ts
+++ b/shared/src/i18n/it/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/it/native.ts b/shared/src/i18n/it/native.ts
new file mode 100644
index 000000000..e1aae0fad
--- /dev/null
+++ b/shared/src/i18n/it/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Connetti a TREK',
+ 'native.connect.hint': 'Inserisci l’indirizzo con cui apri TREK nel browser.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Connetti',
+ 'native.connect.checking': 'Verifica in corso…',
+ 'native.connect.errorInvalid': 'Non sembra un indirizzo web.',
+ 'native.connect.errorUnreachable': 'Nessun server risponde a questo indirizzo. Controlla l’indirizzo e la connessione.',
+ 'native.connect.errorNotTrek': 'Il server a questo indirizzo non è TREK.',
+ 'native.connect.errorTooOld': 'Questo server TREK è troppo vecchio per l’app. Deve prima essere aggiornato.',
+ 'native.offline.title': 'Server non raggiungibile',
+ 'native.offline.hint': 'TREK non è riuscito a raggiungere {server}. Controlla la connessione e riprova.',
+ 'native.offline.retry': 'Riprova',
+ 'native.changeServer': 'Cambia server',
+ 'native.login.failed': 'L’accesso non è riuscito. Riprova.',
+ 'native.handoff.title': 'Continua nell’app',
+ 'native.handoff.hint': 'Hai effettuato l’accesso come {name}. Apri l’app TREK per completare lì l’accesso.',
+ 'native.handoff.open': 'Apri l’app TREK',
+ 'native.handoff.switchAccount': 'Usa un altro account',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Questa app è collegata a {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/ja/index.ts b/shared/src/i18n/ja/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/ja/index.ts
+++ b/shared/src/i18n/ja/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/ja/native.ts b/shared/src/i18n/ja/native.ts
new file mode 100644
index 000000000..7b7b9bd19
--- /dev/null
+++ b/shared/src/i18n/ja/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'TREK に接続',
+ 'native.connect.hint': 'ブラウザで TREK を開くときのアドレスを入力してください。',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': '接続',
+ 'native.connect.checking': '確認中…',
+ 'native.connect.errorInvalid': 'Web アドレスではないようです。',
+ 'native.connect.errorUnreachable': 'このアドレスで応答するサーバーがありません。アドレスと接続を確認してください。',
+ 'native.connect.errorNotTrek': 'このアドレスのサーバーは TREK ではありません。',
+ 'native.connect.errorTooOld': 'この TREK サーバーはアプリに対して古すぎます。先に更新が必要です。',
+ 'native.offline.title': 'サーバーに接続できません',
+ 'native.offline.hint': 'TREK は {server} に接続できませんでした。接続を確認して、もう一度お試しください。',
+ 'native.offline.retry': '再試行',
+ 'native.changeServer': 'サーバーを変更',
+ 'native.login.failed': 'ログインできませんでした。もう一度お試しください。',
+ 'native.handoff.title': 'アプリで続行',
+ 'native.handoff.hint': '{name} としてログインしています。TREK アプリを開いて、そちらでログインを完了してください。',
+ 'native.handoff.open': 'TREK アプリを開く',
+ 'native.handoff.switchAccount': '別のアカウントを使う',
+ 'native.settings.server': 'サーバー',
+ 'native.settings.serverHint': 'このアプリは {server} に接続されています。',
+};
+
+export default native;
diff --git a/shared/src/i18n/ko/index.ts b/shared/src/i18n/ko/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/ko/index.ts
+++ b/shared/src/i18n/ko/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/ko/native.ts b/shared/src/i18n/ko/native.ts
new file mode 100644
index 000000000..5a451d621
--- /dev/null
+++ b/shared/src/i18n/ko/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'TREK에 연결',
+ 'native.connect.hint': '브라우저에서 TREK을 여는 주소를 입력하세요.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': '연결',
+ 'native.connect.checking': '확인 중…',
+ 'native.connect.errorInvalid': '웹 주소가 아닌 것 같습니다.',
+ 'native.connect.errorUnreachable': '이 주소에서 응답하는 서버가 없습니다. 주소와 연결 상태를 확인하세요.',
+ 'native.connect.errorNotTrek': '이 주소의 서버는 TREK이 아닙니다.',
+ 'native.connect.errorTooOld': '이 TREK 서버는 앱에서 사용하기에 너무 오래되었습니다. 먼저 업데이트가 필요합니다.',
+ 'native.offline.title': '서버에 연결할 수 없음',
+ 'native.offline.hint': 'TREK이 {server}에 연결하지 못했습니다. 연결 상태를 확인한 후 다시 시도하세요.',
+ 'native.offline.retry': '다시 시도',
+ 'native.changeServer': '서버 변경',
+ 'native.login.failed': '로그인하지 못했습니다. 다시 시도하세요.',
+ 'native.handoff.title': '앱에서 계속',
+ 'native.handoff.hint': '{name}(으)로 로그인되어 있습니다. TREK 앱을 열어 로그인을 완료하세요.',
+ 'native.handoff.open': 'TREK 앱 열기',
+ 'native.handoff.switchAccount': '다른 계정 사용',
+ 'native.settings.server': '서버',
+ 'native.settings.serverHint': '이 앱은 {server}에 연결되어 있습니다.',
+};
+
+export default native;
diff --git a/shared/src/i18n/nl/index.ts b/shared/src/i18n/nl/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/nl/index.ts
+++ b/shared/src/i18n/nl/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/nl/native.ts b/shared/src/i18n/nl/native.ts
new file mode 100644
index 000000000..2ebfc9be5
--- /dev/null
+++ b/shared/src/i18n/nl/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Verbinden met TREK',
+ 'native.connect.hint': 'Voer het adres in waarop je TREK in je browser opent.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Verbinden',
+ 'native.connect.checking': 'Controleren…',
+ 'native.connect.errorInvalid': 'Dat lijkt geen webadres.',
+ 'native.connect.errorUnreachable': 'Er reageert geen server op dit adres. Controleer het adres en je verbinding.',
+ 'native.connect.errorNotTrek': 'De server op dit adres is geen TREK.',
+ 'native.connect.errorTooOld': 'Deze TREK-server is te oud voor de app. Hij moet eerst worden bijgewerkt.',
+ 'native.offline.title': 'Server niet bereikbaar',
+ 'native.offline.hint': 'TREK kon {server} niet bereiken. Controleer je verbinding en probeer het opnieuw.',
+ 'native.offline.retry': 'Opnieuw proberen',
+ 'native.changeServer': 'Server wijzigen',
+ 'native.login.failed': 'Inloggen is niet gelukt. Probeer het opnieuw.',
+ 'native.handoff.title': 'Verder in de app',
+ 'native.handoff.hint': 'Je bent ingelogd als {name}. Open de TREK-app om daar het inloggen af te ronden.',
+ 'native.handoff.open': 'TREK-app openen',
+ 'native.handoff.switchAccount': 'Ander account gebruiken',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Deze app is verbonden met {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/pl/index.ts b/shared/src/i18n/pl/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/pl/index.ts
+++ b/shared/src/i18n/pl/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/pl/native.ts b/shared/src/i18n/pl/native.ts
new file mode 100644
index 000000000..4e8b72bc6
--- /dev/null
+++ b/shared/src/i18n/pl/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Połącz z TREK',
+ 'native.connect.hint': 'Wpisz adres, pod którym otwierasz TREK w przeglądarce.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Połącz',
+ 'native.connect.checking': 'Sprawdzanie…',
+ 'native.connect.errorInvalid': 'To nie wygląda na adres internetowy.',
+ 'native.connect.errorUnreachable': 'Pod tym adresem nie odpowiada żaden serwer. Sprawdź adres i połączenie.',
+ 'native.connect.errorNotTrek': 'Serwer pod tym adresem to nie TREK.',
+ 'native.connect.errorTooOld': 'Ten serwer TREK jest za stary dla aplikacji. Najpierw wymaga aktualizacji.',
+ 'native.offline.title': 'Serwer nieosiągalny',
+ 'native.offline.hint': 'TREK nie mógł połączyć się z {server}. Sprawdź połączenie i spróbuj ponownie.',
+ 'native.offline.retry': 'Spróbuj ponownie',
+ 'native.changeServer': 'Zmień serwer',
+ 'native.login.failed': 'Logowanie się nie powiodło. Spróbuj ponownie.',
+ 'native.handoff.title': 'Kontynuuj w aplikacji',
+ 'native.handoff.hint': 'Zalogowano jako {name}. Otwórz aplikację TREK, aby tam dokończyć logowanie.',
+ 'native.handoff.open': 'Otwórz aplikację TREK',
+ 'native.handoff.switchAccount': 'Użyj innego konta',
+ 'native.settings.server': 'Serwer',
+ 'native.settings.serverHint': 'Ta aplikacja jest połączona z {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/ru/index.ts b/shared/src/i18n/ru/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/ru/index.ts
+++ b/shared/src/i18n/ru/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/ru/native.ts b/shared/src/i18n/ru/native.ts
new file mode 100644
index 000000000..5f5590c45
--- /dev/null
+++ b/shared/src/i18n/ru/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Подключение к TREK',
+ 'native.connect.hint': 'Введите адрес, по которому вы открываете TREK в браузере.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Подключиться',
+ 'native.connect.checking': 'Проверка…',
+ 'native.connect.errorInvalid': 'Это не похоже на веб-адрес.',
+ 'native.connect.errorUnreachable': 'По этому адресу не отвечает ни один сервер. Проверьте адрес и подключение.',
+ 'native.connect.errorNotTrek': 'Сервер по этому адресу не является TREK.',
+ 'native.connect.errorTooOld': 'Этот сервер TREK слишком старый для приложения. Сначала его нужно обновить.',
+ 'native.offline.title': 'Сервер недоступен',
+ 'native.offline.hint': 'TREK не удалось подключиться к {server}. Проверьте подключение и повторите попытку.',
+ 'native.offline.retry': 'Повторить',
+ 'native.changeServer': 'Сменить сервер',
+ 'native.login.failed': 'Не удалось войти. Попробуйте ещё раз.',
+ 'native.handoff.title': 'Продолжить в приложении',
+ 'native.handoff.hint': 'Вы вошли как {name}. Откройте приложение TREK, чтобы завершить вход в нём.',
+ 'native.handoff.open': 'Открыть приложение TREK',
+ 'native.handoff.switchAccount': 'Использовать другой аккаунт',
+ 'native.settings.server': 'Сервер',
+ 'native.settings.serverHint': 'Приложение подключено к {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/sv/index.ts b/shared/src/i18n/sv/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/sv/index.ts
+++ b/shared/src/i18n/sv/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/sv/native.ts b/shared/src/i18n/sv/native.ts
new file mode 100644
index 000000000..d5d80c32f
--- /dev/null
+++ b/shared/src/i18n/sv/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Anslut till TREK',
+ 'native.connect.hint': 'Ange adressen du öppnar TREK på i webbläsaren.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Anslut',
+ 'native.connect.checking': 'Kontrollerar…',
+ 'native.connect.errorInvalid': 'Det ser inte ut som en webbadress.',
+ 'native.connect.errorUnreachable': 'Ingen server svarar på den här adressen. Kontrollera adressen och din anslutning.',
+ 'native.connect.errorNotTrek': 'Servern på den här adressen är inte TREK.',
+ 'native.connect.errorTooOld': 'Den här TREK-servern är för gammal för appen. Den behöver uppdateras först.',
+ 'native.offline.title': 'Servern går inte att nå',
+ 'native.offline.hint': 'TREK kunde inte nå {server}. Kontrollera din anslutning och försök igen.',
+ 'native.offline.retry': 'Försök igen',
+ 'native.changeServer': 'Byt server',
+ 'native.login.failed': 'Inloggningen misslyckades. Försök igen.',
+ 'native.handoff.title': 'Fortsätt i appen',
+ 'native.handoff.hint': 'Du är inloggad som {name}. Öppna TREK-appen för att slutföra inloggningen där.',
+ 'native.handoff.open': 'Öppna TREK-appen',
+ 'native.handoff.switchAccount': 'Använd ett annat konto',
+ 'native.settings.server': 'Server',
+ 'native.settings.serverHint': 'Den här appen är ansluten till {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/tr/index.ts b/shared/src/i18n/tr/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/tr/index.ts
+++ b/shared/src/i18n/tr/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/tr/native.ts b/shared/src/i18n/tr/native.ts
new file mode 100644
index 000000000..12636aca5
--- /dev/null
+++ b/shared/src/i18n/tr/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'TREK’e bağlan',
+ 'native.connect.hint': 'TREK’i tarayıcında açtığın adresi gir.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Bağlan',
+ 'native.connect.checking': 'Kontrol ediliyor…',
+ 'native.connect.errorInvalid': 'Bu bir web adresine benzemiyor.',
+ 'native.connect.errorUnreachable': 'Bu adreste yanıt veren bir sunucu yok. Adresi ve bağlantını kontrol et.',
+ 'native.connect.errorNotTrek': 'Bu adresteki sunucu TREK değil.',
+ 'native.connect.errorTooOld': 'Bu TREK sunucusu uygulama için çok eski. Önce güncellenmesi gerekiyor.',
+ 'native.offline.title': 'Sunucuya ulaşılamıyor',
+ 'native.offline.hint': 'TREK, {server} adresine ulaşamadı. Bağlantını kontrol edip tekrar dene.',
+ 'native.offline.retry': 'Tekrar dene',
+ 'native.changeServer': 'Sunucuyu değiştir',
+ 'native.login.failed': 'Giriş yapılamadı. Lütfen tekrar dene.',
+ 'native.handoff.title': 'Uygulamada devam et',
+ 'native.handoff.hint': '{name} olarak giriş yaptın. Girişi tamamlamak için TREK uygulamasını aç.',
+ 'native.handoff.open': 'TREK uygulamasını aç',
+ 'native.handoff.switchAccount': 'Başka bir hesap kullan',
+ 'native.settings.server': 'Sunucu',
+ 'native.settings.serverHint': 'Bu uygulama {server} adresine bağlı.',
+};
+
+export default native;
diff --git a/shared/src/i18n/uk/index.ts b/shared/src/i18n/uk/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/uk/index.ts
+++ b/shared/src/i18n/uk/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/uk/native.ts b/shared/src/i18n/uk/native.ts
new file mode 100644
index 000000000..401bf9f98
--- /dev/null
+++ b/shared/src/i18n/uk/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Підключення до TREK',
+ 'native.connect.hint': 'Введіть адресу, за якою ви відкриваєте TREK у браузері.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Підключитися',
+ 'native.connect.checking': 'Перевірка…',
+ 'native.connect.errorInvalid': 'Це не схоже на вебадресу.',
+ 'native.connect.errorUnreachable': 'За цією адресою не відповідає жоден сервер. Перевірте адресу та з’єднання.',
+ 'native.connect.errorNotTrek': 'Сервер за цією адресою не є TREK.',
+ 'native.connect.errorTooOld': 'Цей сервер TREK застарий для застосунку. Спершу його потрібно оновити.',
+ 'native.offline.title': 'Сервер недоступний',
+ 'native.offline.hint': 'TREK не вдалося підключитися до {server}. Перевірте з’єднання та спробуйте ще раз.',
+ 'native.offline.retry': 'Спробувати ще раз',
+ 'native.changeServer': 'Змінити сервер',
+ 'native.login.failed': 'Не вдалося увійти. Спробуйте ще раз.',
+ 'native.handoff.title': 'Продовжити в застосунку',
+ 'native.handoff.hint': 'Ви увійшли як {name}. Відкрийте застосунок TREK, щоб завершити вхід у ньому.',
+ 'native.handoff.open': 'Відкрити застосунок TREK',
+ 'native.handoff.switchAccount': 'Використати інший обліковий запис',
+ 'native.settings.server': 'Сервер',
+ 'native.settings.serverHint': 'Застосунок підключено до {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/vi/index.ts b/shared/src/i18n/vi/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/vi/index.ts
+++ b/shared/src/i18n/vi/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/vi/native.ts b/shared/src/i18n/vi/native.ts
new file mode 100644
index 000000000..c4b136d77
--- /dev/null
+++ b/shared/src/i18n/vi/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': 'Kết nối với TREK',
+ 'native.connect.hint': 'Nhập địa chỉ bạn dùng để mở TREK trong trình duyệt.',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': 'Kết nối',
+ 'native.connect.checking': 'Đang kiểm tra…',
+ 'native.connect.errorInvalid': 'Đây có vẻ không phải là địa chỉ web.',
+ 'native.connect.errorUnreachable': 'Không có máy chủ nào phản hồi tại địa chỉ này. Hãy kiểm tra địa chỉ và kết nối của bạn.',
+ 'native.connect.errorNotTrek': 'Máy chủ tại địa chỉ này không phải TREK.',
+ 'native.connect.errorTooOld': 'Máy chủ TREK này quá cũ đối với ứng dụng. Máy chủ cần được cập nhật trước.',
+ 'native.offline.title': 'Không kết nối được máy chủ',
+ 'native.offline.hint': 'TREK không thể kết nối tới {server}. Hãy kiểm tra kết nối và thử lại.',
+ 'native.offline.retry': 'Thử lại',
+ 'native.changeServer': 'Đổi máy chủ',
+ 'native.login.failed': 'Đăng nhập không thành công. Vui lòng thử lại.',
+ 'native.handoff.title': 'Tiếp tục trong ứng dụng',
+ 'native.handoff.hint': 'Bạn đã đăng nhập với tên {name}. Mở ứng dụng TREK để hoàn tất đăng nhập ở đó.',
+ 'native.handoff.open': 'Mở ứng dụng TREK',
+ 'native.handoff.switchAccount': 'Dùng tài khoản khác',
+ 'native.settings.server': 'Máy chủ',
+ 'native.settings.serverHint': 'Ứng dụng này đang kết nối với {server}.',
+};
+
+export default native;
diff --git a/shared/src/i18n/zh-TW/index.ts b/shared/src/i18n/zh-TW/index.ts
index b51d0de98..5d351d89d 100644
--- a/shared/src/i18n/zh-TW/index.ts
+++ b/shared/src/i18n/zh-TW/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/zh-TW/native.ts b/shared/src/i18n/zh-TW/native.ts
new file mode 100644
index 000000000..66bc01fb5
--- /dev/null
+++ b/shared/src/i18n/zh-TW/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': '連線到 TREK',
+ 'native.connect.hint': '請輸入你在瀏覽器中開啟 TREK 時使用的網址。',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': '連線',
+ 'native.connect.checking': '正在檢查…',
+ 'native.connect.errorInvalid': '這看起來不像網址。',
+ 'native.connect.errorUnreachable': '此網址沒有伺服器回應。請檢查網址與網路連線。',
+ 'native.connect.errorNotTrek': '此網址上的伺服器不是 TREK。',
+ 'native.connect.errorTooOld': '此 TREK 伺服器版本過舊,無法用於 App。請先更新伺服器。',
+ 'native.offline.title': '無法連線到伺服器',
+ 'native.offline.hint': 'TREK 無法連線到 {server}。請檢查網路連線後再試一次。',
+ 'native.offline.retry': '再試一次',
+ 'native.changeServer': '更換伺服器',
+ 'native.login.failed': '登入未成功,請再試一次。',
+ 'native.handoff.title': '在 App 中繼續',
+ 'native.handoff.hint': '你已以 {name} 身分登入。請開啟 TREK App,在那裡完成登入。',
+ 'native.handoff.open': '開啟 TREK App',
+ 'native.handoff.switchAccount': '使用其他帳號',
+ 'native.settings.server': '伺服器',
+ 'native.settings.serverHint': '此 App 已連線到 {server}。',
+};
+
+export default native;
diff --git a/shared/src/i18n/zh/index.ts b/shared/src/i18n/zh/index.ts
index d7fad9f62..a2746abe6 100644
--- a/shared/src/i18n/zh/index.ts
+++ b/shared/src/i18n/zh/index.ts
@@ -17,6 +17,7 @@ import help from './help';
import inspector from './inspector';
import journey from './journey';
import login from './login';
+import native from './native';
import map from './map';
import members from './members';
import memories from './memories';
@@ -66,6 +67,7 @@ const locale = {
...share,
...shared,
...login,
+ ...native,
...register,
...vacay,
...collection,
diff --git a/shared/src/i18n/zh/native.ts b/shared/src/i18n/zh/native.ts
new file mode 100644
index 000000000..a919207bd
--- /dev/null
+++ b/shared/src/i18n/zh/native.ts
@@ -0,0 +1,26 @@
+import type { TranslationStrings } from '../types';
+
+const native: TranslationStrings = {
+ 'native.connect.title': '连接到 TREK',
+ 'native.connect.hint': '请输入你在浏览器中打开 TREK 时使用的地址。',
+ 'native.connect.placeholder': 'trek.example.com',
+ 'native.connect.submit': '连接',
+ 'native.connect.checking': '正在检查…',
+ 'native.connect.errorInvalid': '这看起来不像网址。',
+ 'native.connect.errorUnreachable': '此地址没有服务器响应。请检查地址和网络连接。',
+ 'native.connect.errorNotTrek': '此地址上的服务器不是 TREK。',
+ 'native.connect.errorTooOld': '此 TREK 服务器版本过旧,无法用于应用。请先更新服务器。',
+ 'native.offline.title': '无法连接服务器',
+ 'native.offline.hint': 'TREK 无法连接到 {server}。请检查网络连接后重试。',
+ 'native.offline.retry': '重试',
+ 'native.changeServer': '更换服务器',
+ 'native.login.failed': '登录未成功,请重试。',
+ 'native.handoff.title': '在应用中继续',
+ 'native.handoff.hint': '你已以 {name} 身份登录。请打开 TREK 应用,在那里完成登录。',
+ 'native.handoff.open': '打开 TREK 应用',
+ 'native.handoff.switchAccount': '使用其他账号',
+ 'native.settings.server': '服务器',
+ 'native.settings.serverHint': '此应用已连接到 {server}。',
+};
+
+export default native;