lokal-react 1.0.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/package.json +42 -0
- package/src/context/LokalContext.tsx +190 -0
- package/src/index.ts +7 -0
- package/src/types/lokal-core.d.ts +127 -0
- package/tsconfig.json +22 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lokal-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React & React Native adapter for LOKAL - Hooks and Context Providers",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"clean": "rm -rf dist",
|
|
18
|
+
"lint": "echo 'Use root lint command'"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"lokal-core": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.10.0",
|
|
25
|
+
"@types/react": "^18.2.0",
|
|
26
|
+
"eslint": "^8.57.0",
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.3.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"react": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
2
|
+
import type { LocaleData } from 'lokal-core';
|
|
3
|
+
|
|
4
|
+
// Type for translation function
|
|
5
|
+
export type TranslateFunction = <K extends string>(
|
|
6
|
+
key: K,
|
|
7
|
+
params?: Record<string, string | number>
|
|
8
|
+
) => string;
|
|
9
|
+
|
|
10
|
+
// Storage interface for different platforms
|
|
11
|
+
export interface StorageInterface {
|
|
12
|
+
getItem(key: string): Promise<string | null>;
|
|
13
|
+
setItem(key: string, value: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Default to localStorage for web
|
|
17
|
+
const defaultStorage: StorageInterface = {
|
|
18
|
+
getItem: (key: string) => {
|
|
19
|
+
if (typeof window === 'undefined') return Promise.resolve(null);
|
|
20
|
+
return Promise.resolve(localStorage.getItem(key));
|
|
21
|
+
},
|
|
22
|
+
setItem: (key: string, value: string) => {
|
|
23
|
+
if (typeof window === 'undefined') return Promise.resolve();
|
|
24
|
+
localStorage.setItem(key, value);
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface LokalContextValue {
|
|
30
|
+
locale: string;
|
|
31
|
+
setLocale: (locale: string) => void;
|
|
32
|
+
locales: string[];
|
|
33
|
+
t: TranslateFunction;
|
|
34
|
+
translations: LocaleData;
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LokalContext = createContext<LokalContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
export interface LokalProviderProps {
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
locale?: string;
|
|
43
|
+
locales?: string[];
|
|
44
|
+
translations?: LocaleData;
|
|
45
|
+
storage?: StorageInterface;
|
|
46
|
+
namespace?: string;
|
|
47
|
+
defaultLocale?: string;
|
|
48
|
+
onLocaleChange?: (locale: string) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface StoredTranslations {
|
|
52
|
+
[locale: string]: LocaleData;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* LokalProvider - Provides localization context to your React app
|
|
57
|
+
*/
|
|
58
|
+
export function LokalProvider({
|
|
59
|
+
children,
|
|
60
|
+
locale: initialLocale = 'en',
|
|
61
|
+
locales = ['en'],
|
|
62
|
+
translations: initialTranslations = {},
|
|
63
|
+
storage = defaultStorage,
|
|
64
|
+
namespace = 'locales',
|
|
65
|
+
defaultLocale = 'en',
|
|
66
|
+
onLocaleChange,
|
|
67
|
+
}: LokalProviderProps) {
|
|
68
|
+
const [locale, setLocaleState] = useState<string>(initialLocale);
|
|
69
|
+
const [translations, setTranslations] = useState<LocaleData>(initialTranslations);
|
|
70
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
71
|
+
|
|
72
|
+
// Load saved locale and translations from storage
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const loadLocale = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const savedLocale = await storage.getItem(`${namespace}-locale`);
|
|
77
|
+
if (savedLocale && locales.includes(savedLocale)) {
|
|
78
|
+
setLocaleState(savedLocale);
|
|
79
|
+
} else {
|
|
80
|
+
// Try to detect from browser
|
|
81
|
+
const browserLocale = navigator.language.split('-')[0];
|
|
82
|
+
if (locales.includes(browserLocale)) {
|
|
83
|
+
setLocaleState(browserLocale);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn('Failed to load locale from storage:', error);
|
|
88
|
+
} finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
loadLocale();
|
|
94
|
+
}, [storage, locales, namespace]);
|
|
95
|
+
|
|
96
|
+
// Load translations for current locale
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const loadTranslations = async () => {
|
|
99
|
+
try {
|
|
100
|
+
const storedData = await storage.getItem(`${namespace}-translations`);
|
|
101
|
+
if (storedData) {
|
|
102
|
+
const parsed: StoredTranslations = JSON.parse(storedData);
|
|
103
|
+
if (parsed[locale]) {
|
|
104
|
+
setTranslations(parsed[locale]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn('Failed to load translations from storage:', error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (!isLoading) {
|
|
113
|
+
loadTranslations();
|
|
114
|
+
}
|
|
115
|
+
}, [locale, storage, namespace, isLoading]);
|
|
116
|
+
|
|
117
|
+
// Set locale and persist
|
|
118
|
+
const setLocale = useCallback((newLocale: string) => {
|
|
119
|
+
if (!locales.includes(newLocale)) {
|
|
120
|
+
console.warn(`Locale ${newLocale} is not in the list of available locales`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setLocaleState(newLocale);
|
|
125
|
+
storage.setItem(`${namespace}-locale`, newLocale);
|
|
126
|
+
|
|
127
|
+
if (onLocaleChange) {
|
|
128
|
+
onLocaleChange(newLocale);
|
|
129
|
+
}
|
|
130
|
+
}, [locales, storage, namespace, onLocaleChange]);
|
|
131
|
+
|
|
132
|
+
// Translation function
|
|
133
|
+
const t = useCallback<TranslateFunction>((key, params) => {
|
|
134
|
+
const keys = key.split('.');
|
|
135
|
+
let value: any = translations;
|
|
136
|
+
|
|
137
|
+
for (const k of keys) {
|
|
138
|
+
if (value && typeof value === 'object' && k in value) {
|
|
139
|
+
value = value[k];
|
|
140
|
+
} else {
|
|
141
|
+
// Return key if not found
|
|
142
|
+
return key;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof value !== 'string') {
|
|
147
|
+
return key;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Replace parameters
|
|
151
|
+
if (params) {
|
|
152
|
+
return Object.entries(params).reduce(
|
|
153
|
+
(str, [paramKey, paramValue]) => str.replace(new RegExp(`{{${paramKey}}}`, 'g'), String(paramValue)),
|
|
154
|
+
value
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return value;
|
|
159
|
+
}, [translations]);
|
|
160
|
+
|
|
161
|
+
const contextValue: LokalContextValue = {
|
|
162
|
+
locale,
|
|
163
|
+
setLocale,
|
|
164
|
+
locales,
|
|
165
|
+
t,
|
|
166
|
+
translations,
|
|
167
|
+
isLoading,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<LokalContext.Provider value={contextValue}>
|
|
172
|
+
{children}
|
|
173
|
+
</LokalContext.Provider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Hook to access the Lokal context
|
|
179
|
+
*/
|
|
180
|
+
export function useLokal(): LokalContextValue {
|
|
181
|
+
const context = useContext(LokalContext);
|
|
182
|
+
|
|
183
|
+
if (!context) {
|
|
184
|
+
throw new Error('useLokal must be used within a LokalProvider');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return context;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default LokalContext;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { LokalProvider, useLokal } from './context/LokalContext';
|
|
3
|
+
export type { LokalProviderProps, TranslateFunction, StorageInterface } from './context/LokalContext';
|
|
4
|
+
export type { LokalContextValue } from './context/LokalContext';
|
|
5
|
+
|
|
6
|
+
// Re-export types from lokal-core
|
|
7
|
+
export type { LocaleData } from 'lokal-core';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
declare module 'lokal-core' {
|
|
2
|
+
export class ASTParser {
|
|
3
|
+
constructor(options: {
|
|
4
|
+
filePath: string;
|
|
5
|
+
functionName?: string;
|
|
6
|
+
componentName?: string;
|
|
7
|
+
});
|
|
8
|
+
scanDirectory(dirPath: string): ScanResult;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractedString {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
location?: {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScanResult {
|
|
22
|
+
strings: ExtractedString[];
|
|
23
|
+
errors: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ScanOptions {
|
|
27
|
+
filePath: string;
|
|
28
|
+
functionName?: string;
|
|
29
|
+
componentName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ConfigLoader {
|
|
33
|
+
load(configPath?: string): Promise<LokalConfig>;
|
|
34
|
+
loadSync(configPath?: string): LokalConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LokalConfig {
|
|
38
|
+
sourceDir: string;
|
|
39
|
+
outputDir: string;
|
|
40
|
+
defaultLocale: string;
|
|
41
|
+
functionName: string;
|
|
42
|
+
componentName: string;
|
|
43
|
+
locales?: string[];
|
|
44
|
+
ai?: {
|
|
45
|
+
provider: 'openai' | 'gemini';
|
|
46
|
+
apiKey: string;
|
|
47
|
+
model?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const defaultConfig: LokalConfig;
|
|
52
|
+
|
|
53
|
+
export class FileStorage {
|
|
54
|
+
constructor(outputDir: string);
|
|
55
|
+
loadLocale(locale: string): LocaleFile | null;
|
|
56
|
+
saveLocale(locale: string, data: LocaleData): void;
|
|
57
|
+
mergeLocaleData(locale: string, newData: Record<string, string>): LocaleData;
|
|
58
|
+
getAvailableLocales(): string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LocaleData {
|
|
62
|
+
[key: string]: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LocaleFile {
|
|
66
|
+
locale: string;
|
|
67
|
+
data: LocaleData;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ContentHasher {
|
|
71
|
+
static hash(content: string): string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class AITranslator {
|
|
75
|
+
constructor(provider: TranslationProvider, options?: {
|
|
76
|
+
batchSize?: number;
|
|
77
|
+
});
|
|
78
|
+
translateBatch(request: TranslationBatch): Promise<TranslationResult[]>;
|
|
79
|
+
translateMissingKeys(
|
|
80
|
+
sourceData: Record<string, string>,
|
|
81
|
+
targetData: Record<string, string>,
|
|
82
|
+
sourceLocale: string,
|
|
83
|
+
targetLocale: string
|
|
84
|
+
): Promise<Record<string, string>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TranslationProvider {
|
|
88
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TranslationRequest {
|
|
92
|
+
text: string;
|
|
93
|
+
sourceLocale: string;
|
|
94
|
+
targetLocale: string;
|
|
95
|
+
context?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TranslationResult {
|
|
99
|
+
translatedText: string;
|
|
100
|
+
success: boolean;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TranslationBatch {
|
|
105
|
+
items: TranslationRequest[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class OpenAIProvider implements TranslationProvider {
|
|
109
|
+
constructor(apiKey: string, options?: {
|
|
110
|
+
model?: string;
|
|
111
|
+
temperature?: number;
|
|
112
|
+
});
|
|
113
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class GeminiProvider implements TranslationProvider {
|
|
117
|
+
constructor(apiKey: string, options?: {
|
|
118
|
+
model?: string;
|
|
119
|
+
temperature?: number;
|
|
120
|
+
});
|
|
121
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class TranslationProviderFactory {
|
|
125
|
+
static create(provider: 'openai' | 'gemini', apiKey: string, model?: string): TranslationProvider;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"target": "ES2020",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"lib": [
|
|
9
|
+
"ES2020",
|
|
10
|
+
"DOM",
|
|
11
|
+
"DOM.Iterable"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
],
|
|
17
|
+
"exclude": [
|
|
18
|
+
"node_modules",
|
|
19
|
+
"dist",
|
|
20
|
+
"**/*.test.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|