@flightdev/i18n 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ // src/adapters/formatjs.ts
2
+ function formatjs(config) {
3
+ const { locales, defaultLocale, messages, timeZone, onError, onWarn } = config;
4
+ let currentLocale = defaultLocale;
5
+ let intl = null;
6
+ let createIntl = null;
7
+ let cache = null;
8
+ function createIntlInstance(locale) {
9
+ if (!createIntl) return null;
10
+ return createIntl(
11
+ {
12
+ locale,
13
+ defaultLocale,
14
+ messages: messages[locale] ?? {},
15
+ timeZone,
16
+ onError: onError ?? ((err) => console.error("[FormatJS]", err)),
17
+ onWarn: onWarn ?? ((warning) => console.warn("[FormatJS]", warning))
18
+ },
19
+ cache
20
+ );
21
+ }
22
+ const adapter = {
23
+ name: "formatjs",
24
+ get locale() {
25
+ return currentLocale;
26
+ },
27
+ get locales() {
28
+ return locales;
29
+ },
30
+ async init() {
31
+ try {
32
+ const formatjsModule = await import("@formatjs/intl");
33
+ createIntl = formatjsModule.createIntl;
34
+ const createIntlCache = formatjsModule.createIntlCache;
35
+ cache = createIntlCache();
36
+ intl = createIntlInstance(currentLocale);
37
+ } catch (error) {
38
+ throw new Error(
39
+ "@flightdev/i18n [formatjs]: @formatjs/intl not installed. Run: npm install @formatjs/intl"
40
+ );
41
+ }
42
+ },
43
+ async setLocale(locale) {
44
+ if (!locales.includes(locale)) {
45
+ throw new Error(
46
+ `@flightdev/i18n [formatjs]: Unsupported locale "${locale}". Supported: ${locales.join(", ")}`
47
+ );
48
+ }
49
+ currentLocale = locale;
50
+ intl = createIntlInstance(locale);
51
+ },
52
+ t(key, options) {
53
+ if (!intl) {
54
+ return options?.defaultValue ?? key;
55
+ }
56
+ try {
57
+ const { count, context: _context, defaultValue, ...values } = options ?? {};
58
+ if (count !== void 0) {
59
+ values.count = count;
60
+ }
61
+ return intl.formatMessage(
62
+ { id: key, defaultMessage: defaultValue },
63
+ values
64
+ );
65
+ } catch (error) {
66
+ return options?.defaultValue ?? key;
67
+ }
68
+ },
69
+ exists(key) {
70
+ return key in (messages[currentLocale] ?? {});
71
+ },
72
+ async loadNamespace(_namespace) {
73
+ }
74
+ };
75
+ return adapter;
76
+ }
77
+ var formatjs_default = formatjs;
78
+ export {
79
+ formatjs_default as default,
80
+ formatjs
81
+ };
@@ -0,0 +1,15 @@
1
+ import { I18nAdapterFactory, Locale } from '../index.js';
2
+
3
+ /**
4
+ * i18next Adapter for @flightdev/i18n
5
+ */
6
+
7
+ interface I18nextConfig {
8
+ locales: Locale[];
9
+ defaultLocale: Locale;
10
+ resources?: Record<Locale, Record<string, Record<string, string>>>;
11
+ loadPath?: string;
12
+ }
13
+ declare const i18next: I18nAdapterFactory<I18nextConfig>;
14
+
15
+ export { type I18nextConfig, i18next as default, i18next };
@@ -0,0 +1,51 @@
1
+ // src/adapters/i18next.ts
2
+ var i18next = (config) => {
3
+ const { locales, defaultLocale, resources } = config;
4
+ let currentLocale = defaultLocale;
5
+ let i18nInstance = null;
6
+ const adapter = {
7
+ name: "i18next",
8
+ get locale() {
9
+ return currentLocale;
10
+ },
11
+ get locales() {
12
+ return locales;
13
+ },
14
+ async init() {
15
+ try {
16
+ const i18nextModule = await import("i18next");
17
+ await i18nextModule.default.init({
18
+ lng: currentLocale,
19
+ fallbackLng: defaultLocale,
20
+ resources,
21
+ interpolation: { escapeValue: false }
22
+ });
23
+ i18nInstance = i18nextModule.default;
24
+ } catch {
25
+ throw new Error("@flightdev/i18n: i18next not installed. Run: npm install i18next");
26
+ }
27
+ },
28
+ async setLocale(locale) {
29
+ if (!locales.includes(locale)) {
30
+ throw new Error(`Unsupported locale: ${locale}`);
31
+ }
32
+ currentLocale = locale;
33
+ await i18nInstance?.changeLanguage(locale);
34
+ },
35
+ t(key, options) {
36
+ if (!i18nInstance) return options?.defaultValue ?? key;
37
+ return i18nInstance.t(key, options);
38
+ },
39
+ exists(key) {
40
+ return i18nInstance?.exists(key) ?? false;
41
+ },
42
+ async loadNamespace(_namespace) {
43
+ }
44
+ };
45
+ return adapter;
46
+ };
47
+ var i18next_default = i18next;
48
+ export {
49
+ i18next_default as default,
50
+ i18next
51
+ };
@@ -0,0 +1,82 @@
1
+ import { Locale, I18nAdapter } from '../index.js';
2
+
3
+ /**
4
+ * Lingui Adapter for @flightdev/i18n
5
+ *
6
+ * Lingui provides:
7
+ * - Macro-based message extraction (`t`, `Trans`)
8
+ * - Compile-time optimization (3kb runtime)
9
+ * - ICU MessageFormat compatibility
10
+ * - First-class React support with hooks
11
+ * - Automatic extraction of translatable strings
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { createI18n } from '@flightdev/i18n';
16
+ * import { lingui } from '@flightdev/i18n/lingui';
17
+ * import { messages as enMessages } from './locales/en/messages.js';
18
+ * import { messages as esMessages } from './locales/es/messages.js';
19
+ *
20
+ * const i18n = createI18n(lingui({
21
+ * locales: ['en', 'es'],
22
+ * defaultLocale: 'en',
23
+ * catalogs: {
24
+ * en: enMessages,
25
+ * es: esMessages,
26
+ * },
27
+ * }));
28
+ *
29
+ * await i18n.init();
30
+ * console.log(i18n.t('Hello, {name}!', { name: 'World' })); // "Hello, World!"
31
+ * ```
32
+ *
33
+ * @see https://lingui.dev/
34
+ */
35
+
36
+ interface LinguiCatalog {
37
+ [messageId: string]: string | string[];
38
+ }
39
+ interface LinguiConfig {
40
+ /** Supported locales */
41
+ locales: Locale[];
42
+ /** Default locale */
43
+ defaultLocale: Locale;
44
+ /** Message catalogs for each locale (generated by Lingui CLI) */
45
+ catalogs: Record<Locale, LinguiCatalog>;
46
+ /** Pre-configured Lingui i18n instance (optional) */
47
+ instance?: LinguiI18n;
48
+ }
49
+ /** Lingui i18n interface (subset of full API) */
50
+ interface LinguiI18n {
51
+ load: (catalogs: Record<string, LinguiCatalog>) => void;
52
+ loadAndActivate: (options: {
53
+ locale: string;
54
+ messages: LinguiCatalog;
55
+ }) => void;
56
+ activate: (locale: string) => void;
57
+ _: (id: string | {
58
+ id: string;
59
+ message?: string;
60
+ }, values?: Record<string, unknown>, options?: {
61
+ message?: string;
62
+ }) => string;
63
+ t: (id: string | {
64
+ id: string;
65
+ message?: string;
66
+ }, values?: Record<string, unknown>) => string;
67
+ locale: string;
68
+ }
69
+ /**
70
+ * Creates a Lingui adapter for Flight's i18n service.
71
+ *
72
+ * Lingui is ideal when you need:
73
+ * 1. Macro-based extraction (no manual key management)
74
+ * 2. Smallest possible runtime (~3kb)
75
+ * 3. ICU MessageFormat with React integration
76
+ *
77
+ * @param config - Lingui configuration
78
+ * @returns I18nAdapter instance
79
+ */
80
+ declare function lingui(config: LinguiConfig): I18nAdapter;
81
+
82
+ export { type LinguiCatalog, type LinguiConfig, lingui as default, lingui };
@@ -0,0 +1,73 @@
1
+ // src/adapters/lingui.ts
2
+ function lingui(config) {
3
+ const { locales, defaultLocale, catalogs } = config;
4
+ let currentLocale = defaultLocale;
5
+ let i18nInstance = null;
6
+ const adapter = {
7
+ name: "lingui",
8
+ get locale() {
9
+ return currentLocale;
10
+ },
11
+ get locales() {
12
+ return locales;
13
+ },
14
+ async init() {
15
+ try {
16
+ if (config.instance) {
17
+ i18nInstance = config.instance;
18
+ } else {
19
+ const linguiCore = await import("@lingui/core");
20
+ i18nInstance = linguiCore.i18n;
21
+ }
22
+ const catalogsToLoad = {};
23
+ for (const [locale, messages] of Object.entries(catalogs)) {
24
+ catalogsToLoad[locale] = messages;
25
+ }
26
+ i18nInstance.load(catalogsToLoad);
27
+ i18nInstance.activate(currentLocale);
28
+ } catch (error) {
29
+ throw new Error(
30
+ "@flightdev/i18n [lingui]: @lingui/core not installed. Run: npm install @lingui/core"
31
+ );
32
+ }
33
+ },
34
+ async setLocale(locale) {
35
+ if (!locales.includes(locale)) {
36
+ throw new Error(
37
+ `@flightdev/i18n [lingui]: Unsupported locale "${locale}". Supported: ${locales.join(", ")}`
38
+ );
39
+ }
40
+ currentLocale = locale;
41
+ i18nInstance?.activate(locale);
42
+ },
43
+ t(key, options) {
44
+ if (!i18nInstance) {
45
+ return options?.defaultValue ?? key;
46
+ }
47
+ try {
48
+ const { count, context: _context, defaultValue, ...values } = options ?? {};
49
+ if (count !== void 0) {
50
+ values.count = count;
51
+ }
52
+ if (defaultValue) {
53
+ return i18nInstance._({ id: key, message: defaultValue }, values);
54
+ }
55
+ return i18nInstance._(key, values);
56
+ } catch (error) {
57
+ return options?.defaultValue ?? key;
58
+ }
59
+ },
60
+ exists(key) {
61
+ const catalog = catalogs[currentLocale];
62
+ return catalog ? key in catalog : false;
63
+ },
64
+ async loadNamespace(_namespace) {
65
+ }
66
+ };
67
+ return adapter;
68
+ }
69
+ var lingui_default = lingui;
70
+ export {
71
+ lingui_default as default,
72
+ lingui
73
+ };
@@ -0,0 +1,65 @@
1
+ import { Locale, I18nAdapter } from '../index.js';
2
+
3
+ /**
4
+ * Paraglide JS Adapter for @flightdev/i18n
5
+ *
6
+ * Paraglide is a compiler-based i18n library that:
7
+ * - Generates type-safe message functions at build time
8
+ * - Produces tree-shakable output (only used messages in bundle)
9
+ * - Provides full TypeScript autocompletion
10
+ * - Zero runtime overhead for unused translations
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createI18n } from '@flightdev/i18n';
15
+ * import { paraglide } from '@flightdev/i18n/paraglide';
16
+ * import * as m from './paraglide/messages.js';
17
+ * import { getLocale, setLocale } from './paraglide/runtime.js';
18
+ *
19
+ * const i18n = createI18n(paraglide({
20
+ * messages: m,
21
+ * runtime: { getLocale, setLocale },
22
+ * locales: ['en', 'es', 'fr'],
23
+ * defaultLocale: 'en',
24
+ * }));
25
+ *
26
+ * await i18n.init();
27
+ * console.log(i18n.t('greeting', { name: 'World' })); // Uses m.greeting({ name: 'World' })
28
+ * ```
29
+ *
30
+ * @see https://inlang.com/m/gerre34r/library-inlang-paraglideJs
31
+ */
32
+
33
+ interface ParaglideMessageFunctions {
34
+ [key: string]: (params?: Record<string, unknown>) => string;
35
+ }
36
+ interface ParaglideRuntime {
37
+ getLocale: () => Locale;
38
+ setLocale: (locale: Locale, options?: {
39
+ reload?: boolean;
40
+ }) => void;
41
+ }
42
+ interface ParaglideConfig {
43
+ /** Message functions generated by Paraglide compiler */
44
+ messages: ParaglideMessageFunctions;
45
+ /** Runtime functions from Paraglide */
46
+ runtime: ParaglideRuntime;
47
+ /** Supported locales */
48
+ locales: Locale[];
49
+ /** Default locale */
50
+ defaultLocale: Locale;
51
+ }
52
+ /**
53
+ * Creates a Paraglide adapter for Flight's i18n service.
54
+ *
55
+ * Paraglide is unique because:
56
+ * 1. Messages are compiled to functions at build time
57
+ * 2. Only imported messages are bundled (tree-shaking)
58
+ * 3. Full type safety for message keys and parameters
59
+ *
60
+ * @param config - Paraglide configuration
61
+ * @returns I18nAdapter instance
62
+ */
63
+ declare function paraglide(config: ParaglideConfig): I18nAdapter;
64
+
65
+ export { type ParaglideConfig, type ParaglideMessageFunctions, type ParaglideRuntime, paraglide as default, paraglide };
@@ -0,0 +1,43 @@
1
+ // src/adapters/paraglide.ts
2
+ function paraglide(config) {
3
+ const { messages, runtime, locales, defaultLocale } = config;
4
+ const adapter = {
5
+ name: "paraglide",
6
+ get locale() {
7
+ return runtime.getLocale();
8
+ },
9
+ get locales() {
10
+ return locales;
11
+ },
12
+ async init() {
13
+ },
14
+ async setLocale(locale) {
15
+ if (!locales.includes(locale)) {
16
+ throw new Error(`@flightdev/i18n [paraglide]: Unsupported locale "${locale}". Supported: ${locales.join(", ")}`);
17
+ }
18
+ runtime.setLocale(locale, { reload: false });
19
+ },
20
+ t(key, options) {
21
+ const messageFn = messages[key];
22
+ if (!messageFn) {
23
+ if (options?.defaultValue !== void 0) {
24
+ return options.defaultValue;
25
+ }
26
+ return key;
27
+ }
28
+ const { count: _count, context: _context, defaultValue: _defaultValue, ...params } = options ?? {};
29
+ return messageFn(params);
30
+ },
31
+ exists(key) {
32
+ return key in messages;
33
+ },
34
+ async loadNamespace(_namespace) {
35
+ }
36
+ };
37
+ return adapter;
38
+ }
39
+ var paraglide_default = paraglide;
40
+ export {
41
+ paraglide_default as default,
42
+ paraglide
43
+ };
@@ -0,0 +1,76 @@
1
+ // src/index.ts
2
+ function createI18n(adapter, _options = {}) {
3
+ return {
4
+ adapter,
5
+ get locale() {
6
+ return adapter.locale;
7
+ },
8
+ get locales() {
9
+ return adapter.locales;
10
+ },
11
+ init: () => adapter.init(),
12
+ setLocale: (locale) => adapter.setLocale(locale),
13
+ t: (key, opts) => adapter.t(key, opts),
14
+ exists: (key) => adapter.exists(key)
15
+ };
16
+ }
17
+ function getLocaleFromRequest(request, supportedLocales, defaultLocale) {
18
+ const acceptLanguage = request.headers.get("accept-language");
19
+ if (!acceptLanguage) return defaultLocale;
20
+ const preferred = acceptLanguage.split(",").map((l) => l.split(";")[0]?.trim().split("-")[0] ?? "");
21
+ return preferred.find((l) => l && supportedLocales.includes(l)) ?? defaultLocale;
22
+ }
23
+ function formatNumber(value, locale, options) {
24
+ return new Intl.NumberFormat(locale, options).format(value);
25
+ }
26
+ function formatDate(date, locale, options) {
27
+ return new Intl.DateTimeFormat(locale, options).format(date);
28
+ }
29
+ function formatRelativeTime(date, locale) {
30
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
31
+ const diff = date.getTime() - Date.now();
32
+ const seconds = Math.floor(diff / 1e3);
33
+ const minutes = Math.floor(seconds / 60);
34
+ const hours = Math.floor(minutes / 60);
35
+ const days = Math.floor(hours / 24);
36
+ if (Math.abs(days) > 0) return rtf.format(days, "day");
37
+ if (Math.abs(hours) > 0) return rtf.format(hours, "hour");
38
+ if (Math.abs(minutes) > 0) return rtf.format(minutes, "minute");
39
+ return rtf.format(seconds, "second");
40
+ }
41
+ function createTypedI18n(adapter, _options = {}) {
42
+ return {
43
+ adapter,
44
+ get locale() {
45
+ return adapter.locale;
46
+ },
47
+ get locales() {
48
+ return adapter.locales;
49
+ },
50
+ init: () => adapter.init(),
51
+ setLocale: (locale) => adapter.setLocale(locale),
52
+ t: (key, opts) => adapter.t(key, opts),
53
+ exists: (key) => adapter.exists(key),
54
+ loadNamespace: (ns) => adapter.loadNamespace(ns)
55
+ };
56
+ }
57
+ function parseNamespacedKey(key) {
58
+ const colonIndex = key.indexOf(":");
59
+ if (colonIndex === -1) {
60
+ return { namespace: null, key };
61
+ }
62
+ return {
63
+ namespace: key.slice(0, colonIndex),
64
+ key: key.slice(colonIndex + 1)
65
+ };
66
+ }
67
+
68
+ export {
69
+ createI18n,
70
+ getLocaleFromRequest,
71
+ formatNumber,
72
+ formatDate,
73
+ formatRelativeTime,
74
+ createTypedI18n,
75
+ parseNamespacedKey
76
+ };
@@ -0,0 +1,150 @@
1
+ // src/routing.ts
2
+ var DEFAULT_CONFIG = {
3
+ routing: "none",
4
+ prefixDefault: false,
5
+ cookieName: "FLIGHT_LOCALE",
6
+ cookieMaxAge: 365 * 24 * 60 * 60,
7
+ // 1 year
8
+ ignorePaths: [
9
+ "/api",
10
+ "/_flight",
11
+ "/_next",
12
+ "/static",
13
+ "/public",
14
+ "/favicon.ico",
15
+ "/robots.txt",
16
+ "/sitemap.xml"
17
+ ]
18
+ };
19
+ function matchLocaleFromPath(pathname, config) {
20
+ const { locales, defaultLocale, prefixDefault = false } = config;
21
+ for (const locale of locales) {
22
+ const prefix = `/${locale}`;
23
+ if (pathname === prefix) {
24
+ return {
25
+ locale,
26
+ pathname: "/",
27
+ isDefault: locale === defaultLocale,
28
+ hasLocalePrefix: true
29
+ };
30
+ }
31
+ if (pathname.startsWith(`${prefix}/`)) {
32
+ return {
33
+ locale,
34
+ pathname: pathname.slice(prefix.length) || "/",
35
+ isDefault: locale === defaultLocale,
36
+ hasLocalePrefix: true
37
+ };
38
+ }
39
+ }
40
+ return {
41
+ locale: defaultLocale,
42
+ pathname,
43
+ isDefault: true,
44
+ hasLocalePrefix: false
45
+ };
46
+ }
47
+ function matchLocaleFromDomain(hostname, config) {
48
+ const { domains, defaultLocale } = config;
49
+ if (!domains) {
50
+ return defaultLocale;
51
+ }
52
+ for (const [locale, domain] of Object.entries(domains)) {
53
+ if (hostname === domain) {
54
+ return locale;
55
+ }
56
+ }
57
+ for (const [locale, domain] of Object.entries(domains)) {
58
+ if (hostname.endsWith(`.${domain}`)) {
59
+ return locale;
60
+ }
61
+ }
62
+ return defaultLocale;
63
+ }
64
+ function matchLocaleFromCookie(cookieHeader, config) {
65
+ if (!cookieHeader) {
66
+ return null;
67
+ }
68
+ const { locales, cookieName = DEFAULT_CONFIG.cookieName } = config;
69
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${cookieName}=([^;]+)`));
70
+ if (match && match[1]) {
71
+ const cookieLocale = match[1].trim();
72
+ if (locales.includes(cookieLocale)) {
73
+ return cookieLocale;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function buildLocalizedUrl(pathname, locale, config) {
79
+ const { defaultLocale, prefixDefault = false, routing = "none", domains } = config;
80
+ switch (routing) {
81
+ case "domain": {
82
+ if (domains && domains[locale]) {
83
+ const domain = domains[locale];
84
+ return `https://${domain}${pathname}`;
85
+ }
86
+ return pathname;
87
+ }
88
+ case "prefix": {
89
+ if (locale === defaultLocale && !prefixDefault) {
90
+ return pathname;
91
+ }
92
+ if (pathname === "/") {
93
+ return `/${locale}`;
94
+ }
95
+ return `/${locale}${pathname}`;
96
+ }
97
+ case "cookie":
98
+ case "none":
99
+ default:
100
+ return pathname;
101
+ }
102
+ }
103
+ function removeLocalePrefix(pathname, config) {
104
+ const match = matchLocaleFromPath(pathname, config);
105
+ return match.pathname;
106
+ }
107
+ function getAlternateUrls(pathname, config) {
108
+ const { locales } = config;
109
+ const alternates = {};
110
+ for (const locale of locales) {
111
+ alternates[locale] = buildLocalizedUrl(pathname, locale, config);
112
+ }
113
+ return alternates;
114
+ }
115
+ function shouldIgnorePath(pathname, config) {
116
+ const { ignorePaths = DEFAULT_CONFIG.ignorePaths } = config;
117
+ for (const ignore of ignorePaths) {
118
+ if (typeof ignore === "string") {
119
+ if (pathname.startsWith(ignore)) {
120
+ return true;
121
+ }
122
+ } else if (ignore instanceof RegExp) {
123
+ if (ignore.test(pathname)) {
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+ const hasExtension = /\.[a-zA-Z0-9]+$/.test(pathname);
129
+ const isHtml = pathname.endsWith(".html");
130
+ if (hasExtension && !isHtml) {
131
+ return true;
132
+ }
133
+ return false;
134
+ }
135
+ function generateLocaleCookie(locale, config) {
136
+ const { cookieName = DEFAULT_CONFIG.cookieName, cookieMaxAge = DEFAULT_CONFIG.cookieMaxAge } = config;
137
+ return `${cookieName}=${locale}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax`;
138
+ }
139
+
140
+ export {
141
+ DEFAULT_CONFIG,
142
+ matchLocaleFromPath,
143
+ matchLocaleFromDomain,
144
+ matchLocaleFromCookie,
145
+ buildLocalizedUrl,
146
+ removeLocalePrefix,
147
+ getAlternateUrls,
148
+ shouldIgnorePath,
149
+ generateLocaleCookie
150
+ };