@next-vibe/checker 1.0.11

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,120 @@
1
+ /**
2
+ * Scoped Translation Factory
3
+ * Creates module-specific translation functions that work only within a defined scope
4
+ */
5
+
6
+ import { type CountryLanguage, defaultLocale, type Languages } from "./config";
7
+ import { getLanguageFromLocale } from "./language-utils";
8
+ import {
9
+ navigateTranslationObject,
10
+ processTranslationValue,
11
+ } from "./shared-translation-utils";
12
+ import type { TParams } from "./static-types";
13
+ import type { DotNotation } from "./static-types";
14
+
15
+ // this value should never be used at runtime
16
+ export type TranslatedKeyType = "createScopedTranslation-key";
17
+
18
+ /**
19
+ * Translation schema type for scoped modules
20
+ * Supports deeply nested structures with recursive type definition
21
+ */
22
+ export interface ScopedTranslationSchema {
23
+ [key: string]: string | ScopedTranslationSchema;
24
+ }
25
+
26
+ /**
27
+ * Creates a scoped translation system for a specific module
28
+ * EN translations are used as the source of truth for type safety
29
+ *
30
+ * @param translationsByLanguage - Object mapping language codes to their translation schemas
31
+ * @returns A simpleT function that works only with the provided translations
32
+ *
33
+ * @example
34
+ * // In src/app/api/[locale]/sms/i18n/index.ts
35
+ * import { createScopedTranslation } from "@/i18n/core/scoped-translation";
36
+ * import { translations as enTranslations } from "./en";
37
+ * import { translations as deTranslations } from "./de";
38
+ * import { translations as plTranslations } from "./pl";
39
+ *
40
+ * export const simpleT = createScopedTranslation({
41
+ * en: enTranslations,
42
+ * de: deTranslations,
43
+ * pl: plTranslations,
44
+ * });
45
+ *
46
+ * // Usage:
47
+ * const { t } = simpleT("en-GLOBAL");
48
+ * t("sms.error.invalid_phone_format"); // Type-safe based on EN schema
49
+ */
50
+ /**
51
+ * Helper type to extract the scoped translation key type from createScopedTranslation return
52
+ */
53
+ export type ExtractScopedTranslationKey<T> = T extends {
54
+ ScopedTranslationKey: infer K;
55
+ }
56
+ ? K
57
+ : never;
58
+
59
+ export function createScopedTranslation<
60
+ const TTranslations extends Record<Languages, ScopedTranslationSchema>,
61
+ >(
62
+ translationsByLanguage: TTranslations,
63
+ ): {
64
+ readonly ScopedTranslationKey: DotNotation<TTranslations["en"]>;
65
+ readonly scopedT: (locale: CountryLanguage) => {
66
+ t: (
67
+ key: DotNotation<TTranslations["en"]>,
68
+ params?: TParams,
69
+ ) => TranslatedKeyType;
70
+ };
71
+ } {
72
+ return {
73
+ ScopedTranslationKey: undefined as DotNotation<TTranslations["en"]>,
74
+
75
+ scopedT: function simpleT(locale: CountryLanguage): {
76
+ t: (
77
+ key: DotNotation<TTranslations["en"]>,
78
+ params?: TParams,
79
+ ) => TranslatedKeyType;
80
+ } {
81
+ return {
82
+ t: (
83
+ key: DotNotation<TTranslations["en"]>,
84
+ params?: TParams,
85
+ ): TranslatedKeyType => {
86
+ // Extract language from locale with safety check
87
+ if (!locale || typeof locale !== "string") {
88
+ return key as TranslatedKeyType; // Return the key as fallback
89
+ }
90
+ if (!key || typeof key !== "string") {
91
+ return key as TranslatedKeyType; // Return the key as fallback
92
+ }
93
+
94
+ const language = locale.split("-")[0] as Languages;
95
+ const defaultLanguage = getLanguageFromLocale(defaultLocale);
96
+
97
+ // Get translations for the requested language
98
+ const languageTranslations = translationsByLanguage[language];
99
+ const fallbackTranslations = translationsByLanguage[defaultLanguage];
100
+
101
+ // Navigate through the translation object using shared logic
102
+ const keys = (key as string).split(".");
103
+ let value = navigateTranslationObject(languageTranslations, keys);
104
+
105
+ // Try fallback language if value not found
106
+ if (value === undefined && language !== defaultLanguage) {
107
+ value = navigateTranslationObject(fallbackTranslations, keys);
108
+ }
109
+
110
+ // Process the translation value using shared logic (handles parameter replacement)
111
+ return processTranslationValue(
112
+ value,
113
+ key,
114
+ params as TParams,
115
+ ) as TranslatedKeyType;
116
+ },
117
+ };
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,30 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import type { CountryLanguage } from "./config";
4
+ import { _simpleT } from "./shared";
5
+ import type { TranslationKey, TranslationValue } from "./static-types";
6
+ import { renderTranslation } from "./translation-utils";
7
+
8
+ interface SimpleTranslationProps<K extends TranslationKey> {
9
+ lang: CountryLanguage;
10
+ i18nKey: K;
11
+ values?: TranslationValue<K> extends string
12
+ ? Record<string, string | number>
13
+ : never;
14
+ }
15
+
16
+ /**
17
+ * Server-side translation component
18
+ * Use this for translations in server components
19
+ */
20
+ export default function SimpleT<K extends TranslationKey>({
21
+ lang,
22
+ i18nKey,
23
+ values,
24
+ }: SimpleTranslationProps<K>): ReactNode {
25
+ // Use simpleT directly which already uses the shared utility
26
+ const translatedValue = _simpleT(lang, i18nKey, values);
27
+
28
+ // Use the shared rendering logic
29
+ return renderTranslation(translatedValue, i18nKey);
30
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared Translation Utilities
3
+ * Core logic for navigating translation objects and processing values
4
+ * Used by both global and scoped translation systems
5
+ */
6
+
7
+ import { translationsKeyMode } from "@/config/debug";
8
+
9
+ import type { TParams } from "./static-types";
10
+
11
+ /**
12
+ * Nested translation value type supporting deep nesting
13
+ */
14
+ export type NestedValue = string | { [key: string]: NestedValue };
15
+
16
+ /**
17
+ * Navigate through a translation object using an array of keys
18
+ * This is the core navigation logic shared between global and scoped translations
19
+ */
20
+ export function navigateTranslationObject(
21
+ startValue: Record<string, NestedValue>,
22
+ keys: string[],
23
+ ): NestedValue | undefined {
24
+ let value: NestedValue | undefined = startValue as NestedValue;
25
+
26
+ for (const k of keys) {
27
+ if (value === undefined || value === null) {
28
+ break;
29
+ }
30
+
31
+ // Handle array access
32
+ if (Array.isArray(value)) {
33
+ const index = Number(k);
34
+ if (!Number.isNaN(index) && index >= 0 && index < value.length) {
35
+ value = value[index] as NestedValue;
36
+ } else {
37
+ value = undefined;
38
+ break;
39
+ }
40
+ }
41
+ // Handle object access
42
+ else if (typeof value === "object" && k in value) {
43
+ value = (value as Record<string, NestedValue>)[k];
44
+ } else {
45
+ value = undefined;
46
+ break;
47
+ }
48
+ }
49
+
50
+ return value;
51
+ }
52
+
53
+ /**
54
+ * Process translation value and handle parameters
55
+ * Shared logic for parameter replacement in translation strings
56
+ */
57
+ export function processTranslationValue<K extends string>(
58
+ value: NestedValue | undefined,
59
+ key: K,
60
+ params?: TParams,
61
+ ): string {
62
+ // If value is undefined, return the key as fallback
63
+ if (value === undefined || value === null) {
64
+ return `${key}${params ? ` (${JSON.stringify(params)})` : ""}`;
65
+ }
66
+
67
+ // If value is a string, process parameters
68
+ if (typeof value === "string") {
69
+ let translationValue: string = value;
70
+ if (params) {
71
+ Object.entries(params).forEach(([paramKey, paramValue]) => {
72
+ translationValue = translationValue.replaceAll(
73
+ new RegExp(`{{${paramKey}}}`, "g"),
74
+ String(paramValue),
75
+ );
76
+ });
77
+ }
78
+
79
+ // Handle translation key mode for debugging
80
+ if (translationsKeyMode) {
81
+ // Return URLs (remote and local) as-is
82
+ if (
83
+ translationValue.startsWith("http://") ||
84
+ translationValue.startsWith("/") ||
85
+ translationValue.startsWith("https://")
86
+ ) {
87
+ return translationValue;
88
+ }
89
+
90
+ return params ? `${key} (${Object.keys(params).join(", ")})` : `${key}`;
91
+ }
92
+ return translationValue;
93
+ }
94
+
95
+ // Handle non-string values - return the key as fallback
96
+ return key;
97
+ }
@@ -0,0 +1,44 @@
1
+ import type { CountryLanguage, Languages } from "./config";
2
+ import type { TFunction, TParams, TranslationKey } from "./static-types";
3
+ import { translateKey } from "./translation-utils";
4
+
5
+ // Server-side translation function
6
+ export function simpleT(locale: CountryLanguage): {
7
+ t: TFunction;
8
+ } {
9
+ return {
10
+ t: <K extends TranslationKey>(key: K, params?: TParams): string => {
11
+ return _simpleT(locale, key, params);
12
+ },
13
+ };
14
+ }
15
+
16
+ // Server-side translation function
17
+ export function _simpleT<K extends TranslationKey>(
18
+ locale: CountryLanguage,
19
+ key: K,
20
+ params?: TParams,
21
+ ): string {
22
+ // Extract language from locale with safety check
23
+ if (!locale || typeof locale !== "string") {
24
+ // oxlint-disable-next-line no-console
25
+ console.error("Invalid locale provided to translation function:", locale);
26
+ return key; // Return the key as fallback
27
+ }
28
+ if (!key || typeof key !== "string") {
29
+ // oxlint-disable-next-line no-console
30
+ console.error("Invalid key provided to translation function:", key);
31
+ return key; // Return the key as fallback
32
+ }
33
+
34
+ const language = locale.split("-")[0] as Languages;
35
+
36
+ // Use the shared translation utility with server context
37
+ return translateKey(
38
+ key,
39
+ language,
40
+ params,
41
+ undefined, // Use default fallback language
42
+ "server",
43
+ );
44
+ }
@@ -0,0 +1,72 @@
1
+ import type { ExplicitObjectType } from "next-vibe/shared/types/utils";
2
+
3
+ import type { translationsKeyTypesafety } from "@/config/debug";
4
+
5
+ import type { TranslationSchema } from "./config";
6
+ import type { TranslatedKeyType } from "./scoped-translation";
7
+
8
+ export interface TranslationElement {
9
+ [key: string]: string | number | string[] | TranslationElement;
10
+ }
11
+
12
+ // Utility type to create dot-notation paths for nested objects
13
+ type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
14
+
15
+ export type DotNotation<T> = (
16
+ T extends ExplicitObjectType
17
+ ? {
18
+ [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNotation<T[K]>>}`;
19
+ }[Exclude<keyof T, symbol>]
20
+ : ""
21
+ ) extends infer D
22
+ ? Extract<D, string>
23
+ : never;
24
+
25
+ // Type for all possible translation keys
26
+ export type TranslationKey = typeof translationsKeyTypesafety extends true
27
+ ? _TranslationKey
28
+ : string;
29
+ export type _TranslationKey =
30
+ | DotNotation<TranslationSchema>
31
+ | TranslatedKeyType;
32
+
33
+ // Utility type to get the type of a value at a specific path
34
+ type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
35
+ ? K extends keyof T
36
+ ? PathValue<T[K], Rest>
37
+ : never
38
+ : P extends keyof T
39
+ ? T[P]
40
+ : never;
41
+
42
+ // Type for getting the value type of a translation key
43
+ export type TranslationValue<K extends TranslationKey> = PathValue<
44
+ TranslationSchema,
45
+ K
46
+ >;
47
+
48
+ export type TParams = Record<string, string | number>;
49
+ export type TFunction = <K extends TranslationKey>(
50
+ key: K,
51
+ params?: TParams,
52
+ ) => string;
53
+
54
+ /**
55
+ * Utility type to extract the scoped key type from a scopedT function or scoped translation object
56
+ *
57
+ * For best results, use with the full createScopedTranslation result which includes ScopedTranslationKey:
58
+ * @example
59
+ * import { scopedTranslation } from "@/app/api/[locale]/contact/i18n";
60
+ * type ContactKeys = ExtractScopedKeyType<typeof scopedTranslation>;
61
+ * // ContactKeys = "title" | "description" | "form.label" | "form.fields.name.label" | ...
62
+ *
63
+ * For scopedT functions, use the string type from the scoped translation object's ScopedTranslationKey:
64
+ * @example
65
+ * type ContactKeys = typeof scopedTranslation.ScopedTranslationKey;
66
+ */
67
+ export type ExtractScopedKeyType<T> =
68
+ // If T has ScopedTranslationKey property (from createScopedTranslation result), extract it
69
+ T extends { ScopedTranslationKey: infer K extends string }
70
+ ? K
71
+ : // Otherwise return never - use the ScopedTranslationKey property directly instead
72
+ never;
@@ -0,0 +1,218 @@
1
+ import { Environment } from "next-vibe/shared/utils";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { envClient } from "@/config/env-client";
5
+
6
+ import { languageConfig } from "..";
7
+ import type { Countries, CountryLanguage, Languages } from "./config";
8
+ import { defaultLocaleConfig, translations } from "./config";
9
+ import type {
10
+ TParams,
11
+ TranslationElement,
12
+ TranslationKey,
13
+ } from "./static-types";
14
+
15
+ // ================================================================================
16
+ // TRANSLATION UTILITIES
17
+ // ================================================================================
18
+
19
+ /**
20
+ * Centralized translation error handling
21
+ * This ensures we only log each error once and in a consistent format
22
+ */
23
+ export function logTranslationError(
24
+ errorType: "missing" | "invalid_type" | "fallback_missing",
25
+ key: string,
26
+ context?: string,
27
+ ): void {
28
+ if (!languageConfig.debug || envClient.NODE_ENV === Environment.PRODUCTION) {
29
+ return;
30
+ }
31
+
32
+ const prefix = context ? `[${context}] ` : "";
33
+
34
+ // Using process.stderr for error logging in development
35
+ // In production, these would be caught by error monitoring
36
+ switch (errorType) {
37
+ case "missing":
38
+ if (typeof process !== "undefined" && process.stderr) {
39
+ process.stderr.write(`${prefix}Translation key not found: ${key}\n`);
40
+ }
41
+ break;
42
+ case "invalid_type":
43
+ if (typeof process !== "undefined" && process.stderr) {
44
+ process.stderr.write(
45
+ `${prefix}Translation key "${key}" has invalid type (expected string)\n`,
46
+ );
47
+ }
48
+ break;
49
+ case "fallback_missing":
50
+ if (typeof process !== "undefined" && process.stderr) {
51
+ process.stderr.write(
52
+ `${prefix}Translation key not found in fallback language: ${key}\n`,
53
+ );
54
+ }
55
+ break;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Navigate through a translation object using an array of keys
61
+ */
62
+ function navigateTranslationPath(
63
+ startValue: TranslationElement,
64
+ keys: string[],
65
+ fullKey: string,
66
+ language: Languages,
67
+ fallbackLanguage: Languages,
68
+ isUsingFallback: boolean,
69
+ context?: string,
70
+ ): TranslationElement | string | undefined {
71
+ // Import shared navigation logic
72
+ const { navigateTranslationObject } = require("./shared-translation-utils");
73
+ const value = navigateTranslationObject(startValue, keys);
74
+
75
+ // Handle error logging for missing keys
76
+ if (
77
+ value === undefined &&
78
+ (isUsingFallback || language === fallbackLanguage)
79
+ ) {
80
+ logTranslationError(
81
+ isUsingFallback ? "fallback_missing" : "missing",
82
+ fullKey,
83
+ context,
84
+ );
85
+ }
86
+
87
+ return value as TranslationElement | string | undefined;
88
+ }
89
+
90
+ /**
91
+ * Try to get translation from a specific language
92
+ */
93
+ function tryGetTranslation<K extends TranslationKey>(
94
+ key: K,
95
+ language: Languages,
96
+ isUsingFallback: boolean,
97
+ fallbackLanguage: Languages,
98
+ context?: string,
99
+ ): TranslationElement | string | undefined {
100
+ const keys = key?.split(".");
101
+ const translationsForLanguage = translations[language];
102
+
103
+ if (!translationsForLanguage) {
104
+ return undefined;
105
+ }
106
+
107
+ return navigateTranslationPath(
108
+ translationsForLanguage,
109
+ keys,
110
+ key,
111
+ language,
112
+ fallbackLanguage,
113
+ isUsingFallback,
114
+ context,
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Get translation value from nested object using dot notation
120
+ */
121
+ export function getTranslationValue<K extends TranslationKey>(
122
+ key: K,
123
+ language: Languages,
124
+ fallbackLanguage: Languages = defaultLocaleConfig.language,
125
+ context?: string,
126
+ ): TranslationElement | string | undefined {
127
+ // Try with the specified language first
128
+ const value = tryGetTranslation(
129
+ key,
130
+ language,
131
+ false,
132
+ fallbackLanguage,
133
+ context,
134
+ );
135
+
136
+ // If translation not found and not already using fallback, try fallback language
137
+ if (value === undefined && language !== fallbackLanguage) {
138
+ return tryGetTranslation(
139
+ key,
140
+ fallbackLanguage,
141
+ true,
142
+ fallbackLanguage,
143
+ context,
144
+ );
145
+ }
146
+
147
+ return value;
148
+ }
149
+
150
+ /**
151
+ * Process translation value and handle parameters
152
+ */
153
+ export function processTranslationValue<K extends TranslationKey>(
154
+ value: TranslationElement | string | undefined,
155
+ key: K,
156
+ params?: TParams,
157
+ context?: string,
158
+ ): string {
159
+ // Import shared processing logic
160
+ const {
161
+ processTranslationValue: sharedProcess,
162
+ } = require("./shared-translation-utils");
163
+ const result = sharedProcess(value, key, params, context);
164
+
165
+ // Log error if value was not a string (only for global translations)
166
+ if (value !== undefined && value !== null && typeof value !== "string") {
167
+ logTranslationError("invalid_type", key, context);
168
+ }
169
+
170
+ return result;
171
+ }
172
+
173
+ /**
174
+ * Main translation function that combines getting and processing the value
175
+ */
176
+ export function translateKey<K extends TranslationKey>(
177
+ key: K,
178
+ language: Languages,
179
+ params?: TParams,
180
+ fallbackLanguage?: Languages,
181
+ context?: string,
182
+ ): string {
183
+ // Use hardcoded fallback to avoid circular dependency during initialization
184
+ const actualFallbackLanguage = fallbackLanguage ?? "en";
185
+ const value = getTranslationValue(
186
+ key,
187
+ language,
188
+ actualFallbackLanguage,
189
+ context,
190
+ );
191
+ return processTranslationValue(value, key, params, context);
192
+ }
193
+
194
+ /**
195
+ * Shared component rendering logic for translation components
196
+ * This can be used by both client and server components
197
+ */
198
+ export function renderTranslation<K extends TranslationKey>(
199
+ translatedValue: string | undefined,
200
+ key: K,
201
+ ): ReactNode {
202
+ // If the translation is empty or not a string, show the key as fallback
203
+ if (!translatedValue) {
204
+ // We don't log here because the error would have already been logged
205
+ // in getTranslationValue or processTranslationValue
206
+ return key;
207
+ }
208
+
209
+ return translatedValue;
210
+ }
211
+
212
+ export function getCountryFromLocale(locale: CountryLanguage): Countries {
213
+ return locale.split("-")[1] as Countries;
214
+ }
215
+
216
+ export function getLanguageFromLocale(locale: CountryLanguage): Languages {
217
+ return locale.split("-")[0] as Languages;
218
+ }
@@ -0,0 +1,8 @@
1
+ import { translations as apiTranslations } from "../../app/i18n/de";
2
+ import { translations as configTranslations } from "../../config/i18n/de";
3
+ import type { translations as enTranslations } from "../en";
4
+
5
+ export const translations: typeof enTranslations = {
6
+ app: apiTranslations,
7
+ config: configTranslations,
8
+ };
@@ -0,0 +1,7 @@
1
+ import { translations as apiTranslations } from "../../app/i18n/en";
2
+ import { translations as configTranslations } from "../../config/i18n/en";
3
+
4
+ export const translations = {
5
+ app: apiTranslations,
6
+ config: configTranslations,
7
+ };
@@ -0,0 +1,100 @@
1
+ import type { LanguageConfig, LanguageDefaults } from "./core/config";
2
+ import { translations as deTranslations } from "./de";
3
+ import { translations as enTranslations } from "./en";
4
+ import { translations as plTranslations } from "./pl";
5
+
6
+ // ----------------
7
+ // CONFIGURATION
8
+ // ----------------
9
+ export const languageDefaults = {
10
+ country: "GLOBAL" as const,
11
+ currency: "USD",
12
+ language: "en" as const,
13
+ translations: enTranslations,
14
+ } satisfies LanguageDefaults<typeof enTranslations>;
15
+
16
+ export const allTranslations = {
17
+ de: deTranslations,
18
+ pl: plTranslations,
19
+ en: enTranslations,
20
+ };
21
+
22
+ export const languageConfig = {
23
+ debug: false as boolean,
24
+ countries: {
25
+ DE: "DE" as const,
26
+ PL: "PL" as const,
27
+ US: "US" as const,
28
+ GLOBAL: "GLOBAL" as const,
29
+ },
30
+ countriesArr: ["DE", "PL", "US", "GLOBAL"] as const,
31
+
32
+ currencies: {
33
+ EUR: "EUR" as const,
34
+ USD: "USD" as const,
35
+ PLN: "PLN" as const,
36
+ },
37
+ currenciesArr: ["EUR", "USD", "PLN"] as const,
38
+
39
+ languages: {
40
+ DE: "de" as const,
41
+ PL: "pl" as const,
42
+ EN: "en" as const,
43
+ },
44
+ languagesArr: ["de", "pl", "en"] as const,
45
+
46
+ mappings: {
47
+ currencyByCountry: {
48
+ DE: "EUR",
49
+ PL: "PLN",
50
+ US: "USD",
51
+ GLOBAL: "USD",
52
+ },
53
+
54
+ languageByCountry: {
55
+ DE: "de",
56
+ PL: "pl",
57
+ US: "en",
58
+ GLOBAL: "en",
59
+ },
60
+ },
61
+
62
+ countryInfo: {
63
+ DE: {
64
+ code: "DE",
65
+ name: "Deutschland",
66
+ language: "de",
67
+ langName: "Deutsch",
68
+ flag: "πŸ‡©πŸ‡ͺ",
69
+ currency: "EUR",
70
+ symbol: "€",
71
+ },
72
+ PL: {
73
+ code: "PL",
74
+ name: "Polska",
75
+ language: "pl",
76
+ langName: "Polski",
77
+ flag: "πŸ‡΅πŸ‡±",
78
+ currency: "PLN",
79
+ symbol: "zΕ‚",
80
+ },
81
+ US: {
82
+ code: "US",
83
+ name: "United States",
84
+ language: "en",
85
+ langName: "English",
86
+ flag: "πŸ‡ΊπŸ‡Έ",
87
+ currency: "USD",
88
+ symbol: "$",
89
+ },
90
+ GLOBAL: {
91
+ code: "GLOBAL",
92
+ name: "Global",
93
+ language: "en",
94
+ langName: "English",
95
+ flag: "🌐",
96
+ currency: "USD",
97
+ symbol: "$",
98
+ },
99
+ },
100
+ } satisfies LanguageConfig;