Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added
- The autosuggest dropdown now shows a preview of the resolved date/time next to each suggestion (e.g. `Next Monday` — `2025-01-06`), so you can see what will actually be inserted before picking one.
- **Korean support** (partial): `오늘`/`내일`/`어제`, weekdays, `이번`/`다음`/`지난` prefixes, and suffix-style relative expressions (`3일 후`, `2주 전`). Vocabulary based on [CreamNuts' nldates-obsidian-korean](https://github.com/CreamNuts/nldates-obsidian-korean) (MIT), with thanks. Like the other partially-supported languages, chrono-node has no Korean parser to fall back on, so some combined phrasings (weekday + specific time, date ranges) aren't recognized yet — tracked in [#40](https://github.com/Amato21/nldates-revived/issues/40).

## [0.9.71] - 2026-07-17

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ Enable any combination in **Settings → Natural Language Dates → Language set
| German | Partially supported |
| Portuguese | Partially supported |
| Japanese | Full support |
| Korean | Partially supported |
| Dutch | Under development |

You can enable several languages at once — the autosuggest and parser understand all of them simultaneously.

Korean support (`오늘`, `내일`, `다음 주 월요일`, `3일 후`...) reuses vocabulary from [CreamNuts' nldates-obsidian-korean](https://github.com/CreamNuts/nldates-obsidian-korean) — many thanks for that work. As with the other "partially supported" languages, chrono-node has no Korean parser to fall back on, so some combined phrasings (e.g. a weekday together with a specific time, or date ranges) aren't recognized yet ([#40](https://github.com/Amato21/nldates-revived/issues/40)).

---

## How to Install
Expand Down Expand Up @@ -131,6 +134,7 @@ This project stands on the shoulders of giants. Huge thanks to:

* **[Argentina Ortega Sainz (argenos)](https://github.com/argenos):** The original creator of this plugin. This revival is based on their excellent work.
* **[RensOliemans](https://github.com/RensOliemans):** For the major contributions and foundational work on the multilingual support.
* **[CreamNuts](https://github.com/CreamNuts):** For [nldates-obsidian-korean](https://github.com/CreamNuts/nldates-obsidian-korean), whose vocabulary this plugin's Korean support is built on.
* **Amato21:** Maintainer of this "Revived" version.

Powered by the [chrono-node](https://github.com/wanasit/chrono) library.
Expand Down
124 changes: 100 additions & 24 deletions main.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/lang/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import it from './it';
import ru from './ru';
import uk from './uk';
import zh from './zh';
import ko from './ko';

const notFoundDefault = "NOTFOUND" as const;

Expand Down Expand Up @@ -39,6 +40,7 @@ const languageModules: Record<string, LangDict> = {
ru,
uk,
'zh.hant': zh,
ko,
};

/**
Expand Down
74 changes: 74 additions & 0 deletions src/lang/ko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Korean support. Vocabulary for today/tomorrow/yesterday, weekdays,
// this/next/last, the hour/minute/day/week/month/year units, and the
// "later"/"agosuffix" relative-expression markers is taken directly from
// CreamNuts' translateKoreanToEnglish() dictionary
// (https://github.com/CreamNuts/nldates-obsidian-korean, MIT), since it's
// vocabulary a real Korean-speaking user base has already been using.
//
// Korean phrases numbers in a suffix position (number + unit + marker, e.g.
// "3일 후" = "3 days" + "후" ("later") = "in 3 days"), not English's prefix
// "in 3 days" -- so inminutes/indays/etc. and daysago/weeksago/etc. below are
// built suffix-style, the same mechanism already used for Chinese (see
// zh.ts's "later"/"agosuffix" comments), rather than mirroring the English
// template literally.
//
// UNVERIFIED: "and"/"at"/"from"/"to"/"of" below don't have an equivalent in
// CreamNuts' file (his translator doesn't support combined durations,
// weekday+time, or date ranges) and are my own best-effort picks, not
// checked by a Korean speaker. Korean normally attaches particles like 에
// ("at")/부터("from")/까지("to")/의("of") directly to the previous word with
// no space, while this plugin's regex patterns require them as a
// space-separated standalone word -- so "next Monday at 3pm"-, "from Monday
// to Friday"-, "in 2 weeks and 3 days"-, and "the 15th of next month"-style
// combined expressions may not match natural Korean phrasing even though the
// simple expressions above do. Tracked in #40.
const dict = {
today: "오늘",
tomorrow: "내일",
yesterday: "어제",
next: "다음",
last: "지난",
this: "이번",
in: "앞으로",
sunday: "일요일",
monday: "월요일",
tuesday: "화요일",
wednesday: "수요일",
thursday: "목요일",
friday: "금요일",
saturday: "토요일",
inminutes: "%{timeDelta}분 후",
inhours: "%{timeDelta}시간 후",
indays: "%{timeDelta}일 후",
inweeks: "%{timeDelta}주 후",
inmonths: "%{timeDelta}개월 후",
daysago: "%{timeDelta}일 전",
weeksago: "%{timeDelta}주 전",
monthsago: "%{timeDelta}개월 전",
minutesago: "%{timeDelta}분 전",
hoursago: "%{timeDelta}시간 전",
// Suffix markers for "N unit" + marker (e.g. "3일 후" = "in 3 days",
// "2주 전" = "2 weeks ago") -- verified against CreamNuts' handleNativeDateExpressions()/processNumberPattern().
later: "후|뒤",
agosuffix: "전",
time: "시간",
now: "지금",
plusminutes: "+%{timeDelta}분",
minusminutes: "-%{timeDelta}분",
plushour: "+%{timeDelta}시간",
minushour: "-%{timeDelta}시간",
minute: "분",
hour: "시간",
day: "일",
week: "주",
month: "개월|달",
year: "년",
and: "그리고",
at: "에",
from: "부터",
to: "까지",
of: "의",
first: "첫",
} as const;

export default dict;
3 changes: 3 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface NLDSettings {
russian: boolean;
ukrainian: boolean;
chinese: boolean;
korean: boolean;

modalToggleTime: boolean;
modalToggleLink: boolean;
Expand Down Expand Up @@ -80,6 +81,7 @@ export const DEFAULT_SETTINGS: NLDSettings = {
russian: false,
ukrainian: false,
chinese: false,
korean: false,

modalToggleTime: false,
modalToggleLink: false,
Expand Down Expand Up @@ -185,6 +187,7 @@ export class NLDSettingsTab extends PluginSettingTab {
this.createLanguageSetting(containerEl, "Russian", "russian", "ru");
this.createLanguageSetting(containerEl, "Ukrainian", "ukrainian", "uk");
this.createLanguageSetting(containerEl, "Chinese (Traditional)", "chinese", "zh.hant", "partially supported");
this.createLanguageSetting(containerEl, "Korean", "korean", "ko", "partially supported");

new Setting(containerEl).setHeading().setName("Hotkey formatting settings");

Expand Down
18 changes: 10 additions & 8 deletions tests/date-suggest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,16 +461,18 @@ describe('DateSuggest', () => {
expect(suggestions).toContain(expected);
});

// Japanese and Chinese "tomorrow" are 2-character CJK words (明日 / 明天).
// Fuzzy matching deliberately excludes queries of 2 characters or fewer
// to avoid noisy matches, so these are never fuzzy-matched -- documented
// as the actual, intentional behavior rather than a gap to fix, since a
// single-character-level "typo" doesn't map onto CJK input the way it
// does for Latin/Cyrillic scripts (characters are typically chosen from
// an IME candidate list, not typed letter-by-letter).
it('does not fuzzy-match Japanese/Chinese short CJK words (by design, queries this short skip the fuzzy fallback)', () => {
// Japanese, Chinese, and Korean "tomorrow" are all 2-character words
// (明日 / 明天 / 내일). Fuzzy matching deliberately excludes queries of 2
// characters or fewer to avoid noisy matches, so these are never
// fuzzy-matched -- documented as the actual, intentional behavior rather
// than a gap to fix, since a single-character-level "typo" doesn't map
// onto CJK/Korean input the way it does for Latin/Cyrillic scripts
// (characters are typically chosen from an IME candidate list, not
// typed letter-by-letter).
it('does not fuzzy-match Japanese/Chinese/Korean short words (by design, queries this short skip the fuzzy fallback)', () => {
expect((suggest as any).defaultSuggestions('明後', 'ja')).toEqual([]);
expect((suggest as any).defaultSuggestions('明后', 'zh.hant')).toEqual([]);
expect((suggest as any).defaultSuggestions('일내', 'ko')).toEqual([]);
});
});

Expand Down
92 changes: 91 additions & 1 deletion tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('NLDParser', () => {
});

// Reset parser with all languages
parser = new NLDParser(['en', 'fr', 'de', 'pt', 'nl', 'es', 'it', 'ja', 'ru', 'uk', 'zh.hant']);
parser = new NLDParser(['en', 'fr', 'de', 'pt', 'nl', 'es', 'it', 'ja', 'ru', 'uk', 'zh.hant', 'ko']);
});

describe('Priority 1: Basic expressions (today, tomorrow, yesterday, now)', () => {
Expand Down Expand Up @@ -258,6 +258,36 @@ describe('NLDParser', () => {
expect(Math.abs(result.getTime() - now.getTime())).toBeLessThan(1000);
});
});

// Korean has no chrono-node locale at all (verified: chrono-node ships
// de/en/es/fi/fr/it/ja/nl/pt/ru/sv/uk/zh, no ko), so -- like Chinese --
// these rely entirely on the plugin's own regex patterns with no
// chrono-node fallback if a pattern doesn't match.
describe('Korean', () => {
it("should parse '오늘'", () => {
const result = parser.getParsedDate('오늘', weekStartPreference);
const today = moment().startOf('day');
expectSameDate(result, today, 'day');
});

it("should parse '내일'", () => {
const result = parser.getParsedDate('내일', weekStartPreference);
const tomorrow = moment().add(1, 'days').startOf('day');
expectSameDate(result, tomorrow, 'day');
});

it("should parse '어제'", () => {
const result = parser.getParsedDate('어제', weekStartPreference);
const yesterday = moment().subtract(1, 'days').startOf('day');
expectSameDate(result, yesterday, 'day');
});

it("should parse '지금'", () => {
const result = parser.getParsedDate('지금', weekStartPreference);
const now = new Date();
expect(Math.abs(result.getTime() - now.getTime())).toBeLessThan(1000);
});
});
});

describe('Priority 1: Simple relative expressions (in 2 days, in 2 weeks)', () => {
Expand Down Expand Up @@ -436,6 +466,35 @@ describe('NLDParser', () => {
expectSameDate(result, expected, 'minute', 60);
});
});

// Korean phrases relative time in suffix position too (number + unit +
// 후/뒤 "later"), same mechanism as Chinese's 後/后 above -- vocabulary
// verified against CreamNuts' nldates-obsidian-korean (MIT).
describe('Korean', () => {
it("should parse '3일 후'", () => {
const result = parser.getParsedDate('3일 후', weekStartPreference);
const expected = moment().add(3, 'days');
expectSameDate(result, expected, 'day');
});

it("should parse '2주 후'", () => {
const result = parser.getParsedDate('2주 후', weekStartPreference);
const expected = moment().add(2, 'weeks');
expectSameDate(result, expected, 'day');
});

it("should parse '3개월 후'", () => {
const result = parser.getParsedDate('3개월 후', weekStartPreference);
const expected = moment().add(3, 'months');
expectSameDate(result, expected, 'day');
});

it("should parse '5분 후'", () => {
const result = parser.getParsedDate('5분 후', weekStartPreference);
const expected = moment().add(5, 'minutes');
expectSameDate(result, expected, 'minute', 60);
});
});
});

describe('Priority 2: Combinations (in 2 weeks and 3 days)', () => {
Expand Down Expand Up @@ -792,6 +851,31 @@ describe('NLDParser', () => {
expect(moment(result).isSame(expected, 'day')).toBe(true);
});
});

// Unlike the suffix-style relative expressions above, Korean's this/
// next/last prefixes ("이번"/"다음"/"지난") come *before* the weekday,
// the same word order as English -- so this uses the same prefix
// mechanism as every other language here, not the suffix one.
describe('Korean', () => {
it("should parse '다음 월요일'", () => {
const result = parser.getParsedDate('다음 월요일', weekStartPreference);
expect(moment(result).day()).toBe(1); // Monday
});

it("should parse '이번 월요일' as Monday of the current week", () => {
const result = parser.getParsedDate('이번 월요일', weekStartPreference);
expect(moment(result).day()).toBe(1); // Monday
const expected = moment().day(1);
expect(moment(result).isSame(expected, 'day')).toBe(true);
});

it("should parse '지난 월요일' as Monday of the previous week", () => {
const result = parser.getParsedDate('지난 월요일', weekStartPreference);
expect(moment(result).day()).toBe(1); // Monday
const expected = moment().day(1).subtract(1, 'week');
expect(moment(result).isSame(expected, 'day')).toBe(true);
});
});
});

describe('Date ranges (from Monday to Friday, next week)', () => {
Expand Down Expand Up @@ -1095,6 +1179,12 @@ describe('NLDParser', () => {
expectSameDate(parser.getParsedDate('30分鐘前', weekStartPreference), moment().subtract(30, 'minutes'), 'minute', 2);
});

it("should parse Korean '전' suffix expressions, which have no chrono-node fallback either", () => {
expectSameDate(parser.getParsedDate('3일 전', weekStartPreference), moment().subtract(3, 'days'), 'day');
expectSameDate(parser.getParsedDate('2주 전', weekStartPreference), moment().subtract(2, 'weeks'), 'day');
expectSameDate(parser.getParsedDate('30분 전', weekStartPreference), moment().subtract(30, 'minutes'), 'minute', 2);
});

it("should parse Portuguese 'há' prefix expressions", () => {
expectSameDate(parser.getParsedDate('há 3 dias', weekStartPreference), moment().subtract(3, 'days'), 'day');
});
Expand Down
Loading