@things-factory/i18n-base 8.0.0-beta.1 → 8.0.0-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/i18n-base",
3
- "version": "8.0.0-beta.1",
3
+ "version": "8.0.0-beta.4",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@operato/i18n": "^8.0.0-beta",
31
- "@things-factory/auth-base": "^8.0.0-beta.1",
32
- "@things-factory/shell": "^8.0.0-beta.1"
31
+ "@things-factory/auth-base": "^8.0.0-beta.4",
32
+ "@things-factory/shell": "^8.0.0-beta.4"
33
33
  },
34
- "gitHead": "36c494e587640c1490318ef7b95adab02606e0c2"
34
+ "gitHead": "d83d12ed4ba07177dff1dac26e37be347d156b43"
35
35
  }
@@ -1,16 +0,0 @@
1
- import { PageView } from '@operato/shell'
2
-
3
- declare module '@operato/shell' {
4
- interface PageView {
5
- languageUpdated(i18next: any): void
6
- }
7
- }
8
-
9
- export default function bootstrap() {
10
- /*
11
- * PageView의 lifecycle에 languageUpdate를 추가한다.
12
- */
13
- PageView.prototype.languageUpdated = function (i18next) {
14
- this.pageReset()
15
- }
16
- }
package/client/index.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * @license Copyright © HatioLab Inc. All rights reserved.
3
- */
4
-
5
- export { i18next } from '@operato/i18n/config.js'
6
- export * from '@operato/i18n/localize.js'
@@ -1,408 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
-
3
- import '@operato/data-grist'
4
- import '@operato/context/ox-context-page-toolbar.js'
5
- import '@operato/input/ox-input-select-buttons.js'
6
-
7
- import { css, html } from 'lit'
8
- import { customElement, query, state } from 'lit/decorators.js'
9
- import { connect } from 'pwa-helpers/connect-mixin.js'
10
- import gql from 'graphql-tag'
11
-
12
- import { store, CustomAlert, PageView } from '@operato/shell'
13
- import { client } from '@operato/graphql'
14
- import { DataGrist, FetchOption, GristRecord } from '@operato/data-grist'
15
- import { i18next, localize } from '@operato/i18n'
16
- import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
17
- import { isMobileDevice } from '@operato/utils'
18
- import { getLanguages } from '@things-factory/auth-base/dist-client'
19
-
20
- @customElement('terminology-page')
21
- export class TerminologyPage extends connect(store)(localize(i18next)(PageView)) {
22
- static styles = [
23
- ScrollbarStyles,
24
- CommonHeaderStyles,
25
- css`
26
- :host {
27
- display: flex;
28
- flex-direction: column;
29
-
30
- overflow: hidden;
31
-
32
- --grid-header-padding: 2px 0 2px 9px;
33
- }
34
-
35
- ox-grist {
36
- overflow-y: auto;
37
- flex: 1;
38
- }
39
-
40
- .header {
41
- grid-template-areas: 'filters actions';
42
- }
43
- `
44
- ]
45
-
46
- @state() private gristConfig: any
47
- @state() private mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'CARD' : 'GRID'
48
- @state() private state?: string[]
49
-
50
- @query('ox-grist') private grist!: DataGrist
51
-
52
- get context() {
53
- return {
54
- title: i18next.t('title.terminology'),
55
- search: {
56
- handler: (search: string) => {
57
- this.grist.searchText = search
58
- },
59
- value: this.grist?.searchText || ''
60
- },
61
- filter: {
62
- handler: () => {
63
- this.grist.toggleHeadroom()
64
- }
65
- },
66
- help: 'mfg-warehouse/term-page',
67
- actions: [
68
- {
69
- title: i18next.t('button.clear-cache'),
70
- action: this.clearCache.bind(this),
71
- icon: 'ink_eraser'
72
- },
73
- {
74
- title: i18next.t('button.save'),
75
- action: this.save.bind(this),
76
- icon: 'save'
77
- },
78
- {
79
- title: i18next.t('button.delete'),
80
- action: this.delete.bind(this),
81
- icon: 'delete',
82
- emphasis: {
83
- danger: true
84
- }
85
- }
86
- ],
87
- exportable: {
88
- name: i18next.t('button.export'),
89
- data: this.exportHandler.bind(this)
90
- },
91
- toolbar: false
92
- }
93
- }
94
-
95
- async getFilterStatus() {
96
- return (await getLanguages()).map(({ code, display }) => {
97
- return { value: code, display }
98
- })
99
- }
100
-
101
- render() {
102
- const mode = this.mode || (isMobileDevice() ? 'LIST' : 'GRID')
103
-
104
- return html`
105
- <ox-grist .mode=${mode} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
106
- <div slot="headroom" class="header">
107
- <div class="filters">
108
- <ox-filters-form autofocus without-search></ox-filters-form>
109
- </div>
110
-
111
- <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
112
- </div>
113
- </ox-grist>
114
- `
115
- }
116
-
117
- async pageInitialized(lifecycle) {
118
- this.gristConfig = {
119
- list: {
120
- fields: ['name', 'description'],
121
- details: ['active', 'updatedAt']
122
- },
123
- columns: [
124
- { type: 'gutter', gutterName: 'sequence' },
125
- { type: 'gutter', gutterName: 'row-selector', multiple: true },
126
- {
127
- type: 'string',
128
- name: 'id',
129
- header: i18next.t('field.id'),
130
- hidden: true,
131
- width: 0
132
- },
133
- {
134
- type: 'string',
135
- name: 'name',
136
- header: i18next.t('field.name'),
137
- record: {
138
- editable: true
139
- },
140
- filter: 'search',
141
- sortable: true,
142
- width: 250
143
- },
144
- {
145
- type: 'select',
146
- name: 'locale',
147
- header: i18next.t('field.locale'),
148
- record: {
149
- editable: true,
150
- options: [{ display: '', value: '' }, ...(await this.getFilterStatus())]
151
- },
152
- width: 90,
153
- filter: {
154
- type: 'select-buttons',
155
- operator: 'in'
156
- }
157
- },
158
- {
159
- type: 'select',
160
- name: 'category',
161
- label: true,
162
- header: i18next.t('field.category'),
163
- record: {
164
- editable: true,
165
- options: ['', 'button', 'field', 'label', 'menu', 'text', 'title']
166
- },
167
- width: 90,
168
- filter: 'in',
169
- imex: true
170
- },
171
- {
172
- type: 'string',
173
- name: 'display',
174
- label: true,
175
- header: i18next.t('field.display'),
176
- record: {
177
- editable: true
178
- },
179
- filter: 'search',
180
- sortable: true,
181
- width: 380
182
- },
183
- {
184
- type: 'resource-object',
185
- name: 'updater',
186
- header: i18next.t('field.updater'),
187
- record: {
188
- editable: false
189
- },
190
- sortable: true,
191
- width: 120
192
- },
193
- {
194
- type: 'datetime',
195
- name: 'updatedAt',
196
- header: i18next.t('field.updated_at'),
197
- record: {
198
- editable: false
199
- },
200
- sortable: true,
201
- width: 180
202
- }
203
- ],
204
- rows: {
205
- appendable: true,
206
- selectable: {
207
- multiple: true
208
- }
209
- /*handlers: {
210
- click: (columns, data, column, record, rowIndex) => {
211
- // TODO Implement the desired sub-view(like item-view) and use it in the popup content after importing
212
- const popup = openPopup(html`
213
- <item-view item-id=${record.id} style="background-color: white;">
214
- </item-view>
215
- `, {
216
- backdrop: true,
217
- size: 'large',
218
- title: i18next.t('text.item view')
219
- })
220
- popup.onclosed = () => {
221
- this.grist.fetch()
222
- }
223
- }
224
- }*/
225
- },
226
- sorters: [
227
- {
228
- name: 'name',
229
- desc: true
230
- }
231
- ]
232
- }
233
- }
234
-
235
- async pageUpdated(changes: any, lifecycle: any) {
236
- if (this.active) {
237
- // do something here when this page just became as active
238
- }
239
- }
240
-
241
- async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
242
- const response = await client.query({
243
- query: gql`
244
- query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
245
- terminologies(filters: $filters, pagination: $pagination, sortings: $sortings) {
246
- items {
247
- id
248
- name
249
- locale
250
- category
251
- display
252
- creator {
253
- id
254
- name
255
- }
256
- createdAt
257
- updater {
258
- id
259
- name
260
- }
261
- updatedAt
262
- }
263
-
264
- total
265
- }
266
- }
267
- `,
268
- variables: {
269
- filters,
270
- pagination: { page, limit },
271
- sortings
272
- }
273
- })
274
-
275
- return {
276
- total: response.data.terminologies.total || 0,
277
- records: response.data.terminologies.items || []
278
- }
279
- }
280
-
281
- exportHandler() {
282
- let records = [] as GristRecord
283
- if (this.grist.selected && this.grist.selected.length > 0) {
284
- records = this.grist.selected
285
- } else {
286
- records = this.grist.data.records
287
- }
288
-
289
- var headerSetting = this.grist.compiledConfig.columns
290
- .filter(column => column.type !== 'gutter' && column.record !== undefined && column.imex !== undefined)
291
- .map(column => {
292
- return column.imex === true
293
- ? {
294
- header: column.header.renderer(column),
295
- key: column.name,
296
- width: column.width,
297
- type: column.type
298
- }
299
- : column.imex
300
- })
301
-
302
- var data = records.map(item => {
303
- return {
304
- id: item.id,
305
- ...this.gristConfig.columns
306
- .filter(column => column.type !== 'gutter' && column.record !== undefined && column.imex !== undefined)
307
- .reduce((record, column) => {
308
- const key = column.imex === true ? column.name : column.imex.key
309
- record[key] = key
310
- .split('.')
311
- .reduce((obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), item)
312
- return record
313
- }, {})
314
- }
315
- })
316
-
317
- return { header: headerSetting, data: data }
318
- }
319
-
320
- async delete() {
321
- const selectedRecords = this.grist.selected
322
-
323
- if (!selectedRecords || selectedRecords.length == 0) {
324
- this.showToast(i18next.t('text.NOTHING_SELECTED'))
325
- } else {
326
- const answer = await CustomAlert({
327
- type: 'question',
328
- title: i18next.t('button.delete'),
329
- text: i18next.t('text.are_you_sure'),
330
- confirmButton: { text: i18next.t('button.delete') },
331
- cancelButton: { text: i18next.t('button.cancel') }
332
- })
333
-
334
- if (!answer) {
335
- return
336
- }
337
-
338
- let ids = selectedRecords.map(record => record.id)
339
-
340
- const response = await client.mutate({
341
- mutation: gql`
342
- mutation ($ids: [String!]!) {
343
- deleteTerminologies(ids: $ids)
344
- }
345
- `,
346
- variables: {
347
- ids
348
- }
349
- })
350
-
351
- this.grist.fetch()
352
-
353
- return response.data.deleteTerminologies
354
- }
355
- }
356
-
357
- async clearCache() {
358
- const response = await client.mutate({
359
- mutation: gql`
360
- mutation {
361
- clearTranslationsCache
362
- }
363
- `
364
- })
365
-
366
- if (!response.errors) {
367
- this.showToast(i18next.t('text.info_x_successfully', { x: i18next.t('text.clearing-cache') }))
368
- }
369
- }
370
-
371
- async save() {
372
- let changedRecords = this.grist.dirtyRecords
373
-
374
- if (!changedRecords || changedRecords.length == 0) {
375
- this.showToast(i18next.t('text.nothing_changed'))
376
- } else {
377
- const patches = changedRecords.map(patch => {
378
- const patchField: any = patch.id ? { id: patch.id } : {}
379
- const dirtyFields = patch.__dirtyfields__
380
- for (let key in dirtyFields) {
381
- patchField[key] = dirtyFields[key].after
382
- }
383
- patchField.cuFlag = patch.__dirty__
384
-
385
- return patchField
386
- })
387
-
388
- const response = await client.mutate({
389
- mutation: gql`
390
- mutation ($patches: [TerminologyPatch!]!) {
391
- updateMultipleTerminologies(patches: $patches) {
392
- id
393
- }
394
- }
395
- `,
396
- variables: {
397
- patches
398
- }
399
- })
400
-
401
- this.grist.fetch()
402
- }
403
- }
404
-
405
- showToast(message: string) {
406
- document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
407
- }
408
- }
package/client/route.ts DELETED
@@ -1,7 +0,0 @@
1
- export default function route(page) {
2
- switch (page) {
3
- case 'terminology-page':
4
- import('./page/terminology-page.js')
5
- return page
6
- }
7
- }
@@ -1 +0,0 @@
1
- export * from './t'
package/server/apis/t.ts DELETED
@@ -1,36 +0,0 @@
1
- import { getRepository } from '@things-factory/shell'
2
-
3
- import { Terminology } from '../service/terminology/terminology'
4
-
5
- export type T_Arugments = {
6
- key: String
7
- locale: String
8
- options?: Object
9
- }
10
-
11
- export async function t({ key, locale = 'en-US', options }: T_Arugments) {
12
- let value = await getTerminology({
13
- key,
14
- locale
15
- })
16
-
17
- return value
18
- }
19
-
20
- async function getTerminology({ key, locale }) {
21
- const repo = getRepository(Terminology)
22
- var value
23
- try {
24
- let term = await repo.findOne({
25
- where: {
26
- name: key,
27
- locale
28
- }
29
- })
30
- value = term.display
31
- } catch (e) {
32
- value = key
33
- }
34
-
35
- return value
36
- }
@@ -1,18 +0,0 @@
1
- import { config } from '@things-factory/env'
2
-
3
- const disableCustomTerminologyFeature = config.get('i18n/disableCustomTerminologyFeature')
4
-
5
- const path = require('path')
6
- var fs = require('fs')
7
-
8
- export function clearTranslationCacheForCertainDomain(subdomain: string) {
9
- if (disableCustomTerminologyFeature) {
10
- return
11
- }
12
-
13
- const cacheFileFolder = path.join('cache', 'translations', subdomain)
14
-
15
- if (fs.existsSync(cacheFileFolder)) {
16
- fs.rmSync(cacheFileFolder, { recursive: true })
17
- }
18
- }
package/server/index.ts DELETED
@@ -1,6 +0,0 @@
1
- import './routes'
2
-
3
- export * from './service'
4
- export * from './apis'
5
- export * from './middlewares'
6
- export * from './controllers/clear-cache-for-translation.js'
@@ -1,36 +0,0 @@
1
- import fs from 'fs'
2
- import i18next from 'i18next'
3
-
4
- import koaI18next from './koa-i18next'
5
- import { build } from './translations'
6
-
7
- i18next.init({
8
- debug: false,
9
- ns: ['translations'],
10
- defaultNS: 'translations',
11
- preload: ['ko', 'en', 'jp', 'zh', 'ms'],
12
- fallbackLng: 'en',
13
- interpolation: {
14
- prefix: '{',
15
- suffix: '}'
16
- },
17
- load: 'languageOnly',
18
- keySeparator: false
19
- })
20
-
21
- build(fs)
22
-
23
- process.on('client-rebuilt' as any, (app, fs) => {
24
- /* rebuild once done webpacking assets. only for development mode. */
25
- build(fs)
26
- })
27
-
28
- export function initMiddlewares(app: any) {
29
- app.use(
30
- koaI18next(i18next, {
31
- lookupCookie: 'i18next',
32
- order: ['cookie', 'header'],
33
- next: true // if koa is version 2
34
- })
35
- )
36
- }
@@ -1,108 +0,0 @@
1
- const DEFAULT_ORDER = ['querystring', 'cookie', 'header']
2
-
3
- const detectors = {
4
- cookie: function (context, options) {
5
- let cookie = options.lookupCookie || 'i18next'
6
- return context.cookies.get(cookie)
7
- },
8
-
9
- // fork from i18next-express-middleware
10
- header: function (context) {
11
- let acceptLanguage = context.get('accept-language')
12
- let found
13
- let locales = []
14
- if (acceptLanguage) {
15
- let lngs = []
16
-
17
- // associate language tags by their 'q' value (between 1 and 0)
18
- acceptLanguage.split(',').forEach(function (l) {
19
- let parts = l.split(';') // 'en-GB;q=0.8' -> ['en-GB', 'q=0.8']
20
-
21
- // get the language tag qvalue: 'q=0.8' -> 0.8
22
- let qvalue = 1 // default qvalue
23
-
24
- for (let i = 0; i < parts.length; i++) {
25
- let part = parts[i].split('=')
26
- if (part[0] === 'q' && !isNaN(part[1])) {
27
- qvalue = Number(part[1])
28
- break
29
- }
30
- }
31
- // add the tag and primary subtag to the qvalue associations
32
- lngs.push({
33
- lng: parts[0],
34
- q: qvalue
35
- })
36
- })
37
-
38
- lngs.sort(function (a, b) {
39
- return b.q - a.q
40
- })
41
-
42
- for (let i = 0; i < lngs.length; i++) {
43
- locales.push(lngs[i].lng)
44
- }
45
-
46
- if (locales.length) found = locales
47
- }
48
-
49
- return found
50
- },
51
- path: function (context, options) {
52
- let found
53
-
54
- if (options.lookupPath !== undefined && context.params) {
55
- found = context.params[options.lookupPath]
56
- }
57
-
58
- if (!found && options.lookupFromPathIndex !== undefined) {
59
- let parts = context.path.split('/')
60
- if (parts[0] === '') {
61
- // Handle paths that start with a slash, i.e., '/foo' -> ['', 'foo']
62
- parts.shift()
63
- }
64
-
65
- if (parts.length > options.lookupFromPathIndex) {
66
- found = parts[options.lookupFromPathIndex]
67
- }
68
- }
69
- return found
70
- },
71
- querystring: function (context, options) {
72
- let name = options.lookupQuerystring || 'lng'
73
- return context.query[name]
74
- },
75
-
76
- session: function (context, options) {
77
- let name = options.lookupSession || 'lng'
78
- return context.session && context.session[name]
79
- }
80
- }
81
-
82
- export default function (context, options = {}) {
83
- let { order, fallback } = options as any
84
- order = order && Array.isArray(order) ? order : DEFAULT_ORDER
85
-
86
- let lngs = []
87
-
88
- for (let i = 0, len = order.length; i < len; i++) {
89
- let detector = detectors[order[i]]
90
- let lng
91
- if (detector) {
92
- lng = detector(context, options)
93
- }
94
- if (lng && typeof lng === 'string') {
95
- lngs.push(lng)
96
- } else {
97
- lngs = lngs.concat(lng)
98
- }
99
- }
100
- let found
101
- for (let i = 0, len = lngs.length; i < len; i++) {
102
- let cleanedLng = context.i18next.services.languageUtils.formatLanguageCode(lngs[i])
103
- if (context.i18next.services.languageUtils.isSupportedCode(cleanedLng)) found = cleanedLng
104
- if (found) break
105
- }
106
-
107
- return found || fallback
108
- }
@@ -1,142 +0,0 @@
1
- import detect from './detect'
2
-
3
- import { config } from '@things-factory/env'
4
-
5
- const disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)
6
- const defaultLanguage = config.get('i18n/defaultLanguage', 'en-US')
7
-
8
- export default function koaI18next(i18next, options = {} as any) {
9
- return async function i18nextMiddleware(ctx, next) {
10
- ctx.i18next = i18next
11
-
12
- let lng = disableUserFavoredLanguage ? defaultLanguage : detect(ctx, options)
13
- lng && setLanguage(ctx, lng, options)
14
-
15
- ctx.t = function (...args) {
16
- // do detect path
17
- if (!lng && isDetectPath(options.order)) {
18
- lng = detect(ctx, Object.assign(options, { order: ['path'] }))
19
- lng && setLanguage(ctx, lng, options)
20
- }
21
-
22
- if (args.length === 1) {
23
- args.push({})
24
- }
25
-
26
- for (let i = 0, len = args.length; i < len; i++) {
27
- let arg = args[i]
28
- if (typeof arg === 'object' && !Array.isArray(arg)) {
29
- arg.lng = lng
30
- }
31
- }
32
- return i18next.t.apply(i18next, args)
33
- }
34
-
35
- await next()
36
- }
37
- }
38
-
39
- function setPath(object, path, newValue) {
40
- let stack
41
- if (typeof path !== 'string') stack = [].concat(path)
42
- if (typeof path === 'string') stack = path.split('.')
43
-
44
- while (stack.length > 1) {
45
- let key = stack.shift()
46
- if (key.indexOf('###') > -1) key = key.replace(/###/g, '.')
47
- if (!object[key]) object[key] = {}
48
- object = object[key]
49
- }
50
-
51
- let key = stack.shift()
52
- if (key.indexOf('###') > -1) key = key.replace(/###/g, '.')
53
- object[key] = newValue
54
- }
55
-
56
- koaI18next.getResourcesHandler = function (i18next, options) {
57
- options = options || {}
58
- let maxAge = options.maxAge || 60 * 60 * 24 * 30
59
- const propertyParam = options.propertyParam || 'query'
60
-
61
- return async function (ctx, next) {
62
- if (options.path && ctx.path !== options.path) {
63
- return await next()
64
- }
65
- if (!i18next.services.backendConnector) return ctx.throw(404, 'koa-i18next-middleware:: no backend configured')
66
-
67
- let resources = {}
68
-
69
- ctx.type = 'json'
70
- if (options.cache !== undefined ? options.cache : process.env.NODE_ENV === 'production') {
71
- ctx.set('Cache-Control', 'public, max-age=' + maxAge)
72
- ctx.set('Expires', new Date(new Date().getTime() + maxAge * 1000).toUTCString())
73
- } else {
74
- ctx.set('Pragma', 'no-cache')
75
- ctx.set('Cache-Control', 'no-cache')
76
- }
77
-
78
- let languages = ctx[propertyParam][options.lngParam || 'lng']
79
- ? ctx[propertyParam][options.lngParam || 'lng'].split(' ')
80
- : []
81
- let namespaces = ctx[propertyParam][options.nsParam || 'ns']
82
- ? ctx[propertyParam][options.nsParam || 'ns'].split(' ')
83
- : []
84
-
85
- // extend ns
86
- namespaces.forEach(ns => {
87
- if (i18next.options.ns && i18next.options.ns.indexOf(ns) < 0) i18next.options.ns.push(ns)
88
- })
89
-
90
- i18next.services.backendConnector.load(languages, namespaces, function () {
91
- languages.forEach(lng => {
92
- namespaces.forEach(ns => {
93
- setPath(resources, [lng, ns], i18next.getResourceBundle(lng, ns))
94
- })
95
- })
96
- ctx.body = resources
97
- })
98
- }
99
- }
100
-
101
- koaI18next.getMissingKeyHandler = function (i18next, options) {
102
- options = options || {}
103
- const propertyParam = options.propertyParam || 'query'
104
-
105
- return async function (ctx, next) {
106
- if (options.path && ctx.path !== options.path) {
107
- return await next()
108
- }
109
- let lng = ctx[propertyParam][options.lngParam || 'lng']
110
- let ns = ctx[propertyParam][options.nsParam || 'ns']
111
-
112
- if (!i18next.services.backendConnector) return ctx.throw(404, 'koa-i18next-middleware:: no backend configured')
113
-
114
- for (var m in ctx.request.body) {
115
- i18next.services.backendConnector.saveMissing([lng], ns, m, ctx.request.body[m])
116
- }
117
- ctx.body = 'ok'
118
- }
119
- }
120
-
121
- function isDetectPath(order = []) {
122
- return order.indexOf('path') !== -1
123
- }
124
-
125
- function setLanguage(context, lng, options = {} as any) {
126
- const { lookupCookie, lookupPath, lookupSession } = options
127
- context.locals = Object.assign(context.locals || {}, { lng })
128
- context.state = Object.assign(context.state || {}, { lng })
129
- context.language = context.lng = lng
130
- context.set('content-language', lng)
131
-
132
- if (lookupCookie) {
133
- context.cookies.set(lookupCookie, lng, {
134
- httpOnly: false,
135
- signed: false,
136
- sameSite: 'strict'
137
- })
138
- }
139
- if (lookupSession && context.session) {
140
- context.session[lookupSession] = lng
141
- }
142
- }
@@ -1,27 +0,0 @@
1
- import FS from 'fs'
2
- import i18next from 'i18next'
3
- import path from 'path'
4
-
5
- const { appRootPath } = require('@things-factory/env')
6
-
7
- export function build(fs) {
8
- fs = fs || FS
9
-
10
- const PUBLIC_ROOT = path.resolve(appRootPath, 'dist-app') // webpack config output.path
11
-
12
- const ns = 'translations'
13
- var folder = path.join(PUBLIC_ROOT, 'translations')
14
- try {
15
- fs.readdirSync(folder).forEach(file => {
16
- try {
17
- var lng = file.split('.')[0]
18
- var resourcePath = path.join(folder, file)
19
- var resources = JSON.parse(fs.readFileSync(resourcePath))
20
- i18next.removeResourceBundle(lng, ns)
21
- i18next.addResourceBundle(lng, ns, resources, true, true)
22
- } catch (e) {
23
- console.error(e)
24
- }
25
- })
26
- } catch (e) {}
27
- }
@@ -1,87 +0,0 @@
1
- import Router from 'koa-router'
2
- import { ILike } from 'typeorm'
3
-
4
- import { getRepository } from '@things-factory/shell'
5
- import { Terminology } from '../service/terminology/terminology'
6
- import { config } from '@things-factory/env'
7
-
8
- const path = require('path')
9
- const cacheFS = require('fs')
10
- var fs = require('fs')
11
-
12
- const disableCustomTerminologyFeature = config.get('i18n/disableCustomTerminologyFeature')
13
-
14
- process.on('client-rebuilt' as any, (app, compilerFileSystem) => {
15
- /* change filesystem whenever webpacking assets done. only for development mode. */
16
- fs = compilerFileSystem
17
-
18
- if (!disableCustomTerminologyFeature) {
19
- const cacheFileFolder = path.join('cache', 'translations')
20
-
21
- if (cacheFS.existsSync(cacheFileFolder)) {
22
- cacheFS.rmSync(cacheFileFolder, { recursive: true })
23
- }
24
- }
25
- })
26
-
27
- export const translationsRouter = new Router()
28
-
29
- translationsRouter.get('/translations/:filename', async (context, next) => {
30
- const { filename } = context.params
31
- const { domain } = context.state
32
-
33
- const locale = (filename || 'en.json').split('.')[0]
34
-
35
- // context.set('Cache-Control', 'public, max-age=31536000') // 캐시 기간 설정 : 1년
36
- // context.set('Expires', new Date(Date.now() + 31536000000).toUTCString()) // 캐시 만료일 설정
37
-
38
- if (disableCustomTerminologyFeature) {
39
- var file = fs.readFileSync(path.resolve(path.join('dist-app', 'translations', filename)))
40
-
41
- context.type = 'application/json'
42
- context.body = file
43
- return
44
- }
45
-
46
- const cacheFileFolder = path.join('cache', 'translations', domain.subdomain)
47
- const cacheFilePath = path.join(cacheFileFolder, filename)
48
-
49
- if (cacheFS.existsSync(cacheFilePath)) {
50
- var file = cacheFS.readFileSync(cacheFilePath)
51
- } else {
52
- var file = fs.readFileSync(path.resolve(path.join('dist-app', 'translations', filename)))
53
-
54
- try {
55
- const terminologies = await getRepository(Terminology).find({
56
- where: { domain: { id: domain.id }, locale: ILike(locale + '%') }
57
- })
58
-
59
- const json = JSON.parse(file)
60
-
61
- terminologies.forEach(term => {
62
- const { name, category, display } = term
63
- json[`${category}.${name}`] = display
64
- })
65
-
66
- try {
67
- if (!cacheFS.existsSync(cacheFileFolder)) {
68
- cacheFS.mkdirSync(cacheFileFolder, { recursive: true })
69
- }
70
-
71
- cacheFS.writeFileSync(cacheFilePath, JSON.stringify(json))
72
- } catch (err) {
73
- console.error(err)
74
- }
75
-
76
- context.type = 'application/json'
77
- context.body = json
78
-
79
- return
80
- } catch (err) {
81
- console.error(err)
82
- }
83
- }
84
-
85
- context.type = 'application/json'
86
- context.body = file
87
- })
package/server/routes.ts DELETED
@@ -1,31 +0,0 @@
1
- import i18next from 'i18next'
2
- import { config } from '@things-factory/env'
3
-
4
- const disableCustomTerminologyFeature = config.get('i18n/disableCustomTerminologyFeature')
5
- const isPathBaseDomain = !config.get('subdomain') && !config.get('useVirtualHostBasedDomain')
6
-
7
- import { translationsRouter } from './routers/translations-router'
8
-
9
- process.on('bootstrap-module-global-public-route' as any, (app, globalPublicRouter) => {
10
- globalPublicRouter.get('/translations/translate/:lng/:key', async (context, next) => {
11
- const { lng, key } = context.params
12
- const translated = i18next.getFixedT(lng, 'translations' /* ns */)(key)
13
-
14
- context.body = translated
15
- })
16
-
17
- globalPublicRouter.get('/translations/translate/:key', async (context, next) => {
18
- const { key } = context.params
19
- const translated = context.t(key)
20
-
21
- context.body = translated
22
- })
23
- })
24
-
25
- process.on('bootstrap-module-domain-public-route' as any, (app, domainPublicRouter) => {
26
- if (isPathBaseDomain) {
27
- domainPublicRouter.use('/domain/:subdomain', translationsRouter.routes(), translationsRouter.allowedMethods())
28
- } else if (!disableCustomTerminologyFeature) {
29
- domainPublicRouter.use('', translationsRouter.routes(), translationsRouter.allowedMethods())
30
- }
31
- })
@@ -1,27 +0,0 @@
1
- /* EXPORT ENTITY TYPES */
2
- export * from './terminology/terminology'
3
- export * from './terminology/terminology-type'
4
-
5
- /* IMPORT ENTITIES AND RESOLVERS */
6
- import {
7
- entities as TerminologyEntities,
8
- resolvers as TerminologyResolvers,
9
- subscribers as TerminologySubscribers
10
- } from './terminology'
11
-
12
- export const entities = [
13
- /* ENTITIES */
14
- ...TerminologyEntities
15
- ]
16
-
17
- export const subscribers = [
18
- /* SUBSCRIBERS */
19
- ...TerminologySubscribers
20
- ]
21
-
22
- export const schema = {
23
- resolverClasses: [
24
- /* RESOLVER CLASSES */
25
- ...TerminologyResolvers
26
- ]
27
- }
@@ -1,7 +0,0 @@
1
- import { Terminology } from './terminology'
2
- import { TerminologyQuery } from './terminology-query'
3
- import { TerminologyMutation } from './terminology-mutation'
4
-
5
- export const entities = [Terminology]
6
- export const resolvers = [TerminologyQuery, TerminologyMutation]
7
- export const subscribers = []
@@ -1,171 +0,0 @@
1
- import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
2
- import { In } from 'typeorm'
3
-
4
- import { Terminology } from './terminology'
5
- import { NewTerminology, TerminologyPatch } from './terminology-type'
6
-
7
- import { clearTranslationCacheForCertainDomain } from '../../controllers/clear-cache-for-translation'
8
-
9
- function clearCache(subdomain: string) {
10
- /** multiple update/delete시 모든 엔티티에 대해서 subscription 이 걸리므로, EventSubscriber를 사용하지 않음. */
11
- try {
12
- clearTranslationCacheForCertainDomain(subdomain)
13
- } catch (err) {
14
- console.error('Failed to delete the translations cache directory:', err)
15
- }
16
- }
17
-
18
- @Resolver(Terminology)
19
- export class TerminologyMutation {
20
- @Directive('@transaction')
21
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
22
- @Mutation(returns => Terminology, { description: 'To create new Terminology' })
23
- async createTerminology(@Arg('terminology') terminology: NewTerminology, @Ctx() context: any): Promise<Terminology> {
24
- const { domain, user, tx } = context.state
25
-
26
- const result = await tx.getRepository(Terminology).save({
27
- ...terminology,
28
- domain,
29
- creator: user,
30
- updater: user
31
- })
32
-
33
- clearCache(domain.subdomain)
34
-
35
- return result
36
- }
37
-
38
- @Directive('@transaction')
39
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
40
- @Mutation(returns => Terminology, { description: 'To modify Terminology information' })
41
- async updateTerminology(
42
- @Arg('id') id: string,
43
- @Arg('patch') patch: TerminologyPatch,
44
- @Ctx() context: any
45
- ): Promise<Terminology> {
46
- const { domain, user, tx } = context.state
47
-
48
- const repository = tx.getRepository(Terminology)
49
- const terminology = await repository.findOne({
50
- where: { domain: { id: domain.id }, id }
51
- })
52
-
53
- const result = await repository.save({
54
- ...terminology,
55
- ...patch,
56
- updater: user
57
- })
58
-
59
- clearCache(domain.subdomain)
60
-
61
- return result
62
- }
63
-
64
- @Directive('@transaction')
65
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
66
- @Mutation(returns => [Terminology], { description: "To modify multiple Terminologies' information" })
67
- async updateMultipleTerminologies(
68
- @Arg('patches', type => [TerminologyPatch]) patches: TerminologyPatch[],
69
- @Ctx() context: any
70
- ): Promise<Terminology[]> {
71
- const { domain, user, tx } = context.state
72
-
73
- let results = []
74
- const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
75
- const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
76
- const terminologyRepo = tx.getRepository(Terminology)
77
-
78
- if (_createRecords.length > 0) {
79
- for (let i = 0; i < _createRecords.length; i++) {
80
- const newRecord = _createRecords[i]
81
-
82
- const result = await terminologyRepo.save({
83
- ...newRecord,
84
- domain,
85
- creator: user,
86
- updater: user
87
- })
88
-
89
- results.push({ ...result, cuFlag: '+' })
90
- }
91
- }
92
-
93
- if (_updateRecords.length > 0) {
94
- for (let i = 0; i < _updateRecords.length; i++) {
95
- const newRecord = _updateRecords[i]
96
- const terminology = await terminologyRepo.findOneBy({ id: newRecord.id })
97
-
98
- const result = await terminologyRepo.save({
99
- ...terminology,
100
- ...newRecord,
101
- updater: user
102
- })
103
-
104
- results.push({ ...result, cuFlag: 'M' })
105
- }
106
- }
107
-
108
- clearCache(domain.subdomain)
109
-
110
- return results
111
- }
112
-
113
- @Directive('@transaction')
114
- @Mutation(returns => Boolean, { description: 'To clear translations cache' })
115
- async clearTranslationsCache(@Ctx() context: any): Promise<boolean> {
116
- const { domain, tx } = context.state
117
-
118
- clearCache(domain.subdomain)
119
-
120
- return true
121
- }
122
-
123
- @Directive('@transaction')
124
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
125
- @Mutation(returns => Boolean, { description: 'To delete Terminology' })
126
- async deleteTerminology(@Arg('id') id: string, @Ctx() context: any): Promise<boolean> {
127
- const { domain, tx } = context.state
128
-
129
- await tx.getRepository(Terminology).delete({ domain: { id: domain.id }, id })
130
-
131
- clearCache(domain.subdomain)
132
-
133
- return true
134
- }
135
-
136
- @Directive('@transaction')
137
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
138
- @Mutation(returns => Boolean, { description: 'To delete multiple Terminologies' })
139
- async deleteTerminologies(@Arg('ids', type => [String]) ids: string[], @Ctx() context: any): Promise<boolean> {
140
- const { domain, tx } = context.state
141
-
142
- await tx.getRepository(Terminology).delete({
143
- domain: { id: domain.id },
144
- id: In(ids)
145
- })
146
-
147
- clearCache(domain.subdomain)
148
-
149
- return true
150
- }
151
-
152
- @Directive('@transaction')
153
- @Directive('@privilege(category: "terminology", privilege: "mutation", domainOwnerGranted: true)')
154
- @Mutation(returns => Boolean, { description: 'To import multiple Terminologies' })
155
- async importTerminologies(
156
- @Arg('terminologies', type => [TerminologyPatch]) terminologies: TerminologyPatch[],
157
- @Ctx() context: any
158
- ): Promise<boolean> {
159
- const { domain, tx } = context.state
160
-
161
- await Promise.all(
162
- terminologies.map(async (terminology: TerminologyPatch) => {
163
- const createdTerminology: Terminology = await tx.getRepository(Terminology).save({ domain, ...terminology })
164
- })
165
- )
166
-
167
- clearCache(domain.subdomain)
168
-
169
- return true
170
- }
171
- }
@@ -1,50 +0,0 @@
1
- import { Arg, Args, Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql'
2
-
3
- import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
4
- import { User } from '@things-factory/auth-base'
5
-
6
- import { Terminology } from './terminology'
7
- import { TerminologyList } from './terminology-type'
8
-
9
- @Resolver(Terminology)
10
- export class TerminologyQuery {
11
- @Query(returns => Terminology, { description: 'To fetch a Terminology' })
12
- async terminology(@Arg('id') id: string, @Ctx() context: any): Promise<Terminology> {
13
- const { domain } = context.state
14
-
15
- return await getRepository(Terminology).findOne({
16
- where: { domain: { id: domain.id }, id }
17
- })
18
- }
19
-
20
- @Query(returns => TerminologyList, { description: 'To fetch multiple Terminologies' })
21
- async terminologies(@Args(type => ListParam) params: ListParam, @Ctx() context: any): Promise<TerminologyList> {
22
- const { domain } = context.state
23
-
24
- const queryBuilder = getQueryBuilderFromListParams({
25
- domain,
26
- params,
27
- repository: await getRepository(Terminology),
28
- searchables: ['name', 'category', 'display']
29
- })
30
-
31
- const [items, total] = await queryBuilder.getManyAndCount()
32
-
33
- return { items, total }
34
- }
35
-
36
- @FieldResolver(type => Domain)
37
- async domain(@Root() terminology: Terminology): Promise<Domain> {
38
- return await getRepository(Domain).findOneBy({ id: terminology.domainId })
39
- }
40
-
41
- @FieldResolver(type => User)
42
- async updater(@Root() terminology: Terminology): Promise<User> {
43
- return await getRepository(User).findOneBy({ id: terminology.updaterId })
44
- }
45
-
46
- @FieldResolver(type => User)
47
- async creator(@Root() terminology: Terminology): Promise<User> {
48
- return await getRepository(User).findOneBy({ id: terminology.creatorId })
49
- }
50
- }
@@ -1,54 +0,0 @@
1
- import { Field, ID, InputType, Int, ObjectType } from 'type-graphql'
2
-
3
- import { Terminology } from './terminology'
4
-
5
- @InputType()
6
- export class NewTerminology {
7
- @Field()
8
- name: string
9
-
10
- @Field({ nullable: true })
11
- description?: string
12
-
13
- @Field()
14
- locale: string
15
-
16
- @Field()
17
- category: string
18
-
19
- @Field()
20
- display: string
21
- }
22
-
23
- @InputType()
24
- export class TerminologyPatch {
25
- @Field(type => ID, { nullable: true })
26
- id?: string
27
-
28
- @Field({ nullable: true })
29
- name?: string
30
-
31
- @Field({ nullable: true })
32
- description?: string
33
-
34
- @Field({ nullable: true })
35
- locale?: string
36
-
37
- @Field({ nullable: true })
38
- category?: string
39
-
40
- @Field({ nullable: true })
41
- display?: string
42
-
43
- @Field({ nullable: true })
44
- cuFlag?: string
45
- }
46
-
47
- @ObjectType()
48
- export class TerminologyList {
49
- @Field(type => [Terminology])
50
- items: Terminology[]
51
-
52
- @Field(type => Int)
53
- total: number
54
- }
@@ -1,75 +0,0 @@
1
- import { Field, ID, ObjectType } from 'type-graphql'
2
- import {
3
- Column,
4
- CreateDateColumn,
5
- Entity,
6
- Index,
7
- ManyToOne,
8
- PrimaryGeneratedColumn,
9
- RelationId,
10
- UpdateDateColumn
11
- } from 'typeorm'
12
-
13
- import { Domain } from '@things-factory/shell'
14
- import { User } from '@things-factory/auth-base'
15
-
16
- @Entity()
17
- @ObjectType({ description: 'Entity for Terminology' })
18
- @Index('ix_term_0', (term: Terminology) => [term.domain, term.locale, term.category, term.name], { unique: true })
19
- @Index('ix_term_1', (term: Terminology) => [term.domain])
20
- @Index('ix_term_2', (term: Terminology) => [term.domain, term.locale])
21
- @Index('ix_term_3', (term: Terminology) => [term.domain, term.locale, term.name])
22
- export class Terminology {
23
- @PrimaryGeneratedColumn('uuid')
24
- @Field(type => ID)
25
- readonly id: string
26
-
27
- @ManyToOne(type => Domain)
28
- @Field(type => Domain)
29
- domain?: Domain
30
-
31
- @RelationId((terminology: Terminology) => terminology.domain)
32
- domainId?: string
33
-
34
- @Column()
35
- @Field()
36
- name?: string
37
-
38
- @Column({ nullable: true })
39
- @Field({ nullable: true })
40
- description?: string
41
-
42
- @Field()
43
- @Column({ length: 20 })
44
- locale?: string
45
-
46
- @Field()
47
- @Column({ length: 20 })
48
- category?: string
49
-
50
- @Field()
51
- @Column()
52
- display?: string
53
-
54
- @CreateDateColumn()
55
- @Field({ nullable: true })
56
- createdAt?: Date
57
-
58
- @UpdateDateColumn()
59
- @Field({ nullable: true })
60
- updatedAt?: Date
61
-
62
- @ManyToOne(type => User, { nullable: true })
63
- @Field(type => User, { nullable: true })
64
- creator?: User
65
-
66
- @RelationId((terminology: Terminology) => terminology.creator)
67
- creatorId?: string
68
-
69
- @ManyToOne(type => User, { nullable: true })
70
- @Field(type => User, { nullable: true })
71
- updater?: User
72
-
73
- @RelationId((terminology: Terminology) => terminology.updater)
74
- updaterId?: string
75
- }