diff --git a/css/styles.css b/css/styles.css index c960d82..a969a2d 100644 --- a/css/styles.css +++ b/css/styles.css @@ -65,6 +65,36 @@ h2 { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.08); } +.dialog-box-lng { + text-align: center; +} + +.dialog-box-lng p, +.dialog-box-lng select { + display: inline; +} + +.dialog-box-lng select { + font-size: 1rem; + padding: 4px; + font-weight: bold; + border-radius: 4px; + border-color: #DEF2D9; + color: #837865; + cursor: pointer; + transition: 0.2s all; +} + +.dialog-box-lng select:hover { + background-color: #EBFEFD; + border-color: #5ECEDB; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16); +} + +.dialog-box-lng select:focus { + outline: none; +} + .dialog-box p { font-family: 'Raleway', sans-serif; font-weight: 800; @@ -267,10 +297,10 @@ input[type=number] { color: #FFF; } -.reset-button { +.button { + color: #686868; font-family: inherit; font-weight: bold; - color: #FFBABA; padding: 8px 16px; border-width: 0px; border-radius: 40px; @@ -281,13 +311,16 @@ input[type=number] { margin: 16px auto; } -.reset-button:hover { +.button:hover { transform: scale(1.1); cursor: pointer; opacity: 1; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05), 0 3px 6px rgba(0, 0, 0, 0.09); } +.button.button--reset { + color: #E45B5B; +} .table-wrapper { display: inline-block; @@ -348,7 +381,7 @@ input[type=number] { #turnipTable td { white-space: nowrap; - max-width: 100px; + max-width: 150px; padding: 6px 4px; text-align: center; border-right: 1px solid rgba(0, 0, 0, 0.03); @@ -402,7 +435,6 @@ input[type=number] { justify-content: center; } - .waves { position: relative; width: 100%; @@ -412,6 +444,74 @@ input[type=number] { max-height: 150px; } +#permalink-input { + display: none; + position: fixed; +} + +.permalink { + display: none; + white-space: nowrap; + font-size: 18px; + user-select: none; + cursor: pointer; +} + +.permalink .fa-copy { + margin: 0 8px; + height: 20px; + color: #0AB5CD; +} + +/* The snackbar - position it at the bottom and in the middle of the screen */ +#snackbar { + visibility: hidden; /* Hidden by default. Visible on click */ + min-width: 250px; /* Set a default minimum width */ + background-color: #FFFAE5; /* Black background color */ + font-family: 'Raleway', sans-serif; + font-weight: 800; + font-size: 1rem; + color: #837865; + letter-spacing: 0.2px; + line-height: 1.8rem; + text-align: center; /* Centered text */ + border-radius: 40px; /* Rounded borders */ + padding: 16px 24px; /* Padding */ + position: fixed; /* Sit on top of the screen */ + z-index: 1; /* Add a z-index if needed */ + bottom: 30px; /* 30px from the bottom */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.08); +} + +/* Show the snackbar when clicking on a button (class added with JavaScript) */ +#snackbar.show { + visibility: visible; /* Show the snackbar */ + /* Add animation: Take 0.5 seconds to fade in and out the snackbar. + However, delay the fade out process for 2.5 seconds */ + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +/* Animations to fade the snackbar in and out */ +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} /* Animation */ .parallax>use { diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..b06b446 Binary files /dev/null and b/favicon.ico differ diff --git a/img/192.png b/img/192.png deleted file mode 100644 index 68b4ae5..0000000 Binary files a/img/192.png and /dev/null differ diff --git a/img/512.png b/img/512.png deleted file mode 100644 index a59c3ec..0000000 Binary files a/img/512.png and /dev/null differ diff --git a/img/favicon-128.png b/img/favicon-128.png new file mode 100644 index 0000000..08a2116 Binary files /dev/null and b/img/favicon-128.png differ diff --git a/img/favicon-152.png b/img/favicon-152.png new file mode 100644 index 0000000..4137fda Binary files /dev/null and b/img/favicon-152.png differ diff --git a/img/favicon-167.png b/img/favicon-167.png new file mode 100644 index 0000000..cee09d8 Binary files /dev/null and b/img/favicon-167.png differ diff --git a/img/favicon-180.png b/img/favicon-180.png new file mode 100644 index 0000000..47147bc Binary files /dev/null and b/img/favicon-180.png differ diff --git a/img/favicon-192.png b/img/favicon-192.png new file mode 100644 index 0000000..876941d Binary files /dev/null and b/img/favicon-192.png differ diff --git a/img/favicon-196.png b/img/favicon-196.png new file mode 100644 index 0000000..1d27fa5 Binary files /dev/null and b/img/favicon-196.png differ diff --git a/img/favicon-32.png b/img/favicon-32.png new file mode 100644 index 0000000..764bb36 Binary files /dev/null and b/img/favicon-32.png differ diff --git a/img/favicon-512.png b/img/favicon-512.png new file mode 100644 index 0000000..124adf7 Binary files /dev/null and b/img/favicon-512.png differ diff --git a/index.html b/index.html index 0bfb4dd..2b93582 100644 --- a/index.html +++ b/index.html @@ -10,8 +10,20 @@ + + + + + + + + + + + + + - @@ -33,13 +45,10 @@
-

Daisy Mae

-

Hello, and welcome to the Turnip Prophet app on your Nook Phone.

-

This app lets you track your island's turnip prices daily, but you'll have to put the prices in yourself! -

-

After that, the Turnip Prophet app will magically predict the turnip prices you'll have for the rest of - the week. -

+

+

+

+

@@ -66,130 +75,133 @@
-
First-Time Buyer
+
- +
- + - +
-
Previous Pattern
+
- +
- + - + - + - + - +
-
Sunday
+
- +
- AM - 8:00 am to 11:59 am - PM - 12:00 pm to 10:00 pm + +
-
Monday
+
- +
- +
-
Tuesday
+
- +
- +
-
Wednesday
+
- +
- +
-
Thursday
+
- +
- +
-
Friday
+
- +
- +
-
Saturday
+
- +
- +
- + + +
-

Output

+

@@ -199,55 +211,55 @@ - - - + + + - - + + @@ -274,34 +286,34 @@
-

Daisy Mae

-

After you've listed some turnip prices, the Turnip Prophet will run some numbers and display the different - possible patterns that your island may experience.

-

This app is still in development, but will improve over time!

+

+

+

-

Daisy Mae

-

- None of this would have been possible without Ninji's work figuring out just how Timmy - and Tommy value their turnips. -

-

- Support, comments and contributions are available through Github -

-

Oh! And let's not forget to thank those who have contributed so far!

-

- Contributors: -

+

+

+

+

+

:

+
+

: +
+
Some text some message...
+ + + + + + - diff --git a/js/chart.js b/js/chart.js index 9ea48c5..83e408c 100644 --- a/js/chart.js +++ b/js/chart.js @@ -22,17 +22,17 @@ function update_chart(input_data, possibilities) { datasets = [ { - label: "Input Price", + label: i18next.t("output.chart.input"), data: input_data.slice(1), fill: false, }, { - label: "Minimum", + label: i18next.t("output.chart.minimum"), data: possibilities[0].prices.slice(1).map(day => day.min), fill: false, }, { - label: "Maximum", + label: i18next.t("output.chart.maximum"), data: possibilities[0].prices.slice(1).map(day => day.max), fill: "-1", }, @@ -45,7 +45,7 @@ function update_chart(input_data, possibilities) { chart_instance = new Chart(ctx, { data: { datasets: datasets, - labels: ["Sunday", "Mon AM", "Mon PM", "Tue AM", "Tue PM", "Wed AM", "Wed PM", "Thu AM", "Thu PM", "Fri AM", "Fri PM", "Sat AM", "Sat PM"], + labels: [i18next.t("weekdays.sunday"), i18next.t("weekdays.abr.monday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.monday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.tuesday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.tuesday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.wednesday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.wednesday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.thursday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.thursday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.friday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.friday") + " " + i18next.t("times.afternoon"), i18next.t("weekdays.abr.saturday") + " " + i18next.t("times.morning"), i18next.t("weekdays.abr.saturday") + " " + i18next.t("times.afternoon")], }, options: chart_options, type: "line", diff --git a/js/predictions.js b/js/predictions.js index f1410a6..a6d00d2 100644 --- a/js/predictions.js +++ b/js/predictions.js @@ -9,13 +9,6 @@ const PATTERN = { SMALL_SPIKE: 3, }; -const PATTERN_COUNTS = { - [PATTERN.FLUCTUATING]: 56, - [PATTERN.LARGE_SPIKE]: 7, - [PATTERN.DECREASING]: 1, - [PATTERN.SMALL_SPIKE]: 8, -}; - const PROBABILITY_MATRIX = { [PATTERN.FLUCTUATING]: { [PATTERN.FLUCTUATING]: 0.20, @@ -57,10 +50,46 @@ function maximum_rate_from_given_and_base(given_price, buy_price) { return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price; } +function rate_range_from_given_and_base(given_price, buy_price) { + return [ + minimum_rate_from_given_and_base(given_price, buy_price), + maximum_rate_from_given_and_base(given_price, buy_price) + ]; +} + function get_price(rate, basePrice) { return intceil(rate * basePrice / RATE_MULTIPLIER); } +function* multiply_generator_probability(generator, probability) { + for (const it of generator) { + yield {...it, probability: it.probability * probability}; + } +} + +function clamp(x, min, max) { + return Math.min(Math.max(x, min), max); +} + +function range_length(range) { + return range[1] - range[0]; +} + +function range_intersect(range1, range2) { + if (range1[0] > range2[1] || range1[1] < range2[0]) { + return null; + } + return [Math.max(range1[0], range2[0]), Math.min(range1[1], range2[1])]; +} + +function range_intersect_length(range1, range2) { + if (range1[0] > range2[1] || range1[1] < range2[0]) { + return 0; + } + return range_length(range_intersect(range1, range2)); +} + + /* * This corresponds to the code: * for (int i = start; i < start + length; i++) @@ -69,8 +98,9 @@ function get_price(rate, basePrice) { * intceil(randfloat(rate_min / RATE_MULTIPLIER, rate_max / RATE_MULTIPLIER) * basePrice); * } * - * Would modify the predicted_prices array. - * If the given_prices won't match, returns false, otherwise returns true + * Would return the conditional probability given the given_prices, and modify + * the predicted_prices array. + * If the given_prices won't match, returns 0. */ function generate_individual_random_price( given_prices, predicted_prices, start, length, rate_min, rate_max) { @@ -78,6 +108,8 @@ function generate_individual_random_price( rate_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; + const rate_range = [rate_min, rate_max]; + let prob = 1; for (let i = start; i < start + length; i++) { let min_pred = get_price(rate_min, buy_price); @@ -85,8 +117,14 @@ function generate_individual_random_price( if (!isNaN(given_prices[i])) { if (given_prices[i] < min_pred - FUDGE_FACTOR || given_prices[i] > max_pred + FUDGE_FACTOR) { // Given price is out of predicted range, so this is the wrong pattern - return false; + return 0; } + // TODO: How to deal with probability when there's fudge factor? + // Clamp the value to be in range now so the probability won't be totally biased to fudged values. + const real_rate_range = + rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); + prob *= range_intersect_length(rate_range, real_rate_range) / + range_length(rate_range); min_pred = given_prices[i]; max_pred = given_prices[i]; } @@ -96,7 +134,144 @@ function generate_individual_random_price( max: max_pred, }); } - return true; + return prob; +} + +/* + * Probability Density Function of rates. + * Since the PDF is continuous*, we approximate it by a discrete probability function: + * the value in range [(x - 0.5), (x + 0.5)) has a uniform probability + * prob[x - value_start]; + * + * Note that we operate all rate on the (* RATE_MULTIPLIER) scale. + * + * (*): Well not really since it only takes values that "float" can represent in some form, but the + * space is too large to compute directly in JS. + */ +class PDF { + /* + * Initialize a PDF in range [a, b], a and b can be non-integer. + * if uniform is true, then initialize the probability to be uniform, else initialize to a + * all-zero (invalid) PDF. + */ + constructor(a, b, uniform = true) { + this.value_start = Math.round(a); + this.value_end = Math.round(b); + const range = [a, b]; + const total_length = range_length(range); + this.prob = Array(this.value_end - this.value_start + 1); + if (uniform) { + for (let i = 0; i < this.prob.length; i++) { + this.prob[i] = + range_intersect_length(this.range_of(i), range) / total_length; + } + } + } + + range_of(idx) { + // TODO: consider doing the "exclusive end" properly. + return [this.value_start + idx - 0.5, this.value_start + idx + 0.5 - 1e-9]; + } + + min_value() { + return this.value_start - 0.5; + } + + max_value() { + return this.value_end + 0.5 - 1e-9; + } + + normalize() { + const total_probability = this.prob.reduce((acc, it) => acc + it, 0); + for (let i = 0; i < this.prob.length; i++) { + this.prob[i] /= total_probability; + } + } + + /* + * Limit the values to be in the range, and return the probability that the value was in this + * range. + */ + range_limit(range) { + let [start, end] = range; + start = Math.max(start, this.min_value()); + end = Math.min(end, this.max_value()); + if (start >= end) { + // Set this to invalid values + this.value_start = this.value_end = 0; + this.prob = []; + return 0; + } + + let prob = 0; + const start_idx = Math.round(start) - this.value_start; + const end_idx = Math.round(end) - this.value_start; + for (let i = start_idx; i <= end_idx; i++) { + const bucket_prob = this.prob[i] * range_intersect_length(this.range_of(i), range); + this.prob[i] = bucket_prob; + prob += bucket_prob; + } + + this.prob = this.prob.slice(start_idx, end_idx + 1); + this.value_start = Math.round(start); + this.value_end = Math.round(end); + this.normalize(); + + return prob; + } + + /* + * Subtract the PDF by a uniform distribution in [rate_decay_min, rate_decay_max] + * + * For simplicity, we assume that rate_decay_min and rate_decay_max are both integers. + */ + decay(rate_decay_min, rate_decay_max) { + const ret = new PDF( + this.min_value() - rate_decay_max, this.max_value() - rate_decay_min, false); + /* + // O(n^2) naive algorithm for reference, which would be too slow. + for (let i = this.value_start; i <= this.value_end; i++) { + const unit_prob = this.prob[i - this.value_start] / (rate_decay_max - rate_decay_min) / 2; + for (let j = rate_decay_min; j < rate_decay_max; j++) { + // ([i - 0.5, i + 0.5] uniform) - ([j, j + 1] uniform) + // -> [i - j - 1.5, i + 0.5 - j] with a triangular PDF + // -> approximate by + // [i - j - 1.5, i - j - 0.5] uniform & + // [i - j - 0.5, i - j + 0.5] uniform + ret.prob[i - j - 1 - ret.value_start] += unit_prob; // Part A + ret.prob[i - j - ret.value_start] += unit_prob; // Part B + } + } + */ + // Transform to "CDF" + for (let i = 1; i < this.prob.length; i++) { + this.prob[i] += this.prob[i - 1]; + } + // Return this.prob[l - this.value_start] + ... + this.prob[r - 1 - this.value_start]; + // This assume that this.prob is already transformed to "CDF". + const sum = (l, r) => { + l -= this.value_start; + r -= this.value_start; + if (l < 0) l = 0; + if (r > this.prob.length) r = this.prob.length; + if (l >= r) return 0; + return this.prob[r - 1] - (l == 0 ? 0 : this.prob[l - 1]); + }; + + for (let x = 0; x < ret.prob.length; x++) { + // i - j - 1 - ret.value_start == x (Part A) + // -> i = x + j + 1 + ret.value_start, j in [rate_decay_min, rate_decay_max) + ret.prob[x] = sum(x + rate_decay_min + 1 + ret.value_start, x + rate_decay_max + 1 + ret.value_start); + + // i - j - ret.value_start == x (Part B) + // -> i = x + j + ret.value_start, j in [rate_decay_min, rate_decay_max) + ret.prob[x] += sum(x + rate_decay_min + ret.value_start, x + rate_decay_max + ret.value_start); + } + this.prob = ret.prob; + this.value_start = ret.value_start; + this.value_end = ret.value_end; + this.normalize(); + } } /* @@ -108,33 +283,37 @@ function generate_individual_random_price( * rate -= randfloat(rate_decay_min, rate_decay_max); * } * - * Would modify the predicted_prices array. - * If the given_prices won't match, returns false, otherwise returns true + * Would return the conditional probability given the given_prices, and modify + * the predicted_prices array. + * If the given_prices won't match, returns 0. */ function generate_decreasing_random_price( - given_prices, predicted_prices, start, length, rate_min, - rate_max, rate_decay_min, rate_decay_max) { - rate_min *= RATE_MULTIPLIER; - rate_max *= RATE_MULTIPLIER; + given_prices, predicted_prices, start, length, start_rate_min, + start_rate_max, rate_decay_min, rate_decay_max) { + start_rate_min *= RATE_MULTIPLIER; + start_rate_max *= RATE_MULTIPLIER; rate_decay_min *= RATE_MULTIPLIER; rate_decay_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; + let rate_pdf = new PDF(start_rate_min, start_rate_max); + let prob = 1; for (let i = start; i < start + length; i++) { - let min_pred = get_price(rate_min, buy_price); - let max_pred = get_price(rate_max, buy_price); + let min_pred = get_price(rate_pdf.min_value(), buy_price); + let max_pred = get_price(rate_pdf.max_value(), buy_price); if (!isNaN(given_prices[i])) { if (given_prices[i] < min_pred - FUDGE_FACTOR || given_prices[i] > max_pred + FUDGE_FACTOR) { // Given price is out of predicted range, so this is the wrong pattern - return false; + return 0; } - if (given_prices[i] >= min_pred || given_prices[i] <= max_pred) { - // The value in the FUDGE_FACTOR range is ignored so the rate range would not be empty. - const real_rate_min = minimum_rate_from_given_and_base(given_prices[i], buy_price); - const real_rate_max = maximum_rate_from_given_and_base(given_prices[i], buy_price); - rate_min = Math.max(rate_min, real_rate_min); - rate_max = Math.min(rate_max, real_rate_max); + // TODO: How to deal with probability when there's fudge factor? + // Clamp the value to be in range now so the probability won't be totally biased to fudged values. + const real_rate_range = + rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); + prob *= rate_pdf.range_limit(real_rate_range); + if (prob == 0) { + return 0; } min_pred = given_prices[i]; max_pred = given_prices[i]; @@ -145,10 +324,9 @@ function generate_decreasing_random_price( max: max_pred, }); - rate_min -= rate_decay_max; - rate_max -= rate_decay_min; + rate_pdf.decay(rate_decay_min, rate_decay_max); } - return true; + return prob; } @@ -159,8 +337,9 @@ function generate_decreasing_random_price( * sellPrices[work++] = intceil(rate * basePrice); * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; * - * Would modify the predicted_prices array. - * If the given_prices won't match, returns false, otherwise returns true + * Would return the conditional probability given the given_prices, and modify + * the predicted_prices array. + * If the given_prices won't match, returns 0. */ function generate_peak_price( given_prices, predicted_prices, start, rate_min, rate_max) { @@ -168,15 +347,86 @@ function generate_peak_price( rate_max *= RATE_MULTIPLIER; const buy_price = given_prices[0]; + let prob = 1; + let rate_range = [rate_min, rate_max]; + + // * Calculate the probability first. + // Prob(middle_price) + const middle_price = given_prices[start + 1]; + if (!isNaN(middle_price)) { + const min_pred = get_price(rate_min, buy_price); + const max_pred = get_price(rate_max, buy_price); + if (middle_price < min_pred - FUDGE_FACTOR || middle_price > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return 0; + } + // TODO: How to deal with probability when there's fudge factor? + // Clamp the value to be in range now so the probability won't be totally biased to fudged values. + const real_rate_range = + rate_range_from_given_and_base(clamp(middle_price, min_pred, max_pred), buy_price); + prob *= range_intersect_length(rate_range, real_rate_range) / + range_length(rate_range); + if (prob == 0) { + return 0; + } + + rate_range = range_intersect(rate_range, real_rate_range); + } + + const left_price = given_prices[start]; + const right_price = given_prices[start + 2]; + // Prob(left_price | middle_price), Prob(right_price | middle_price) + // + // A = rate_range[0], B = rate_range[1], C = rate_min, X = rate, Y = randfloat(rate_min, rate) + // rate = randfloat(A, B); sellPrices[work++] = intceil(randfloat(C, rate) * basePrice) - 1; + // + // => X->U(A,B), Y->U(C,X), Y-C->U(0,X-C), Y-C->U(0,1)*(X-C), Y-C->U(0,1)*U(A-C,B-C), + // let Z=Y-C, Z1=A-C, Z2=B-C, Z->U(0,1)*U(Z1,Z2) + // Prob(Z<=t) = integral_{x=0}^{1} [min(t/x,Z2)-min(t/x,Z1)]/ (Z2-Z1) + // let F(t, ZZ) = integral_{x=0}^{1} min(t/x, ZZ) + // 1. if ZZ < t, then min(t/x, ZZ) = ZZ -> F(t, ZZ) = ZZ + // 2. if ZZ >= t, then F(t, ZZ) = integral_{x=0}^{t/ZZ} ZZ + integral_{x=t/ZZ}^{1} t/x + // = t - t log(t/ZZ) + // Prob(Z<=t) = (F(t, Z2) - F(t, Z1)) / (Z2 - Z1) + // Prob(Y<=t) = Prob(Z>=t-C) + for (const price of [left_price, right_price]) { + if (isNaN(price)) { + continue; + } + const min_pred = get_price(rate_min, buy_price) - 1; + const max_pred = get_price(rate_range[1], buy_price) - 1; + if (price < min_pred - FUDGE_FACTOR || price > max_pred + FUDGE_FACTOR) { + // Given price is out of predicted range, so this is the wrong pattern + return 0; + } + // TODO: How to deal with probability when there's fudge factor? + // Clamp the value to be in range now so the probability won't be totally biased to fudged values. + const rate2_range = rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price); + const F = (t, ZZ) => { + if (t <= 0) { + return 0; + } + return ZZ < t ? ZZ : t - t * (Math.log(t) - Math.log(ZZ)); + }; + const [A, B] = rate_range; + const C = rate_min; + const Z1 = A - C; + const Z2 = B - C; + const PY = (t) => (F(t - C, Z2) - F(t - C, Z1)) / (Z2 - Z1); + prob *= PY(rate2_range[1]) - PY(rate2_range[0]); + if (prob == 0) { + return 0; + } + } + // * Then generate the real predicted range. + // We're doing things in different order then how we calculate probability, + // since forward prediction is more useful here. + // // Main spike 1 min_pred = get_price(rate_min, buy_price) - 1; max_pred = get_price(rate_max, buy_price) - 1; if (!isNaN(given_prices[start])) { - if (given_prices[start] < min_pred - FUDGE_FACTOR || given_prices[start] > max_pred + FUDGE_FACTOR) { - // Given price is out of predicted range, so this is the wrong pattern - return false; - } min_pred = given_prices[start]; max_pred = given_prices[start]; } @@ -187,12 +437,8 @@ function generate_peak_price( // Main spike 2 min_pred = predicted_prices[start].min; - max_pred = intceil(2.0 * buy_price); + max_pred = get_price(rate_max, buy_price); if (!isNaN(given_prices[start + 1])) { - if (given_prices[start + 1] < min_pred - FUDGE_FACTOR || given_prices[start + 1] > max_pred + FUDGE_FACTOR) { - // Given price is out of predicted range, so this is the wrong pattern - return false; - } min_pred = given_prices[start + 1]; max_pred = given_prices[start + 1]; } @@ -202,13 +448,9 @@ function generate_peak_price( }); // Main spike 3 - min_pred = intceil(1.4 * buy_price) - 1; + min_pred = get_price(rate_min, buy_price) - 1; max_pred = predicted_prices[start + 1].max - 1; if (!isNaN(given_prices[start + 2])) { - if (given_prices[start + 2] < min_pred - FUDGE_FACTOR || given_prices[start + 2] > max_pred + FUDGE_FACTOR) { - // Given price is out of predicted range, so this is the wrong pattern - return false; - } min_pred = given_prices[start + 2]; max_pred = given_prices[start + 2]; } @@ -217,7 +459,7 @@ function generate_peak_price( max: max_pred, }); - return true; + return prob; } function* @@ -271,31 +513,36 @@ function* max: buy_price, }, ]; + let probability = 1; // High Phase 1 - if (!generate_individual_random_price( - given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4)) { + probability *= generate_individual_random_price( + given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4); + if (probability == 0) { return; } // Dec Phase 1 - if (!generate_decreasing_random_price( - given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len, - 0.6, 0.8, 0.04, 0.1)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len, + 0.6, 0.8, 0.04, 0.1); + if (probability == 0) { return; } // High Phase 2 - if (!generate_individual_random_price(given_prices, predicted_prices, - 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4)) { + probability *= generate_individual_random_price(given_prices, predicted_prices, + 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4); + if (probability == 0) { return; } // Dec Phase 2 - if (!generate_decreasing_random_price( - given_prices, predicted_prices, - 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len, - dec_phase_2_len, 0.6, 0.8, 0.04, 0.1)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, + 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len, + dec_phase_2_len, 0.6, 0.8, 0.04, 0.1); + if (probability == 0) { return; } @@ -306,16 +553,17 @@ function* const prev_length = 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len; - if (!generate_individual_random_price( - given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, - 1.4)) { + probability *= generate_individual_random_price( + given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4); + if (probability == 0) { return; } yield { - pattern_description: "Fluctuating", + pattern_description: i18next.t("patterns.fluctuating"), pattern_number: 0, - prices: predicted_prices + prices: predicted_prices, + probability, }; } @@ -330,7 +578,9 @@ function* generate_pattern_0(given_prices) { for (var dec_phase_1_len = 2; dec_phase_1_len < 4; dec_phase_1_len++) { for (var high_phase_1_len = 0; high_phase_1_len < 7; high_phase_1_len++) { for (var high_phase_3_len = 0; high_phase_3_len < (7 - high_phase_1_len - 1 + 1); high_phase_3_len++) { - yield* generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len); + yield* multiply_generator_probability( + generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len), + 1 / (4 - 2) / 7 / (7 - high_phase_1_len)); } } } @@ -369,10 +619,11 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) { max: buy_price, }, ]; + let probability = 1; - if (!generate_decreasing_random_price( - given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, - 0.05)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05); + if (probability == 0) { return; } @@ -380,22 +631,24 @@ function* generate_pattern_1_with_peak(given_prices, peak_start) { min_randoms = [0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4] max_randoms = [1.4, 2.0, 6.0, 2.0, 1.4, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9] for (let i = peak_start; i < 14; i++) { - if (!generate_individual_random_price( - given_prices, predicted_prices, i, 1, min_randoms[i - peak_start], - max_randoms[i - peak_start])) { + probability *= generate_individual_random_price( + given_prices, predicted_prices, i, 1, min_randoms[i - peak_start], + max_randoms[i - peak_start]); + if (probability == 0) { return; } } yield { - pattern_description: "Large spike", + pattern_description: i18next.t("patterns.large-spike"), pattern_number: 1, - prices: predicted_prices + prices: predicted_prices, + probability, }; } function* generate_pattern_1(given_prices) { for (var peak_start = 3; peak_start < 10; peak_start++) { - yield* generate_pattern_1_with_peak(given_prices, peak_start); + yield* multiply_generator_probability(generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3)); } } @@ -424,16 +677,19 @@ function* generate_pattern_2(given_prices) { max: buy_price, }, ]; + let probability = 1; - if (!generate_decreasing_random_price( - given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05); + if (probability == 0) { return; } yield { - pattern_description: "Decreasing", + pattern_description: i18next.t("patterns.decreasing"), pattern_number: 2, - prices: predicted_prices + prices: predicted_prices, + probability, }; } @@ -482,109 +738,128 @@ function* generate_pattern_3_with_peak(given_prices, peak_start) { ]; let probability = 1; - if (!generate_decreasing_random_price( - given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, - 0.05)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, 0.05); + if (probability == 0) { return; } // The peak - if (!generate_individual_random_price( - given_prices, predicted_prices, peak_start, 2, 0.9, 1.4)) { + probability *= generate_individual_random_price( + given_prices, predicted_prices, peak_start, 2, 0.9, 1.4); + if (probability == 0) { return; } - if (!generate_peak_price( - given_prices, predicted_prices, peak_start + 2, 1.4, 2.0)) { + probability *= generate_peak_price( + given_prices, predicted_prices, peak_start + 2, 1.4, 2.0); + if (probability == 0) { return; } if (peak_start + 5 < 14) { - if (!generate_decreasing_random_price( - given_prices, predicted_prices, peak_start + 5, - 14 - (peak_start + 5), 0.4, 0.9, 0.03, 0.05)) { + probability *= generate_decreasing_random_price( + given_prices, predicted_prices, peak_start + 5, 14 - (peak_start + 5), + 0.4, 0.9, 0.03, 0.05); + if (probability == 0) { return; } } yield { - pattern_description: "Small spike", + pattern_description: i18next.t("patterns.small-spike"), pattern_number: 3, - prices: predicted_prices + prices: predicted_prices, + probability, }; } function* generate_pattern_3(given_prices) { - for (var peak_start = 2; peak_start < 10; peak_start++) { - yield* generate_pattern_3_with_peak(given_prices, peak_start); + for (let peak_start = 2; peak_start < 10; peak_start++) { + yield* multiply_generator_probability(generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2)); } } -function* generate_possibilities(sell_prices, first_buy) { +function get_transition_probability(previous_pattern) { + if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) { + // TODO: Fill the steady state pattern (https://github.com/mikebryant/ac-nh-turnip-prices/pull/90) here. + return [0.346278, 0.247363, 0.147607, 0.258752]; + } + + return PROBABILITY_MATRIX[previous_pattern]; +} + +function* generate_all_patterns(sell_prices, previous_pattern) { + const generate_pattern_fns = [generate_pattern_0, generate_pattern_1, generate_pattern_2, generate_pattern_3]; + const transition_probability = get_transition_probability(previous_pattern); + + for (let i = 0; i < 4; i++) { + yield* multiply_generator_probability(generate_pattern_fns[i](sell_prices), transition_probability[i]); + } +} + +function* generate_possibilities(sell_prices, first_buy, previous_pattern) { if (first_buy || isNaN(sell_prices[0])) { for (var buy_price = 90; buy_price <= 110; buy_price++) { sell_prices[0] = sell_prices[1] = buy_price; if (first_buy) { yield* generate_pattern_3(sell_prices); } else { - yield* generate_pattern_0(sell_prices); - yield* generate_pattern_1(sell_prices); - yield* generate_pattern_2(sell_prices); - yield* generate_pattern_3(sell_prices); + // All buy prices are equal probability and we're at the outmost layer, + // so don't need to multiply_generator_probability here. + yield* generate_all_patterns(sell_prices, previous_pattern) } } } else { - yield* generate_pattern_0(sell_prices); - yield* generate_pattern_1(sell_prices); - yield* generate_pattern_2(sell_prices); - yield* generate_pattern_3(sell_prices); + yield* generate_all_patterns(sell_prices, previous_pattern) } } -function row_probability(possibility, previous_pattern) { - return PROBABILITY_MATRIX[previous_pattern][possibility.pattern_number] / PATTERN_COUNTS[possibility.pattern_number]; -} +function analyze_possibilities(sell_prices, first_buy, previous_pattern) { + const generated_possibilities = Array.from(generate_possibilities(sell_prices, first_buy, previous_pattern)); + console.log(generated_possibilities); -function get_probabilities(possibilities, previous_pattern) { - if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) { - return possibilities + const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0); + for (const it of generated_possibilities) { + it.probability /= total_probability; } - var max_percent = possibilities.map(function (poss) { - return row_probability(poss, previous_pattern); - }).reduce(function (prev, current) { - return prev + current; - }, 0); - - return possibilities.map(function (poss) { - poss.probability = row_probability(poss, previous_pattern) / max_percent; - return poss; - }); -} - -function analyze_possibilities(sell_prices, first_buy, previous_pattern) { - generated_possibilities = Array.from(generate_possibilities(sell_prices, first_buy)); - generated_possibilities = get_probabilities(generated_possibilities, previous_pattern); - for (let poss of generated_possibilities) { var weekMins = []; var weekMaxes = []; for (let day of poss.prices.slice(2)) { - weekMins.push(day.min); - weekMaxes.push(day.max); + // Check for a future date by checking for a range of prices + if(day.min !== day.max){ + weekMins.push(day.min); + weekMaxes.push(day.max); + } else { + // If we find a set price after one or more ranged prices, the user has missed a day. Discard that data and start again. + weekMins = []; + weekMaxes = []; + } + } + if (!weekMins.length && !weekMaxes.length) { + weekMins.push(poss.prices[poss.prices.length -1].min); + weekMaxes.push(poss.prices[poss.prices.length -1].max); } poss.weekGuaranteedMinimum = Math.max(...weekMins); poss.weekMax = Math.max(...weekMaxes); } + category_totals = {} + for (let i of [0, 1, 2, 3]) { + category_totals[i] = generated_possibilities + .filter(value => value.pattern_number == i) + .map(value => value.probability) + .reduce((previous, current) => previous + current, 0); + } + + for (let pos of generated_possibilities) { + pos.category_total_probability = category_totals[pos.pattern_number]; + } + generated_possibilities.sort((a, b) => { - if (a.weekMax < b.weekMax) { - return 1; - } else if (a.weekMax > b.weekMax) { - return -1; - } else { - return 0; - } + return b.category_total_probability - a.category_total_probability || b.probability - a.probability; }); global_min_max = []; @@ -605,11 +880,11 @@ function analyze_possibilities(sell_prices, first_buy, previous_pattern) { } generated_possibilities.unshift({ - pattern_description: "All patterns", + pattern_description: i18next.t("patterns.all"), pattern_number: 4, prices: global_min_max, weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)), - weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)), + weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)) }); return generated_possibilities; diff --git a/js/scripts.js b/js/scripts.js index de1351d..25d0f89 100644 --- a/js/scripts.js +++ b/js/scripts.js @@ -40,6 +40,9 @@ const sell_inputs = getSellFields() const buy_input = $("#buy") const first_buy_radios = getFirstBuyRadios() const previous_pattern_radios = getPreviousPatternRadios() +const permalink_input = $('#permalink-input') +const permalink_button = $('#permalink-btn') +const snackbar = $('#snackbar') //Functions const fillFields = function (prices, first_buy, previous_pattern) { @@ -74,15 +77,20 @@ const initialize = function () { } else { fillFields(prices, first_buy, previous_pattern) } - $(document).trigger("input"); } catch (e) { console.error(e); } + $(document).trigger("input"); + + $("#permalink-btn").on("click", copyPermalink) + $("#reset").on("click", function () { - sell_inputs.forEach(input => input.value = '') - fillFields([], false, -1) - update() + if (window.confirm(i18next.t("prices.reset-warning"))) { + sell_inputs.forEach(input => input.value = '') + fillFields([], false, -1) + update() + } }) } @@ -258,13 +266,22 @@ const calculateOutput = function (data, first_buy, previous_pattern) { } let output_possibilities = ""; let analyzed_possibilities = analyze_possibilities(data, first_buy, previous_pattern); + previous_pattern_number = "" for (let poss of analyzed_possibilities) { var out_line = "" - out_line += ``; + if (previous_pattern_number != poss.pattern_number) { + previous_pattern_number = poss.pattern_number + pattern_count = analyzed_possibilities + .filter(val => val.pattern_number == poss.pattern_number) + .length + percentage_display = percent => Number.isFinite(percent) ? ((percent * 100).toPrecision(3) + '%') : '—' + out_line += ``; + } + out_line += ``; for (let day of poss.prices.slice(1)) { var price_class = getPriceClass(day.max); if (day.min !== day.max) { - out_line += ``; + out_line += ``; } else { out_line += ``; } @@ -280,6 +297,53 @@ const calculateOutput = function (data, first_buy, previous_pattern) { update_chart(data, analyzed_possibilities); } +const generatePermalink = function (buy_price, sell_prices, first_buy, previous_pattern) { + let searchParams = new URLSearchParams(); + let pricesParam = buy_price ? buy_price.toString() : ''; + + if (!isEmpty(sell_prices)) { + const filtered = sell_prices.map(price => isNaN(price) ? '' : price).join('.'); + pricesParam = pricesParam.concat('.', filtered); + } + + if (pricesParam) { + searchParams.append('prices', pricesParam); + } + + if (first_buy) { + searchParams.append('first', true); + } + + if (previous_pattern !== -1) { + searchParams.append('pattern', previous_pattern); + } + + return searchParams.toString() && window.location.origin.concat('?', searchParams.toString()); +} + +const copyPermalink = function () { + let text = permalink_input[0]; + + permalink_input.show(); + text.select(); + text.setSelectionRange(0, 99999); /* for mobile devices */ + + document.execCommand('copy'); + permalink_input.hide(); + + flashMessage(i18next.t("prices.permalink-copied")); +} + +const flashMessage = function(message) { + snackbar.text(message); + snackbar.addClass('show'); + + setTimeout(function () { + snackbar.removeClass('show') + snackbar.text(''); + }, 3000); +} + const update = function () { const sell_prices = getSellPrices(); const buy_price = parseInt(buy_input.val()); @@ -289,6 +353,14 @@ const update = function () { buy_input[0].disabled = first_buy; buy_input[0].placeholder = first_buy ? '—' : '...' + const permalink = generatePermalink(buy_price, sell_prices, first_buy, previous_pattern); + if (permalink) { + permalink_button.show(); + } else { + permalink_button.hide(); + } + permalink_input.val(permalink); + const prices = [buy_price, buy_price, ...sell_prices]; if (!window.populated_from_query) { @@ -297,7 +369,3 @@ const update = function () { calculateOutput(prices, first_buy, previous_pattern); } - -$(document).ready(initialize); -$(document).on("input", update); -$('input[type = radio]').on("change", update); diff --git a/js/translations.js b/js/translations.js new file mode 100644 index 0000000..5d6d029 --- /dev/null +++ b/js/translations.js @@ -0,0 +1,52 @@ +function updateContent() { + update(); + $('body').localize(); +} +const defaultLanguage = 'en'; +i18next +.use(i18nextXHRBackend) +.use(i18nextBrowserLanguageDetector) +.init({ + fallbackLng: defaultLanguage, + debug: true, + backend: { + loadPath: 'locales/{{lng}}.json', + }, +}, (err, t) => { + const languages = [ + ['de', 'Deutsch'], + ['en', 'English'], + ['es-ES', 'Español'], + ['fr', 'Français'], + ['it', 'Italiano'], + ['ru', 'Русский'], + ['ja', '日本語'], + ['pt-BR', 'Português'], + ['zh-CN', '简体中文'], + ['zh-TW', '繁體中文'] + ], + languageSelector = $('#language'); + languages.map(([code, name]) => { + languageSelector.append(``); + }); + if (!languageSelector.find('[selected]').length) + languageSelector.val(defaultLanguage); + languageSelector.on('change', function () { + if (this.value == i18next.language) + return; + i18next.changeLanguage(this.value); + }); + jqueryI18next.init(i18next, $); + i18next.on('languageChanged', lng => { + if (!languageSelector.find(`[value=${lng}]`).length) { + i18next.changeLanguage(defaultLanguage); + return; + } + languageSelector.val(lng); + updateContent(); + }); + // init set content + $(document).ready(initialize); + $(document).on('input', updateContent); + $('input[type = radio]').on('change', updateContent); +}); diff --git a/locales/de.json b/locales/de.json new file mode 100644 index 0000000..d0f45cd --- /dev/null +++ b/locales/de.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Jorna" + }, + "welcome": { + "salutation": "Hallo, und Willkommen bei der Turnip Prophet App auf deinem Nook Phone.", + "description": "Mit dieser App kannst du die Rübenpreise deiner Insel täglich verfolgen, aber du musst die Preise selbst eingeben!", + "conclusion": "Danach wird die Turnip Prophet App magisch deine Rübenpreise vorhersagen, welche du den Rest der Woche haben wirst." + }, + "first-time": { + "title": "Erstmaliger Einkäufer", + "description": "Kaufst du zum ersten Mal Rüben von Jorna auf deiner Insel? (Dies beeinflusst dein Verkaufsmuster)", + "yes": "Ja", + "no": "Nein" + }, + "patterns": { + "title": "Vorheriges Verkaufsmuster", + "description": "Wie war das Verkaufsmuster der letzten Woche? (Dies beeinflusst dein Verkaufsmuster)", + "pattern": "Verkaufsmuster", + "all": "Alle Verkaufsmuster", + "decreasing": "Absteigend", + "fluctuating": "Schwankend", + "unknown": "Ich weiß nicht", + "large-spike": "Stark Ansteigend", + "small-spike": "Leicht Ansteigend" + }, + "prices": { + "description": "Wie hoch war der Preis für Rüben diese Woche auf deiner Insel? (Wenn du zum ersten Mal Rüben kaufst, wird dieses Feld deaktiviert)", + "open": { + "am": "Vorm. (Vormittag) - 8:00 Uhr bis 11:59 Uhr", + "pm": "Nachm. (Nachmittag) - 12:00 Uhr bis 22:00 Uhr" + }, + "copy-permalink": "Seite teilen", + "permalink-copied": "Seitenlink kopiert!", + "reset": "Eingegebene Daten zurücksetzen", + "reset-warning": "Bist du sicher, dass du deine eingegebenen Daten zurücksetzen möchtest?\n\nDies kann nicht rückgängig gemacht werden!" + }, + "weekdays": { + "monday": "Montag", + "tuesday": "Dienstag", + "wednesday": "Mittwoch", + "thursday": "Donnerstag", + "friday": "Freitag", + "saturday" : "Samstag", + "sunday": "Sonntag", + "abr": { + "monday": "Mo", + "tuesday": "Di", + "wednesday": "Mi", + "thursday": "Do", + "friday": "Fr", + "saturday" : "Sa" + } + }, + "times": { + "morning": "Vorm.", + "afternoon": "Nachm." + }, + "output": { + "title": "Berechnung", + "chance": "% Chance", + "to": "bis", + "minimum": "Garantiertes Minimum", + "maximum": "Potentielles Maximum", + "chart": { + "input": "Eingegebener Preis", + "minimum": "Garantiertes Minimum", + "maximum": "Potentielles Maximum" + } + }, + "textbox": { + "description": "Nachdem du einige Rübenpreise eingegeben hast, macht der Turnip Prophet etwas Magie und zeigt dir die verschiedenen möglichen Verkaufsmuster an, die auf deiner Insel auftreten können.", + "development": "Diese App befindet sich noch in der Entwicklung, wird sich aber mit der Zeit verbessern!", + "thanks": "Nichts von all dem wäre möglich gewesen, ohne das Ninji herausgefunden hätte, wie Nepp und Schlepp ihre Rübenpreise kalkulieren.", + "support": "Hilfe, Kommentare und Beiträge sind auffindbar über GitHub.com (nur in Englisch).", + "contributors-text": "Oh! Und vergessen wir nicht, denen zu danken, die bis jetzt dazu beigetragen haben!", + "contributors": "Mitwirkende", + "language": "Sprache" + } +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..7be8c55 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Daisy Mae" + }, + "welcome": { + "salutation": "Hello, and welcome to the Turnip Prophet app on your Nook Phone.", + "description": "This app lets you track your island's turnip prices daily, but you'll have to put the prices in yourself!", + "conclusion": "After that, the Turnip Prophet app will magically predict the turnip prices you'll have for the rest of the week." + }, + "first-time": { + "title": "First-Time Buyer", + "description": "Is this your first time buying turnips from Daisy Mae on your island?(This affects your pattern)", + "yes": "Yes", + "no": "No" + }, + "patterns": { + "title": "Previous Pattern", + "description": "What was last week's turnip price pattern?(This affects your pattern)", + "pattern": "Pattern", + "all": "All patterns", + "decreasing": "Decreasing", + "fluctuating": "Fluctuating", + "unknown": "I don't know", + "large-spike": "Large Spike", + "small-spike": "Small Spike" + }, + "prices": { + "description": "What was the price of turnips this week on your island? (If this is your first time buying turnips, this field will be disabled)", + "open": { + "am": "AM - 8:00 am to 11:59 am", + "pm": "PM - 12:00 pm to 10:00 pm" + }, + "copy-permalink": "Copy permalink", + "permalink-copied": "Permalink copied!", + "reset": "Reset Turnip Prophet", + "reset-warning": "Are you sure you want to reset all fields?\n\nThis cannot be undone!" + }, + "weekdays": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday" : "Saturday", + "sunday": "Sunday", + "abr": { + "monday": "Mon", + "tuesday": "Tue", + "wednesday": "Wed", + "thursday": "Thu", + "friday": "Fri", + "saturday" : "Sat" + } + }, + "times": { + "morning": "AM", + "afternoon": "PM" + }, + "output": { + "title": "Output", + "chance": "% Chance", + "to": "to", + "minimum": "Guaranteed Minimum", + "maximum": "Potential Maximum", + "chart": { + "input": "Input Price", + "minimum": "Guaranteed Minimum", + "maximum": "Potential Maximum" + } + }, + "textbox": { + "description": "After you've listed some turnip prices, the Turnip Prophet will run some numbers and display the different possible patterns that your island may experience.", + "development": "This app is still in development, but will improve over time!", + "thanks": "None of this would have been possible without Ninji's work figuring out just how Timmy and Tommy value their turnips.", + "support": "Support, comments and contributions are available through Github", + "contributors-text": "Oh! And let's not forget to thank those who have contributed so far!", + "contributors": "Contributors", + "language": "Language" + } +} diff --git a/locales/es.json b/locales/es.json new file mode 100644 index 0000000..4538693 --- /dev/null +++ b/locales/es.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Juliana" + }, + "welcome": { + "salutation": "¡Hola! Te damos la bienvenida a la aplicación Turnip Prophet para tu Nookófono.", + "description": "Esta aplicación te permite monitorizar la fluctuación del precio de los nabos en tu isla, ¡pero tendrás que introducir tú manualmente los precios diarios!", + "conclusion": "Una vez hecho, la aplicación Turnip Prophet predecirá mágicamente el precio que tendrán los nabos el resto de la semana." + }, + "first-time": { + "title": "Comprador(a) primerizo(a)", + "description": "¿Ha sido esta la primera vez que compras nabos?(Esta información afectará a tu patrón)", + "yes": "Sí", + "no": "No" + }, + "patterns": { + "title": "Patrón anterior", + "description": "¿Qué patrón describió el precio de los nabos la semana pasada?(Esta información afectará a tu patrón)", + "pattern": "Patrón", + "all": "Todos los patrones", + "decreasing": "Decreciente", + "fluctuating": "Fluctuante", + "unknown": "No lo sé", + "large-spike": "Pico alto", + "small-spike": "Pico moderado" + }, + "prices": { + "description": "¿Cuál fue el precio más alto que alcanzaron los nabos en tu isla durante la semana? (Si esta ha sido tu primera vez comprando nabos, este campo se desactivará)", + "open": { + "am": "AM - De 8:00 a 11:59", + "pm": "PM - De 12:00 a 10:00" + }, + "copy-permalink": "Copiar permalink", + "permalink-copied": "¡Permalink copiado!", + "reset": "Reiniciar Turnip Prophet", + "reset-warning": "¿Seguro que quieres reiniciar todos los campos?\n\n¡Esto no se puede deshacer!" + }, + "weekdays": { + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday" : "Sábado", + "sunday": "Domingo", + "abr": { + "monday": "LU", + "tuesday": "MA", + "wednesday": "MI", + "thursday": "JU", + "friday": "VI", + "saturday" : "SA" + } + }, + "times": { + "morning": "AM", + "afternoon": "PM" + }, + "output": { + "title": "Predicción", + "chance": "Probabilidad (%)", + "to": "a", + "minimum": "Mínimo garantizado", + "maximum": "Máximo potencial", + "chart": { + "input": "Precio de entrada", + "minimum": "Mínimo garantizado", + "maximum": "´Máximo potencial" + } + }, + "textbox": { + "description": "Cuando introduzcas algunos precios, el Turnip Prophet empezará a hacer sus cálculos y te mostrará algunos posibles patrones para el precio de los nabos en tu isla.", + "development": "Esta aplicación está aún en desarrollo, ¡pero la seguiremos mejorando!", + "thanks": "Nada de esto habría sido posible sin el trabajo de Ninji para averiguar cómo Tendo y Nendo valoran los nabos.", + "support": "Para asistencia, comentarios y contribuciones, no dudes en pasarte por Github.", + "contributors-text": "¡Ah! ¡Y no nos olvidemos de todos los que han puesto su granito de arena hasta ahora!", + "contributors": "Contribuidores", + "language": "Idioma" + } +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 0000000..d9e527f --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Porcelette" + }, + "welcome": { + "salutation": "Bonjour et bienvenue sur l'application du Turnip Prophet de ton Nook Phone.", + "description": "Cette appli te permet de garder un œil quotidien sur le cours du navet de ton île, en le renseignant ici par toi-même !", + "conclusion": "Une fois les prix renseignés, l'appli du Turnip Prophet va magiquement prédire le cours du navet de ton île pour le reste de la semaine." + }, + "first-time": { + "title": "Premier Achat", + "description": "Est-ce la toute première fois que t'achètes des navets à Porcelette sur ton île ?(Cela influencera le type de ta courbe actuelle)", + "yes": "Oui", + "no": "Non" + }, + "patterns": { + "title": "Courbe du cours précédent", + "description": "De quel type était la courbe de ton cours du navet la semaine dernière ?(Cela influencera le type de ta courbe actuelle)", + "pattern": "Type de Courbe", + "all": "Tous les types", + "decreasing": "Décroissante", + "fluctuating": "Variable", + "unknown": "Je ne sais pas", + "large-spike": "Grand Pic", + "small-spike": "Petit Pic" + }, + "prices": { + "description": "À quel prix Porcelette vendait ses navets sur ton île cette semaine ?(Si c'est la première fois que t'en achètes, ce champ sera desactivé)", + "open": { + "am": "Matin (AM) - de 8:00 à 11:59", + "pm": "Après-midi (PM) - de 12:00 à 22:00" + }, + "copy-permalink": "Copier le permalien", + "permalink-copied": "Permalien copié !", + "reset": "Réinitialiser les données", + "reset-warning": "Es-tu sûr·e de vouloir réinitialiser tous les champs ?\n\nCe choix est définitif !" + }, + "weekdays": { + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday" : "Samedi", + "sunday": "Dimanche", + "abr": { + "monday": "Lun", + "tuesday": "Mar", + "wednesday": "Mer", + "thursday": "Jeu", + "friday": "Ven", + "saturday" : "Sam" + } + }, + "times": { + "morning": "AM", + "afternoon": "PM" + }, + "output": { + "title": "Résultats", + "chance": "% Chance", + "to": "à", + "minimum": "Minimum Garanti", + "maximum": "Maximum Potentiel", + "chart": { + "input": "Prix renseigné", + "minimum": "Minimum Garanti", + "maximum": "Maximum Potentiel" + } + }, + "textbox": { + "description": "Après avoir renseigné quelques prix, le Turnip Prophet fera des calculs et affichera les différents types possibles pour les courbes que ton île pourrait avoir.", + "development": "Cette appli est en développement, mais elle s'améliore jour après jour !", + "thanks": "Rien de tout ça n'aurait pu être possible sans le travail de Ninji et son analyse sur comment Méli et Mélo déterminent la valeur des Navets.", + "support": "Aide, commentaires et contributions sont disponibles via Github", + "contributors-text": "Oh ! Et n'oublions pas de remercier toutes les personnes ayant contribué jusqu'à maintenant !", + "contributors": "Contributeurs", + "language": "Langue" + } +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json new file mode 100644 index 0000000..a6c2112 --- /dev/null +++ b/locales/it.json @@ -0,0 +1,80 @@ +{ + "general": { + "name": "Turnip Prophet", + "daisy-mae": "Brunella" + }, + "welcome": { + "salutation": "Ciao e benvenuto nell'app Turnip Prophet del tuo Nook Phone.", + "description": "Questa applicazione ti permetterà di tenere traccia del prezzo giornaliero delle rape. Ma dovrai inserire i prezzi da te!", + "conclusion": "Se lo fai Turnip Prophet predirrà magicamente i prezzi delle rape che avrai per il resto della settimana." + }, + "first-time": { + "title": "Primo Acquisto", + "description": "È la prima volta che acquisti sulla tua isola le rape da Brunella?(influisce sul comportamento dei prezzi)", + "yes": "Sì", + "no": "No" + }, + "patterns": { + "title": "Comportamento Precedente", + "description": "Qual è stato il comportamento dei prezzi delle rape nella scorsa settimana?(influisce sul comportamento dei prezzi)", + "pattern": "Comportamento", + "all": "Tutti i comportamenti", + "decreasing": "Decrescente", + "fluctuating": "Oscillante", + "unknown": "Non lo so!", + "large-spike": "Grande picco", + "small-spike": "Piccolo picco" + }, + "prices": { + "description": "Qual era il prezzo di acquisto delle rape sulla tua isola questa settimana? (Se è la prima volta che le compri questo campo è disabilitato)", + "open": { + "am": "Mattina - dalle 8:00 alle 11:59", + "pm": "Pomeriggio - dalle 12:00 alle 22:00" + }, + "copy-permalink": "Copia permalink", + "permalink-copied": "Permalink copiato!", + "reset": "Resetta Turnip Prophet", + "reset-warning": "Sei sicuro di voler resettare tutti i campi?\n\nNon può essere annullato!" + }, + "weekdays": { + "monday": "Lunedì", + "tuesday": "Martedì", + "wednesday": "Mercoledì", + "thursday": "Giovedì", + "friday": "Venerdì", + "saturday": "Sabato", + "sunday": "Domenica", + "abr": { + "monday": "Lun", + "tuesday": "Mar", + "wednesday": "Mer", + "thursday": "Gio", + "friday": "Ven", + "saturday": "Sab" + } + }, + "times": { + "morning": "AM", + "afternoon": "PM" + }, + "output": { + "title": "Risultati", + "chance": "Probabilità %", + "to": "a", + "minimum": "Minimo Garantito", + "maximum": "Massimo Potenziale", + "chart": { + "input": "Prezzo Iniziale", + "minimum": "Minimo Gatantito", + "maximum": "Massimo Potenziale" + } + }, + "textbox": { + "description": "Dopo aver inserito alcuni prezzi, Turnip Prophet calcolerà e mostrerà i possibili comportamenti del prezzo delle rape nella tua isola.", + "development": "Quest'applicazione è in ancora in sviluppo ma migliorerà col tempo!", + "thanks": "Niente di questo sarebbe possibile senza il lavoro di Ninji nello scoprire come Mirko e Marco valutano le rape.", + "support": "Chiedi supporto o lascia commenti e contributi su Github", + "contributors-text": "Oh! Non dimentichiamoci di ringraziare chi ha contribuito fin'ora!", + "contributors": "Collaboratori" + } +} diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 0000000..074f78b --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,79 @@ +{ + "general": { + "daisy-mae": "ウリ" + }, + "welcome": { + "salutation": "こんにちは!タヌキ開発特製スマホの最新アプリ Turnip Prophet へようこそ。", + "description": "このアプリは、無人島のカブ価を毎日予測することができます。そのためには、まず自分でデータを入力することが必要です!", + "conclusion": "そうすれば、Turnip Prophetは魔法のように、あなたのこの一週間のカブ価を予測します。" + }, + "first-time": { + "title": "はじめての購入", + "description": "あなたが自分の島でカブを購入したのは今回が初めてですか?(答えによってパターンが変化します)", + "yes": "はい", + "no": "いいえ" + }, + "patterns": { + "title": "先週のパターン", + "description": "先週のカブ価変化パターンを選んでください。(答えによってパターンが変化します)", + "pattern": "パターン", + "all": "全てのパターン", + "decreasing": "ジリ貧型", + "fluctuating": "波型", + "unknown": "わかりません", + "large-spike": "跳ね大型(3期型)", + "small-spike": "跳ね小型(4期型)" + }, + "prices": { + "description": "今週のカブ価は? (初めてカブを購入した場合は、この入力欄は無効にされています)", + "open": { + "am": "午前 - AM 8:00 ~ AM 11:59", + "pm": "午後 - PM 12:00 ~ PM 10:00" + }, + "copy-permalink": "パーマリンクをコピー", + "permalink-copied": "パーマリンクがコピーされました!", + "reset": "Turnip Prophetをリセット", + "reset-warning": "本当に全てをリセットしますか?\n\nリセットしたら後戻りはできませんよ!" + }, + "weekdays": { + "monday": "月曜日", + "tuesday": "火曜日", + "wednesday": "水曜日", + "thursday": "木曜日", + "friday": "金曜日", + "saturday" : "土曜日", + "sunday": "日曜日", + "abr": { + "monday": "月", + "tuesday": "火", + "wednesday": "水", + "thursday": "木", + "friday": "金", + "saturday" : "土" + } + }, + "times": { + "morning": "午前", + "afternoon": "午後" + }, + "output": { + "title": "結果", + "chance": "% チャンス", + "to": "~", + "minimum": "保証される最小の収入", + "maximum": "予測される限界の収入", + "chart": { + "input": "カブ価", + "minimum": "保証される最小の収入", + "maximum": "予測される限界の収入" + } + }, + "textbox": { + "description": "複数のカブ価を入力すると、Turnip Prophetはそれをもとに、可能性のある変動パターンを計算して表示します。", + "development": "このアプリはまだ開発中ですが、より良いデータを提供できるようにがんばっています!", + "thanks": "このアプリが作成できたのは、タヌキ商店のカブ価パターンを解析したNinji氏の成果のおかげです。ありがとうございます!", + "support": "疑問·コメント·提案などは、Githubまでお願いします。", + "contributors-text": "そして、このアプリの作成を手伝っていただいた方々に感謝します!", + "contributors": "貢献者" + } +} diff --git a/locales/pt-BR.json b/locales/pt-BR.json new file mode 100644 index 0000000..6b81af8 --- /dev/null +++ b/locales/pt-BR.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Daisy Mae" + }, + "welcome": { + "salutation": "Olá, e bem-vindo ao aplicativo Turnip Prophet em seu Nook Phone.", + "description": "Este aplicativo lhe permite acompanhar os preços diários de nabo em sua ilha, porém você terá que colocar os preços você mesmo!", + "conclusion": "Depois disso, o aplicativo Turnip Prophet irá magicamente prever os preços dos nabos que você terá pelo resto da semana." + }, + "first-time": { + "title": "Comprador de primeira viagem", + "description": "Esta é a primeira vez que você compra nabos de Daisy Mae em sua ilha? (Isso afeta seu padrão)", + "yes": "Sim", + "no": "Não" + }, + "patterns": { + "title": "Padrão Anterior", + "description": "Qual foi o padrão de preços de nabo da semana passada? (Isso afeta seu padrão)", + "pattern": "Padrão", + "all": "Todos padrões", + "decreasing": "Diminuindo", + "fluctuating": "Flutuante", + "unknown": "Eu não sei", + "large-spike": "Grande Pico", + "small-spike": "Pequeno Pico" + }, + "prices": { + "description": "Qual foi o preço dos nabos esta semana em sua ilha? (Se esta é a primeira vez que compra nabos, este campo ficará desativado)", + "open": { + "am": "AM - 8:00 am até 11:59 am", + "pm": "PM - 12:00 pm até 10:00 pm" + }, + "copy-permalink": "Copiar permalink", + "permalink-copied": "Permalink copiado!", + "reset": "Redefinir Turnip Prophet", + "reset-warning": "Tem certeza de que deseja redefinir todos os campos?\n\nIsso não pode ser desfeito!" + }, + "weekdays": { + "monday": "Segunda-feira", + "tuesday": "Terça-feira", + "wednesday": "Quarta-feira", + "thursday": "Quinta-feira", + "friday": "Sexta-feira", + "saturday" : "Sábado", + "sunday": "Domingo", + "abr": { + "monday": "Seg", + "tuesday": "Ter", + "wednesday": "Qua", + "thursday": "Qui", + "friday": "Sex", + "saturday" : "Sab" + } + }, + "times": { + "morning": "AM", + "afternoon": "PM" + }, + "output": { + "title": "Resultado", + "chance": "% Chance", + "to": "to", + "minimum": "Mínimo Garantido", + "maximum": "Potencial Máximo", + "chart": { + "input": "Preço de entrada", + "minimum": "Mínimo Garantido", + "maximum": "Potencial Máximo" + } + }, + "textbox": { + "description": "Depois de listar alguns preços de nabo, o Turnip Prophet irá calcular alguns números e exibirá os diferentes padrões possíveis que sua ilha pode experienciar.", + "development": "Este aplicativo ainda está em desenvolvimento, mas melhorará com o tempo!", + "thanks": "Nada disso seria possível sem o trabalho de Ninji's descobrindo como Timmy e Tommy valorizam seus nabos.", + "support": "Suporte, comentários e contribuições estão disponíveis através do Github", + "contributors-text": "Oh! E não vamos nos esquecer de agradecer àqueles que contribuíram até agora!", + "contributors": "Contribuidores", + "language": "Linguagem" + } +} diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 0000000..ce67a7e --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "Дейзи Мэй" + }, + "welcome": { + "salutation": "Добрый день! Добро пожаловать в приложение Препсказатель на Вашем Нукофоне.", + "description": "Это приложение позволяет Вам ежедневно отслеживать стоимость репы на Вашем острове. Будьте внимательны, Вам придется вводить ее вручную!", + "conclusion": "После этого Препсказатель как по волшебству предскажет стоимость репы в Вашем магазине на этой неделе." + }, + "first-time": { + "title": "Новичок рынка репы", + "description": "Вы впервые приобретаете репу у Дейзи Мэй на своем острове?(Это влияет на модель стоимости)", + "yes": "Да", + "no": "Нет" + }, + "patterns": { + "title": "Предыдущая модель", + "description": "Какая модель стоимости репы была у Вас на прошлой неделе?(Это влияет на модель стоимости)", + "pattern": "Модель", + "all": "Все модели", + "decreasing": "Постоянное снижение", + "fluctuating": "Колебание стоимости", + "unknown": "Не могу сказать точно", + "large-spike": "Большой скачок", + "small-spike": "Малый скачок" + }, + "prices": { + "description": "Какова была стоимость репы у Дейзи Мэй на Вашем острове на этой неделе? (Если вы новичок рынка репы, данное поле будет недоступно)", + "open": { + "am": "Утро - 8:00 - 11:59", + "pm": "День - 12:00 - 22:00" + }, + "copy-permalink": "Скопировать постоянную ссылку", + "permalink-copied": "Постоянная ссылка скопирована!", + "reset": "Перезагрузить Препсказателя", + "reset-warning": "Вы уверены, что хотите обнулить все поля?\n\nДанное действие необратимо!" + }, + "weekdays": { + "monday": "Понедельник", + "tuesday": "Вторник", + "wednesday": "Среда", + "thursday": "Четверг", + "friday": "Пятница", + "saturday" : "Суббота", + "sunday": "Воскресенье", + "abr": { + "monday": "Пн", + "tuesday": "Вт", + "wednesday": "Ср", + "thursday": "Чт", + "friday": "Пт", + "saturday" : "Сб" + } + }, + "times": { + "morning": "Утро", + "afternoon": "День" + }, + "output": { + "title": "Вывод модели", + "chance": "Вероятность в %", + "to": "-", + "minimum": "Гарантированный минимум", + "maximum": "Возможный максимум", + "chart": { + "input": "Введенная стоимость", + "minimum": "Гарантированный минимум", + "maximum": "Возможный максимум" + } + }, + "textbox": { + "description": "После того, как Вы введете уже известную Вам стоимость репы, Препсказатель посчитает и покажет различные вероятные для Вашего острова модели стоимости репы.", + "development": "Это приложение пока еще в разработке, но со временем обязательно станет лучше!", + "thanks": "Если бы Ninji не смог разузнать, как Тимми и Томми определяют стоимость репы, данного приложения могло бы и не быть!", + "support": "Поддержать нас, прокомментировать, а также внести свой вклад Вы можете на Github", + "contributors-text": "Точно! Нельзя забывать тех, кто уже помог проекту!", + "contributors": "Вклад внесли", + "language": "Язык" + } +} diff --git a/locales/zh-CN.json b/locales/zh-CN.json new file mode 100644 index 0000000..35a5a19 --- /dev/null +++ b/locales/zh-CN.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "曹卖" + }, + "welcome": { + "salutation": "大家好,欢迎使用Nook手机上的大头菜预测工具。", + "description": "这个APP可以让你每天跟踪自己岛上大头菜的价格,但你得自己把价格填写进去!", + "conclusion": "之后,大头菜预测工具会神奇地预测出本周剩余时间的大头菜价格。" + }, + "first-time": { + "title": "首次购买", + "description": "你是第一次在自己岛上购买大头菜吗?(将影响预测趋势)", + "yes": "是", + "no": "否" + }, + "patterns": { + "title": "上周趋势", + "description": "上周大头菜的价格趋势是?(将影响预测趋势)", + "pattern": "趋势", + "all": "所有趋势", + "decreasing": "递减型", + "fluctuating": "波动型", + "unknown": "不知道", + "large-spike": "大幅上涨(三期型)", + "small-spike": "小幅上涨(四期型)" + }, + "prices": { + "description": "本周你的岛上大头菜的购买价格是多少?(如果你是第一次购买大头菜,这个字段不可用)", + "open": { + "am": "上午 - 8:00 ~ 11:59", + "pm": "下午 - 12:00 ~ 22:00" + }, + "copy-permalink": "复制价格分享链接", + "permalink-copied": "链接已复制!", + "reset": "重置大头菜预测工具", + "reset-warning": "你确定要重置所有字段吗?\n\n此操作不可撤销!" + }, + "weekdays": { + "monday": "周一", + "tuesday": "周二", + "wednesday": "周三", + "thursday": "周四", + "friday": "周五", + "saturday" : "周六", + "sunday": "周日", + "abr": { + "monday": "周一", + "tuesday": "周二", + "wednesday": "周三", + "thursday": "周四", + "friday": "周五", + "saturday" : "周六" + } + }, + "times": { + "morning": "上午", + "afternoon": "下午" + }, + "output": { + "title": "结果", + "chance": "几率(%)", + "to": "~", + "minimum": "保底价格", + "maximum": "最高价格", + "chart": { + "input": "输入价格", + "minimum": "保底价格", + "maximum": "最高价格" + } + }, + "textbox": { + "description": "在填写一些大头菜价格后,大头菜预测工具将预测大头菜的价格并显示本周可能的趋势。", + "development": "APP仍在开发中,但会随着时间的推移不断完善!", + "thanks": "如果不是Ninji发现豆狸和粒狸如何给大头菜定价的,这一切将不可能实现。", + "support": "可以在Github获得支持,或讨论和贡献", + "contributors-text": "哦!别忘记感谢那些至今为止做出过贡献的人。", + "contributors": "贡献者", + "language": "语言" + } +} diff --git a/locales/zh-TW.json b/locales/zh-TW.json new file mode 100644 index 0000000..ad274e5 --- /dev/null +++ b/locales/zh-TW.json @@ -0,0 +1,80 @@ +{ + "general": { + "daisy-mae": "曹賣" + }, + "welcome": { + "salutation": "你好,歡迎使用Nook手機上的 大頭菜預測工具 。", + "description": "這個程式可以讓你每天追蹤你島上的大頭菜價格,但你必須自己記錄價格!", + "conclusion": "接下來,大頭菜預測工具將 神奇地 預測本週剩餘時間的大頭菜價格。" + }, + "first-time": { + "title": "首次購買", + "description": "這是您第一次從自己的島上和曹賣購買大頭菜嗎?(將影響您這次的模型)", + "yes": "是", + "no": "否" + }, + "patterns": { + "title": "上次的模型", + "description": "上週的大頭菜價格模型如何?(將影響您這次的模型)", + "pattern": "模型", + "all": "所有模型", + "decreasing": "遞減型", + "fluctuating": "波型", + "unknown": "不知道", + "large-spike": "三期型", + "small-spike": "四期型" + }, + "prices": { + "description": "本週大頭菜的購買價格是多少?(如果這是您第一次購買大頭菜,則此欄位會被停用)", + "open": { + "am": "上午 - 08:00 to 11:59", + "pm": "下午 - 12:00 to 22:00" + }, + "copy-permalink": "複製價格分享網址", + "permalink-copied": "網址已複製!", + "reset": "清除資料", + "reset-warning": "是否確定要清除所有資料?\n\n此動作無法復原!" + }, + "weekdays": { + "monday": "星期一", + "tuesday": "星期二", + "wednesday": "星期三", + "thursday": "星期四", + "friday": "星期五", + "saturday" : "星期六", + "sunday": "星期日", + "abr": { + "monday": "週一", + "tuesday": "週二", + "wednesday": "週三", + "thursday": "週四", + "friday": "週五", + "saturday" : "週六" + } + }, + "times": { + "morning": "上午", + "afternoon": "下午" + }, + "output": { + "title": "結果", + "chance": "機率(%)", + "to": "~", + "minimum": "保底價格", + "maximum": "最高價格", + "chart": { + "input": "輸入價格", + "minimum": "保底價格", + "maximum": "最高價格" + } + }, + "textbox": { + "description": "在您紀錄了一些大頭菜價格後,大頭菜預測工具會預測,並顯示你島上可能出現的不同模型。", + "development": "此工具仍在開發中,但會隨著時間的推移而改善!", + "thanks": "如果沒有Ninji 的幫忙來弄清楚豆狸和粒狸如何對他們的大頭菜的估價,這一切都是不可能實現的。", + "support": "可透過 Github獲得支援、討論及貢獻", + "contributors-text": "哦!讓我們不要忘記感謝那些迄今為止作出貢獻的人!", + "contributors": "貢獻者", + "language": "語言" + } +} diff --git a/manifest.json b/manifest.json index 94f06a7..b6763ed 100644 --- a/manifest.json +++ b/manifest.json @@ -8,11 +8,11 @@ "theme_color": "#def2d9", "icons": [ { - "src": "/img/192.png", + "src": "/img/favicon-192.png", "sizes": "192x192" }, { - "src": "/img/512.png", + "src": "/img/favicon-512.png", "sizes": "512x512" } ] diff --git a/service-worker.js b/service-worker.js index e68b22d..499d0ca 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,90 +1,75 @@ // PWA Code adapted from https://github.com/pwa-builder/PWABuilder -const CACHE = "pwa-precache"; +const CACHE = "pwa-precache-v1"; const precacheFiles = [ - "/index.html", - "/js/predictions.js", - "/js/scripts.js", - "/css/styles.css", - "https://code.jquery.com/jquery-3.4.1.min.js", + "/index.html", + "/js/predictions.js", + "/js/scripts.js", + "/css/styles.css", + "https://code.jquery.com/jquery-3.4.1.min.js", ]; self.addEventListener("install", function (event) { - console.log("[PWA] Install Event processing"); + console.log("[PWA] Install Event processing"); - console.log("[PWA] Skip waiting on install"); - self.skipWaiting(); + console.log("[PWA] Skip waiting on install"); + self.skipWaiting(); - event.waitUntil( - caches.open(CACHE).then(function (cache) { - console.log("[PWA] Caching pages during install"); - return cache.addAll(precacheFiles); - }) - ); + event.waitUntil( + caches.open(CACHE).then(function (cache) { + console.log("[PWA] Caching pages during install"); + return cache.addAll(precacheFiles); + }) + ); }); // Allow sw to control of current page self.addEventListener("activate", function (event) { - console.log("[PWA] Claiming clients for current page"); - event.waitUntil(self.clients.claim()); + console.log("[PWA] Claiming clients for current page"); + event.waitUntil(self.clients.claim()); }); // If any fetch fails, it will look for the request in the cache and serve it from there first self.addEventListener("fetch", function (event) { - if (event.request.method !== "GET") return; + if (event.request.method !== "GET") return; - event.respondWith( - fromCache(event.request).then( - (response) => { - // The response was found in the cache so we responde with it and update the entry - - // This is where we call the server to get the newest version of the - // file to use the next time we show view - event.waitUntil( - fetch(event.request).then(function (response) { - return updateCache(event.request, response); - }) - ); - - return response; - }, - () => { - // The response was not found in the cache so we look for it on the server - return fetch(event.request) - .then(function (response) { - // If request was success, add or update it in the cache - event.waitUntil( - updateCache(event.request, response.clone()) - ); - - return response; - }) - .catch(function (error) { - console.log( - "[PWA] Network request failed and no cache." + error - ); - }); - } - ) - ); + event.respondWith( + (async () => { + let response; + try { + // Fetch from network first. + response = await fetch(event.request); + event.waitUntil(updateCache(event.request, response.clone())); + } catch (error) { + try { + // Try if there's locally cached version. + response = await fromCache(event.request); + } catch (error) { + console.log("[PWA] Network request failed and no cache." + error); + throw error; + } + } + return response; + })() + ); }); function fromCache(request) { - // Check to see if you have it in the cache - // Return response - // If not in the cache, then return - return caches.open(CACHE).then(function (cache) { - return cache.match(request).then(function (matching) { - if (!matching || matching.status === 404) { - return Promise.reject("no-match"); - } + // Check to see if you have it in the cache + // Return response + // If not in the cache, then return + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + if (!matching || matching.status === 404) { + return Promise.reject("no-match"); + } - return matching; - }); - }); + return matching; + }); + }); } function updateCache(request, response) { - return caches.open(CACHE).then(function (cache) { - return cache.put(request, response); - }); + return caches.open(CACHE).then(function (cache) { + return cache.put(request, response); + }); }
Pattern% ChanceSunday -
Monday
+
- AM - PM + +
-
Tuesday
+
- AM - PM + +
-
Wednesday
+
- AM - PM + +
-
Thursday
+
- AM - PM + +
-
Friday
+
- AM - PM + +
-
Saturday
+
- AM - PM + +
Guaranteed MinimumPotential Maximum
" + poss.pattern_description + "${Number.isFinite(poss.probability) ? ((poss.probability * 100).toPrecision(3) + '%') : '—'}${percentage_display(poss.category_total_probability)}${percentage_display(poss.probability)}${day.min} to ${day.max}${day.min} ${i18next.t("output.to")} ${day.max}${day.min}