@things-factory/organization 10.1.28 → 10.1.29

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/organization",
3
- "version": "10.1.28",
3
+ "version": "10.1.29",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -36,10 +36,10 @@
36
36
  "@operato/p13n": "^10.0.0",
37
37
  "@operato/popup": "^10.15.0",
38
38
  "@operato/shell": "^10.0.0",
39
- "@things-factory/auth-base": "^10.1.28",
40
- "@things-factory/code-ui": "^10.1.28",
41
- "@things-factory/contact": "^10.1.28",
42
- "@things-factory/shell": "^10.1.28"
39
+ "@things-factory/auth-base": "^10.1.29",
40
+ "@things-factory/code-ui": "^10.1.29",
41
+ "@things-factory/contact": "^10.1.29",
42
+ "@things-factory/shell": "^10.1.29"
43
43
  },
44
- "gitHead": "3a260a0f79bc6b41a692a103955bfcc541aff60e"
44
+ "gitHead": "e6bf9dec003fe5e794746aaeb5852b3b50478232"
45
45
  }
@@ -1,110 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
2
- import path from 'path'
3
-
4
- /**
5
- * A screen has to be able to say its own words.
6
- *
7
- * i18next answers an unknown key with the key itself, so a missing word is not an error anywhere -
8
- * it is printed on the screen. The department screen's delete dialog asked for `text.sure_to_x`
9
- * and `text.delete`; both are declared only in `integration-ui`, which plant does not load, so the
10
- * dialog read `text.sure_to_x` and the toast after it read `text.delete했습니다` (plant, 2026-09-18).
11
- *
12
- * A word is reachable when this package declares it, or a package this one depends on declares it,
13
- * or the call carries a defaultValue. Anything else is a bet on which modules the app happens to
14
- * load, and the bet is settled on the user's screen.
15
- */
16
-
17
- const PACKAGE = path.resolve(__dirname, '..')
18
- const PACKAGES = path.resolve(PACKAGE, '..')
19
- const LANGUAGES = ['en', 'ja', 'ko', 'ms', 'zh']
20
-
21
- /** `i18next.t('key')` or `i18next.t('key', { … })`, with the options text kept. */
22
- const CALL = /i18next\.t\(\s*'([^']+)'\s*(,\s*\{[^)]*)?\)/g
23
-
24
- function clientSources(): { file: string; code: string }[] {
25
- const found: { file: string; code: string }[] = []
26
-
27
- const walk = (dir: string) => {
28
- for (const entry of readdirSync(dir)) {
29
- if (entry === 'node_modules' || entry.startsWith('dist')) continue
30
-
31
- const full = path.join(dir, entry)
32
- if (statSync(full).isDirectory()) walk(full)
33
- else if (full.endsWith('.ts')) found.push({ file: path.relative(PACKAGE, full), code: readFileSync(full, 'utf-8') })
34
- }
35
- }
36
-
37
- walk(path.join(PACKAGE, 'client'))
38
- return found
39
- }
40
-
41
- /** Keys this package declares, plus the keys every package it depends on declares. */
42
- function reachableKeys(): Set<string> {
43
- const keys = new Set<string>(Object.keys(JSON.parse(readFileSync(path.join(PACKAGE, 'translations/ko.json'), 'utf-8'))))
44
- const { dependencies = {} } = JSON.parse(readFileSync(path.join(PACKAGE, 'package.json'), 'utf-8'))
45
-
46
- for (const name of Object.keys(dependencies)) {
47
- if (!name.startsWith('@things-factory/')) continue
48
-
49
- const file = path.join(PACKAGES, name.slice('@things-factory/'.length), 'translations/ko.json')
50
- if (!existsSync(file)) continue
51
-
52
- for (const key of Object.keys(JSON.parse(readFileSync(file, 'utf-8')))) keys.add(key)
53
- }
54
-
55
- return keys
56
- }
57
-
58
- describe('the words a screen says', () => {
59
- it('★ every one of them is reachable, or carries a defaultValue', () => {
60
- const reachable = reachableKeys()
61
- const unreachable: string[] = []
62
-
63
- for (const { file, code } of clientSources()) {
64
- for (const [, key, options] of code.matchAll(CALL)) {
65
- if ((options || '').includes('defaultValue')) continue
66
- if (!reachable.has(key)) unreachable.push(`${key} (${file})`)
67
- }
68
- }
69
-
70
- expect(unreachable).toEqual([])
71
- })
72
-
73
- it('★ and the five languages say the same set of them', () => {
74
- /*
75
- * A key missing from one language falls back to the key. Malay was missing
76
- * `button.register-account` while the other four had it.
77
- */
78
- const keys = LANGUAGES.map(language => ({
79
- language,
80
- keys: new Set<string>(
81
- Object.keys(JSON.parse(readFileSync(path.join(PACKAGE, `translations/${language}.json`), 'utf-8')))
82
- )
83
- }))
84
-
85
- const everything = new Set<string>(keys.flatMap(({ keys }) => [...keys]))
86
- const gaps = keys
87
- .map(({ language, keys }) => ({ language, missing: [...everything].filter(key => !keys.has(key)).sort() }))
88
- .filter(({ missing }) => missing.length > 0)
89
-
90
- expect(gaps).toEqual([])
91
- })
92
-
93
- it('★ and no sentence is assembled out of two of them', () => {
94
- /*
95
- * `t('text.info_x_successfully', { x: t('text.delete') })` resolves its two halves separately:
96
- * when the inner one is missing the sentence still renders, with a key inside it. The word
97
- * order of the halves is also not the same in every language. A whole sentence per key -
98
- * `text.deleted`, `text.saved` - says the same thing and cannot come apart.
99
- */
100
- const assembled: string[] = []
101
-
102
- for (const { file, code } of clientSources()) {
103
- for (const [, key, options] of code.matchAll(CALL)) {
104
- if ((options || '').includes('i18next.t(')) assembled.push(`${key} (${file})`)
105
- }
106
- }
107
-
108
- expect(assembled).toEqual([])
109
- })
110
- })