Dev.to · 6 min read

Modeling 恋みくじ Results as Structured Content, Not Random Strings

Modeling 恋みくじ Results as Structured Content, Not Random Strings

Disclosure: I am involved in the project discussed in this article. I am sharing the content-modeling lessons behind it rather than presenting this as an independent review. The first version of a fortune application is usually built around an array of strings: javascript const fortunes = [ "A new relationship may begin soon.", "Be patient and wait for the right moment.", "Someone may be thinking about you." ]; const result = fortunes[Math.floor(Math.random() * fortunes.length)]; This is enough for a prototype. It is also where many fortune applications stop. But a Japanese 恋みくじ —a love-focused form of omikuji—needs more than a collection of interchangeable messages. A visitor may be thinking about unrequited love, reconciliation, a delayed reply, a long-distance relationship, or a new encounter. A result that feels appropriate in one situation may feel careless or confusing in another. Once the number of results grows, a flat string array becomes difficult to write, localize, test, and maintain. The challenge is no longer “How do I choose a random sentence?” It becomes: How do I model emotional content so that every result remains coherent, culturally understandable, and safe to present? This article explores one possible architecture. Why a flat array does not scale Imagine that the application has 100 fortune messages. Some messages are optimistic. Others recommend patience. A few are written for reconciliation, while others assume the visitor has not started a relationship yet. With a flat array, all of these messages are treated as equal candidates. That creates several problems: A reconciliation message may appear to someone asking about a new encounter. A strongly positive headline may be paired with cautious advice. Two nearly identical results may appear consecutively. Translators may understand the sentence but miss its emotional purpose. Editors cannot easily find every result associated with a particular situation. Automated tests can verify the data type, but not the content structure. Randomness should decide among suitable results. It should not decide whether a result is suitable. Treat each fortune as structured content Instead of storing a fortune as one string, I prefer treating it as a small content object. typescript type RelationshipSituation = | "new_encounter" | "unrequited_love" | "waiting_for_reply" | "reconciliation" | "long_distance" | "general"; type FortuneTone = | "bright" | "gentle" | "reflective" | "cautious"; type Fortune = { id: string; situations: RelationshipSituation[]; tone: FortuneTone; headline: string; interpretation: string; action: string; reflection: string; weight: number; locale: string; }; A single result might look like this: json { "id": "reply-gentle-014", "situations": ["waiting_for_reply", "general"], "tone": "gentle", "headline": "Let silence have a little space", "interpretation": "A delayed reply does not always mean that someone has lost interest.", "action": "Avoid sending another message only to escape the discomfort of waiting.", "reflection": "What would help you feel calm even before the reply arrives?", "weight": 1, "locale": "en" } This structure separates four different jobs: Headline creates the memorable moment. Interpretation connects the result to the visitor’s situation. Action offers a small and realistic next step. Reflection gives the visitor something to consider after leaving. It also makes each result easier to review. An editor can check whether the headline, interpretation, and action belong together instead of evaluating one long paragraph. Separate eligibility from randomness A useful selection process has at least two stages. First, determine which results are eligible. Then select one from that smaller group. typescript function getEligibleFortunes( fortunes: Fortune[], situation: RelationshipSituation, locale: string ): Fortune[] { return fortunes.filter((fortune) => { const matchesLocale = fortune.locale === locale; const matchesSituation = fortune.situations.includes(situation) || fortune.situations.includes("general"); return matchesLocale && matchesSituation; }); } The random function only receives results that match the selected context. typescript function selectFortune(fortunes: Fortune[]): Fortune { const index = Math.floor(Math.random() * fortunes.length); return fortunes[index]; } This is still a simple implementation, but it prevents many obvious content mistakes. The important design decision is that random selection happens after the application has applied its content rules. Use weighting carefully Not every result needs to appear with identical frequency. A broadly applicable result may be suitable for several situations, while a highly specific result should appear only occasionally. Weighting can provide more control. typescript function selectWeightedFortune(fortunes: Fortune[]): Fortune { const totalWeight = fortunes.reduce( (sum, fortune) => sum + fortune.weight, 0 ); let position = Math.random() * totalWeight; for (const fortune of fortunes) { position -= fortune.weight; if (position recentId !== id) ].slice(0, HISTORY_LIMIT); localStorage.setItem(HISTORY_KEY, JSON.stringify(updated)); } The recent IDs can be excluded before selection: typescript function excludeRecentResults( fortunes: Fortune[], recentIds: string[] ): Fortune[] { const filtered = fortunes.filter( (fortune) => !recentIds.includes(fortune.id) ); return filtered.length > 0 ? filtered : fortunes; } This does not require a user account or a server-side history. It is also important not to create an endless loop of redraws. Removing immediate repetition improves quality, but the interface should still encourage visitors to reflect on a result rather than repeatedly drawing until they receive the answer they want. Model meaning before translating text Localization becomes easier when translators can see the function of each field. Consider the Japanese term 恋みくじ itself. An English interface might use “love fortune,” but that translation does not fully explain the cultural ritual associated with omikuji. The content model can preserve the original product term while localizing the explanation: json { "product_name": "恋みくじ", "short_explanation": "A Japanese-style love fortune", "draw_instruction": "Think about your romantic question, then draw one fortune." } The goal is not to translate every Japanese expression literally. It is to preserve the emotional purpose. A result that sounds gentle in Japanese can become unusually commanding when translated directly into English. A phrase intended as poetic ambiguity can accidentally sound like a factual promise. For this reason, localization review should ask: Does the result still have the same emotional tone? Does it sound like reflection or prediction? Is the suggested action reasonable in the target culture? Does the translation preserve uncertainty? Could the result be misunderstood as professional advice? The tone and situations fields provide translators with context that a standalone sentence cannot. Validate content like code Structured content can be validated before deployment. typescript function validateFortune(fortune: Fortune): string[] { const errors: string[] = []; if (!fortune.id) { errors.push("Missing ID"); } if (fortune.situations.length === 0) { errors.push(${fortune.id}: no situations assigned); } if (fortune.headline.length > 80) { errors.push(${fortune.id}: headline is too long); } if (fortune.weight

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News