diff --git a/.gitignore b/.gitignore index d7ebf511784b..b2b96db7a830 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,6 @@ vite.config.ts.timestamp-* # TypeScript incremental build cache *.tsbuildinfo + +# inlang manages this file itself (^2.5); keep it untracked to avoid churn +**/*.inlang/.gitignore diff --git a/.prettierignore b/.prettierignore index 4e3db204c092..afc52cac9d8e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -66,3 +66,7 @@ web-admin/src/client/gen # generated by nearly web-common/src/features/dashboards/url-state/filters/expression.cjs + +# generated by paraglidejs +web-common/src/lib/i18n/gen +web-common/src/lib/i18n/project.inlang/cache diff --git a/package-lock.json b/package-lock.json index d18f5b7eb96f..73060352e9a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "web-local" ], "devDependencies": { + "@inlang/paraglide-js": "^2.10.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@types/eslint": "^8.56.9", "@vitest/eslint-plugin": "^1.1.42", @@ -6242,6 +6243,93 @@ "mlly": "^1.8.0" } }, + "node_modules/@inlang/paraglide-js": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.10.0.tgz", + "integrity": "sha512-3xQveEyZMV9IOLP7Vy9Ttye+Yzryqz6KM06tvVwvmbCPDTdzmFoc34KlREXGpHuBAlxRZGfAhcJKfnSXXQDmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inlang/recommend-sherlock": "^0.2.1", + "@inlang/sdk": "2.6.2", + "commander": "11.1.0", + "consola": "3.4.0", + "json5": "2.2.3", + "unplugin": "^2.1.2", + "urlpattern-polyfill": "^10.0.0" + }, + "bin": { + "paraglide-js": "bin/run.js" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz", + "integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3" + } + }, + "node_modules/@inlang/sdk": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.6.2.tgz", + "integrity": "sha512-eOgAX+eQpHvD/H4BMILc4tZ85XviTlwr/51RKkKUHozVVthj5avUPKP+4N4vcTUrqSscl2atTh9NbNTuvoBN0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lix-js/sdk": "0.4.7", + "@sinclair/typebox": "^0.31.17", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^13.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@inlang/sdk/node_modules/@sinclair/typebox": { + "version": "0.31.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", + "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inlang/sdk/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/@internationalized/date": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", @@ -7019,6 +7107,46 @@ "@lezer/lr": "^1.4.0" } }, + "node_modules/@lix-js/sdk": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.7.tgz", + "integrity": "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lix-js/server-protocol-schema": "0.1.1", + "dedent": "1.5.1", + "human-id": "^4.1.1", + "js-sha256": "^0.11.0", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@lix-js/sdk/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@lix-js/server-protocol-schema": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz", + "integrity": "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -9107,6 +9235,16 @@ ], "license": "MIT" }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.48.0-build4", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz", + "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -12517,6 +12655,13 @@ "dev": true, "license": "MIT" }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -14255,6 +14400,20 @@ "node": ">=18" } }, + "node_modules/comment-json": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -15945,6 +16104,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -19775,6 +19949,16 @@ "node": ">= 14" } }, + "node_modules/human-id": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", + "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -20877,6 +21061,13 @@ "node": ">=0.10.0" } }, + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -21184,6 +21375,16 @@ "dev": true, "license": "MIT" }, + "node_modules/kysely": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/langium": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", @@ -30803,6 +31004,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sqlite-wasm-kysely": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz", + "integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==", + "dev": true, + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.48.0-build2" + }, + "peerDependencies": { + "kysely": "*" + } + }, "node_modules/srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", @@ -33292,6 +33505,22 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -33528,6 +33757,13 @@ "dev": true, "license": "BSD" }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -34881,6 +35117,13 @@ "node": ">=10.13.0" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 0bee62bf7584..a3e4b51adfe3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "web-local" ], "scripts": { - "build": "npm run build -w web-local", + "build:i18n": "paraglide-js compile --project web-common/src/lib/i18n/project.inlang --outdir web-common/src/lib/i18n/gen", + "build": "npm run build:i18n && npm run build -w web-local", "dev": "sh -c 'npm run dev-runtime -- \"$@\" & npm run dev-web -- --port 3001 & wait' --", "dev-web": "npm run dev -w web-local -- ", "dev-runtime": "node scripts/dev.js", @@ -24,6 +25,7 @@ "check:edit-route-parity": "node ./scripts/check-edit-route-parity.js" }, "devDependencies": { + "@inlang/paraglide-js": "^2.10.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@types/eslint": "^8.56.9", "@vitest/eslint-plugin": "^1.1.42", diff --git a/scripts/i18n-guard.js b/scripts/i18n-guard.js new file mode 100644 index 000000000000..6d6e8ed757c1 --- /dev/null +++ b/scripts/i18n-guard.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node +// Heuristic guard against hardcoded user-facing strings in already-migrated +// areas of the frontend. It scans `.svelte` markup (not - {title} overview - Rill + {m.organizations_overview_page_title({ title })} diff --git a/web-admin/vite.config.ts b/web-admin/vite.config.ts index 111d208462df..c010847ee0b9 100644 --- a/web-admin/vite.config.ts +++ b/web-admin/vite.config.ts @@ -1,3 +1,4 @@ +import { paraglideVitePlugin } from "@inlang/paraglide-js"; import { sveltekit } from "@sveltejs/kit/vite"; import dns from "dns"; import { defineConfig } from "vitest/config"; @@ -39,7 +40,14 @@ export default defineConfig({ ], exclude: ["sveltekit-superforms"], }, - plugins: [sveltekit()], + plugins: [ + sveltekit(), + paraglideVitePlugin({ + project: "../web-common/src/lib/i18n/project.inlang", + outdir: "../web-common/src/lib/i18n/gen", + strategy: ["localStorage", "preferredLanguage", "baseLocale"], + }), + ], envDir: "../", envPrefix: "RILL_UI_PUBLIC_", }); diff --git a/web-common/src/lib/i18n/README.md b/web-common/src/lib/i18n/README.md new file mode 100644 index 000000000000..10c89d33fafb --- /dev/null +++ b/web-common/src/lib/i18n/README.md @@ -0,0 +1,98 @@ +# Internationalization (i18n) + +Rill's frontend is localized with [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) +(inlang). Translatable strings live in this directory and compile to +tree-shakeable, type-safe message functions consumed by both `web-local` +(Rill Developer) and `web-admin` (Rill Cloud). + +## Layout + +``` +lib/i18n/ +├── messages/en.json # source translations (edit these) +├── project.inlang/ # inlang project config +└── gen/ # compiled message functions (auto-generated, gitignored) +``` + +- `messages/en.json` is the base locale and the source of truth for keys. + English is currently the only configured locale; add more by listing them in + `project.inlang/settings.json` and adding a `messages/{locale}.json` file. +- `gen/` is compiled output. It is gitignored — never edit or import individual + files by hand other than the documented entry points below. + +## Build + +```sh +npm run build:i18n # compile messages/ -> gen/ +``` + +The Vite plugin in both `web-local` and `web-admin` recompiles `gen/` on dev and +build, so you rarely need to run `build:i18n` manually during development. + +## Usage + +```svelte + + +

{m.welcome_greeting({ name })}

+``` + +- Import the `m` namespace; call messages as functions. +- Pass interpolation values as a named object: `m.welcome_greeting({ name })`. +- Override the locale per call when needed: `m.common_cancel({}, { locale: "de" })`. + +Locale detection is configured per app in each `vite.config.ts`. `web-local` +uses `["preferredLanguage", "baseLocale"]` (the browser's preferred language, +falling back to English); `web-admin` adds a `localStorage` step in front so a +user's explicit choice persists. + +## Conventions + +### Key naming + +Use `feature_component_purpose`, lower snake_case, grouped by prefix in +`en.json`: + +```jsonc +{ + "common_cancel": "Cancel", + "common_save": "Save", + "welcome_greeting": "Welcome, {name}", + "dashboards_filters_clear_all": "Clear all filters", +} +``` + +- `common_` — copy reused across features. Reuse an existing `common_` key + rather than duplicating identical copy. +- Otherwise prefix with the feature directory name. + +### Interpolation + +Use named placeholders, never string concatenation: + +```jsonc +{ "exports_row_count": "Exporting {count} rows" } +``` + +### Pluralization and variants + +Use Paraglide [variants](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/variants) +rather than hand-rolled `count === 1 ? ... : ...` logic. + +## Adding or migrating a string + +1. Add the key to `messages/en.json` following the naming convention. +2. Replace the literal in code with `m.key()` (or `m.key({ var })`). +3. `npm run build:i18n` (or rely on the Vite plugin in dev). +4. Run `npm run test -w web-common` and `npm run quality`. + +## Guard against new hardcoded strings + +`scripts/i18n-guard.js` scans already-migrated areas for hardcoded +user-facing strings and runs in `npm run quality`. It is a heuristic and +currently **warning-only**; the final migration chunk flips it to `--strict` +(fatal). Each migration chunk appends its directories to `MIGRATED_GLOBS` in +that script. Suppress an intentional literal with an `i18n-ignore` comment on +the line or the line above it. diff --git a/web-common/src/lib/i18n/messages/en.json b/web-common/src/lib/i18n/messages/en.json new file mode 100644 index 000000000000..08f23297e2cd --- /dev/null +++ b/web-common/src/lib/i18n/messages/en.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "organizations_overview_page_title": "{title} overview - Rill" +} diff --git a/web-common/src/lib/i18n/project.inlang/.meta.json b/web-common/src/lib/i18n/project.inlang/.meta.json new file mode 100644 index 000000000000..d39728f7d73e --- /dev/null +++ b/web-common/src/lib/i18n/project.inlang/.meta.json @@ -0,0 +1,3 @@ +{ + "highestSdkVersion": "2.6.2" +} diff --git a/web-common/src/lib/i18n/project.inlang/README.md b/web-common/src/lib/i18n/project.inlang/README.md new file mode 100644 index 000000000000..33a7fcbf7016 --- /dev/null +++ b/web-common/src/lib/i18n/project.inlang/README.md @@ -0,0 +1,106 @@ +## What is this folder? + +This is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) inlang project. + +## At a glance + +Purpose: + +- This folder stores inlang project configuration and plugin cache data. +- Translation files live outside this folder and are referenced from `settings.json`. + +Safe to edit: + +- `settings.json` + +Do not edit: + +- `cache/` +- `.gitignore` + +Key files: + +- `settings.json` — locales, plugins, file patterns (source of truth) +- `cache/` — plugin caches (safe to delete) +- `.gitignore` — generated + +``` +*.inlang/ +├── settings.json # Locales, plugins, and file patterns (source of truth) +├── cache/ # Plugin caches (gitignored) +└── .gitignore # Ignores everything except settings.json +``` + +Translation files (like `messages/en.json`) live **outside** this folder and are referenced via plugins in `settings.json`. + +## What is inlang? + +[Inlang](https://inlang.com) is an open file format for building custom localization (i18n) tooling. It provides: + +- **CRUD API** — Read and write translations programmatically via SQL +- **Plugin system** — Import/export any format (JSON, XLIFF, etc.) +- **Version control** — Built-in version control via [lix](https://lix.dev) + +``` +┌──────────┐ ┌───────────┐ ┌────────────┐ +│ i18n lib │ │Translation│ │ CI/CD │ +│ │ │ Tool │ │ Automation │ +└────┬─────┘ └─────┬─────┘ └─────┬──────┘ + │ │ │ + └─────────┐ │ ┌──────────┘ + ▼ ▼ ▼ + ┌──────────────────────────────────┐ + │ *.inlang file │ + └──────────────────────────────────┘ +``` + +## Quick start + +```bash +npm install @inlang/sdk +``` + +```ts +import { loadProjectFromDirectory, saveProjectToDirectory } from "@inlang/sdk"; + +const project = await loadProjectFromDirectory({ path: "./project.inlang" }); +// Query messages with SQLite + [Kysely](https://kysely.dev/) under the hood. +const messages = await project.db.selectFrom("message").selectAll().execute(); + +// Use project.db to update messages. +await saveProjectToDirectory({ path: "./project.inlang", project }); +``` + +## Ideas for custom tooling + +- Translation health dashboard (missing/empty/stale messages) +- Locale coverage report in CI +- Auto-PR for new keys with placeholders +- Migration tool between file formats via plugins +- Glossary/term consistency checker + +## Data model ([docs](https://inlang.com/docs/data-model)) + +``` +bundle (a concept, e.g., "welcome_header") + └── message (per locale, e.g., "en", "de") + └── variant (plural forms, gender, etc.) +``` + +- **bundle**: Groups messages by ID (e.g., `welcome_header`) +- **message**: A translation for a specific locale +- **variant**: Handles pluralization/selectors (most messages have one variant) + +## Common tasks + +- List bundles: `project.db.selectFrom("bundle").selectAll().execute()` +- List messages for locale: `project.db.selectFrom("message").where("locale", "=", "en").selectAll().execute()` +- Find missing translations: compare message counts across locales +- Update a message: `project.db.updateTable("message").set({ ... }).where("id", "=", "...").execute()` + +## Links + +- [SDK documentation](https://inlang.com/docs) +- [inlang.com](https://inlang.com) +- [List of plugins](https://inlang.com/c/plugins) +- [List of tools](https://inlang.com/c/tools) diff --git a/web-common/src/lib/i18n/project.inlang/settings.json b/web-common/src/lib/i18n/project.inlang/settings.json new file mode 100644 index 000000000000..20ec58d92061 --- /dev/null +++ b/web-common/src/lib/i18n/project.inlang/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/web-local/src/app.html b/web-local/src/app.html index 3274bf3fb9c9..40918cd5e081 100644 --- a/web-local/src/app.html +++ b/web-local/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/web-local/src/hooks.server.ts b/web-local/src/hooks.server.ts new file mode 100644 index 000000000000..05424484474c --- /dev/null +++ b/web-local/src/hooks.server.ts @@ -0,0 +1,18 @@ +import type { Handle } from "@sveltejs/kit"; +import { paraglideMiddleware } from "@rilldata/web-common/lib/i18n/gen/server"; + +// creating a handle to use the paraglide middleware +const paraglideHandle: Handle = ({ event, resolve }) => + paraglideMiddleware( + event.request, + ({ request: localizedRequest, locale }) => { + event.request = localizedRequest; + return resolve(event, { + transformPageChunk: ({ html }) => { + return html.replace("%lang%", locale); + }, + }); + }, + ); + +export const handle: Handle = paraglideHandle; diff --git a/web-local/src/hooks.ts b/web-local/src/hooks.ts new file mode 100644 index 000000000000..7bbe52dc202f --- /dev/null +++ b/web-local/src/hooks.ts @@ -0,0 +1,6 @@ +import type { Reroute } from "@sveltejs/kit"; +import { deLocalizeUrl } from "@rilldata/web-common/lib/i18n/gen/runtime"; + +export const reroute: Reroute = ({ url }) => { + return deLocalizeUrl(url).pathname; +}; diff --git a/web-local/vite.config.ts b/web-local/vite.config.ts index 1adbbe781555..0e3ae84fe33a 100644 --- a/web-local/vite.config.ts +++ b/web-local/vite.config.ts @@ -1,6 +1,7 @@ import { sveltekit } from "@sveltejs/kit/vite"; import dns from "dns"; import { defineConfig } from "vitest/config"; +import { paraglideVitePlugin } from "@inlang/paraglide-js"; // print dev server as `localhost` not `127.0.0.1` dns.setDefaultResultOrder("verbatim"); @@ -46,7 +47,14 @@ const config = defineConfig({ "memoize-weak", ], }, - plugins: [sveltekit()], + plugins: [ + sveltekit(), + paraglideVitePlugin({ + project: "../web-common/src/lib/i18n/project.inlang", + outdir: "../web-common/src/lib/i18n/gen", + strategy: ["preferredLanguage", "baseLocale"], + }), + ], envDir: "../", envPrefix: "RILL_UI_PUBLIC_", });