diff --git a/rfcs/README.md b/rfcs/README.md index a848170e6e..163c511e1f 100644 --- a/rfcs/README.md +++ b/rfcs/README.md @@ -8,6 +8,7 @@ RFC stands for _request for comments_. RFCs are written proposals that seek feed **RFC's** +- [RFC-004 JavaScript SDK](sdk-js/RFC.md) - [RFC-003 `@inlang/core/lint`](@inlang_core_lint/RFC.md) - [RFC-002 Tech Stack](tech-stack/RFC.md) - [RFC-001 Core Architecture](core-architecture/RFC.md) diff --git a/rfcs/sdk-js/RFC.md b/rfcs/sdk-js/RFC.md new file mode 100644 index 0000000000..13e30897e9 --- /dev/null +++ b/rfcs/sdk-js/RFC.md @@ -0,0 +1,533 @@ +# RFC for `@inlang/sdk-js` + +This package will provide an SDK on top of inlang for different frameworks and meta-frameworks. +The RFC is the first draft on how this could look like. + +## Goals of this package + +- the SDK should become the go-to solution for i18n + - should be easy to integrate (a minimum amount of steps required) + - should provide reasonable defaults (SSR, caching assets, etc.) and edge cases (shared state on server, etc.) + - should be configurable depending on user's needs +- for now we focus on delivering a great SDK for `SvelteKit` + - the SvelteKit community has no great i18n solution yet + - out of all frameworks, we have the best knowledge of `SvelteKit` +- at a later point the SDK should also work with plain `Svelte` and other (meta-)frameworks + +## Won't do's (for now) + +- optimizations + - we just read all `Resource`s via the `inlang.config.js` and use that `AST` at runtime + - optimizing (minimizing) the `AST` can be an improvement for the future + - we don't split `Resource`s into route-specific parts +- come up with a generic way to do things + - we focus on `SvelteKit` and will experiment with common `Svelte` concepts + - coming up with a cross-framework solution can be done when we make the SDK compatible with other frameworks +- typesafety for the lookup function + - this should probably not be part of the SDK itself, but there could exist a package that reads a `Resource` and outputs `TypeScript` type definitions + - could also work for other programming languages +- format functions, switch-case, plurals, pronouns, gender rules and other common i18n library stuff + - `inlang` currently only has the concepts of plain strings, so we leave that out for now + - but the `inlang` `AST` should at least be extended with the concept of `parameters` + - formatter functions probably need to be a concept for the full inlang ecosystem. So translators can already see how the formatted string will look in the UI +- localizing routes e.g. `en/settings` => `de/einstellungen` + - this is a content problem and not a translation problem. `inlang` just serves translations and not content (at least currently). + - we could come up with an approach and provide some guides how to set it up in any project + - potential questions: + - how do we link to the correct page in multiple languages? + - how to transform `Settings` to `Einstellungen`? + - routes could collide; Some words exist in different languages and can have different meanings + - e.g. the link `/bank` could lead to the english and german version. If the language is not encoded in the url, how do we render the correct language? + - some routes may not exist in certain languages + - layouts could differ between languages (a simple if/else should be good enough to support that use case) + +## How this SDK should function + +### runtime + +The SDK should include some core functionality (called `runtime`) that can be shared across all frameworks. + +> By exposing the `runtime` functionality, other developers could use it to build their own i18n solution that is compatible with `inlang` for any given framework. + +Runtime functionality will be: + +- `loadResource`: function that [loads a `Resource`](#loading-resources) +- `changeLanguage`: function to change language +- the [`lookup function`](#lookup-function) +- a function to create [`alternate` links](#alternate-links) + +The `runtime` functions itself will not deal with reactivity. They just store `Resource`s in memory and the lookup function accesses them. + +### adapters + +On top of the core functionality we will have framework-specific implementations that will use the `runtime` functionality to deliver a tailored i18n experience. +An adapter will provide a build plugin (vite, rollup, webpack, etc. via [`unplugin`](https://github.com/unjs/unplugin)). This build plugin will inject `runtime` functionality at specific parts of the codebase. Because most meta-frameworks have a convention-based setup, we can detect where to put things and how to best configure the framework to add i18n functionality. More information [here](#implementation-details-with-plugin) + +The adapter is also responsible to add reactivity to the `runtime` functionality where needed or if wanted by the developer (via a configuration option). An adapter will re-export the same functions as the `runtime` package does. + +Per default no reactivity gets used as it only makes sense for SPAs. If the language get's encoded into the url, then a simple redirect is better than to change the language client-side. Changing language is a rarely performed action and users will probably not expect that the page get's immediately updated. A page navigation is good enough. + +### loading resources + +Resources will be loaded from disk via the functions provided by `inlang.config.js`. Once loaded the `Resource`s will be kept in memory. A server then can transform (and in the future also optimize) those `Resource`s because it does not make sense to ship all `Resource`s to the client. This all happens during runtime, so we will probably also need to provide an `GET` endpoint so the client can load the optimized Resources from the server. + +In an ideal world we will always have all data needed. But in reality, things may be missing from a certain `Resource`. In that case, we transform the `Resource`s and include the reference if a `Id` is missing. + +### lookup function + +The lookup function will be the only thing that a developer needs to add to the source code. This function get's passed an `Id`, traverses the `AST` and returns a translated `Message`. + +The SDK will provide that function as the default export, so anyone can name it however he wants. We can also export some common aliases so users get useful auto-import capabilities from the IDE. + +This lookup function needs to be called with an `Id` e.g. like this: + +```ts +i("welcome", { name: "Inlang" }) +``` + +### alternate links + +[Alternate links](https://developers.google.com/search/docs/specialty/international/localized-versions?hl=de) are an important way to tell search engines that this page exists in multiple languages. + +If we don't care about localized routes, we can auto-generate them. `inlang` knows about all languages a project can have. An url will just slightly change depending on the [`language detection`](#language-detection) strategy. + +If developers want to customize the behavior, they can manually call a `setAlternateLink` function. + +The adapter should auto-inject this metadata into the rendered HTML. + +### configuration + +Internationalization can be done in many different ways. Some things will work across all strategies e.g. how to display a translated `Message` on screen (see [lookup function](#lookup-function)). But other things need to be configured because each company/team may have it's own opinion how on things should work. + +That's why we need some kind of configuration options that an adapter can parse and follow to output different behavior. + +Having those options in the `inlang.config.js` means that the configuration can be moved to a different repository with any other framework and (as long as there exists an SDK for that) we will have the same i18n experience without setting up anything other than the adapter. + +Configuration options can be: + +#### language detection + +There are many ways how to detect the language that should be used: + +- `rootSlug` (default): e.g. `www.inlang.com/de/docs` with option to leave out the reference language +- `TLD`: e.g. `www.inlang.de/docs` +- `subdomain`: e.g. `de.inlang.com/docs` +- `queryParameter`: e.g. `www.inlang.com/docs?lang=de` +- auto-detection + - `acceptLanguageHeader`: e.g. `'accept-language: en;q=0.8, de;q=0.7, *;q=0.5'` + - `cookie`: reads a cookie value + - `header`: e.g. read the Cloudflare `CF-IPCountry` header + - `navigator`: for SPAs to get the language information client-side + - others not mentioned here, can be easily added +- other functions that should be left to user land and therefore will not be part of the configuration + - get country information from the user's IP address + - get language information from the user object stored in DB + - custom; some things we didn't think of + +We can find better names for those strategies once we implement them. + +There is the need to support multiple strategies. If the first one does not match, the next strategies will be tried until a match is found. If nothing matches, the `referenceLanguage` will be used. + +Setting up auto detection is optional. Developers can also just call the `changeLanguage` function directly. + +Each strategy will need a special framework agnostic implementation, to pass the necessary information to those detection functions. + +For the first version we probably won't need all of the strategies. We will implement `rootSlug`, `acceptLanguageHeader` and `navigator` to cover a majority of use cases. + +#### config structure + +The configuration object could look like this: + +_inlang.config.js_ + +```ts +const config = { + sdk: { + adapter: { + SvelteKit: { + // ... SvelteKit specific options if there are any + }, + }, + alternateLinks: false, // turn feature off + languageDetection: [ + { + type: "rootSlug", + }, + { + type: "cookie", + // can / needs to be configured + name: "lang", + }, + ], + }, +} +``` + +## Error handling + +The adapter should also warn if something was not configured correctly. Such things could be: + +- if the config is invalid e.g. has unknown options +- if there are incompatible configurations e.g. adapter static with cookies as detection strategy +- missing `%lang%` placeholder in template + +## Structure + +This repository will be included in the inlang monorepo and contain following sub packages: + +- `runtime`: the core functionality of the SDK +- `adapter-sveltekit`: `SvelteKit` specific adapter +- `adapter-*`: other (meta-)framework specific adapter +- `detectors`: functions to detect languages depending on the strategy +- `config`: functions that deal with the inlang config +- `utils`: other shared utility functions that do not belong anywhere else + +## Documentation, Guides & Examples + +An important part of the SDK will also be the documentation. The basics should be pretty straight forward, but as soon as a developer needs something customized, he needs to be able to find it easily and well described. + +On top of that we should create a few examples of how to use the SDK. + +For things that are not part of the SDK, but probably needed in a lot of applications, we can write guides on how to set it up. e.g. providing a language menu does not make sense, since it probably needs to be slightly customized and styled independently. + +> We could think of a renderless component that just provides the functionality but leaves the rendering to the user. + +## implementation details (current state) + +The SvelteKit adapter will be a bundler plugin that rewrites some files during the bundling process to enable the desired i18n functionality. + +There are a couple of things that need to be considered when implementing the `SvelteKit` adapter (and probably others too). Where do we need to load what things? Is it done in an optimal way? Do we handle all edge cases? + +As of my current knowledge, this is needed to implement i18n in a `SvelteKit` project the best possible way: + +> The diff shows what is needed to be changed to support i18n. I didn't test the code so not everything might work 100% like this. But it should give you a first idea what we need to think of. + +### `hooks.server.ts` + +This is the entry point of a SvelteKit application. Each requests will start at the `handle` hook. This is the best place to load things into memory and attach certain objects to the request. + +```diff +import type { Handle } from '@sveltejs/kit' ++import { loadAllResources, detectLanguage, createLookupFunctionForLanguage } from '$i18n' + +// load ´Resources` of all languages into memory ++loadAllResources() + +export const handle = (async ({ event, resolve }) => { + // detect the language depending on some strategy ++ const language = detectLanguage(event) + + // initialize the lookup function for the selected language ++ const i18n = createLookupFunctionForLanguage(language) + + // attach the language information to the request ++ event.locals.language = language + // attach the i18n function to the request ++ event.locals.i18n = i18n + +- return resolve(event) + // when the request was completed, replace the HTML lang attribute with the language ++ return resolve(event, { transformPageChunk: ({ html }) => html.replace('%lang%', language) }) +}) satisfies Handle +``` + +### `app.html` + +This is the entry point of the rendered HTML output. For SEO purposes we want to set the correct lang attribute. + +```diff + +- ++ +
%sveltekit.head% +