@cellarnode/i18n 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CellarNode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @cellarnode/i18n
2
+
3
+ Shared i18n configuration for CellarNode frontends — language constants, i18next factory, browser locale detection, and common translations.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @cellarnode/i18n react-i18next i18next i18next-http-backend i18next-browser-languagedetector
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Initialize i18next
14
+
15
+ ```ts
16
+ // src/lib/i18n.ts
17
+ import { createI18nInstance } from "@cellarnode/i18n";
18
+
19
+ const i18n = createI18nInstance({
20
+ defaultNS: "common",
21
+ ns: ["common", "navigation"],
22
+ loadPath: "/locales/{{lng}}/{{ns}}.json",
23
+ });
24
+
25
+ export default i18n;
26
+ ```
27
+
28
+ ### Wrap your app
29
+
30
+ ```tsx
31
+ import { I18nextProvider } from "react-i18next";
32
+ import i18n from "@/lib/i18n";
33
+
34
+ function App() {
35
+ return (
36
+ <I18nextProvider i18n={i18n}>
37
+ <YourApp />
38
+ </I18nextProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Translate strings
44
+
45
+ ```tsx
46
+ import { useTranslation } from "react-i18next";
47
+
48
+ function MyComponent() {
49
+ const { t } = useTranslation("common");
50
+ return <button>{t("save")}</button>;
51
+ }
52
+ ```
53
+
54
+ ### Detect browser language
55
+
56
+ ```ts
57
+ import { resolveLanguageFromBrowser } from "@cellarnode/i18n";
58
+
59
+ // Maps regional variants to supported languages
60
+ resolveLanguageFromBrowser("fr-CA"); // "fr"
61
+ resolveLanguageFromBrowser("zh-Hans-CN"); // "zh"
62
+ resolveLanguageFromBrowser("ja"); // "en" (fallback)
63
+ ```
64
+
65
+ ### Copy common translations (build script)
66
+
67
+ Add to your `package.json` scripts to copy shared `common.json` files into your project:
68
+
69
+ ```json
70
+ {
71
+ "scripts": {
72
+ "predev": "node --input-type=module -e \"import '@cellarnode/i18n/scripts/copy-common'\"",
73
+ "prebuild": "node --input-type=module -e \"import '@cellarnode/i18n/scripts/copy-common'\""
74
+ }
75
+ }
76
+ ```
77
+
78
+ This copies `locales/{lang}/common.json` from the installed package into `public/locales/{lang}/common.json`.
79
+
80
+ ## Exports
81
+
82
+ ### `@cellarnode/i18n`
83
+
84
+ | Export | Description |
85
+ |--------|-------------|
86
+ | `createI18nInstance(options)` | i18next factory with HttpBackend + LanguageDetector |
87
+ | `resolveLanguageFromBrowser(locale)` | Maps browser locale to supported language |
88
+ | `getLanguageDisplayName(lang)` | Returns native display name (e.g., "Svenska") |
89
+ | `SUPPORTED_LANGUAGES` | `['en', 'zh', 'fr', 'de', 'it', 'es', 'sv']` |
90
+ | `DEFAULT_LANGUAGE` | `'en'` |
91
+ | `LANGUAGE_DISPLAY_NAMES` | Map of codes to native names |
92
+ | `SupportedLanguage` | TypeScript union type |
93
+
94
+ ### `@cellarnode/i18n/locales/*`
95
+
96
+ JSON translation files for all 7 languages. Currently includes `common.json`.
97
+
98
+ ### `@cellarnode/i18n/scripts/copy-common`
99
+
100
+ CLI script that copies common locale files from the package into `public/locales/`.
101
+
102
+ ## Supported Languages
103
+
104
+ | Code | Language | Native |
105
+ |------|----------|--------|
106
+ | `en` | English | English |
107
+ | `zh` | Chinese (Simplified) | 中文 |
108
+ | `fr` | French | Francais |
109
+ | `de` | German | Deutsch |
110
+ | `it` | Italian | Italiano |
111
+ | `es` | Spanish | Espanol |
112
+ | `sv` | Swedish | Svenska |
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, LANGUAGE_DISPLAY_NAMES, } from '../constants';
3
+ describe('SUPPORTED_LANGUAGES', () => {
4
+ it('contains exactly 7 languages', () => {
5
+ expect(SUPPORTED_LANGUAGES).toHaveLength(7);
6
+ });
7
+ it('contains all required language codes', () => {
8
+ expect(SUPPORTED_LANGUAGES).toEqual(expect.arrayContaining(['en', 'zh', 'fr', 'de', 'it', 'es', 'sv']));
9
+ });
10
+ it('has en as the first element', () => {
11
+ expect(SUPPORTED_LANGUAGES[0]).toBe('en');
12
+ });
13
+ });
14
+ describe('DEFAULT_LANGUAGE', () => {
15
+ it('is en', () => {
16
+ expect(DEFAULT_LANGUAGE).toBe('en');
17
+ });
18
+ it('is included in SUPPORTED_LANGUAGES', () => {
19
+ expect(SUPPORTED_LANGUAGES).toContain(DEFAULT_LANGUAGE);
20
+ });
21
+ });
22
+ describe('LANGUAGE_DISPLAY_NAMES', () => {
23
+ it('has an entry for every supported language', () => {
24
+ for (const lang of SUPPORTED_LANGUAGES) {
25
+ expect(LANGUAGE_DISPLAY_NAMES[lang]).toBeDefined();
26
+ }
27
+ });
28
+ it('returns native display names', () => {
29
+ expect(LANGUAGE_DISPLAY_NAMES['sv']).toBe('Svenska');
30
+ expect(LANGUAGE_DISPLAY_NAMES['de']).toBe('Deutsch');
31
+ expect(LANGUAGE_DISPLAY_NAMES['zh']).toBe('中文');
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createI18nInstance } from '../create-instance';
3
+ import { SUPPORTED_LANGUAGES } from '../constants';
4
+ describe('createI18nInstance', () => {
5
+ it('returns an i18next instance', () => {
6
+ const i18n = createI18nInstance({ ns: ['common'] });
7
+ expect(i18n).toBeDefined();
8
+ expect(typeof i18n.t).toBe('function');
9
+ });
10
+ it('sets fallback language to en', () => {
11
+ const i18n = createI18nInstance({ ns: ['common'] });
12
+ expect(i18n.options.fallbackLng).toEqual(['en']);
13
+ });
14
+ it('sets supported languages', () => {
15
+ const i18n = createI18nInstance({ ns: ['common'] });
16
+ expect(i18n.options.supportedLngs).toEqual([...SUPPORTED_LANGUAGES, 'cimode']);
17
+ });
18
+ it('respects explicit lng option', () => {
19
+ const i18n = createI18nInstance({ ns: ['common'], lng: 'sv' });
20
+ expect(i18n.options.lng).toBe('sv');
21
+ });
22
+ it('sets default namespace', () => {
23
+ const i18n = createI18nInstance({ defaultNS: 'dashboard', ns: ['common', 'dashboard'] });
24
+ expect(i18n.options.defaultNS).toBe('dashboard');
25
+ });
26
+ it('configures fallback namespace as common', () => {
27
+ const i18n = createI18nInstance({ ns: ['common'] });
28
+ expect(i18n.options.fallbackNS).toEqual(['common']);
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getLanguageDisplayName } from '../display-names';
3
+ describe('getLanguageDisplayName', () => {
4
+ it('returns native name', () => {
5
+ expect(getLanguageDisplayName('sv')).toBe('Svenska');
6
+ expect(getLanguageDisplayName('de')).toBe('Deutsch');
7
+ expect(getLanguageDisplayName('zh')).toBe('中文');
8
+ expect(getLanguageDisplayName('en')).toBe('English');
9
+ });
10
+ it('returns English name for unknown codes', () => {
11
+ expect(getLanguageDisplayName('xx')).toBe('English');
12
+ });
13
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveLanguageFromBrowser } from '../resolve-language';
3
+ describe('resolveLanguageFromBrowser', () => {
4
+ it('returns exact match for supported language', () => {
5
+ expect(resolveLanguageFromBrowser('en')).toBe('en');
6
+ expect(resolveLanguageFromBrowser('sv')).toBe('sv');
7
+ expect(resolveLanguageFromBrowser('fr')).toBe('fr');
8
+ });
9
+ it('maps regional variants to base language', () => {
10
+ expect(resolveLanguageFromBrowser('fr-CA')).toBe('fr');
11
+ expect(resolveLanguageFromBrowser('de-AT')).toBe('de');
12
+ expect(resolveLanguageFromBrowser('de-CH')).toBe('de');
13
+ expect(resolveLanguageFromBrowser('es-MX')).toBe('es');
14
+ expect(resolveLanguageFromBrowser('es-AR')).toBe('es');
15
+ });
16
+ it('maps all Chinese variants to zh', () => {
17
+ expect(resolveLanguageFromBrowser('zh')).toBe('zh');
18
+ expect(resolveLanguageFromBrowser('zh-CN')).toBe('zh');
19
+ expect(resolveLanguageFromBrowser('zh-TW')).toBe('zh');
20
+ expect(resolveLanguageFromBrowser('zh-Hans')).toBe('zh');
21
+ expect(resolveLanguageFromBrowser('zh-Hant')).toBe('zh');
22
+ expect(resolveLanguageFromBrowser('zh-Hans-CN')).toBe('zh');
23
+ });
24
+ it('returns en for unsupported languages', () => {
25
+ expect(resolveLanguageFromBrowser('ja')).toBe('en');
26
+ expect(resolveLanguageFromBrowser('ko')).toBe('en');
27
+ expect(resolveLanguageFromBrowser('ar')).toBe('en');
28
+ expect(resolveLanguageFromBrowser('pt-BR')).toBe('en');
29
+ });
30
+ it('handles empty, null, and undefined input', () => {
31
+ expect(resolveLanguageFromBrowser('')).toBe('en');
32
+ expect(resolveLanguageFromBrowser(null)).toBe('en');
33
+ expect(resolveLanguageFromBrowser(undefined)).toBe('en');
34
+ });
35
+ it('is case-insensitive', () => {
36
+ expect(resolveLanguageFromBrowser('EN')).toBe('en');
37
+ expect(resolveLanguageFromBrowser('Fr-ca')).toBe('fr');
38
+ expect(resolveLanguageFromBrowser('ZH-HANS')).toBe('zh');
39
+ });
40
+ });
@@ -0,0 +1,4 @@
1
+ export declare const SUPPORTED_LANGUAGES: readonly ["en", "zh", "fr", "de", "it", "es", "sv"];
2
+ export declare const DEFAULT_LANGUAGE: "en";
3
+ export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
4
+ export declare const LANGUAGE_DISPLAY_NAMES: Record<SupportedLanguage, string>;
@@ -0,0 +1,13 @@
1
+ export const SUPPORTED_LANGUAGES = [
2
+ 'en', 'zh', 'fr', 'de', 'it', 'es', 'sv',
3
+ ];
4
+ export const DEFAULT_LANGUAGE = 'en';
5
+ export const LANGUAGE_DISPLAY_NAMES = {
6
+ en: 'English',
7
+ zh: '中文',
8
+ fr: 'Français',
9
+ de: 'Deutsch',
10
+ it: 'Italiano',
11
+ es: 'Español',
12
+ sv: 'Svenska',
13
+ };
@@ -0,0 +1,12 @@
1
+ import { type i18n } from 'i18next';
2
+ import type { I18nInstanceOptions } from './types';
3
+ /**
4
+ * Creates and initializes an i18next instance.
5
+ * Each frontend project calls this once in src/lib/i18n.ts.
6
+ *
7
+ * NOTE: init() is called on the instance but translation loading is async.
8
+ * Options (fallbackLng, supportedLngs, etc.) are set immediately.
9
+ * Translations load in the background via i18next-http-backend.
10
+ * react-i18next handles the loading state via its `ready` prop / Suspense.
11
+ */
12
+ export declare function createI18nInstance(options: I18nInstanceOptions): i18n;
@@ -0,0 +1,44 @@
1
+ import i18next from 'i18next';
2
+ import HttpBackend from 'i18next-http-backend';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+ import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from './constants';
5
+ /**
6
+ * Creates and initializes an i18next instance.
7
+ * Each frontend project calls this once in src/lib/i18n.ts.
8
+ *
9
+ * NOTE: init() is called on the instance but translation loading is async.
10
+ * Options (fallbackLng, supportedLngs, etc.) are set immediately.
11
+ * Translations load in the background via i18next-http-backend.
12
+ * react-i18next handles the loading state via its `ready` prop / Suspense.
13
+ */
14
+ export function createI18nInstance(options) {
15
+ const { defaultNS = 'common', ns = ['common'], loadPath = '/locales/{{lng}}/{{ns}}.json', lng, debug = false, } = options;
16
+ const instance = i18next.createInstance();
17
+ instance
18
+ .use(HttpBackend)
19
+ .use(LanguageDetector)
20
+ .init({
21
+ lng,
22
+ fallbackLng: [DEFAULT_LANGUAGE],
23
+ supportedLngs: [...SUPPORTED_LANGUAGES],
24
+ defaultNS,
25
+ ns,
26
+ fallbackNS: 'common',
27
+ debug,
28
+ interpolation: {
29
+ escapeValue: false, // React already escapes
30
+ },
31
+ detection: {
32
+ order: ['localStorage', 'navigator'],
33
+ lookupLocalStorage: 'cellarnode-language',
34
+ caches: ['localStorage'],
35
+ },
36
+ backend: {
37
+ loadPath,
38
+ },
39
+ react: {
40
+ useSuspense: false,
41
+ },
42
+ });
43
+ return instance;
44
+ }
@@ -0,0 +1,6 @@
1
+ import { type SupportedLanguage } from './constants';
2
+ /**
3
+ * Returns the native display name for a language code.
4
+ * e.g., "Svenska" for sv, "Deutsch" for de.
5
+ */
6
+ export declare function getLanguageDisplayName(lang: SupportedLanguage): string;
@@ -0,0 +1,8 @@
1
+ import { LANGUAGE_DISPLAY_NAMES, DEFAULT_LANGUAGE } from './constants';
2
+ /**
3
+ * Returns the native display name for a language code.
4
+ * e.g., "Svenska" for sv, "Deutsch" for de.
5
+ */
6
+ export function getLanguageDisplayName(lang) {
7
+ return LANGUAGE_DISPLAY_NAMES[lang] ?? LANGUAGE_DISPLAY_NAMES[DEFAULT_LANGUAGE];
8
+ }
@@ -0,0 +1,5 @@
1
+ export { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, LANGUAGE_DISPLAY_NAMES, type SupportedLanguage, } from './constants';
2
+ export type { TranslationNamespace, I18nInstanceOptions } from './types';
3
+ export { resolveLanguageFromBrowser } from './resolve-language';
4
+ export { createI18nInstance } from './create-instance';
5
+ export { getLanguageDisplayName } from './display-names';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, LANGUAGE_DISPLAY_NAMES, } from './constants';
2
+ export { resolveLanguageFromBrowser } from './resolve-language';
3
+ export { createI18nInstance } from './create-instance';
4
+ export { getLanguageDisplayName } from './display-names';
@@ -0,0 +1,7 @@
1
+ import { type SupportedLanguage } from './constants';
2
+ /**
3
+ * Maps a browser navigator.language string to a supported CellarNode language.
4
+ * Handles regional variants (fr-CA -> fr), Chinese scripts (zh-Hans -> zh),
5
+ * and unsupported languages (ja -> en).
6
+ */
7
+ export declare function resolveLanguageFromBrowser(navigatorLanguage: string): SupportedLanguage;
@@ -0,0 +1,23 @@
1
+ import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from './constants';
2
+ const supportedSet = new Set(SUPPORTED_LANGUAGES);
3
+ /**
4
+ * Maps a browser navigator.language string to a supported CellarNode language.
5
+ * Handles regional variants (fr-CA -> fr), Chinese scripts (zh-Hans -> zh),
6
+ * and unsupported languages (ja -> en).
7
+ */
8
+ export function resolveLanguageFromBrowser(navigatorLanguage) {
9
+ if (!navigatorLanguage || typeof navigatorLanguage !== 'string') {
10
+ return DEFAULT_LANGUAGE;
11
+ }
12
+ const normalized = navigatorLanguage.toLowerCase().trim();
13
+ // Exact match
14
+ if (supportedSet.has(normalized)) {
15
+ return normalized;
16
+ }
17
+ // Extract base language (first segment before hyphen)
18
+ const base = normalized.split('-')[0];
19
+ if (supportedSet.has(base)) {
20
+ return base;
21
+ }
22
+ return DEFAULT_LANGUAGE;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { cpSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ // Resolve the locales directory relative to the installed package,
5
+ // NOT relative to import.meta.url (which is wrong when run via `node -e`)
6
+ const require = createRequire(import.meta.url);
7
+ const pkgDir = dirname(require.resolve('@cellarnode/i18n/package.json'));
8
+ const localesSource = join(pkgDir, 'locales');
9
+ const targetDir = join(process.cwd(), 'public', 'locales');
10
+ const languages = readdirSync(localesSource).filter((f) => !f.startsWith('.'));
11
+ for (const lang of languages) {
12
+ const src = join(localesSource, lang, 'common.json');
13
+ const dest = join(targetDir, lang, 'common.json');
14
+ if (existsSync(src)) {
15
+ mkdirSync(dirname(dest), { recursive: true });
16
+ cpSync(src, dest);
17
+ console.log(`Copied common.json -> ${lang}`);
18
+ }
19
+ }
20
+ console.log(`Done. Copied ${languages.length} locale files.`);
@@ -0,0 +1,14 @@
1
+ import type { SupportedLanguage } from './constants';
2
+ export type TranslationNamespace = string;
3
+ export interface I18nInstanceOptions {
4
+ /** Default namespace to use */
5
+ defaultNS?: string;
6
+ /** Namespaces to load */
7
+ ns?: string[];
8
+ /** Path template for loading translations. Use {{lng}} and {{ns}} placeholders. */
9
+ loadPath?: string;
10
+ /** Initial language (overrides detection) */
11
+ lng?: SupportedLanguage;
12
+ /** Enable debug logging */
13
+ debug?: boolean;
14
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "save": "Save",
3
+ "cancel": "Cancel",
4
+ "delete": "Delete",
5
+ "edit": "Edit",
6
+ "create": "Create",
7
+ "confirm": "Confirm",
8
+ "close": "Close",
9
+ "back": "Back",
10
+ "next": "Next",
11
+ "submit": "Submit",
12
+ "loading": "Loading...",
13
+ "error": "Error",
14
+ "success": "Success",
15
+ "warning": "Warning",
16
+ "search": "Search",
17
+ "filter": "Filter",
18
+ "sort": "Sort",
19
+ "refresh": "Refresh",
20
+ "retry": "Retry",
21
+ "yes": "Yes",
22
+ "no": "No",
23
+ "ok": "OK",
24
+ "noResults": "No results found",
25
+ "required": "This field is required",
26
+ "invalidEmail": "Please enter a valid email",
27
+ "somethingWentWrong": "Something went wrong",
28
+ "tryAgain": "Please try again",
29
+ "unsavedChanges": "You have unsaved changes",
30
+ "areYouSure": "Are you sure?",
31
+ "copiedToClipboard": "Copied to clipboard",
32
+ "actions": "Actions",
33
+ "status": "Status",
34
+ "name": "Name",
35
+ "description": "Description",
36
+ "date": "Date",
37
+ "type": "Type",
38
+ "viewAll": "View all",
39
+ "showMore": "Show more",
40
+ "showLess": "Show less",
41
+ "selectAll": "Select all",
42
+ "deselectAll": "Deselect all",
43
+ "pagination": {
44
+ "previous": "Previous",
45
+ "next": "Next",
46
+ "page": "Page {{current}} of {{total}}",
47
+ "showing": "Showing {{from}}-{{to}} of {{total}}"
48
+ },
49
+ "validation": {
50
+ "required": "This field is required",
51
+ "minLength": "Must be at least {{min}} characters",
52
+ "maxLength": "Must be at most {{max}} characters",
53
+ "positiveNumber": "Must be a positive number",
54
+ "invalidFormat": "Invalid format"
55
+ }
56
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@cellarnode/i18n",
3
+ "version": "0.1.0",
4
+ "description": "Shared i18n configuration for CellarNode frontends — language constants, i18next factory, browser locale detection, and common translations.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CellarNode/i18n.git"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./locales/*": "./locales/*",
22
+ "./scripts/copy-common": "./dist/scripts/copy-common.js"
23
+ },
24
+ "sideEffects": false,
25
+ "files": [
26
+ "dist",
27
+ "locales"
28
+ ],
29
+ "keywords": [
30
+ "cellarnode",
31
+ "i18n",
32
+ "i18next",
33
+ "internationalization",
34
+ "localization"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --noEmit",
40
+ "prepublishOnly": "pnpm build"
41
+ },
42
+ "dependencies": {
43
+ "i18next": "^24.0.0",
44
+ "i18next-browser-languagedetector": "^8.0.0",
45
+ "i18next-http-backend": "^3.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.5.0",
49
+ "typescript": "^5.8.0",
50
+ "vitest": "^3.0.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=20.0.0"
54
+ }
55
+ }