Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@dcdunkan
Copy link
Member

i18n plugin v2 introduces a lot of changes, mostly breaking. The following is the list of major changes made:

No longer bound to a specific syntax / l10n system

There is now the concept of "Adapters" much like session storage adapters. There will be few official ones, and it should be easy to make a custom one. Adapters can be directly plugged into an i18n instance, and they should just work. Adapters should expose locales registered and a translate function that of the signature translate(locale, message, variables?).

const fluent = new FluentAdapter({/* ... */}); // or any other
const i18n = new I18n({	adapter: fluent, /* other options */ });

The system we currently we have built-in to i18n is Fluent. This will be converted to an official adapter. This adapter system allows to bring back the JSON system we had in pre-1.0 since lot of people still uses it. Other than these two, we could also bring support for GNU getttext PO files, or JSON based ICU MessageFormat, I18nnext format, or any other format as official or unofficial adapters.

The reasons for this change are,

  1. Flexibility. Not to enforce Fluent as the only option.
  2. We had pre-1.0 JSON/YAML format supporting deeply nested messages. Fluent only allowed message.attribute (2 level depth). Now its possible to have deeply nested messages conversations.greeting.initial-message.help-button (just saying that it's possible, no intention to recommend this).

Example adapters:

Strict TypeScript types Support

Could use improvements, and need reviews.

Optional, but recommended of course. Type messages and pass it as a generic type param. TS gives full typings for locales (optional), messages, variables. It can even restrict passing any variables to message requiring no variables. Although, I'd like to know general DX on this one.

type Typings = {
	locales: "en" | "zh"; // or just string, to not have any strict stuff here.
	messages: {
		"msg-without-vars": never;
		"msg-with-vars": {
			theme: string;
			banCount: number;
			joinDate: Date;
			anything: string | number | Date /* ... */;
		};
	};
};

const adapter = new CustomAdapter<Typings>(/* .. */);
const i18n = new I18n<Context, Typings>({ adapter });

i18n.translate("de", /* ... */); // ts error: no locale
i18n.translate("en", "msg-with-vars"); // ts error: expected third argument.
i18n.translate("en", "msg-without-vars"); // no error
i18n.translate("en", "msg-without-vars", {}); // ts error: did not expect variables
i18n.translate("zh", "msg-with-vars", { theme: "simple" }); // ts error: expected all the variables

It tends to be really strict. Syncing the typings with the translations can be really hard, with all the variables and their types, etc. So, v2 introduces,

Type Generation through CLI

There is also a CLI with (possibly more) tools related with i18n. It's written using Deno APIs as there is a watch mode. cli/main.ts is the entry point.

generate-types subcommand:

  • --format: specify the format to use. e.g., fluent.
  • --output / -o: output file to write the types to.
  • --watch: watch the concerned files.

There are two modes: locales directory and list of files to watch.

  1. If provided a list of files and folders after the arguments, then the files are read and parsed and types are generated, which is then written to the output file.
  2. If locales directory mode (explained in next section), specify two more arguments, --locales-dir which is the path to locales directory and --fallback which should be the locale to focus, it should be a directory inside the --locales-dir. Just the name of the directory is enough.

Possible improvements: Useful i18n tools like, linting or checking for errors like overriding and stuff. Or source analysis to find messages that are not referenced in code.

Locales directory & loading

Loading locales directories used to be a method of the main I18n class, but no longer. Also uses node:fs for more cross-runtime compatibility (except for Cloudflare workers). It lies under a different module. loadLocalesDirectory is now a function that takes in an ResourceLoadable (has method loadResource) and some options. It walks through the specified directory, and adds those sources to the adapter.

loadLocalesDirectory is now a little bit more standardized. Example structure:

locales/
├── de/
│   └── main.ftl
├── en/
│   ├── nested/
│   │   └── buttons.ftl
│   ├── help.ftl
│   └── main.ftl
├── ru/
│   └── main.ftl
├── common.ftl
└── another-common.ftl

Locales are now separated as directories, and not as single files. Can be nested and all. Common files, although I can't really say that I'd use it a lot, but we can have them. Common files will be treated as one of the locale sources files, so will be added to each locale.

Possible addition: Could introduce namespacing like i18nnext. (e.g., supporting categorised (namespaced) keys like rewards:message, commands:start, commands:start.button).

Users can always resort to the old structure, where single files were considered as locales: en.ftl, de.ftl, etc, by writing their own simple functions to load to the adapter:

const dir = "./locales";
for await (const dirent of Deno.readDir(dir)) {
	if (!dirent.isFile) continue;
	const content = await Deno.readTextFile(join(dir, dirent.name));
	const locale = dirent.name.splice(0, -extname(dirent.name).length);
	adapter.loadResource(locale, content, /* options? */);
}

Or this can be built-in if needed.

Other stuff

  • Got rid of warning handlers (it didn't feel useful). And introduced a simple handler option for handling "missing key" events. If the handler returns a string, it's returned as the translate message. If not, continued to next negotiated locale. If still didn't match, fell back to fall back locale. If still not, an error is thrown! (Because fall back locale must have all the messages one references). Need to discuss this error throwing.

     new I18n({
     	/* other options */
     	onMissingKey: (event) => {
     		// can log stuff out for later
     		console.warn(`Missing ${event.key} in ${event.currentLocale}`);
     		console.warn(`requested: ${event.requestedLocale}`);
     		
     		if (event.fallback) { // whether called in fallback
     			// return a string to resolve as the translation
     			return "Sorry, this translation is broken. Please contact administrator to fix this with the following data: " + /* mmm */;
     		}
     		
     		// or even, throw an error, for the developer to manage
     		throw new Error("this should not have happened");
     	},
     });
  • There is no ultimate fallback locale like we used to have (en). The user should set a reliable fallback locale on their own. Less surprises when it errors.

  • JSR compatible, except for grammY v1 imports; thought of replacing after grammY v2 is closer to completion, assuming Context and MiddlewareFn have not changed much in v2.

  • Removed session dependency. Use locale negotiators. (plugins: remove i18n→sessions dependency #57)

  • I18nFlavor is now transformative (for v2). (chore: make context flavor transformative #58)

  • Documented almost everything.

  • Lots of tests.

@KnorpelSenf
Copy link
Member

@dcdunkan are you still interested in working on this? If there is a chance that this will be abandoned, then I won't spend so much time reviewing it

@dcdunkan
Copy link
Member Author

Yes, I am still interested in working on this. Sorry, I got busy with some things in the offline life. I expect to get back on this in one or two weeks' time.

@dcdunkan
Copy link
Member Author

dcdunkan commented Sep 21, 2025

Other than for the work on the suggestion Loskir made on making the CLI extendable to allow type generation for arbitrary formats, I think the current state is ready for an initial review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugins: remove i18n→sessions dependency Allow overriding translations

2 participants