wovn-nextjs 0.0.4

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/README.md ADDED
@@ -0,0 +1 @@
1
+ # Wovn Next.js plugin
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('node:path')
5
+ const https = require('node:https')
6
+ const fs = require('node:fs')
7
+ const fsp = fs.promises
8
+ const parse = require('typescript-eslint').parser.parseForESLint
9
+
10
+ const command = process.argv[2]
11
+ const options = process.argv.slice(3)
12
+ const targetRegexp = /\.(?:jsx|tsx)$/
13
+ const ignoreRegexp = /^\..*|node_modules$/
14
+ const buildDir = options[0] || '.'
15
+ const parseOption = {
16
+ ecmaFeatures: {
17
+ jsx: true,
18
+ },
19
+ sourceType: 'module'
20
+ }
21
+
22
+ async function main() {
23
+ const context = {
24
+ uvs: new Map(), // insertion order is expected for wovn-nextjs.test.js
25
+ pathname: '',
26
+ components: [],
27
+ wovnJsData: {},
28
+ projectToken: process.env.WOVN_PROJECT_TOKEN,
29
+ hostname: process.env.WOVN_HOSTNAME
30
+ }
31
+ if (command === 'build' && context.projectToken && context.hostname) {
32
+ context.wovnJsData = await httpsRequest(`https://data.wovn.io/js_data/json/1/${context.projectToken}/?u=http%3A%2F%2F${context.hostname}`).then(body => JSON.parse(body.toString()))
33
+ await overwriteTsxAndJsxFiles(context, undo) // Make sure to collect translatable texts
34
+ await overwriteTsxAndJsxFiles(context, transform).then(() => reportToWovn(context))
35
+ await undoIndexJavaScriptFiles()
36
+ await overwriteInitializationProcess(context)
37
+ await createMiddlewareFile(context)
38
+ if (process.env.WOVN_DEBUG) {
39
+ console.dir(context, {depth: null})
40
+ }
41
+ } else if (command === 'build-config' && context.projectToken && context.hostname) {
42
+ context.wovnJsData = await httpsRequest(`https://data.wovn.io/js_data/json/1/${context.projectToken}/?u=http%3A%2F%2F${context.hostname}`).then(body => JSON.parse(body.toString()))
43
+ await undoIndexJavaScriptFiles()
44
+ await overwriteInitializationProcess(context)
45
+ await createMiddlewareFile(context)
46
+ if (process.env.WOVN_DEBUG) {
47
+ console.dir(context, {depth: null})
48
+ }
49
+ } else if (command === 'undo') {
50
+ await overwriteTsxAndJsxFiles(context, undo)
51
+ await undoIndexJavaScriptFiles()
52
+ await unlinkMiddlewareFileSafely()
53
+ } else {
54
+ console .log(`Usage:
55
+ WOVN_PROJECT_TOKEN=<token> WOVN_HOSTNAME=<host> wovn-nextjs build overwrite jsx and tsx files
56
+ wovn-nextjs undo restore overwritten jsx and tsx files`)
57
+ }
58
+ }
59
+
60
+ if (require.main === module) {
61
+ main()
62
+ } else {
63
+ module.exports = { transform, toReport, undo }
64
+ }
65
+
66
+ // the following are helper functions
67
+
68
+ const singleContentTags = new Set('title'.split(' '))
69
+ const inlineTags = new Set('a abbr b bdi bdo button cite code data dfn em i kbd label legend mark meter q rb rp rt rtc ruby s samp small span strong sub sup time u var'.split(' ')) // option tag as a blcok
70
+ const voidElementTags = new Set('area base br col embed hr img input link meta param source track wbr'.split(' '))
71
+ function transform(context, source) {
72
+ const tokens = parse(source, parseOption).ast.tokens
73
+ const component = {
74
+ pathname: context.pathname,
75
+ uvs: new Set(),
76
+ attributes: []
77
+ }
78
+ let prev = { type: '', value: '' }
79
+ let prev2 = { type: '', value: '' }
80
+ let tag = ''
81
+ let currentLevel = 0
82
+ let startedLevel = 0
83
+ function normalizeText(s) {
84
+ return s.replace(/[\n \t\u0020\u0009\u000C\u200B\u000D\u000A]+/g, ' ').replace(/^[\s\u00A0\uFEFF\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]+|[\s\u00A0\uFEFF\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]+$/g, '')
85
+ }
86
+ function reportAttribute(attr, s) {
87
+ component.attributes.push({tag, attr, value: normalizeText(s)})
88
+ }
89
+
90
+ // Scrape translatable texts from tokens
91
+ const groups = [[]]
92
+ const len = tokens.length
93
+ for (let i=0; i<len; i++) {
94
+ const token = tokens[i]
95
+ const isTagName = token.type === 'JSXIdentifier' || (token.type === 'Keyword' && token.value === 'var')
96
+ if (prev.type === 'Punctuator' && prev.value === '<' && isTagName) {
97
+ tag = token.value
98
+ const headPos = i - 1
99
+ const attrs = {}
100
+ for (i++; i<len; i++) {
101
+ // Scrape attributes in the tag
102
+ const t = tokens[i]
103
+ if (t.type === 'Punctuator' && t.value === '>') {
104
+ break
105
+ } else if (t.type === 'JSXText' && t.value.trim().length && prev.type === 'Punctuator' && prev.value === '=') {
106
+ const value = t.value.slice(1, -1) // Strip leading and trailing double quotes
107
+ if(isTranslatableAttribute(tag, prev2.value, attrs)) {
108
+ if (prev2.value === 'srcset') {
109
+ for (const src of value.split(',')) {
110
+ reportAttribute(prev2.value, src.trim().split(' ')[0])
111
+ }
112
+ } else {
113
+ reportAttribute(prev2.value, value)
114
+ }
115
+ if (value.length) {
116
+ t.wovnCode = `{ wovnInternalT(${ JSON.stringify(value) }) }`
117
+ }
118
+ }
119
+ attrs[prev2.value] = value
120
+ }
121
+ prev2 = prev
122
+ prev = t
123
+ }
124
+ const isSelfClosing = prev.type === 'Punctuator' && prev.value === '/' && tokens[i].type === 'Punctuator' && tokens[i].value === '>'
125
+ currentLevel += isSelfClosing ? 0: 1
126
+ if (inlineTags.has(tag)) {
127
+ startedLevel = startedLevel === 0 ? currentLevel : startedLevel
128
+ groups.at(-1).push({value: `<${tag}>`, headPos})
129
+ } else if (voidElementTags.has(tag)) {
130
+ groups.at(-1).push({value: `<${tag}>`})
131
+ } else {
132
+ if (startedLevel) {
133
+ groups.push([])
134
+ }
135
+ startedLevel = 0
136
+ }
137
+ } else if (prev.type === 'Punctuator' && prev.value === '/' && isTagName) {
138
+ if (startedLevel) {
139
+ groups.at(-1).push({value: `</${token.value}>`, headPos: i - 2, tailPos: i + 1})
140
+ }
141
+ if (startedLevel === currentLevel) {
142
+ groups.push([])
143
+ startedLevel = 0
144
+ }
145
+ currentLevel--
146
+ tag = ''
147
+ } else if (token.type === 'JSXText') {
148
+ if (prev.value === '=') {
149
+ // the `token` could be value of attribute
150
+ continue
151
+ }
152
+ if (startedLevel === 0) {
153
+ if (token.value.trim().length) {
154
+ if (singleContentTags.has(tag)) {
155
+ token.wovnCode = `{ wovnInternalT(${ JSON.stringify(token.value) }) }`
156
+ } else {
157
+ groups.at(-1).push(token)
158
+ groups.push([])
159
+ }
160
+ }
161
+ } else {
162
+ groups.at(-1).push(token)
163
+ }
164
+ }
165
+ prev2 = prev
166
+ prev = token
167
+ }
168
+
169
+ // Set wovnCode for each token to modify the tsx or jsx file
170
+ function internalComponent(major, minor, source) {
171
+ return `<WovnInternalComponent major={${ major }} minor={${ minor }} source={${ JSON.stringify(source) }} />`
172
+ }
173
+
174
+ for (const group of groups.filter(tokens => tokens.some(t => t.type === 'JSXText' && t.value.trim().length))) {
175
+ const normalizedUv = group.map(token => token.value.startsWith('<') ? token.value : normalizeText(token.value)).join('')
176
+ const major = context.uvs.has(normalizedUv) ? context.uvs.get(normalizedUv) : context.uvs.size
177
+ let minor = 0
178
+ context.uvs.set(normalizedUv, major)
179
+ component.uvs.add(normalizedUv)
180
+ let isPrevTag = false
181
+ if ('headPos' in group[0]) {
182
+ tokens[group[0].headPos].wovnCode = '<>{/* wovn-nextjs-command */}' + internalComponent(major, minor++, '') + tokens[group[0].headPos].value
183
+ }
184
+ for (const token of group) {
185
+ if (token.value.startsWith('<')) {
186
+ if (isPrevTag && token.headPos) {
187
+ tokens[token.headPos].wovnCode = internalComponent(major, minor++, '') + tokens[token.headPos].value
188
+ }
189
+ isPrevTag = true
190
+ } else {
191
+ token.wovnCode = internalComponent(major, minor++, token.value)
192
+ isPrevTag = false
193
+ }
194
+ }
195
+ if ('tailPos' in group.at(-1)) {
196
+ tokens[group.at(-1).tailPos].wovnCode = tokens[group.at(-1).tailPos].value + internalComponent(major, minor++, '') + '{/* wovn-nextjs-command */}</>'
197
+ }
198
+ }
199
+
200
+ // Return modified code
201
+ let pos = 0
202
+ let code = ''
203
+ let isModified = false
204
+ for (const token of tokens) {
205
+ isModified ||= !!token.wovnCode
206
+ code += token.wovnCode || source.slice(pos, token.range[1])
207
+ pos = token.range[1]
208
+ }
209
+ code += source.match(/\s*$/)[0] // Keep white spaces at tail
210
+ if (isModified) {
211
+ const importer = 'import { WovnInternalComponent, wovnInternalT } from "wovn-nextjs" // wovn-nextjs-command-appended\n'
212
+ code = code.replaceAll(/^(import Link from ["'`]next\/link.*)/mg, 'import { Link } from "wovn-nextjs" //$1 wovn-nextjs-command-comment')
213
+ code = code.replaceAll(/^(import { *useRouter *} from ["'`]next\/router.*)/mg, 'import { usePageRouter as useRouter } from "wovn-nextjs" //$1 wovn-nextjs-command-comment')
214
+ code = code.replaceAll(/^(import { *useRouter *} from ["'`]next\/navigation.*)/mg, 'import { useAppRouter as useRouter } from "wovn-nextjs" //$1 wovn-nextjs-command-comment')
215
+ code = code.match(/^['"]use /) ? code.replace(/\n/, '\n' + importer) : importer + code
216
+ }
217
+ context.components.push(component)
218
+ return code
219
+ }
220
+
221
+ function undo(context, code) {
222
+ code = code.replaceAll(/^import [^\n]* from "wovn-nextjs" \/\/ wovn-nextjs-command-appended\n/mg, '')
223
+ code = code.replaceAll(/^(?:.*?)\/\/(.*?) wovn-nextjs-command-comment$/mg, '$1')
224
+ code = code.replaceAll(/(?:<>\{\/\* wovn-nextjs-command \*\/\})?<WovnInternalComponent major={[0-9.]*} minor={[0-9.]*} source={(".*?")} \/>(?:\{\/\* wovn-nextjs-command \*\/\}<\/>)?/g, (...m) => JSON.parse(m[1]))
225
+ code = code.replaceAll(/=\s*{ wovnInternalT\((".+")\) }/g, (...m) => '="' + JSON.parse(m[1]) + '"') // for attribute
226
+ code = code.replaceAll(/{ wovnInternalT\((".+")\) }/g, (...m) => JSON.parse(m[1])) // for single contet tag like <title>
227
+ return code
228
+ }
229
+
230
+ const appendMarker = '\n// wovn-nextjs-command-appended'
231
+
232
+ async function overwriteInitializationProcess(context) {
233
+ const sourceLanguage = context.wovnJsData.language
234
+ const targetLanguages = context.wovnJsData.convert_langs?.map(({code}) => code).filter(l => l !== sourceLanguage)
235
+ const extraCode = `${appendMarker}
236
+ state.projectToken = ${ JSON.stringify(context.projectToken) }
237
+ state.sources = ${ JSON.stringify([...context.uvs.keys()]) }
238
+ wovnMiddlewareConfig.matcher = ${ JSON.stringify(targetLanguages.map(language => '/' + language + '/:path*')) }
239
+ setTranslations(${ JSON.stringify(context.wovnJsData) })`
240
+ await overwriteIndexJavaScriptFiles(code => code.split(appendMarker)[0] + extraCode)
241
+ }
242
+
243
+ async function undoIndexJavaScriptFiles() {
244
+ await overwriteIndexJavaScriptFiles(code => code.split(appendMarker)[0])
245
+ }
246
+
247
+ async function overwriteIndexJavaScriptFiles(convert) {
248
+ const promises = []
249
+ for (const dir of ['cjs', 'esm']) {
250
+ const filepath = path.join(__dirname, '..', 'dist', dir, 'index.js')
251
+ const source = await fsp.readFile(filepath, 'utf-8')
252
+ const convertedSource = convert(source)
253
+ if (source !== convertedSource) {
254
+ promises.push(fsp.writeFile(filepath, convertedSource))
255
+ }
256
+ }
257
+ return Promise.all(promises)
258
+ }
259
+
260
+ async function overwriteTsxAndJsxFiles(context, convert) {
261
+ async function* targetFiles(dir) {
262
+ const dirents = await fsp.readdir(dir, { withFileTypes: true });
263
+ for (const dirent of dirents) {
264
+ const res = path.resolve(dir, dirent.name);
265
+ if (dirent.isDirectory()) {
266
+ if (!ignoreRegexp.test(dirent.name)) {
267
+ yield* targetFiles(res);
268
+ }
269
+ } else if (targetRegexp.test(dirent.name)) {
270
+ yield res;
271
+ }
272
+ }
273
+ }
274
+ const promises = []
275
+ const pwd = process.env.PWD
276
+ for await (const pathname of targetFiles(buildDir)) {
277
+ context.pathname = pathname.slice(pwd.length + buildDir.length)
278
+ const source = await fsp.readFile(pathname, 'utf-8')
279
+ const convertedSource = convert(context, source)
280
+ if (source !== convertedSource) {
281
+ promises.push(fsp.writeFile(pathname, convertedSource))
282
+ }
283
+ }
284
+ return Promise.all(promises)
285
+ }
286
+
287
+ async function reportToWovn(context) {
288
+ function toBody(context, id, uvs, attributes) {
289
+ const form = []
290
+ for (const [k, v] of Object.entries(toReport(context, id, uvs, attributes))) {
291
+ form.push(`${ encodeURIComponent(k) }=${ encodeURIComponent(typeof v === 'object' ? JSON.stringify(v) : v) }`)
292
+ }
293
+ return form.join('&')
294
+ }
295
+ await httpsRequest(`https://ee.wovn.io/report_values/${ encodeURIComponent(context.projectToken) }`, 'post', toBody(context, context.wovnJsData.id, context.uvs, context.components.flatMap(c => c.attributes)))
296
+ for (const component of context.components) {
297
+ const { id } = await httpsRequest(`https://data.wovn.io/js_data/json/1/${context.projectToken}/?u=http%3A%2F%2F${ encodeURIComponent(context.hostname + '/' + component.pathname ) }`).then(body => JSON.parse(body.toString()))
298
+ await httpsRequest(`https://ee.wovn.io/report_values/${ encodeURIComponent(context.projectToken) }`, 'post', toBody(context, id, component.uvs, component.attributes))
299
+ }
300
+ }
301
+
302
+ function httpsRequest(url, method='get', body='') {
303
+ return new Promise((resolve, reject) => {
304
+ try {
305
+ const req = https.request(url, { method }, res => {
306
+ if (res.statusCode >= 200 && res.statusCode < 300) {
307
+ const chunks = []
308
+ res.on('data', chunk => chunks.push(chunk))
309
+ res.on('end', () => resolve(Buffer.concat(chunks)))
310
+ } else {
311
+ reject(new Error(`${url} returns ${res.statusCode}`))
312
+ }
313
+ })
314
+ req.on('error', reject)
315
+ body.length > 0 && req.write(body)
316
+ req.end()
317
+ } catch (e) {
318
+ reject(e)
319
+ }
320
+ })
321
+ }
322
+
323
+ function toReport(context, pageId, uvs, attributes) {
324
+ return {
325
+ page_id: pageId,
326
+ url: `http://${context.hostname}`,
327
+ no_record_vals: [...uvs.keys()].map(src => ({
328
+ src,
329
+ xpath: '/html/body/text()',
330
+ unified: true,
331
+ exists: true,
332
+ scrape_number: 0
333
+ })).concat(attributes.map(a => ({
334
+ src: a.value,
335
+ xpath: `/html/body/${a.tag}[@${a.attr}]`,
336
+ unified: false,
337
+ exists: true,
338
+ scrape_number: 0
339
+ }))),
340
+ links: [],
341
+ diagnostics: {},
342
+ report_count: 1,
343
+ source: 'custom',
344
+ page_metadata: {}
345
+ }
346
+ }
347
+
348
+ const attributes = {
349
+ option: ['label'],
350
+ a: ['title'],
351
+ optgroup: ['label'],
352
+ img: ['alt', 'srcset', 'src'],
353
+ textarea: ['placeholder'],
354
+ source: ['srcset']
355
+ }
356
+ const inputAttributes = {
357
+ search: ['value', 'placeholder'],
358
+ button: ['value', 'placeholder', 'data-confirm'],
359
+ submit: ['value', 'placeholder', 'data-confirm'],
360
+ image: ['src', 'alt', 'placeholder', 'data-confirm'],
361
+ reset: ['value']
362
+ }
363
+ const metaNameAttributes = new Set([
364
+ 'description',
365
+ 'title',
366
+ 'og:description',
367
+ 'og:title',
368
+ 'twitter:description',
369
+ 'twitter:title'
370
+ ])
371
+ const metaPropertyAttributes = new Set([
372
+ 'og:description',
373
+ 'og:title',
374
+ 'og:site_name',
375
+ 'twitter:description',
376
+ 'twitter:title'
377
+ ])
378
+ function isTranslatableAttribute(tag, attr, attrs) {
379
+ if (tag === 'meta' && (metaNameAttributes.has(attr) || (attr === 'content' && (metaNameAttributes.has(attrs.name) || metaPropertyAttributes.has(attrs.property))))) {
380
+ return true
381
+ }
382
+ const targets = tag === 'input' ? inputAttributes[attrs.type] : attributes[tag]
383
+ return targets && targets.includes(attr)
384
+ }
385
+
386
+ const middlewarePath = path.join(buildDir, 'middleware.js')
387
+ const targetMiddlewarePaths = ['', 'src'].flatMap(dir => ['.js', '.ts'].flatMap(ext => path.join(buildDir, dir, 'middleware' + ext)))
388
+
389
+ async function canMiddlewareDelete() {
390
+ try {
391
+ return (await fsp.readFile(middlewarePath, 'utf-8')).startsWith('// wovn-nextjs-command-appended')
392
+ } catch (e) {
393
+ if (e.message.startsWith('ENOENT:')) {
394
+ return true
395
+ } else {
396
+ throw e
397
+ }
398
+ }
399
+ }
400
+
401
+ async function createMiddlewareFile(context) {
402
+ const found = targetMiddlewarePaths.find(pathname => fs.existsSync(pathname))
403
+ if (found && found !== middlewarePath) {
404
+ return
405
+ }
406
+ if (await canMiddlewareDelete(middlewarePath)) {
407
+ await fsp.writeFile(middlewarePath, `// wovn-nextjs-command-appended
408
+ import { wovnMiddleware as middleware, wovnMiddlewareConfig as config } from 'wovn-nextjs';
409
+ export { middleware, config }
410
+ `)
411
+ }
412
+ }
413
+
414
+ async function unlinkMiddlewareFileSafely() {
415
+ if (await canMiddlewareDelete(middlewarePath)) {
416
+ try {
417
+ await fsp.unlink(middlewarePath)
418
+ } catch {
419
+ // ignore the error
420
+ }
421
+ }
422
+ }
@@ -0,0 +1,62 @@
1
+ import type { UrlObject } from "url";
2
+ import { type MiddlewareConfig, NextRequest, NextResponse } from "next/server";
3
+ import { AppContext, AppProps } from "next/app";
4
+ import * as React from "react";
5
+ import { NextRouter } from "next/router";
6
+ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
7
+ export declare function t(source: string): string;
8
+ export declare function currentLanguage(): string;
9
+ export declare function useTranslation(): {
10
+ t: typeof t;
11
+ i18n: {
12
+ changeLanguage(language: string): void;
13
+ };
14
+ };
15
+ export declare const Link: React.ForwardRefExoticComponent<Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof {
16
+ href: string | UrlObject;
17
+ as?: string | UrlObject;
18
+ replace?: boolean;
19
+ scroll?: boolean;
20
+ shallow?: boolean;
21
+ passHref?: boolean;
22
+ prefetch?: boolean;
23
+ locale?: string | false;
24
+ legacyBehavior?: boolean;
25
+ onMouseEnter?: any;
26
+ onTouchStart?: any;
27
+ onClick?: any;
28
+ }> & {
29
+ href: string | UrlObject;
30
+ as?: string | UrlObject;
31
+ replace?: boolean;
32
+ scroll?: boolean;
33
+ shallow?: boolean;
34
+ passHref?: boolean;
35
+ prefetch?: boolean;
36
+ locale?: string | false;
37
+ legacyBehavior?: boolean;
38
+ onMouseEnter?: any;
39
+ onTouchStart?: any;
40
+ onClick?: any;
41
+ } & {
42
+ children?: React.ReactNode | undefined;
43
+ } & React.RefAttributes<HTMLAnchorElement>>;
44
+ export declare function usePageRouter(): NextRouter;
45
+ export declare function useAppRouter(): AppRouterInstance;
46
+ declare const wovnMiddlewareConfig: MiddlewareConfig & {
47
+ matcher: string[];
48
+ };
49
+ export { wovnMiddlewareConfig };
50
+ export declare function wovnMiddleware(request: NextRequest): NextResponse<unknown>;
51
+ export declare function appWithWovn(TargetComponent: React.FunctionComponent<any> | React.ComponentClass<any, any>): {
52
+ (props: AppProps): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
53
+ getInitialProps(appContext: AppContext): Promise<{
54
+ pageProps: any;
55
+ }>;
56
+ };
57
+ export declare function WovnInternalComponent({ major, minor, source }: {
58
+ major: number;
59
+ minor: number;
60
+ source: string;
61
+ }): string;
62
+ export declare function wovnInternalT(source: string): string;
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.wovnMiddlewareConfig = exports.Link = void 0;
5
+ exports.t = t;
6
+ exports.currentLanguage = currentLanguage;
7
+ exports.useTranslation = useTranslation;
8
+ exports.usePageRouter = usePageRouter;
9
+ exports.useAppRouter = useAppRouter;
10
+ exports.wovnMiddleware = wovnMiddleware;
11
+ exports.appWithWovn = appWithWovn;
12
+ exports.WovnInternalComponent = WovnInternalComponent;
13
+ exports.wovnInternalT = wovnInternalT;
14
+ const server_1 = require("next/server");
15
+ const app_1 = require("next/app");
16
+ const React = require("react");
17
+ const link_1 = require("next/link");
18
+ const server_2 = require("./server");
19
+ const format_url_1 = require("next/dist/shared/lib/router/utils/format-url");
20
+ const router_1 = require("next/router");
21
+ const navigation_1 = require("next/navigation");
22
+ function t(source) {
23
+ return wovnInternalT(source);
24
+ }
25
+ function currentLanguage() {
26
+ if (typeof window === 'undefined') {
27
+ return getLanguageInServer() || state.sourceLanguage;
28
+ }
29
+ else {
30
+ const p = location.pathname;
31
+ return state.languages.find(language => p === `/${language}` || p.startsWith(`/${language}/`)) || state.sourceLanguage;
32
+ }
33
+ }
34
+ function useTranslation() {
35
+ const pageRouter = usePageRouter();
36
+ const appRouter = useAppRouter();
37
+ const router = pageRouter || appRouter;
38
+ return {
39
+ t,
40
+ i18n: { changeLanguage(language) {
41
+ const href = link(undefined, language);
42
+ log('changeLanguage', language, href);
43
+ console.log(pageRouter, appRouter);
44
+ router.push(href);
45
+ }
46
+ }
47
+ };
48
+ }
49
+ function link(href, locale) {
50
+ const newPathname = linkImpl(href, locale);
51
+ log('link', locale, href, ' -> ', newPathname);
52
+ return newPathname;
53
+ }
54
+ function linkImpl(href, locale) {
55
+ const language = currentLanguage();
56
+ const pathnameWithoutLanguage = typeof window === 'undefined' ? getPathWithoutLanguage() : language === state.sourceLanguage ? location.pathname : location.pathname.slice(language.length + 1);
57
+ let targetHref = '';
58
+ if (typeof href === 'string') {
59
+ if (/^https?:\.\./.test(href)) {
60
+ return href;
61
+ }
62
+ if (!locale && state.languages.find(language => href === `/${language}` || href.startsWith(`/${language}/`))) {
63
+ return href;
64
+ }
65
+ targetHref = href;
66
+ }
67
+ else if (typeof href === 'object') {
68
+ targetHref = href.pathname || '';
69
+ }
70
+ else {
71
+ targetHref = pathnameWithoutLanguage;
72
+ }
73
+ const absPathnameWithoutLanguage = new URL(targetHref, 'http://wovn.io' + pathnameWithoutLanguage).pathname;
74
+ const targetLanguage = locale || language;
75
+ const newPathname = (targetLanguage === state.sourceLanguage ? '' : '/' + targetLanguage) + absPathnameWithoutLanguage;
76
+ if (typeof href === 'object') {
77
+ return (0, format_url_1.formatUrl)({ ...href, pathname: newPathname });
78
+ }
79
+ else {
80
+ return newPathname;
81
+ }
82
+ }
83
+ exports.Link = React.forwardRef(function LinkComponent(props, ref) {
84
+ fetchJsonDataIfNeeded();
85
+ const newProps = { ...props, ref, locale: undefined, href: link(props.href, props.locale) };
86
+ return React.createElement(link_1.default, newProps, props.children);
87
+ });
88
+ function usePageRouter() {
89
+ const target = (0, router_1.useRouter)();
90
+ return new Proxy(target, {
91
+ get(target, prop, receiver) {
92
+ switch (prop) {
93
+ case 'push':
94
+ return (url, as, options) => target.push(link(url), as ? link(as) : as, options);
95
+ case 'replace':
96
+ return (url, as, options) => target.replace(link(url), as ? link(as) : as, options);
97
+ case 'prefetch':
98
+ return (url, asPath, options) => target.prefetch(link(url), asPath ? link(asPath) : asPath, options);
99
+ default:
100
+ return Reflect.get(target, prop, receiver);
101
+ }
102
+ }
103
+ });
104
+ }
105
+ function useAppRouter() {
106
+ return new Proxy((0, navigation_1.useRouter)(), {
107
+ get(target, prop, receiver) {
108
+ switch (prop) {
109
+ case 'push':
110
+ return (url, options) => target.push(link(url), options);
111
+ case 'replace':
112
+ return (url, options) => target.replace(link(url), options);
113
+ case 'prefetch':
114
+ return (url) => target.prefetch(link(url));
115
+ default:
116
+ return Reflect.get(target, prop, receiver);
117
+ }
118
+ }
119
+ });
120
+ }
121
+ const wovnMiddlewareConfig = { matcher: [] }; // this values will be override by wovn-nextjs build command
122
+ exports.wovnMiddlewareConfig = wovnMiddlewareConfig;
123
+ function wovnMiddleware(request) {
124
+ return rewritePath(state.languages.filter(l => l !== state.sourceLanguage), request);
125
+ }
126
+ function appWithWovn(TargetComponent) {
127
+ function AppWithWovn(props) {
128
+ wovnAsyncLocalStorage.enterWith(props.pageProps.wovn);
129
+ return React.createElement(TargetComponent, props);
130
+ }
131
+ AppWithWovn.getInitialProps = async (appContext) => {
132
+ const appProps = await app_1.default.getInitialProps(appContext);
133
+ const headers = appContext.ctx.req?.headers || {};
134
+ return {
135
+ ...appProps,
136
+ pageProps: {
137
+ ...appProps.pageProps,
138
+ wovn: {
139
+ 'x-wovn-nextjs-language': headers['x-wovn-nextjs-language'] || '',
140
+ 'x-wovn-nextjs-path-without-language': headers['x-wovn-nextjs-path-without-language'] || ''
141
+ }
142
+ }
143
+ };
144
+ };
145
+ return AppWithWovn;
146
+ }
147
+ // the followings are exported but for internal purpose
148
+ function WovnInternalComponent({ major, minor, source }) {
149
+ const text = state.translations[currentLanguage()]?.[major]?.[minor] ?? unescapeHtml(source);
150
+ log("internal component", major, minor, state.translations[currentLanguage()]?.[major]?.[minor], ' <- ', source);
151
+ fetchJsonDataIfNeeded();
152
+ return text;
153
+ }
154
+ function wovnInternalT(source) {
155
+ fetchJsonDataIfNeeded();
156
+ return translate(source);
157
+ }
158
+ // the followings are helper functions
159
+ function unescapeHtml(s) {
160
+ return s.replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#039;/g, "'");
161
+ }
162
+ function fetchJsonDataIfNeeded() {
163
+ if (typeof window === 'undefined') {
164
+ return; // do nothing when calling by server component
165
+ }
166
+ if (!state.projectToken.length) {
167
+ return;
168
+ }
169
+ if (!state.fetched) {
170
+ window.wovnnextjs = { state, t, currentLanguage };
171
+ state.fetched = true;
172
+ fetch(`https://wovn.global.ssl.fastly.net/js_data/json/1/${encodeURIComponent(state.projectToken)}?u=` + encodeURIComponent(`${location.protocol}//${location.hostname}`)).then(res => {
173
+ if (res.status === 200) {
174
+ res.json().then(setTranslations);
175
+ }
176
+ });
177
+ }
178
+ }
179
+ function translate(source) {
180
+ const language = currentLanguage();
181
+ const key = source.replace(/ \/>/g, '>').replace(/ +</g, '<').replace(/> +/g, '>').replace(/ +/g, ' ');
182
+ log('try to translate', language, key, state.rawTranslations?.[key]?.[language]?.[0]?.data);
183
+ if (language === state.sourceLanguage) {
184
+ return source;
185
+ }
186
+ const translation = state.rawTranslations?.[key]?.[language]?.[0]?.data;
187
+ return translation || source;
188
+ }
189
+ const wovnAsyncLocalStorage = typeof window === 'undefined' ? new global.AsyncLocalStorage() : {
190
+ enterWith(_) { },
191
+ getStore() { return null; }
192
+ };
193
+ function get(key) {
194
+ if (typeof window === 'undefined') {
195
+ try {
196
+ const s = wovnAsyncLocalStorage.getStore();
197
+ return s ? s[key] : (0, server_2.headers)().get(key);
198
+ }
199
+ catch (e) {
200
+ return e instanceof Error ? `(${e.message})` : '';
201
+ }
202
+ }
203
+ else {
204
+ throw new Error('This function should not be called in client');
205
+ }
206
+ }
207
+ function getLanguageInServer() { return get('x-wovn-nextjs-language'); }
208
+ function getPathWithoutLanguage() { return get('x-wovn-nextjs-path-without-language'); }
209
+ function rewritePath(targetLanguages, request) {
210
+ if (request.nextUrl.pathname.startsWith('/_next') || request.nextUrl.pathname.startsWith('/favicon')) {
211
+ return server_1.NextResponse.next();
212
+ }
213
+ for (const language of targetLanguages) {
214
+ const url = new URL(request.url);
215
+ if (url.pathname === '/' + language) {
216
+ const headers = new Headers(request.headers);
217
+ headers.set("x-wovn-nextjs-language", language);
218
+ headers.set("x-wovn-nextjs-path-without-language", '/');
219
+ log('rewrite', language, request.nextUrl.pathname);
220
+ return server_1.NextResponse.rewrite(new URL("/" + url.search, url), { headers });
221
+ }
222
+ if (url.pathname.startsWith('/' + language + '/')) {
223
+ const headers = new Headers(request.headers);
224
+ headers.set("x-wovn-nextjs-language", language);
225
+ headers.set("x-wovn-nextjs-path-without-language", url.pathname.slice(language.length + 1));
226
+ log('rewrite', language, request.nextUrl.pathname);
227
+ return server_1.NextResponse.rewrite(new URL(url.pathname.slice(language.length + 1) + url.search, url), { headers });
228
+ }
229
+ }
230
+ log('do not rewrite', request.nextUrl.pathname);
231
+ return server_1.NextResponse.next();
232
+ }
233
+ const state = {
234
+ fetched: false,
235
+ debug: !!process.env.WOVN_DEBUG,
236
+ projectToken: '',
237
+ sourceLanguage: '',
238
+ languages: [],
239
+ sources: [], // [major] == source text
240
+ translations: {}, // [language][major][minor] == translation
241
+ rawTranslations: {} // [source]?.[currentLanguage()]?.[0]?.data == translation
242
+ };
243
+ function setTranslations(root) {
244
+ state.sourceLanguage = root.language;
245
+ state.rawTranslations = root.html_text_vals;
246
+ state.translations = {};
247
+ state.languages = root.convert_langs?.map((lang) => lang.code) || [];
248
+ const sourceLength = state.sources.length;
249
+ for (const language of state.languages) {
250
+ const translations = state.translations[language] = [];
251
+ for (let i = 0; i < sourceLength; i++) {
252
+ const source = state.sources[i];
253
+ const translation = state.rawTranslations[source]?.[language]?.[0]?.data;
254
+ translations.push(translation ? translation.split(/<.*?>/) : []);
255
+ }
256
+ }
257
+ if (typeof window !== 'undefined') {
258
+ window.wovnnextjs = { state, t };
259
+ }
260
+ log('set translation');
261
+ }
262
+ // for debug
263
+ const log = function (...args) {
264
+ if (state.debug) {
265
+ console.log('wovn-nextjs', ...args);
266
+ }
267
+ };
@@ -0,0 +1 @@
1
+ export { headers } from "next/headers";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.headers = void 0;
4
+ // To avoid Next.js errors because index.ts has 'use client' and can not import next/headers
5
+ var headers_1 = require("next/headers");
6
+ Object.defineProperty(exports, "headers", { enumerable: true, get: function () { return headers_1.headers; } });
@@ -0,0 +1,62 @@
1
+ import type { UrlObject } from "url";
2
+ import { type MiddlewareConfig, NextRequest, NextResponse } from "next/server";
3
+ import { AppContext, AppProps } from "next/app";
4
+ import * as React from "react";
5
+ import { NextRouter } from "next/router";
6
+ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
7
+ export declare function t(source: string): string;
8
+ export declare function currentLanguage(): string;
9
+ export declare function useTranslation(): {
10
+ t: typeof t;
11
+ i18n: {
12
+ changeLanguage(language: string): void;
13
+ };
14
+ };
15
+ export declare const Link: React.ForwardRefExoticComponent<Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof {
16
+ href: string | UrlObject;
17
+ as?: string | UrlObject;
18
+ replace?: boolean;
19
+ scroll?: boolean;
20
+ shallow?: boolean;
21
+ passHref?: boolean;
22
+ prefetch?: boolean;
23
+ locale?: string | false;
24
+ legacyBehavior?: boolean;
25
+ onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
26
+ onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>;
27
+ onClick?: React.MouseEventHandler<HTMLAnchorElement>;
28
+ }> & {
29
+ href: string | UrlObject;
30
+ as?: string | UrlObject;
31
+ replace?: boolean;
32
+ scroll?: boolean;
33
+ shallow?: boolean;
34
+ passHref?: boolean;
35
+ prefetch?: boolean;
36
+ locale?: string | false;
37
+ legacyBehavior?: boolean;
38
+ onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
39
+ onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>;
40
+ onClick?: React.MouseEventHandler<HTMLAnchorElement>;
41
+ } & {
42
+ children?: React.ReactNode | undefined;
43
+ } & React.RefAttributes<HTMLAnchorElement>>;
44
+ export declare function usePageRouter(): NextRouter;
45
+ export declare function useAppRouter(): AppRouterInstance;
46
+ declare const wovnMiddlewareConfig: MiddlewareConfig & {
47
+ matcher: string[];
48
+ };
49
+ export { wovnMiddlewareConfig };
50
+ export declare function wovnMiddleware(request: NextRequest): NextResponse<unknown>;
51
+ export declare function appWithWovn(TargetComponent: React.FunctionComponent<any> | React.ComponentClass<any, any>): {
52
+ (props: AppProps): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
53
+ getInitialProps(appContext: AppContext): Promise<{
54
+ pageProps: any;
55
+ }>;
56
+ };
57
+ export declare function WovnInternalComponent({ major, minor, source }: {
58
+ major: number;
59
+ minor: number;
60
+ source: string;
61
+ }): string;
62
+ export declare function wovnInternalT(source: string): string;
@@ -0,0 +1,255 @@
1
+ 'use client';
2
+ import { NextResponse } from "next/server";
3
+ import App from "next/app";
4
+ import * as React from "react";
5
+ import NextLink from "next/link";
6
+ import { headers } from "./server";
7
+ import { formatUrl } from "next/dist/shared/lib/router/utils/format-url";
8
+ import { useRouter as useInternalPageRouter } from "next/router";
9
+ import { useRouter as useInternalAppRouter } from "next/navigation";
10
+ export function t(source) {
11
+ return wovnInternalT(source);
12
+ }
13
+ export function currentLanguage() {
14
+ if (typeof window === 'undefined') {
15
+ return getLanguageInServer() || state.sourceLanguage;
16
+ }
17
+ else {
18
+ const p = location.pathname;
19
+ return state.languages.find(language => p === `/${language}` || p.startsWith(`/${language}/`)) || state.sourceLanguage;
20
+ }
21
+ }
22
+ export function useTranslation() {
23
+ const pageRouter = usePageRouter();
24
+ const appRouter = useAppRouter();
25
+ const router = pageRouter || appRouter;
26
+ return {
27
+ t,
28
+ i18n: { changeLanguage(language) {
29
+ const href = link(undefined, language);
30
+ log('changeLanguage', language, href);
31
+ console.log(pageRouter, appRouter);
32
+ router.push(href);
33
+ }
34
+ }
35
+ };
36
+ }
37
+ function link(href, locale) {
38
+ const newPathname = linkImpl(href, locale);
39
+ log('link', locale, href, ' -> ', newPathname);
40
+ return newPathname;
41
+ }
42
+ function linkImpl(href, locale) {
43
+ const language = currentLanguage();
44
+ const pathnameWithoutLanguage = typeof window === 'undefined' ? getPathWithoutLanguage() : language === state.sourceLanguage ? location.pathname : location.pathname.slice(language.length + 1);
45
+ let targetHref = '';
46
+ if (typeof href === 'string') {
47
+ if (/^https?:\.\./.test(href)) {
48
+ return href;
49
+ }
50
+ if (!locale && state.languages.find(language => href === `/${language}` || href.startsWith(`/${language}/`))) {
51
+ return href;
52
+ }
53
+ targetHref = href;
54
+ }
55
+ else if (typeof href === 'object') {
56
+ targetHref = href.pathname || '';
57
+ }
58
+ else {
59
+ targetHref = pathnameWithoutLanguage;
60
+ }
61
+ const absPathnameWithoutLanguage = new URL(targetHref, 'http://wovn.io' + pathnameWithoutLanguage).pathname;
62
+ const targetLanguage = locale || language;
63
+ const newPathname = (targetLanguage === state.sourceLanguage ? '' : '/' + targetLanguage) + absPathnameWithoutLanguage;
64
+ if (typeof href === 'object') {
65
+ return formatUrl({ ...href, pathname: newPathname });
66
+ }
67
+ else {
68
+ return newPathname;
69
+ }
70
+ }
71
+ export const Link = React.forwardRef(function LinkComponent(props, ref) {
72
+ fetchJsonDataIfNeeded();
73
+ const newProps = { ...props, ref, locale: undefined, href: link(props.href, props.locale) };
74
+ return React.createElement(NextLink, newProps, props.children);
75
+ });
76
+ export function usePageRouter() {
77
+ const target = useInternalPageRouter();
78
+ return new Proxy(target, {
79
+ get(target, prop, receiver) {
80
+ switch (prop) {
81
+ case 'push':
82
+ return (url, as, options) => target.push(link(url), as ? link(as) : as, options);
83
+ case 'replace':
84
+ return (url, as, options) => target.replace(link(url), as ? link(as) : as, options);
85
+ case 'prefetch':
86
+ return (url, asPath, options) => target.prefetch(link(url), asPath ? link(asPath) : asPath, options);
87
+ default:
88
+ return Reflect.get(target, prop, receiver);
89
+ }
90
+ }
91
+ });
92
+ }
93
+ export function useAppRouter() {
94
+ return new Proxy(useInternalAppRouter(), {
95
+ get(target, prop, receiver) {
96
+ switch (prop) {
97
+ case 'push':
98
+ return (url, options) => target.push(link(url), options);
99
+ case 'replace':
100
+ return (url, options) => target.replace(link(url), options);
101
+ case 'prefetch':
102
+ return (url) => target.prefetch(link(url));
103
+ default:
104
+ return Reflect.get(target, prop, receiver);
105
+ }
106
+ }
107
+ });
108
+ }
109
+ const wovnMiddlewareConfig = { matcher: [] }; // this values will be override by wovn-nextjs build command
110
+ export { wovnMiddlewareConfig };
111
+ export function wovnMiddleware(request) {
112
+ return rewritePath(state.languages.filter(l => l !== state.sourceLanguage), request);
113
+ }
114
+ export function appWithWovn(TargetComponent) {
115
+ function AppWithWovn(props) {
116
+ wovnAsyncLocalStorage.enterWith(props.pageProps.wovn);
117
+ return React.createElement(TargetComponent, props);
118
+ }
119
+ AppWithWovn.getInitialProps = async (appContext) => {
120
+ const appProps = await App.getInitialProps(appContext);
121
+ const headers = appContext.ctx.req?.headers || {};
122
+ return {
123
+ ...appProps,
124
+ pageProps: {
125
+ ...appProps.pageProps,
126
+ wovn: {
127
+ 'x-wovn-nextjs-language': headers['x-wovn-nextjs-language'] || '',
128
+ 'x-wovn-nextjs-path-without-language': headers['x-wovn-nextjs-path-without-language'] || ''
129
+ }
130
+ }
131
+ };
132
+ };
133
+ return AppWithWovn;
134
+ }
135
+ // the followings are exported but for internal purpose
136
+ export function WovnInternalComponent({ major, minor, source }) {
137
+ const text = state.translations[currentLanguage()]?.[major]?.[minor] ?? unescapeHtml(source);
138
+ log("internal component", major, minor, state.translations[currentLanguage()]?.[major]?.[minor], ' <- ', source);
139
+ fetchJsonDataIfNeeded();
140
+ return text;
141
+ }
142
+ export function wovnInternalT(source) {
143
+ fetchJsonDataIfNeeded();
144
+ return translate(source);
145
+ }
146
+ // the followings are helper functions
147
+ function unescapeHtml(s) {
148
+ return s.replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#039;/g, "'");
149
+ }
150
+ function fetchJsonDataIfNeeded() {
151
+ if (typeof window === 'undefined') {
152
+ return; // do nothing when calling by server component
153
+ }
154
+ if (!state.projectToken.length) {
155
+ return;
156
+ }
157
+ if (!state.fetched) {
158
+ window.wovnnextjs = { state, t, currentLanguage };
159
+ state.fetched = true;
160
+ fetch(`https://wovn.global.ssl.fastly.net/js_data/json/1/${encodeURIComponent(state.projectToken)}?u=` + encodeURIComponent(`${location.protocol}//${location.hostname}`)).then(res => {
161
+ if (res.status === 200) {
162
+ res.json().then(setTranslations);
163
+ }
164
+ });
165
+ }
166
+ }
167
+ function translate(source) {
168
+ const language = currentLanguage();
169
+ const key = source.replace(/ \/>/g, '>').replace(/ +</g, '<').replace(/> +/g, '>').replace(/ +/g, ' ');
170
+ log('try to translate', language, key, state.rawTranslations?.[key]?.[language]?.[0]?.data);
171
+ if (language === state.sourceLanguage) {
172
+ return source;
173
+ }
174
+ const translation = state.rawTranslations?.[key]?.[language]?.[0]?.data;
175
+ return translation || source;
176
+ }
177
+ const wovnAsyncLocalStorage = typeof window === 'undefined' ? new global.AsyncLocalStorage() : {
178
+ enterWith(_) { },
179
+ getStore() { return null; }
180
+ };
181
+ function get(key) {
182
+ if (typeof window === 'undefined') {
183
+ try {
184
+ const s = wovnAsyncLocalStorage.getStore();
185
+ return s ? s[key] : headers().get(key);
186
+ }
187
+ catch (e) {
188
+ return e instanceof Error ? `(${e.message})` : '';
189
+ }
190
+ }
191
+ else {
192
+ throw new Error('This function should not be called in client');
193
+ }
194
+ }
195
+ function getLanguageInServer() { return get('x-wovn-nextjs-language'); }
196
+ function getPathWithoutLanguage() { return get('x-wovn-nextjs-path-without-language'); }
197
+ function rewritePath(targetLanguages, request) {
198
+ if (request.nextUrl.pathname.startsWith('/_next') || request.nextUrl.pathname.startsWith('/favicon')) {
199
+ return NextResponse.next();
200
+ }
201
+ for (const language of targetLanguages) {
202
+ const url = new URL(request.url);
203
+ if (url.pathname === '/' + language) {
204
+ const headers = new Headers(request.headers);
205
+ headers.set("x-wovn-nextjs-language", language);
206
+ headers.set("x-wovn-nextjs-path-without-language", '/');
207
+ log('rewrite', language, request.nextUrl.pathname);
208
+ return NextResponse.rewrite(new URL("/" + url.search, url), { headers });
209
+ }
210
+ if (url.pathname.startsWith('/' + language + '/')) {
211
+ const headers = new Headers(request.headers);
212
+ headers.set("x-wovn-nextjs-language", language);
213
+ headers.set("x-wovn-nextjs-path-without-language", url.pathname.slice(language.length + 1));
214
+ log('rewrite', language, request.nextUrl.pathname);
215
+ return NextResponse.rewrite(new URL(url.pathname.slice(language.length + 1) + url.search, url), { headers });
216
+ }
217
+ }
218
+ log('do not rewrite', request.nextUrl.pathname);
219
+ return NextResponse.next();
220
+ }
221
+ const state = {
222
+ fetched: false,
223
+ debug: !!process.env.WOVN_DEBUG,
224
+ projectToken: '',
225
+ sourceLanguage: '',
226
+ languages: [],
227
+ sources: [], // [major] == source text
228
+ translations: {}, // [language][major][minor] == translation
229
+ rawTranslations: {} // [source]?.[currentLanguage()]?.[0]?.data == translation
230
+ };
231
+ function setTranslations(root) {
232
+ state.sourceLanguage = root.language;
233
+ state.rawTranslations = root.html_text_vals;
234
+ state.translations = {};
235
+ state.languages = root.convert_langs?.map((lang) => lang.code) || [];
236
+ const sourceLength = state.sources.length;
237
+ for (const language of state.languages) {
238
+ const translations = state.translations[language] = [];
239
+ for (let i = 0; i < sourceLength; i++) {
240
+ const source = state.sources[i];
241
+ const translation = state.rawTranslations[source]?.[language]?.[0]?.data;
242
+ translations.push(translation ? translation.split(/<.*?>/) : []);
243
+ }
244
+ }
245
+ if (typeof window !== 'undefined') {
246
+ window.wovnnextjs = { state, t };
247
+ }
248
+ log('set translation');
249
+ }
250
+ // for debug
251
+ const log = function (...args) {
252
+ if (state.debug) {
253
+ console.log('wovn-nextjs', ...args);
254
+ }
255
+ };
@@ -0,0 +1 @@
1
+ export { headers } from "next/headers";
@@ -0,0 +1,2 @@
1
+ // To avoid Next.js errors because index.ts has 'use client' and can not import next/headers
2
+ export { headers } from "next/headers";
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "wovn-nextjs",
3
+ "version": "0.0.4",
4
+ "description": "Localize the Next.js web application without changing the code",
5
+ "main": "./dist/cjs/index.js",
6
+ "module": "./dist/esm/index.js",
7
+ "types": "./dist/esm/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/esm/index.d.ts",
11
+ "require": "./dist/cjs/index.js",
12
+ "import": "./dist/esm/index.js",
13
+ "default": "./dist/cjs/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "wovn-nextjs": "bin/wovn-nextjs"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test --watch",
21
+ "build": "npx tsc src/index.ts --declaration --target esnext --module esnext --moduleResolution bundler --strict true --skipLibCheck true --outDir dist/esm && npx tsc src/index.ts --declaration --target esnext --module CommonJS --strict true --skipLibCheck true --outDir dist/cjs"
22
+ },
23
+ "files": [
24
+ "./bin/wovn-nextjs",
25
+ "./dist"
26
+ ],
27
+ "directories": {
28
+ "example": "example"
29
+ },
30
+ "author": "",
31
+ "license": "UNLICENSED",
32
+ "volta": {
33
+ "node": "20.16.0"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/dom": "^10.4.0",
37
+ "@testing-library/react": "^16.0.1",
38
+ "@types/react": "^18.3.7",
39
+ "@types/react-dom": "^18.3.0",
40
+ "jsdom": "^25.0.0",
41
+ "tsx": "^4.19.1",
42
+ "typescript": "5.5.4",
43
+ "typescript-eslint": "^8.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@types/node": "^20.0.0",
47
+ "@types/react": "^18.0.0",
48
+ "@types/react-dom": "^18.0.0",
49
+ "next": "^14.0.0",
50
+ "react": "^18.0.0",
51
+ "react-dom": "^18.0.0",
52
+ "typescript-eslint": "^8.0.0"
53
+ },
54
+ "dependencies": {
55
+ "wovn-nextjs": "file:wovn-nextjs-0.0.1.tgz"
56
+ }
57
+ }