- {$virtual ? $virtual.name : driveLetter || driveLabel}
+ {#if $virtual}
+ {$virtual.name}
+ {:else}
+ {driveLetter || driveLabel}
+ {/if}
{#if name && !$virtual}
diff --git a/src/apps/user/filemanager/runtime.ts b/src/apps/user/filemanager/runtime.ts
index a1f472a88..e191305bb 100755
--- a/src/apps/user/filemanager/runtime.ts
+++ b/src/apps/user/filemanager/runtime.ts
@@ -40,12 +40,12 @@ export class FileManagerRuntime extends AppProcess {
directoryListing = Store();
virtualLocations: Record = {
my_arcos: {
- name: "My ArcOS",
+ name: "%virtualLocations.my_arcos%",
icon: "computer",
component: MyArcOs as any,
},
recycle_bin: {
- name: "Recycle Bin",
+ name: "%virtualLocations.recycle_bin%",
icon: "trash-2",
component: TrashCan as any,
},
diff --git a/src/lang/en.json b/src/lang/en.json
new file mode 100644
index 000000000..5b014791f
--- /dev/null
+++ b/src/lang/en.json
@@ -0,0 +1,649 @@
+{
+ "apps": {
+ "bootScreen": {
+ "_name": "Boot screen",
+ "pressAnyKey": "Press any key or click to start",
+ "safeMode": "Entering Safe Mode",
+ "arcTerm": "Starting ArcTerm"
+ },
+ "initialSetupWizard": {
+ "_name": "Initial Setup Wizard",
+ "buttons": {
+ "viewLicense": "View License",
+ "iAgree": "I agree",
+ "iClickedIt": "I clicked it",
+ "letsBegin": "Let's begin",
+ "serverAllGood": "Server's all good"
+ },
+ "licenseConfirmation": {
+ "title": "Just making sure...",
+ "message": "By using ArcOS, you agree to the License Agreement. You may not violate any of the rules contained within this license. Continue?"
+ },
+ "viewLicense": {
+ "title": "ArcOS License - GPLv3",
+ "message": "By using ArcOS, you agree to the GPLv3 License contained within"
+ },
+ "createAccount": {
+ "passwordMismatch": {
+ "title": "You made a typo!",
+ "message": "The passwords you entered don't match. Please re-enter them, and then try again."
+ },
+ "genericError": {
+ "title": "Something went wrong",
+ "message": "An error occured while creating your account. We might be experiencing some technical difficulties, please try again later."
+ }
+ },
+ "checkAccountActivationError": {
+ "title": "Did you click the link?",
+ "message": "Our systems tell me that your account hasn't been activated yet. Are you sure you clicked the link? If you did, and you're still seeing this, please contact support."
+ },
+ "page": {
+ "checkInbox": {
+ "title": "Check your inbox",
+ "subtitle": "You've got mail!",
+ "message": "We sent you a link to activate your account. Open it to continue. Be sure to check your spam if you can’t find it."
+ },
+ "finish": {
+ "title": "All finished!",
+ "subtitle": "You're all set.",
+ "message": "Your account has been set up successfully. Click Finish to start using ArcOS."
+ },
+ "freshDeployment": {
+ "title": "Fresh deployment",
+ "message": "It appears this ReArc deployment is fresh. Before continuing, please make sure that that's supposed to be the case. If it is, click Next. The user you're about to create is considered user #0, and will be set up as a God Admin by default. The owner of this account is then automatically seen as the owner of the server. Also, please check the server configuration:",
+ "note": "Note: disableRegistration is ignored for the first user. This configuration option will be retained once the first user has been created."
+ },
+ "identity": {
+ "title": "Your ArcOS Identity",
+ "subtitle": "We'll use this information to create your ArcOS account.",
+ "fields": {
+ "displayName": "Display Name",
+ "username": "Username",
+ "emailAddress": "Email address *",
+ "password": "Password",
+ "passwordConfirm": "Confirm"
+ },
+ "errors": {
+ "usernameAndEmailTaken": "Username and email address are both taken!",
+ "usernameTaken": "Username is already taken!",
+ "emailTaken": "Email is already taken!",
+ "emailAndUsernameInvalid": "Username and email address are both invalid!",
+ "usernameInvalid": "Username is invalid!",
+ "emailInvalid": "Email address is invalid!",
+ "passwordtooWeak": "Password is too weak!",
+ "passwordweak": "Password is weak!",
+ "passwordmedium": "Password is medium!",
+ "passwordstrong": "Password is strong!"
+ },
+ "disclaimer": "* You will receive an email with a link to activate your account. Your display name, username and password can be changed later on. To change your email, contact an administrator."
+ },
+ "license": {
+ "title": "License Agreement",
+ "subtitle": "Scary stuff, I know",
+ "message": "By using ArcOS, you agree to the GPLv3 license."
+ },
+ "welcome": {
+ "title": "Welcome",
+ "subtitle": "We're happy to have you",
+ "message": "Let's get you an ArcOS account. Click Next to get started, or Cancel to go back to the login screen."
+ }
+ }
+ },
+ "loginApp": {
+ "_name": "LogonUI",
+ "welcomeString": {
+ "night": "Hi, go to sleep",
+ "morning": "Good morning",
+ "afternoon": "Good afternoon",
+ "evening": "Good evening"
+ },
+ "errors": {
+ "noDaemon": "Failed to start user daemon",
+ "noUserInfo": "Failed to request user info",
+ "totpInvalid": "You didn't enter a valid 2FA code!",
+ "credentialIncorrect": "Username or password incorrect."
+ },
+ "startDaemon": {
+ "savingToken": "Saving token",
+ "loadingSettings": "Loading your settings",
+ "request2fa": "Requesting 2FA",
+ "startingFilesystem": "Starting filesystem",
+ "startingSync": "Starting synchronization",
+ "profileCustomization": "Reading profile customization",
+ "notifyLoginActivity": "Notifying login activity",
+ "startServiceHost": {
+ "initial": "Starting service host",
+ "specific": "Starting {{0}}"
+ },
+ "loadingApps": {
+ "initial": "Loading apps...",
+ "specific": "Loaded {{0}}"
+ },
+ "checkingAssoc": "Checking associations",
+ "globalDispatch": "Connecting global dispatch",
+ "welcome": "Welcome to ArcOS",
+ "driveNotifierWatcher": "Starting drive notifier watcher",
+ "shareManagement": "Starting share management",
+ "adminBootstrapper": "Activating admin bootstrapper",
+ "statusRefresh": "Starting status refresh",
+ "letsGo": "Let's go!",
+ "startingWorkspaces": "Starting workspaces",
+ "autoload": "Spawning autorun"
+ },
+ "exit": {
+ "logoff": "Goodbye, ",
+ "shutdown": "Shutting down...",
+ "restart": "Restarting..."
+ },
+ "loginForm": {
+ "switchUser": "Switch user",
+ "noAccount": "No account?",
+ "username": "Username",
+ "password": "Password"
+ }
+ },
+ "AcceleratorOverview": {
+ "header": {
+ "title": "Keyboard shortcuts",
+ "subtitle": "Get more work done faster with these handy shortcuts!"
+ }
+ },
+ "AppInfo": {
+ "_name": "App Info",
+ "noTargetApp": {
+ "title": "App not found",
+ "message": "AppInfo couldn't find any information about \"{{0}}\" Is it installed?"
+ },
+ "killAll": {
+ "what": "ArcOS needs your permission to kill all instances of an app"
+ },
+ "header": {
+ "isDisabled": "{{0}} is disabled!",
+ "deleteApp": "Delete app",
+ "launch": "Launch"
+ },
+ "indepthInfo": {
+ "size": "Size",
+ "minSize": "Minimal Size",
+ "maxSize": "Maximal Size",
+ "controls": "Controls",
+ "initialPosition": "Initial Position",
+ "origin": "Origin",
+ "core": "Core",
+ "hidden": "Hidden",
+ "centered": "Centered",
+ "cornerOfScreen": "Corner of screen"
+ },
+ "processInfo": {
+ "processes": "Processes",
+ "instances": "{{0}} instance(s)",
+ "firstPid": "First PID"
+ },
+ "thirdPartyInfo": {
+ "thirdParty": "Third-party",
+ "signatures": "Signatures",
+ "hasProcess": "Has process",
+ "workingDirectory": "Working directory",
+ "entryPoint": "Entry point"
+ },
+ "internalInfo": {
+ "loadTime": "Load time",
+ "systemVersion": "System version",
+ "minimalAssembly": "Minimal assembly",
+ "codePath": "Code path"
+ },
+ "actions": {
+ "killAll": "Kill all"
+ }
+ },
+ "AppInstaller": {
+ "noDistrib": {
+ "title": "Can't install package",
+ "message": "The Distribution Service isn't running anymore. Please restart ArcOS to fix this problem."
+ },
+ "noEnableThirdParty": {
+ "title": "Can't install app",
+ "message": "Third-party apps aren't enabled on your account. Please enable third-party apps in the Settings app to install this app.",
+ "takeMeThere": "Take me there"
+ },
+ "rollback": "Rolling back changes...",
+ "header": {
+ "title": {
+ "installed": "Package installed!",
+ "installing": "Installing package...",
+ "failed": "Installation failed"
+ },
+ "subtitle": {
+ "completed": "Click Open now to launch the app",
+ "installing": "{{0}} by {{1}}",
+ "generic": "{{0}} - {{1}}"
+ }
+ },
+ "actions": {
+ "revert": "Revert",
+ "openNow": "Open now",
+ "done": "Done",
+ "install": "Install"
+ },
+ "readyToInstall": {
+ "title": "Ready to install",
+ "message": "Click Install to install this package."
+ },
+ "logType": {
+ "file": "Writing file",
+ "mkdir": "Creating directory",
+ "registration": "Registering",
+ "generic": "Status"
+ }
+ },
+ "AppPreInstall": {
+ "noEnableThirdParty": {
+ "title": "Can't install app",
+ "message": "Third-party apps aren't enabled on your account. Please enable third-party apps in the Settings app to install this app.",
+ "takeMeThere": "Take me there"
+ },
+ "readingPackage": "Reading ArcOS package",
+ "errors": {
+ "noContents": "The package contents could not be read",
+ "noMeta": "The package metadata could not be read",
+ "missingFiles": "Package is corrupt; missing package or app metadata.",
+ "appIdMalformed": "The application ID is malformed: it contains periods or dashes. If you're the creator of the app, be sure to use the suggested format for application IDs.",
+ "fsError": "Filesystem error"
+ },
+ "fail": {
+ "title": "Failed to open package",
+ "messagePartial": "ArcOS failed to open the specified package."
+ },
+ "elevation": {
+ "what": "ArcOS wants to install an application",
+ "description": "{{0}} - {{1}}"
+ },
+ "header": {
+ "title": "Do you want to install this package?"
+ },
+ "info": {
+ "author": "Author",
+ "version": "Version"
+ },
+ "installPackage": "Install package"
+ },
+ "ArcFindProc": {
+ "fsSupplier": {
+ "shortcut": "Shortcut - {{0}}"
+ },
+ "appSupplier": {
+ "description": "By {{0}}"
+ },
+ "powerOptions": {
+ "shutdown": {
+ "caption": "Shut down",
+ "description": "Leave the desktop and turn off ArcOS"
+ },
+ "restart": {
+ "caption": "Restart",
+ "description": "Leave the desktop and restart ArcOS"
+ },
+ "logoff": {
+ "caption": "Log off",
+ "description": "Leave the desktop and log out of ArcOS"
+ }
+ }
+ },
+ "contextMenu": {
+ "_name": "Context Menu",
+ "system": {
+ "windowTitlebar": {
+ "appInfo": "App Info",
+ "processInfo": "Process Info",
+ "windowSnapping": "Window snapping",
+ "snappingLeft": "Left",
+ "snappingRight": "Right",
+ "snappingTop": "Top",
+ "snappingBottom": "Bottom",
+ "snappingTopLeft": "Top Left",
+ "snappingTopRight": "Top Right",
+ "snappingBottomLeft": "Bottom Left",
+ "snappingBottomRight": "Bottom Right",
+ "moveWorkspace": "Move to workspace",
+ "moveWorkspaceLeft": "Left workspace",
+ "moveWorkspaceRight": "Right workspace"
+ }
+ }
+ },
+ "DriveInfo": {
+ "_name": "Drive Info",
+ "quota": {
+ "used": "Used ({{0}}%)",
+ "free": "Free ({{0}}%)"
+ },
+ "legend": {
+ "system": "System ({{0}}%)",
+ "trash": "Trash ({{0}}%)",
+ "home": "Home ({{0}}%)",
+ "apps": "Apps ({{0}}%)"
+ },
+ "advanced": {
+ "id": "ID",
+ "mountpoint": "Mountpoint",
+ "label": "Label",
+ "identifiesAs": "Identifies As",
+ "flags": "Flags",
+ "busy": "Busy",
+ "fixed": "Fixed",
+ "readonly": "Readonly",
+ "removable": "Removable"
+ },
+ "actions": {
+ "simple": "Simple",
+ "advanced": "Advanced"
+ }
+ },
+ "ExitApp": {
+ "_name": "Exit",
+ "header": {
+ "title": "Exit ArcOS",
+ "message": "What's your escape route?"
+ },
+ "exitActions": {
+ "restart": "Restart",
+ "shutdown": "Shutdown",
+ "logoff": "Log off"
+ }
+ },
+ "FirstRun": {
+ "_name": "First Run",
+ "finishingUp": "Finishing up...",
+ "creatingShortcut": "Creating shortcut for {{0}}",
+ "pages": {
+ "welcome": {
+ "_name": "Welcome",
+ "subtitle": "Let's customize your account.",
+ "later": "Later",
+ "next": "Next"
+ },
+ "style": {
+ "_name": "What's your style?",
+ "subtitle": "Pick which style suits you best. You can always change it in the Settings app.",
+ "goBack": "Go back",
+ "next": "Next"
+ },
+ "profilePicture": {
+ "_name": "Choose a profile picture",
+ "subtitle": "Do you want to upload or pick a profile picture?",
+ "upload": "Upload...",
+ "choose": "Choose",
+ "next": "Next"
+ },
+ "thirdParty": {
+ "_name": "Enable third-party apps?",
+ "subtitle": "Do you want to be able to install apps made by other ArcOS users? You can always turn it off later if you change your mind.",
+ "goBack": "Go back",
+ "notNow": "Not now",
+ "enable": "Enable"
+ },
+ "finish": {
+ "_name": "You're all set!",
+ "subtitle": "Enjoy using ArcOS! If you have any questions or concerns, feel free to talk about them in the Discord.",
+ "finish": "Finish"
+ }
+ },
+ "ChooseProfilePicture": {
+ "_name": "Choose Profile Picture",
+ "title": "Choose profile picture",
+ "subtitle": "What do you want to be?"
+ },
+ "themes": {
+ "darkMode": "Dark mode",
+ "lightMode": "Light mode"
+ }
+ },
+ "FsNewFile": {
+ "title": "New file",
+ "subtitle": "Think of a wonderful name for this new file:",
+ "create": "Create"
+ },
+ "FsNewFolder": {
+ "title": "New folder",
+ "subtitle": "Think of a wonderful name for this new folder:",
+ "create": "Create"
+ },
+ "FsProgress": {
+ "quantity": "{{0}} / {{1}} done",
+ "size": "{{0}} / {{1}} done"
+ },
+ "FsProgressFail": {
+ "subtitle": "This file operation encountered an error"
+ },
+ "FsRenameItem": {
+ "title": "Rename file or folder",
+ "subtitle": "Enter a new name for the item:",
+ "rename": "Rename"
+ },
+ "IconEditDialog": {
+ "_name": "Change Icon",
+ "title": "Change {{0}}",
+ "modeToggle": {
+ "app": "App",
+ "fs": "File",
+ "builtin": "Built-in"
+ },
+ "fileType": {
+ "loadSaveTitle": "Choose an icon to load",
+ "title": "File path:",
+ "buttonTitle": "Choose file"
+ },
+ "appType": {
+ "title": "App:",
+ "option": "{{0}} by {{1}}"
+ },
+ "builtinType": {
+ "title": "Icon ID:",
+ "buttonTitle": "Choose icon"
+ },
+ "default": "Default"
+ },
+ "IconPicker": {
+ "_name": "Icon Picker",
+ "groups": {
+ "Branding": "ArcOS logos",
+ "General": "General icons",
+ "Apps": "Application icons",
+ "Filesystem": "Filesystem-related icons",
+ "Power": "Power icons",
+ "Dialog": "Dialog icons",
+ "Status": "Status indicators",
+ "Mimetypes": "File mimetypes"
+ },
+ "random": "Random",
+ "choose": "Choose",
+ "reset": "Reset",
+ "title": "Pick an icon for {{0}}"
+ },
+ "ItemInfo": {
+ "_name": "Item Info",
+ "subtitle": "in {{0}}",
+ "renameItem": "Rename item",
+ "location": {
+ "fullPath": "Full Path",
+ "extension": "Extension",
+ "parent": "Parent",
+ "drive": "Drive",
+ "filesystem": "Filesystem"
+ },
+ "meta": {
+ "sort": "Sort",
+ "type": "Type",
+ "size": "Size",
+ "created": "Created",
+ "modified": "Modified"
+ },
+ "actions": {
+ "editShortcut": "Edit shortcut..."
+ }
+ },
+ "MessageComposer": {
+ "_name": "New Message",
+ "sendProg": {
+ "caption": "Sending message",
+ "subtitleInitial": "Preparing",
+ "subtitleUploading": "Uploading"
+ },
+ "discardMessage": {
+ "title": "Discard message?",
+ "message": "Are you sure you want to discard this message? This cannot be undone."
+ },
+ "sendFailed": {
+ "title": "Failed to send message",
+ "message": "ArcOS failed to send the message! It might be too large, or none of the recipients exist. Please check the recipients or try shrinking it down, and then resend it. If it still doesn't work, contact an ArcOS administrator."
+ },
+ "addAttachment": {
+ "title": "Choose one or more files to attach",
+ "fileProgress": {
+ "caption": "Just a moment...",
+ "readingFileKnown": "Reading {{0}}",
+ "readingFileUnknown": "Readon file..."
+ }
+ },
+ "actionBar": {
+ "body": "Body",
+ "attachments": "Attachments",
+ "addAttachment": "Add attachment",
+ "discardMessage": "Discard message",
+ "send": "Send"
+ },
+ "attachmentBar": {
+ "removeAttachment": "Remove attachment"
+ },
+ "subjectField": {
+ "subject": "Subject:"
+ },
+ "toField": {
+ "to": "To:",
+ "removeRecipient": "Remove {{0}}",
+ "enterUsername": "Enter username"
+ }
+ },
+ "MultiUpdateGui": {
+ "_name": "App Updater",
+ "elevation": "ArcOS needs your permission to update {{0}} {{1}}.",
+ "title": {
+ "updating": "Updating {{0}}",
+ "loading": "Just a moment",
+ "done": "Finished updating",
+ "ready": "Ready to update"
+ },
+ "allPackagesUpdated": "All packages were updated.",
+ "clickUpdate": "Click Update to begin",
+ "itemType": {
+ "file": "Writing file",
+ "mkdir": "Creating directory",
+ "registration": "Registering",
+ "generic": "Status"
+ },
+ "showLog": "Show log",
+ "hideLog": "Hide log",
+ "startUpdate": "Update",
+ "finish": "Finish"
+ }
+ },
+ "general": {
+ "betaPill": "BETA",
+ "cancel": "Cancel",
+ "okay": "Okay",
+ "open": "Open",
+ "previous": "Previous",
+ "next": "Next",
+ "continue": "Continue",
+ "decline": "Decline",
+ "iAgree": "I agree",
+ "unknown": "Unknown",
+ "noAuthor": "No author",
+ "enable": "Enable",
+ "disable": "Disable",
+ "minimize": "Minimize",
+ "maximize": "Maximize",
+ "close": "Close",
+ "yes": "Yes",
+ "no": "No",
+ "none": "None",
+ "confirm": "Confirm",
+ "ArcOSTeam": "The ArcOS Team",
+ "genericStatus": "Just a moment...",
+ "save": "Save",
+ "by": "By",
+ "loading": "Loading...",
+ "discard": "Discard"
+ },
+ "appOrigins": {
+ "builtin": "Built-in",
+ "userApps": "Third-party",
+ "injected": "Other",
+ "administrative": "Administrative"
+ },
+ "virtualLocations": {
+ "my_arcos": "My ArcOS",
+ "recycle_bin": "Recycle Bin"
+ },
+ "userPaths": {
+ "Home": "Home folder",
+ "Documents": "Documents",
+ "Pictures": "Pictures",
+ "Downloads": "Downloads",
+ "Wallpapers": "Wallpapers",
+ "Desktop": "Desktop",
+ "Music": "Music",
+ "Applications": "Applications",
+ "Trashcan": "Recycle Bin",
+ "Root": "Your Drive",
+ "System": "System Folder",
+ "Migrations": "Migration Files",
+ "Configuration": "Configuration",
+ "AppShortcuts": "Application Shortcuts",
+ "AppRepository": "Application Repository"
+ },
+ "associations": {
+ "tpa": "Third-party app file",
+ "tpab": "Third-party app binary",
+ "json": "JSON file",
+ "pdf": "PDF document",
+ "svg": "SVG image",
+ "zip": "Archive",
+ "txt": "Text file",
+ "arctermConf": "ArcTerm configuration",
+ "arcterm": "ArcTerm script (unsupported)",
+ "arctheme": "ArcOS theme",
+ "md": "Markdown document",
+ "html": "HTML document",
+ "htm": "HTML document",
+ "js": "JS script",
+ "dTs": "Type definitions",
+ "ts": "TS script",
+ "mjs": "JS module",
+ "xml": "XML file",
+ "arcpl": "ArcOS playlist",
+ "arclnk": "Shortcut",
+ "arc": "ArcOS package",
+ "msg": "ArcOS message",
+ "css": "CSS file",
+ "mig": "Migration status",
+ "lock": "Migration lockfile",
+ "RegisteredVersion": "Version registration",
+ "exe": "Windows executable (unsupported)",
+ "msi": "Windows installer (unsupported)",
+ "com": "MS-DOS executable (unsupported)",
+ "img": "Floppy image (unsupported)",
+ "iso": "CD-ROM image (unsupported)",
+ "osl": "OriginOS script (unsupported)",
+ "wasm": "WebAssembly (unsupported",
+ "bin": "Binary (unsupported)",
+ "mp3": "Audio file",
+ "opus": "Audio file",
+ "wav": "Audio file",
+ "m4a": "Audio file",
+ "flac": "Audio file",
+ "imageFile": "Image file",
+ "audioFile": "Audio file",
+ "videoFile": "Video file",
+ "shortcut": "Shortcut"
+ }
+}
diff --git a/src/lang/nl.json b/src/lang/nl.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/src/lang/nl.json
@@ -0,0 +1 @@
+{}
diff --git a/src/ts/apps/store.ts b/src/ts/apps/store.ts
index a5b5de0bd..87393bb18 100755
--- a/src/ts/apps/store.ts
+++ b/src/ts/apps/store.ts
@@ -67,8 +67,8 @@ export const BuiltinAppImportPathAbsolutes = import.meta.glob([
export const appShortcuts: [number, AppKeyCombinations][] = [];
export const AppOrigins: Record = {
- builtin: "Built-in",
- userApps: "Third-party",
- injected: "Other",
- aefs: "Administrative",
+ builtin: "%appOrigins.builtin%",
+ userApps: "%appOrigins.userApps%",
+ injected: "%appOrigins.injected%",
+ aefs: "%appOrigins.administrative%",
};
diff --git a/src/ts/kernel/init.ts b/src/ts/kernel/init.ts
index 63fb6c851..40c7aec32 100755
--- a/src/ts/kernel/init.ts
+++ b/src/ts/kernel/init.ts
@@ -8,6 +8,7 @@ import { textToBlob } from "$ts/util/convert";
import type { ServerManagerType } from "$types/kernel";
import { Process } from "../process/instance";
import { StateHandler } from "../state";
+import { I18n } from "./mods/i18n";
export class InitProcess extends Process {
//#region LIFECYCLE
@@ -28,6 +29,7 @@ export class InitProcess extends Process {
await KernelStack().startRenderer(this.pid);
const server = getKMod("server");
+ const i18n = getKMod("i18n");
const connected = server.connected;
const state = await KernelStack().spawn(StateHandler, undefined, "SYSTEM", this.pid, "ArcOS", States);
const kernel = Kernel();
@@ -41,6 +43,8 @@ export class InitProcess extends Process {
__Console__.timeEnd("** Init jumpstart");
this.name = "InitProcess";
+ i18n.startObserver(KernelStack().renderer?.target!);
+
if (ArcMode() === "nightly") this.nightly();
this.setSource(__SOURCE__);
diff --git a/src/ts/kernel/mods/i18n/index.ts b/src/ts/kernel/mods/i18n/index.ts
new file mode 100644
index 000000000..8929d36cf
--- /dev/null
+++ b/src/ts/kernel/mods/i18n/index.ts
@@ -0,0 +1,145 @@
+import EnglishLanguage from "$lang/en.json";
+import DutchLanguage from "$lang/nl.json";
+import { getKMod } from "$ts/env";
+import { getJsonHierarchy } from "$ts/hierarchy";
+import { KernelModule } from "$ts/kernel/module";
+import type { ConstructedWaveKernel, SystemDispatchType } from "$types/kernel";
+
+export class I18n extends KernelModule {
+ REGEX = /%(?[\w.=\-]+)(?:\((?(.*?))\)|)%/gm;
+ language: string = "en";
+ dispatch: SystemDispatchType;
+ observer: MutationObserver | null = null;
+ TARGET?: HTMLDivElement;
+
+ // Attributes to translate
+ TRANSLATABLE_ATTRIBUTES = ["placeholder", "title", "alt", "aria-label", "data-tooltip"];
+
+ LANGUAGES: Record = {
+ en: EnglishLanguage,
+ nl: DutchLanguage,
+ };
+
+ constructor(kernel: ConstructedWaveKernel, id: string) {
+ super(kernel, id);
+ this.dispatch = getKMod("dispatch");
+ }
+
+ async _init(): Promise {
+ this.startObserver(document.querySelector("#appRenderer")!);
+ }
+
+ translateString(str: string, prefix?: string): string | undefined {
+ const results = [...str.matchAll(this.REGEX)].map(({ groups, [0]: key }) => ({
+ id: groups?.id,
+ inlays: groups?.inlays ? groups.inlays.split("::") : [],
+ key,
+ }));
+
+ if (!results.length) return undefined;
+
+ let resultString = str;
+
+ for (const result of results) {
+ const fullId = prefix ? `${prefix}.${result.id}` : result.id;
+
+ const value =
+ getJsonHierarchy(this.LANGUAGES[this.language], fullId ?? "") ||
+ getJsonHierarchy(this.LANGUAGES[this.language], result.id ?? "");
+
+ if (!result.id || !value) {
+ resultString = resultString.replace(result.key, "??");
+ continue;
+ }
+
+ let partialResultString = value as string;
+
+ if (result.inlays) {
+ for (let i = 0; i < result.inlays.length; i++) {
+ partialResultString = partialResultString.replace(
+ `{{${i}}}`,
+ this.translateString(result.inlays[i]) || result.inlays[i]
+ );
+ }
+ }
+
+ resultString = resultString.replace(result.key, partialResultString);
+ }
+
+ return resultString;
+ }
+
+ replaceInNode(node: Node): void {
+ if (node.nodeType === node.TEXT_NODE) {
+ const str = node.textContent ?? "";
+ const prefix = node.parentElement?.closest("[data-prefix]")?.getAttribute("data-prefix") ?? undefined;
+ const resultString = this.translateString(str, prefix);
+
+ if (!resultString) return;
+
+ if (node.parentElement) {
+ node.parentElement.setAttribute("data-i18n-original", str);
+ node.textContent = resultString;
+
+ let raf: number;
+
+ const x = () => {
+ if (node.isConnected) {
+ if (node.textContent !== resultString) {
+ this.replaceInNode(node);
+ }
+
+ raf = requestAnimationFrame(x);
+ } else {
+ cancelAnimationFrame(raf);
+ }
+ };
+
+ raf = requestAnimationFrame(x);
+ }
+ } else if (node.nodeType === node.ELEMENT_NODE) {
+ const el = node as HTMLElement;
+ const prefix = el.closest("[data-prefix]")?.getAttribute("data-prefix") ?? undefined;
+
+ for (const attr of this.TRANSLATABLE_ATTRIBUTES) {
+ const raw = el.getAttribute(attr);
+ if (!raw) continue;
+
+ const resultString = this.translateString(raw, prefix);
+ if (!resultString) continue;
+
+ el.setAttribute(`data-i18n-original-${attr}`, raw);
+ el.setAttribute(attr, resultString);
+ }
+
+ Array.from(node.childNodes).forEach((n) => this.replaceInNode(n));
+ }
+ }
+
+ startObserver(target: HTMLDivElement) {
+ if (!target) throw new Error(`I18n: target does not exist`);
+
+ this.TARGET = target;
+ this.observer = new MutationObserver((mutations) => {
+ for (const m of mutations) {
+ m.addedNodes.forEach((n) => this.replaceInNode(n));
+ }
+ });
+
+ this.observer.observe(this.TARGET, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ characterData: true,
+ characterDataOldValue: true,
+ attributeOldValue: true,
+ });
+
+ this.replaceInNode(this.TARGET);
+ }
+
+ stopObserver() {
+ this.observer?.disconnect();
+ this.observer = null;
+ }
+}
diff --git a/src/ts/kernel/module/store.ts b/src/ts/kernel/module/store.ts
index c3af08ba7..38b64e23b 100755
--- a/src/ts/kernel/module/store.ts
+++ b/src/ts/kernel/module/store.ts
@@ -3,12 +3,14 @@ import { SystemDispatch } from "$ts/kernel/mods/dispatch";
import { Filesystem } from "$ts/kernel/mods/fs";
import { SoundBus } from "$ts/kernel/mods/soundbus";
import { Environment } from "../mods/env";
+import { I18n } from "../mods/i18n";
import { ServerManager } from "../mods/server";
import { ProcessHandler } from "../mods/stack";
export const KernelModules: Record = {
env: Environment,
dispatch: SystemDispatch,
+ i18n: I18n,
soundbus: SoundBus,
stack: ProcessHandler,
server: ServerManager,
diff --git a/src/ts/server/user/assoc/store/audio.ts b/src/ts/server/user/assoc/store/audio.ts
index 05ff31459..0ab8d4080 100755
--- a/src/ts/server/user/assoc/store/audio.ts
+++ b/src/ts/server/user/assoc/store/audio.ts
@@ -2,23 +2,23 @@ import type { FileDefinition } from "$types/assoc";
export const AudioFileDefinitions: Record = {
".mp3": {
- friendlyName: "Audio file",
+ friendlyName: "%associations.audioFile%",
icon: "AudioMimeIcon",
},
".opus": {
- friendlyName: "Audio file",
+ friendlyName: "%associations.audioFile%",
icon: "AudioMimeIcon",
},
".wav": {
- friendlyName: "Audio file",
+ friendlyName: "%associations.audioFile%",
icon: "AudioMimeIcon",
},
".m4a": {
- friendlyName: "Audio file",
+ friendlyName: "%associations.audioFile%",
icon: "AudioMimeIcon",
},
".flac": {
- friendlyName: "Audio file",
+ friendlyName: "%associations.audioFile%",
icon: "AudioMimeIcon",
},
};
diff --git a/src/ts/server/user/assoc/store/image.ts b/src/ts/server/user/assoc/store/image.ts
index 2500be877..a1c84ff33 100755
--- a/src/ts/server/user/assoc/store/image.ts
+++ b/src/ts/server/user/assoc/store/image.ts
@@ -2,39 +2,39 @@ import type { FileDefinition } from "$types/assoc";
export const ImageFileDefinitions: Record = {
".png": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".jpg": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".gif": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".webp": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".ico": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".bmp": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".tif": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".tiff": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
".jpeg": {
- friendlyName: "Image file",
+ friendlyName: "%associations.imageFile%",
icon: "ImageMimeIcon",
},
};
diff --git a/src/ts/server/user/assoc/store/index.ts b/src/ts/server/user/assoc/store/index.ts
index c505fcd2b..c8c03a0af 100755
--- a/src/ts/server/user/assoc/store/index.ts
+++ b/src/ts/server/user/assoc/store/index.ts
@@ -5,139 +5,139 @@ import { VideoFileDefinitions } from "./video";
export const DefaultFileDefinitions: Record = {
".tpa": {
- friendlyName: "Third-party app file",
+ friendlyName: "%associations.tpa%",
icon: "ArcAppMimeIcon",
},
".tpab": {
- friendlyName: "Third-party app binary",
+ friendlyName: "%associations.tpab%",
icon: "ArcAppMimeIcon",
},
".json": {
- friendlyName: "JSON file",
+ friendlyName: "%associations.json%",
icon: "JsonMimeIcon",
},
".pdf": {
- friendlyName: "PDF document",
+ friendlyName: "%associations.pdf%",
icon: "PdfMimeIcon",
},
".svg": {
- friendlyName: "SVG image",
+ friendlyName: "%associations.svg%",
icon: "SvgMimeIcon",
},
".zip": {
- friendlyName: "Archive",
+ friendlyName: "%associations.zip%",
icon: "CompressMimeIcon",
},
".txt": {
- friendlyName: "Text file",
+ friendlyName: "%associations.txt%",
icon: "TextMimeIcon",
},
"arcterm.conf": {
- friendlyName: "ArcTerm configuration",
+ friendlyName: "%associations.arctermConf%",
icon: "TextMimeIcon",
},
".arcterm": {
- friendlyName: "ArcTerm script (unsupported)",
+ friendlyName: "%associations.arcterm%",
icon: "TextMimeIcon",
},
".arctheme": {
- friendlyName: "ArcOS theme",
+ friendlyName: "%associations.arctheme%",
icon: "TextMimeIcon",
},
".md": {
- friendlyName: "Markdown document",
+ friendlyName: "%associations.md%",
icon: "TextMimeIcon",
},
".html": {
- friendlyName: "HTML document",
+ friendlyName: "%associations.html%",
icon: "XmlMimeIcon",
},
".htm": {
- friendlyName: "HTML document",
+ friendlyName: "%associations.htm%",
icon: "XmlMimeIcon",
},
".js": {
- friendlyName: "JS script",
+ friendlyName: "%associations.js%",
icon: "JavascriptMimeIcon",
},
".d.ts": {
- friendlyName: "Type definitions",
+ friendlyName: "%associations.dTs%",
icon: "JavascriptMimeIcon",
},
".ts": {
- friendlyName: "TS script",
+ friendlyName: "%associations.ts%",
icon: "JavascriptMimeIcon",
},
".mjs": {
- friendlyName: "JS module",
+ friendlyName: "%associations.js%",
icon: "JavascriptMimeIcon",
},
".xml": {
- friendlyName: "XML file",
+ friendlyName: "%associations.xml%",
icon: "XmlMimeIcon",
},
".arcpl": {
- friendlyName: "ArcOS playlist",
+ friendlyName: "%associations.arcpl%",
icon: "PlaylistMimeIcon",
},
".arclnk": {
- friendlyName: "Shortcut",
+ friendlyName: "%associations.shortcut%",
icon: "ShortcutMimeIcon",
},
".arc": {
- friendlyName: "ArcOS package",
+ friendlyName: "%associations.arc%",
icon: "ArcAppMimeIcon",
},
".msg": {
- friendlyName: "ArcOS message",
+ friendlyName: "%associations.msg%",
icon: "MessagingIcon",
},
".css": {
- friendlyName: "CSS file",
+ friendlyName: "%associations.css%",
icon: "DefaultMimeIcon",
},
".mig": {
- friendlyName: "Migration status",
+ friendlyName: "%associations.mig%",
icon: "ComponentIcon",
},
".lock": {
- friendlyName: "Migration lockfile",
+ friendlyName: "%associations.lock%",
icon: "ComponentIcon",
},
RegisteredVersion: {
- friendlyName: "Version registration",
+ friendlyName: "%associations.RegisteredVersion%",
icon: "ComponentIcon",
},
".exe": {
- friendlyName: "Windows executable (unsupported)",
+ friendlyName: "%associations.exe%",
icon: "UnknownFileIcon",
},
".msi": {
- friendlyName: "Windows installer (unsupported)",
+ friendlyName: "%associations.msi%",
icon: "UnknownFileIcon",
},
".com": {
- friendlyName: "MS-DOS executable (unsupported)",
+ friendlyName: "%associations.com%",
icon: "UnknownFileIcon",
},
".img": {
- friendlyName: "Floppy image (unsupported)",
+ friendlyName: "%associations.img%",
icon: "UnknownFileIcon",
},
".iso": {
- friendlyName: "CD-ROM image (unsupported)",
+ friendlyName: "%associations.iso%",
icon: "UnknownFileIcon",
},
".osl": {
- friendlyName: "OriginOS script (unsupported)",
+ friendlyName: "%associations.osl%",
icon: "UnknownFileIcon",
},
".wasm": {
- friendlyName: "WebAssembly (unsupported",
+ friendlyName: "%associations.wasm%",
icon: "UnknownFileIcon",
},
".bin": {
- friendlyName: "Binary (unsupported)",
+ friendlyName: "%associations.bin%",
icon: "UnknownFileIcon",
},
...AudioFileDefinitions, // AudioMimeIcon
diff --git a/src/ts/server/user/assoc/store/video.ts b/src/ts/server/user/assoc/store/video.ts
index 0b9e09c51..09abc5300 100755
--- a/src/ts/server/user/assoc/store/video.ts
+++ b/src/ts/server/user/assoc/store/video.ts
@@ -2,19 +2,19 @@ import type { FileDefinition } from "$types/assoc";
export const VideoFileDefinitions: Record = {
".mp4": {
- friendlyName: "Video file",
+ friendlyName: "%associations.videoFile%",
icon: "VideoMimeIcon",
},
".mkv": {
- friendlyName: "Video file",
+ friendlyName: "%associations.videoFile%",
icon: "VideoMimeIcon",
},
".mov": {
- friendlyName: "Video file",
+ friendlyName: "%associations.videoFile%",
icon: "VideoMimeIcon",
},
".avi": {
- friendlyName: "Video file",
+ friendlyName: "%associations.videoFile%",
icon: "VideoMimeIcon",
},
};
diff --git a/src/ts/server/user/store.ts b/src/ts/server/user/store.ts
index ac63f9fa3..eb9e44981 100755
--- a/src/ts/server/user/store.ts
+++ b/src/ts/server/user/store.ts
@@ -352,21 +352,21 @@ export const SystemFolders = [
];
export const UserPathCaptions: Record = {
- Home: "Home folder",
- Documents: "Documents",
- Pictures: "Pictures",
- Downloads: "Downloads",
- Wallpapers: "Wallpapers",
- Desktop: "Desktop",
- Music: "Music",
- Applications: "Applications",
- Trashcan: "Recycle Bin",
- Root: "Your Drive",
- System: "System Folder",
- Migrations: "Migration Files",
- Configuration: "Configuration",
- AppShortcuts: "Application Shortcuts",
- AppRepository: "Application Repository",
+ Home: "%userPaths.Home%",
+ Documents: "%userPaths.Documents%",
+ Pictures: "%userPaths.Pictures%",
+ Downloads: "%userPaths.Downloads%",
+ Wallpapers: "%userPaths.Wallpapers%",
+ Desktop: "%userPaths.Desktop%",
+ Music: "%userPaths.Music%",
+ Applications: "%userPaths.Applications%",
+ Trashcan: "%userPaths.Trashcan%",
+ Root: "%userPaths.Root%",
+ System: "%userPaths.System%",
+ Migrations: "%userPaths.Migrations%",
+ Configuration: "%userPaths.Configuration%",
+ AppShortcuts: "%userPaths.AppShortcuts%",
+ AppRepository: "%userPaths.AppRepository%",
Libraries: "System Libraries",
};
diff --git a/src/ts/writable.ts b/src/ts/writable.ts
index e2b96d664..5bab3a384 100755
--- a/src/ts/writable.ts
+++ b/src/ts/writable.ts
@@ -1,4 +1,6 @@
import { get, writable } from "svelte/store";
+import { getKMod } from "./env";
+import { I18n } from "./kernel/mods/i18n";
/** Callback to inform of a value updates. */
export type Subscriber = (value: T) => void;
@@ -39,8 +41,22 @@ export type StringStore = ReadableStore;
export type NumberStore = ReadableStore;
export function Store(initial?: T): ReadableStore {
+ function translateValue(v: any) {
+ if (typeof v !== "string") return v;
+
+ const i18n = getKMod("i18n", true);
+
+ return i18n?.translateString(v) || v;
+ }
+
const store = writable(initial);
- const obj = { ...store, get: () => get(store) };
+ const obj = {
+ ...store,
+ get: () => get(store),
+ set: (v: any) => store.set(translateValue(v)),
+ update: (v: (v: any) => any) => store.set(translateValue(v(get(store)))),
+ };
+
const fn = () => obj.get();
fn.get = obj.get;
diff --git a/tsconfig.app.json b/tsconfig.app.json
index dd0c2f749..3e1f935f3 100755
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -22,7 +22,8 @@
"$css/*": ["./src/css/*"],
"$types/*": ["./src/types/*"],
"$lib/*": ["./src/lib/*"],
- "$ts/*": ["./src/ts/*"]
+ "$ts/*": ["./src/ts/*"],
+ "$lang/*": ["./src/lang/*"]
},
"esModuleInterop": true
},
diff --git a/vite.config.ts b/vite.config.ts
index ad17bd7fa..84553cc8b 100755
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -26,6 +26,7 @@ export default defineConfig({
$types: resolve(__dirname, "./src/types"),
$ts: resolve(__dirname, "./src/ts"),
$lib: resolve(__dirname, "./src/lib"),
+ $lang: resolve(__dirname, "./src/lang"),
},
},
build: {