@jeongpd/korean-kinship 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tiger-dreams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # korean-kinship
2
+
3
+ **Resolve any Korean family-relationship path to its correct kinship term, reverse
4
+ term, and 촌수 (degree of kinship) — including in-laws and 사돈.**
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@jeongpd/korean-kinship.svg)](https://www.npmjs.com/package/@jeongpd/korean-kinship)
7
+ [![license](https://img.shields.io/npm/l/@jeongpd/korean-kinship.svg)](https://github.com/tiger-dreams/korean-kinship/blob/main/LICENSE)
8
+ [![node](https://img.shields.io/node/v/@jeongpd/korean-kinship.svg)](https://www.npmjs.com/package/@jeongpd/korean-kinship)
9
+
10
+ Korean kinship terms depend on blood vs. marriage (혈족/인척), 친가/외가/처가/시가
11
+ side, birth order, and generation — genuinely easy to get wrong, and there was no
12
+ open-source library that solved this for Korean (the closest equivalent,
13
+ [`mumuy/relationship`](https://github.com/mumuy/relationship), solves it for Chinese
14
+ kinship and has 3,700+ stars).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @jeongpd/korean-kinship
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```js
25
+ import { resolve } from '@jeongpd/korean-kinship';
26
+
27
+ resolve(['father', 'olderBro', 'spouse']);
28
+ // → {
29
+ // path: ['father', 'olderBro', 'spouse'],
30
+ // label: '큰어머니',
31
+ // reverseLabel: '조카',
32
+ // chon: 3,
33
+ // formal: '백모',
34
+ // note: '큰아버지의 아내',
35
+ // relationClass: 'inlaw',
36
+ // source: 'golden'
37
+ // }
38
+
39
+ resolve(['spouse'], { selfGender: 'F' });
40
+ // → {
41
+ // path: ['spouse'],
42
+ // label: '남편',
43
+ // reverseLabel: '아내',
44
+ // chon: 0,
45
+ // formal: '부군',
46
+ // note: '인척',
47
+ // relationClass: 'inlaw',
48
+ // source: 'golden'
49
+ // }
50
+ ```
51
+
52
+ A relation path is an ordered array built from these 9 base relations:
53
+
54
+ `father`, `mother`, `olderBro`, `olderSis`, `youngerBro`, `youngerSis`, `son`,
55
+ `daughter`, `spouse`
56
+
57
+ Chain them to describe any relative — `['mother', 'olderSis', 'spouse']` is "어머니의
58
+ 언니의 남편" (이모부). `resolve()` returns:
59
+
60
+ | field | meaning |
61
+ |---|---|
62
+ | `path` | the input path echoed back |
63
+ | `label` | the kinship term |
64
+ | `reverseLabel` | what that relative would call you back (when known) |
65
+ | `chon` | 촌수 — degree of kinship |
66
+ | `relationClass` | `'blood'` or `'inlaw'` — any spouse hop in the path makes it `'inlaw'` |
67
+ | `formal` | a more formal/written variant of the term, where one exists |
68
+ | `note` | a short usage note (present on golden-table entries, omitted elsewhere) |
69
+ | `source` | `'golden'` (exact match against 145 validated real-usage entries), `'fallback-table'`, or `'generic'` (computed pattern for anything past common terminology) |
70
+
71
+ `opts.selfGender` (`'M'` or `'F'`) disambiguates terms that depend on the speaker's own
72
+ gender — e.g. your spouse is 아내 if you're male, 남편 if you're female.
73
+
74
+ ## Other exports
75
+
76
+ Alongside `resolve`, the package exports the underlying data so you can drive your own
77
+ UI off it directly rather than through the function.
78
+
79
+ ### `relationIds`
80
+
81
+ The array of the 9 valid relation ids, in a sensible display order — everything
82
+ `resolve()` will accept in a path. Useful for building a relation picker; this is
83
+ exactly what the bundled demo does:
84
+
85
+ ```js
86
+ import { relationIds } from '@jeongpd/korean-kinship';
87
+
88
+ relationIds;
89
+ // → ['father', 'mother', 'olderBro', 'olderSis', 'youngerBro', 'youngerSis',
90
+ // 'son', 'daughter', 'spouse']
91
+
92
+ for (const id of relationIds) {
93
+ const option = document.createElement('option');
94
+ option.value = id;
95
+ option.textContent = LABELS[id]; // your own Korean display labels
96
+ picker.appendChild(option);
97
+ }
98
+ ```
99
+
100
+ See [`demo/demo.js`](./demo/demo.js) for the full version.
101
+
102
+ ### `termTable`
103
+
104
+ The raw 145-entry golden table, keyed by dot-joined relation path, with each entry's
105
+ original fields intact (`M`, `F`, `rev_M`, `rev_F`, `formal`/`formal_M`/`formal_F`,
106
+ `chon`, `note`). Handy for inspecting the data, generating your own lookup structures,
107
+ or rendering a browsable table of every known relationship:
108
+
109
+ ```js
110
+ import { termTable } from '@jeongpd/korean-kinship';
111
+
112
+ termTable['father.olderBro.spouse'];
113
+ // → { M: '큰어머니', F: '큰어머니', rev_M: '조카', rev_F: '조카',
114
+ // formal: '백모', chon: 3, note: '큰아버지의 아내' }
115
+
116
+ Object.keys(termTable).length; // → 145
117
+ ```
118
+
119
+ It's plain data with no behavior attached — treat it as read-only.
120
+
121
+ ## Where this data comes from
122
+
123
+ The core ~145 relationship entries aren't guessed — they're ported from a real, live
124
+ Korean family-tree app, validated against actual usage. See
125
+ [`docs/DATA_PROVENANCE.md`](./docs/DATA_PROVENANCE.md) for details, or
126
+ `scripts/import-golden-terms.mjs` for the exact extraction method.
127
+
128
+ ## Why not just a lookup table?
129
+
130
+ For the ~145 most common relationships, it is exactly a lookup table — that's Tier 1,
131
+ and it's returned verbatim because it encodes real, occasionally non-obvious Korean
132
+ usage conventions (for example: your sibling's spouse and your spouse's parents are
133
+ conventionally given `chon: 0`, "just family," while your uncle's wife keeps your
134
+ uncle's own `chon: 3` — these aren't derivable from one clean formula). For anything
135
+ past that table, `resolve()` falls back to a compositional algorithm that computes
136
+ 촌수 and blood/in-law class from the path directly, so arbitrarily deep or unusual
137
+ relationship chains still get a sensible answer instead of an error.
138
+
139
+ ## License
140
+
141
+ MIT © [tiger-dreams](https://github.com/tiger-dreams) — see [LICENSE](./LICENSE).
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@jeongpd/korean-kinship",
3
+ "version": "0.1.0",
4
+ "description": "Resolve any Korean family-relationship path to its correct kinship term, reverse term, and 촌수 (degree of kinship) — including in-laws and 사돈.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test tests/*.test.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "korean",
18
+ "kinship",
19
+ "family-tree",
20
+ "chon",
21
+ "촌수",
22
+ "genealogy"
23
+ ],
24
+ "author": "tiger-dreams",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/tiger-dreams/korean-kinship.git"
29
+ },
30
+ "homepage": "https://github.com/tiger-dreams/korean-kinship#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/tiger-dreams/korean-kinship/issues"
33
+ },
34
+ "files": [
35
+ "src"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
@@ -0,0 +1,66 @@
1
+ // Tier-2 supplementary term table. Starts empty on purpose (YAGNI) — every entry in
2
+ // the ported golden table (src/data/golden-terms.js) already covers common usage
3
+ // including most in-law/사돈 vocabulary. Add an entry here only when you have a real,
4
+ // justifiable standard term for a specific canonical descriptor (see resolve.js for
5
+ // the descriptor key format); otherwise let genericFallback() handle it.
6
+ export const FALLBACK_TERMS = {};
7
+
8
+ // Base terms by generation distance. Korean does not use one word for "any ancestor"
9
+ // and another for "any descendant" — the term changes with how many generations away
10
+ // the relative is — so the base term is picked from |generationDelta|, not its sign.
11
+ // The `X` column is the gender-unknown form, only reachable when a path is nothing but
12
+ // spouse hops (there is no gendered person to anchor on).
13
+ const SAME_GENERATION = { M: '형제', F: '자매', X: '형제자매' };
14
+
15
+ const ASCENDING = [
16
+ null, // |delta| 0 is handled by SAME_GENERATION
17
+ { M: '아저씨', F: '아주머니', X: '어른' },
18
+ { M: '할아버지', F: '할머니', X: '조부모' },
19
+ { M: '증조할아버지', F: '증조할머니', X: '증조부모' },
20
+ { M: '고조할아버지', F: '고조할머니', X: '고조부모' },
21
+ ];
22
+
23
+ const DESCENDING = [
24
+ null, // |delta| 0 is handled by SAME_GENERATION
25
+ { M: '조카', F: '조카딸', X: '조카' },
26
+ { M: '손자', F: '손녀', X: '손주' },
27
+ { M: '증손자', F: '증손녀', X: '증손' },
28
+ { M: '고손자', F: '고손녀', X: '고손' },
29
+ ];
30
+
31
+ // Past 고조/고손 (4 generations) Korean switches to the counted forms 'N대조부/N대조모'
32
+ // and 'N대손', which is exactly what genealogical usage does for distant ancestors and
33
+ // descendants.
34
+ function distantAncestor(n, key) {
35
+ if (key === 'F') return `${n}대조모`;
36
+ if (key === 'M') return `${n}대조부`;
37
+ return `${n}대조`;
38
+ }
39
+
40
+ function baseTerm(generationDelta, terminalGender) {
41
+ const key = terminalGender ?? 'X';
42
+ const n = Math.abs(generationDelta);
43
+
44
+ if (n === 0) return SAME_GENERATION[key];
45
+ if (generationDelta > 0) {
46
+ return n <= 4 ? ASCENDING[n][key] : distantAncestor(n, key);
47
+ }
48
+ return n <= 4 ? DESCENDING[n][key] : `${n}대손`;
49
+ }
50
+
51
+ /**
52
+ * Last-resort label for a path with no Tier-1 or Tier-2 entry.
53
+ *
54
+ * `chon: 0` means "close family, no numeral" in this codebase's convention (the golden
55
+ * entries with chon 0 — 장인어른, 며느리, 동서 — never carry a number), so the numeral
56
+ * is omitted entirely rather than rendered as a nonsensical "0촌".
57
+ */
58
+ export function genericFallback({ chon, generationDelta, relationClass, terminalGender }) {
59
+ const base = baseTerm(generationDelta, terminalGender);
60
+ const numeral = chon > 0 ? `${chon}촌 ` : '';
61
+
62
+ if (relationClass === 'inlaw') {
63
+ return chon > 0 ? `인척 ${numeral}${base}` : `가까운 인척 ${base}`;
64
+ }
65
+ return `${numeral}${base}`;
66
+ }