Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0dc7952
clean up
dcdunkan May 15, 2025
419b25b
v2 refactoring
dcdunkan May 15, 2025
15ad921
cleanup
dcdunkan May 15, 2025
24f61bb
fix: wrong export as type
dcdunkan May 19, 2025
8552add
feat: add missing key handler option (experimental)
dcdunkan May 19, 2025
c299312
refactor: move the main logic from adapter to i18n
dcdunkan May 19, 2025
db40729
make context flavor transformative
dcdunkan May 19, 2025
269b7b0
chore: format
dcdunkan May 20, 2025
aead83d
move fallbackLocale to i18n instance instead of adapter
dcdunkan May 20, 2025
9df1767
name better
dcdunkan May 20, 2025
657376c
fix types
dcdunkan May 20, 2025
d216c54
ok, but simpler typings
dcdunkan May 20, 2025
0cbd90d
make locales a property than a method
dcdunkan May 20, 2025
1eb71c7
decouple and organize imports and exports
dcdunkan May 21, 2025
4e71145
cli: small cleanup
dcdunkan May 21, 2025
4fd7a37
loads of tests and subtle fixes
dcdunkan May 24, 2025
cac2ea0
fix lint issues
dcdunkan May 24, 2025
73acfb3
add example
dcdunkan May 25, 2025
0a90cef
allow multiple extensions in load directory
dcdunkan May 25, 2025
7488abb
update imports and fix errors
dcdunkan Mar 12, 2026
d234ab4
chore: add zed editor configuration
dcdunkan Mar 13, 2026
015d126
extract the config type to types
dcdunkan Mar 13, 2026
688475c
update LICENSE year
dcdunkan Mar 13, 2026
3e7d83b
feat: arbitrary format support & an actual cli application
dcdunkan Mar 13, 2026
253c45e
slight rearranging, linting
dcdunkan Mar 13, 2026
ba2d02e
fmt
dcdunkan Mar 13, 2026
4623d39
go through todos and resolve some
dcdunkan Mar 13, 2026
e770747
handle symlinks and update logs
dcdunkan Mar 13, 2026
bd6f496
cli: switch to cleye
dcdunkan Mar 13, 2026
c769a23
feat: method to get locale in use
dcdunkan Mar 17, 2026
4cec019
fix: type check for fs.Dir
dcdunkan Mar 17, 2026
e0890ca
test: cover all the middleware methods
dcdunkan Mar 17, 2026
1011905
feat: implement hears() and extract variables type
dcdunkan Mar 22, 2026
121e285
chore: use tasks
dcdunkan Mar 22, 2026
6f83b39
docs: document more stuff
dcdunkan Mar 23, 2026
98e37ba
feat: pass raw arguments to features
dcdunkan Mar 23, 2026
088e650
feat: let adapters handle internal locale negotiation
dcdunkan Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .editorconfig

This file was deleted.

32 changes: 0 additions & 32 deletions .github/workflows/build.yml

This file was deleted.

4 changes: 0 additions & 4 deletions .gitignore

This file was deleted.

6 changes: 0 additions & 6 deletions .vscode/extensions.json

This file was deleted.

7 changes: 0 additions & 7 deletions .vscode/settings.json

This file was deleted.

60 changes: 60 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {
"deno": {
"settings": {
"deno": {
"enable": true,
},
},
},
"json-language-server": {
"settings": {
"json": {
"schemas": [
{
"fileMatch": ["deno.json", "deno.jsonc"],
"url": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json",
},
{
"fileMatch": ["package.json"],
"url": "https://www.schemastore.org/package",
},
],
},
},
},
},
"languages": {
"JavaScript": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint",
],
"formatter": "language_server",
},
"TypeScript": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint",
],
"formatter": "language_server",
},
"TSX": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint",
],
"formatter": "language_server",
},
},
}
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022-2024 Dunkan
Copyright (c) 2022-2026 Dunkan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
83 changes: 0 additions & 83 deletions README.md

This file was deleted.

167 changes: 167 additions & 0 deletions adapters/fluent/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { createDebug } from "@grammyjs/debug";
import { FluentBundle, FluentResource, type Message } from "@fluent/bundle";
import type {
FormatAdapter,
Locales,
LocalesTypings,
MessageKey,
Messages,
ResourceLoadable,
} from "../../types.ts";
import { isValidLocale } from "../../utilities.ts";
import { negotiateLanguages } from "@fluent/langneg";

const debug = createDebug("grammy:i18n-fluent");

export type FluentPattern = Message["attributes"][string];
export type FluentBundleOptions = ConstructorParameters<typeof FluentBundle>[1];
export interface ResourceOptions {
allowOverrides?: boolean;
bundleOptions?: Partial<FluentBundleOptions>;
}
export interface FluentMessageKey {
id: string;
attr?: string;
}

const DEFAULT_ALLOW_OVERRIDES = false;

/**
* Official {@link FormatAdapter} for the Fluent syntax by Mozilla. This adapter
* also supports loading resources; hence this can be plugged in with the
* locales directory loading utilities for convenience.
*
* @see https://projectfluent.org/fluent/guide Syntax guide for Fluent syntax.
*/
export class FluentAdapter<LT extends LocalesTypings = LocalesTypings>
implements FormatAdapter<LT>, ResourceLoadable<ResourceOptions> {
// While FluentBundle-s are capable of being the carrier of more than one
// locales at a time, here each bundle can carry only one locale.
#bundles: Map<string, FluentBundle>;

#locales: string[];

constructor(
private options?: {
/**
* Bundle options to be used when creating a Fluent bundle. This
* configuration is added to every bundle (each bundle is for each
* registered locale). This can be overridden by passing a different
* set of bundle options when loading a resource.
*
* One of the common usage of this option would be to load bundles
* with `useIsolating` set to false by default, to globally disable
* the Unicode Isolation done by Fluent, or to install custom Fluent
* functions.
*/
bundleOptions?: FluentBundleOptions;
},
) {
this.#bundles = new Map<string, FluentBundle>();
this.#locales = [];
}

get locales(): string[] {
return this.#locales;
}

loadResource(
locale: string,
source: string,
resourceOptions?: ResourceOptions,
): Error[] {
if (!isValidLocale(locale))
throw new Error(`The locale ${locale} seems invalid.`);

let bundle: FluentBundle | undefined = this.#bundles.get(locale);
if (bundle == null || !(bundle instanceof FluentBundle)) {
bundle = new FluentBundle(locale, {
...this.options?.bundleOptions,
...resourceOptions?.bundleOptions,
});
debug(`Creating a bundle for the locale '${locale}'`);
this.#bundles.set(locale, bundle);

for (const locale of bundle.locales)
if (!this.#locales.includes(locale))
this.#locales.push(locale);
}

const resource = new FluentResource(source);
const errors = bundle.addResource(resource, {
allowOverrides: resourceOptions?.allowOverrides ??
DEFAULT_ALLOW_OVERRIDES,
});
// todo: do something better with this
return errors;
}

negotiateLocales(requestedLocale: string): string[] {
const negotiatedLocales = negotiateLanguages(
[requestedLocale],
this.locales,
{ strategy: "filtering" },
);
return negotiatedLocales;
}

translate<
L extends Locales<LT>,
M extends Messages<LT>,
MK extends MessageKey<LT, M>,
>(
locale: L,
messageKey: MK,
...args: M[MK] extends never ? []
: Messages<LocalesTypings>[string] extends M[MK]
? [variables?: M[MK]]
: [variables: M[MK]]
): string | undefined {
const variables = args[0];
const bundle = this.#bundles.get(locale);
if (bundle == null) return;
const pattern = getPattern(bundle, messageKey);
if (pattern == null) return;
return formatPattern(bundle, pattern, variables);
}
}

function getPattern(
bundle: FluentBundle,
messageKey: string,
): FluentPattern | null | undefined {
const key = parseMessageKey(messageKey);
const message = bundle.getMessage(key.id);
if (message == null)
return undefined;
return key.attr === undefined
? message?.value
: message?.attributes[key.attr];
}

function formatPattern<
LT extends LocalesTypings,
M extends Messages<LT>,
MK extends MessageKey<LT, M>,
>(
bundle: FluentBundle,
pattern: FluentPattern,
variables?: M[MK],
): string {
const errors: Error[] = [];
const formatted = bundle.formatPattern(pattern, variables, errors);
for (const error of errors)
console.error(error); // todo: handle this
return formatted;
}

export function parseMessageKey(key: string): FluentMessageKey {
const segments = key.trim().split(".");
if (
segments.length > 2 ||
segments.some((s) => s.trim().length === 0)
) {
throw new Error(`Invalid message key segments in key: '${key}'`);
}
return { id: segments[0], attr: segments[1] };
}
Loading