@terrafirmagreg-team/translate 0.1.0 → 1.2.1

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 CHANGED
@@ -1,26 +1,34 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/translate",
3
- "version": "0.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
+ "description": "Localization runtime and catalogs",
6
+ "license": "LGPL-3.0-only",
5
7
  "exports": {
6
- ".": "./src/runtime.tsx",
7
- "./provider": "./src/provider.tsx",
8
+ ".": "./src/components/runtime.tsx",
9
+ "./provider": "./src/components/provider.tsx",
8
10
  "./lang": "./src/lang.ts",
9
11
  "./create": "./src/create.ts",
10
12
  "./config": "./src/config.ts",
11
13
  "./server": "./src/server.ts",
12
14
  "./hooks": "./src/hooks/index.ts",
13
- "./hooks/*": "./src/hooks/*.ts",
14
- "./lib/*": "./src/lib/*.ts",
15
+ "./hooks/*": [
16
+ "./src/hooks/*.ts",
17
+ "./src/hooks/*/index.ts"
18
+ ],
19
+ "./lib/*": [
20
+ "./src/lib/*.ts",
21
+ "./src/lib/*/index.ts"
22
+ ],
15
23
  "./types/*": "./src/types/*.ts",
16
- "./package.json": "./package.json",
17
- "./runtime": "./src/runtime.tsx"
24
+ "./runtime": "./src/components/runtime.tsx"
18
25
  },
19
26
  "files": [
20
- "src",
21
- "styles",
22
- "styles.css"
27
+ "src"
23
28
  ],
29
+ "dependencies": {
30
+ "@fuma-translate/react": "1.0.2"
31
+ },
24
32
  "peerDependencies": {
25
33
  "react": ">=19"
26
34
  },
@@ -36,6 +44,5 @@
36
44
  },
37
45
  "publishConfig": {
38
46
  "access": "public"
39
- },
40
- "scripts": {}
47
+ }
41
48
  }
@@ -7,8 +7,8 @@ import {
7
7
  type ReactNode,
8
8
  } from 'react';
9
9
  import { TranslationProvider } from './runtime';
10
- import { LangScopeContext } from './hooks/use-resolved-lang';
11
- import type { LangProviderProps, LocaleItem } from './lang';
10
+ import { LangScopeContext } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
11
+ import type { LangProviderProps, LocaleItem } from '@terrafirmagreg-team/translate/lang';
12
12
 
13
13
  export interface LangContextValue {
14
14
  locale?: string;
@@ -63,3 +63,4 @@ export function LangLocalesScope({
63
63
  );
64
64
  return <LangContext value={value}>{children}</LangContext>;
65
65
  }
66
+
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ export {
3
+ T,
4
+ TranslationProvider,
5
+ fromTranslations,
6
+ useTranslations,
7
+ type TranslationsHook,
8
+ } from '@fuma-translate/react';
9
+
package/src/config.ts CHANGED
@@ -12,3 +12,4 @@ export function registerTranslateConfig(next: TranslateConfig): void {
12
12
  export function translateConfig(): TranslateConfig {
13
13
  return config;
14
14
  }
15
+
package/src/create.ts CHANGED
@@ -6,7 +6,6 @@ import type {
6
6
  import type { LangConfig, LangProviderProps, LocaleItem } from './lang';
7
7
  import { nativeLanguageName } from './lib/code';
8
8
 
9
-
10
9
  export function localeItems(codes: readonly string[]): LocaleItem[] {
11
10
  return codes.map((code) => ({
12
11
  locale: code,
@@ -62,3 +61,4 @@ export function createLocaleSet(registry: LocaleRegistry) {
62
61
  }
63
62
 
64
63
  export type LocaleSet = ReturnType<typeof createLocaleSet>;
64
+
@@ -8,3 +8,4 @@ export {
8
8
  LocaleRegistryContext,
9
9
  useLocaleRegistry,
10
10
  } from './use-locale-registry';
11
+
@@ -9,3 +9,4 @@ export function useLocaleRegistry(): LocaleRegistry {
9
9
  if (!registry) throw new Error('Missing LocaleRegistryContext');
10
10
  return registry;
11
11
  }
12
+
@@ -34,3 +34,4 @@ export function useDefaultLang(): Lang {
34
34
  if (!ctx) throw new Error('Missing ResolvedLangContext');
35
35
  return ctx.defaultLang;
36
36
  }
37
+
@@ -1,12 +1,24 @@
1
1
  'use client';
2
+ import { useMemo } from 'react';
2
3
  import { useTranslations as useFumaTranslations } from '@terrafirmagreg-team/translate/runtime';
3
4
 
4
5
  export function useTranslations<K extends string = string>(options?: {
5
6
  note?: string;
6
7
  }) {
7
8
  const base = useFumaTranslations(options);
8
- return base as (key: K, opts?: {
9
- note?: string;
10
- variables?: Record<string, string>;
11
- }) => string;
9
+ return useMemo(
10
+ () =>
11
+ ((key: K, opts?: {
12
+ note?: string;
13
+ variables?: Record<string, string>;
14
+ }) => {
15
+ const value = base(key, opts);
16
+ return opts?.note && value === key ? base(key) : value;
17
+ }) as (key: K, opts?: {
18
+ note?: string;
19
+ variables?: Record<string, string>;
20
+ }) => string,
21
+ [base],
22
+ );
12
23
  }
24
+
package/src/lang.ts CHANGED
@@ -26,3 +26,4 @@ export interface LangProviderProps
26
26
  locales?: LocaleItem[];
27
27
  children?: ReactNode;
28
28
  }
29
+
package/src/lib/code.ts CHANGED
@@ -48,3 +48,4 @@ export function nativeLanguageName(lang: Lang): string | undefined {
48
48
  return undefined;
49
49
  }
50
50
  }
51
+
@@ -1,8 +1,8 @@
1
1
  import 'server-only';
2
2
  import type { ReactNode } from 'react';
3
3
  import { loadLocaleRegistry } from './registry.server';
4
- import type { Lang } from '../types/locale';
5
- import type { LangContent } from '../types/content';
4
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
5
+ import type { LangContent } from '@terrafirmagreg-team/translate/types/content';
6
6
 
7
7
  type Resolve<P> = (
8
8
  slug: string[] | undefined,
@@ -17,19 +17,21 @@ export function langContent<P>(
17
17
  render: (page: P, lang: Lang) => ReactNode,
18
18
  ): LangContent | null {
19
19
  const { messages, defaultLanguage } = loadLocaleRegistry();
20
- const content: LangContent = {};
20
+ const primary = resolve(slug, defaultLanguage);
21
+ if (primary && !primary.isFallback) {
22
+ return {
23
+ lang: defaultLanguage,
24
+ node: render(primary.page, defaultLanguage),
25
+ };
26
+ }
21
27
  for (const locale of Object.keys(messages)) {
22
28
  const resolved = resolve(slug, locale as Lang);
23
29
  if (!resolved || resolved.isFallback) continue;
24
- content[locale] = { node: render(resolved.page, locale as Lang) };
25
- }
26
- if (!Object.keys(content).length) {
27
- const resolved = resolve(slug, defaultLanguage);
28
- if (resolved) {
29
- content[defaultLanguage] = { node: render(resolved.page, defaultLanguage) };
30
- }
30
+ return { lang: locale as Lang, node: render(resolved.page, locale as Lang) };
31
31
  }
32
- return Object.keys(content).length ? content : null;
32
+ return primary
33
+ ? { lang: defaultLanguage, node: render(primary.page, defaultLanguage) }
34
+ : null;
33
35
  }
34
36
 
35
37
  export function firstLocalizedPage<P>(
@@ -48,3 +50,4 @@ export function firstLocalizedPage<P>(
48
50
  }
49
51
  return undefined;
50
52
  }
53
+
@@ -68,3 +68,4 @@ export function readPageSource(
68
68
  return undefined;
69
69
  }
70
70
  }
71
+
@@ -14,3 +14,4 @@ export function flattenMessages(
14
14
  }
15
15
  return out;
16
16
  }
17
+
@@ -0,0 +1,21 @@
1
+ const NOTE_KEY = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$/;
2
+
3
+ export function buildKeyIndex(
4
+ messages: Partial<Record<string, string>>,
5
+ ): Map<string, string> {
6
+ const index = new Map<string, string>();
7
+ for (const key of Object.keys(messages)) {
8
+ const end = key.lastIndexOf('(');
9
+ if (!key.endsWith(')') || end < 0) continue;
10
+ const note = key.slice(end + 1, -1);
11
+ if (NOTE_KEY.test(note)) index.set(note, key);
12
+ }
13
+ return index;
14
+ }
15
+
16
+ export function stripKeyNote(full: string, key: string): string {
17
+ return full !== key && full.endsWith(`(${key})`)
18
+ ? full.slice(0, -(key.length + 2))
19
+ : full;
20
+ }
21
+
@@ -8,3 +8,17 @@ export function createLocaleCatalog(
8
8
  ]),
9
9
  );
10
10
  }
11
+
12
+ export function createLocaleCatalogs(
13
+ modules: Record<string, unknown>,
14
+ ): Record<string, Record<string, unknown>> {
15
+ const catalogs: Record<string, Record<string, unknown>> = {};
16
+ for (const [path, messages] of Object.entries(modules)) {
17
+ const dir = path.slice(0, path.lastIndexOf('/'));
18
+ const catalog = dir.slice(dir.lastIndexOf('/') + 1);
19
+ const lang = path.slice(path.lastIndexOf('/') + 1, -'.json'.length);
20
+ (catalogs[catalog] ??= {})[lang] = messages;
21
+ }
22
+ return catalogs;
23
+ }
24
+
@@ -1,9 +1,9 @@
1
1
  import 'server-only';
2
- import { createLocaleSet, type LocaleSet } from '../create';
3
- import { translateConfig } from '../config';
2
+ import { createLocaleSet, type LocaleSet } from '@terrafirmagreg-team/translate/create';
3
+ import { translateConfig } from '@terrafirmagreg-team/translate/config';
4
4
  import { flattenMessages } from './flatten';
5
5
  import { sortLangCodes } from './code';
6
- import type { LocaleMessages, LocaleRegistry } from '../types/locale';
6
+ import type { LocaleMessages, LocaleRegistry } from '@terrafirmagreg-team/translate/types/locale';
7
7
 
8
8
  export { sortLangCodes };
9
9
 
@@ -56,3 +56,4 @@ export function loadLocaleSet(): LocaleSet {
56
56
  cachedSet ??= createLocaleSet(loadLocaleRegistry());
57
57
  return cachedSet;
58
58
  }
59
+
package/src/lib/slug.ts CHANGED
@@ -27,3 +27,4 @@ export function toPublicSlug(
27
27
  if (!dataset || !languages.includes(maybeLang)) return slugs;
28
28
  return [dataset, ...rest];
29
29
  }
30
+
package/src/lib/tree.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cloneElement, isValidElement } from 'react';
2
- import type * as PageTree from '../types/tree';
3
- import type { Lang } from '../types/locale';
2
+ import type * as PageTree from '@terrafirmagreg-team/translate/types/tree';
3
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
4
4
  import { isLang } from './code';
5
5
  import { loadLocaleRegistry } from './registry.server';
6
6
  import { stripLangFromUrl } from './slug';
@@ -246,3 +246,4 @@ export function localizePageTree(
246
246
  children: merged.map(keyTreeNode),
247
247
  };
248
248
  }
249
+
package/src/lib/url.ts CHANGED
@@ -89,3 +89,4 @@ export function withLang(href: string, lang: Lang, fallback?: Lang): string {
89
89
  const frag = fragQuery ? `${anchor}&${fragQuery}` : anchor;
90
90
  return `${pathname}${q ? `?${q}` : ''}#${frag}`;
91
91
  }
92
+
package/src/server.ts CHANGED
@@ -4,3 +4,4 @@ export * from './lib/content';
4
4
  export * from './lib/coverage';
5
5
  export * from './lib/registry.server';
6
6
  export * from './lib/tree';
7
+
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { Lang } from './locale';
3
3
 
4
- export type LangContent = Partial<Record<Lang, { node: ReactNode }>>;
4
+ export type LangContent = { lang: Lang; node: ReactNode };
5
+
@@ -7,3 +7,4 @@ export type LocaleRegistry = {
7
7
  defaultLanguage: string;
8
8
  messages: Record<string, LocaleMessages>;
9
9
  };
10
+
package/src/types/tree.ts CHANGED
@@ -53,3 +53,4 @@ export interface Folder extends ID {
53
53
  icon?: ReactNode;
54
54
  children: Node[];
55
55
  }
56
+
package/src/runtime.tsx DELETED
@@ -1,247 +0,0 @@
1
- "use client";
2
- import {
3
- cloneElement,
4
- createContext,
5
- isValidElement,
6
- ReactElement,
7
- use,
8
- useMemo,
9
- type ReactNode,
10
- } from "react";
11
-
12
- const Context = createContext<Partial<Record<string, string>>>({});
13
-
14
-
15
- export function TranslationProvider({
16
- translations,
17
- children,
18
- }: {
19
- translations: Partial<Record<string, string>>;
20
- children: ReactNode;
21
- }) {
22
- const parent = use(Context);
23
- return (
24
- <Context value={useMemo(() => ({ ...parent, ...translations }), [parent, translations])}>
25
- {children}
26
- </Context>
27
- );
28
- }
29
-
30
- type GetVariables<T extends string> = T extends `${infer Before}{${infer K}}${infer After}`
31
- ? (Before extends `${string}\\` ? never : K) | GetVariables<After>
32
- : never;
33
-
34
- type GetTags<T extends string> = T extends `${infer Before}<${infer K}>${infer After}`
35
- ?
36
- | (Before extends `${string}\\`
37
- ? never
38
- : K extends `/${string}`
39
- ? never
40
- : K extends `${infer Tag}/`
41
- ? Tag
42
- : K)
43
- | GetTags<After>
44
- : never;
45
-
46
- interface HookOptions {
47
-
48
- note?: string;
49
- }
50
-
51
- export interface TranslationsHook {
52
- translations: Partial<Record<string, string>>;
53
-
54
- <Text extends string>(
55
- text: Text,
56
- opts?: {
57
-
58
- note?: string;
59
- variables?: Record<GetVariables<Text>, string>;
60
- },
61
- ): string;
62
-
63
- jsx<Text extends string>(
64
- text: Text,
65
- opts?: {
66
-
67
- note?: string;
68
- variables?: Record<GetVariables<Text>, ReactNode>;
69
- tags?: Record<GetTags<Text>, TagRenderer>;
70
- },
71
- ): ReactNode;
72
- }
73
-
74
- const REGEX_VAR = /\\?\{([^}]+)\}/g;
75
-
76
- export function useTranslations({ note }: HookOptions = {}): TranslationsHook {
77
- const translations = use(Context);
78
- return useMemo(() => fromTranslations(translations, { note }), [translations, note]);
79
- }
80
-
81
-
82
- export function fromTranslations(
83
- translations: Partial<Record<string, string>>,
84
- { note: hookNote }: HookOptions = {},
85
- ): TranslationsHook {
86
- const res: TranslationsHook = (rawText, opts = {}) => {
87
- const { note, variables } = opts;
88
- const notes: string[] = [];
89
- if (hookNote) notes.push(hookNote);
90
- if (note) notes.push(note);
91
- const k = encodeKey(rawText, notes);
92
- let text = translations[k] ?? rawText;
93
-
94
- if (variables) {
95
- text = text.replaceAll(REGEX_VAR, (m, name: string) => {
96
- if (m[0] === "\\") return m.slice(1);
97
- if (name in variables) return variables[name as never];
98
- return m;
99
- });
100
- }
101
-
102
- return text;
103
- };
104
-
105
- res.jsx = (rawText, opts = {}) => {
106
- const { note, variables, tags } = opts;
107
- const notes: string[] = [];
108
- if (hookNote) notes.push(hookNote);
109
- if (note) notes.push(note);
110
- const k = encodeKey(rawText, notes);
111
- const text = translations[k] ?? rawText;
112
- if (tags) {
113
- return onJsx(text, variables, tags);
114
- }
115
- if (variables) {
116
- return onJsxVariables(text, variables);
117
- }
118
- return text;
119
- };
120
-
121
- res.translations = translations;
122
- return res;
123
- }
124
-
125
- function encodeKey(text: string, notes: string[]): string {
126
- return text + notes.map((n) => `(${n})`).join("");
127
- }
128
-
129
- function onJsxVariables(text: string, variables: Record<string, ReactNode>): ReactNode {
130
- let idx = 0;
131
- const out: ReactNode[] = [];
132
- for (const match of text.matchAll(REGEX_VAR)) {
133
- const [s, name] = match as unknown as [string, string];
134
- if (idx < match.index) {
135
- out.push(text.slice(idx, match.index));
136
- }
137
- idx = match.index + s.length;
138
-
139
- if (s[0] === "\\") {
140
- out.push(s.slice(1));
141
- } else if (name in variables) {
142
- out.push(enforceElementKey(variables[name], idx));
143
- } else {
144
- out.push(s);
145
- }
146
- }
147
- out.push(text.slice(idx));
148
- return out;
149
- }
150
-
151
- const REGEX_TAG = /\\?<([^>]+)>/g;
152
-
153
- function onJsx(
154
- text: string,
155
- variables: Record<string, ReactNode> | undefined,
156
- tags: Record<string, TagRenderer>,
157
- ) {
158
- const stack: { children: ReactNode[]; tag?: string }[] = [{ children: [] }];
159
- let idx = 0;
160
- const closeTag = () => {
161
- const current = stack.pop()!;
162
-
163
- stack[stack.length - 1]!.children.push(
164
- enforceElementKey(
165
- current.tag && tags[current.tag]
166
- ? renderTag(tags[current.tag]!, current.children)
167
- : current.children,
168
- idx,
169
- ),
170
- );
171
- };
172
-
173
- for (const match of text.matchAll(REGEX_TAG)) {
174
- const current = stack[stack.length - 1]!;
175
- const [s, content] = match as unknown as [string, string];
176
-
177
- if (idx < match.index) {
178
- const str = text.slice(idx, match.index);
179
- current.children.push(variables ? onJsxVariables(str, variables) : str);
180
- }
181
- idx = match.index + s.length;
182
-
183
- if (s[0] === "\\") {
184
- const str = s.slice(1);
185
- current.children.push(variables ? onJsxVariables(str, variables) : str);
186
- } else if (content[0] === "/") {
187
- const name = content.slice(1);
188
-
189
- if (current.tag !== name) {
190
- current.children.push(variables ? onJsxVariables(s, variables) : s);
191
- } else {
192
- closeTag();
193
- }
194
- } else if (content.at(-1) === "/") {
195
- const name = content.slice(0, -1);
196
-
197
- if (tags[name]) {
198
- current.children.push(enforceElementKey(renderTag(tags[name]), idx));
199
- }
200
- } else {
201
- stack.push({ children: [], tag: content });
202
- }
203
- }
204
-
205
- if (idx < text.length) {
206
- const str = text.slice(idx);
207
- stack[stack.length - 1]!.children.push(variables ? onJsxVariables(str, variables) : str);
208
- }
209
-
210
- while (stack.length > 1) closeTag();
211
- return stack[0]!.children;
212
- }
213
-
214
- interface TProps<Text extends string> {
215
- text: Text;
216
-
217
- note?: string;
218
- variables?: Record<GetVariables<Text>, ReactNode>;
219
- tags?: Record<GetTags<Text>, TagRenderer>;
220
- }
221
-
222
- export function T<const Text extends string>({
223
- text,
224
- note,
225
- variables,
226
- tags,
227
- }: TProps<Text>): ReactNode {
228
- const translations = use(Context)!;
229
- return useMemo(
230
- () => fromTranslations(translations).jsx(text, { note, variables, tags }),
231
- [translations, text, note, variables, tags],
232
- );
233
- }
234
-
235
-
236
- type TagRenderer = ReactElement | ((children: ReactNode) => ReactNode);
237
-
238
- function renderTag(renderer: TagRenderer, child?: ReactNode) {
239
- if (isValidElement(renderer))
240
- return child === undefined ? renderer : cloneElement(renderer, undefined, child);
241
- return renderer(child);
242
- }
243
-
244
- function enforceElementKey(value: ReactNode, key: number | string): ReactNode {
245
- if (isValidElement(value)) return cloneElement(value, { key });
246
- return value;
247
- }