2014-03-28

שלום, מונגו! (MongoDB)

לפני כחודשיים, בערך, התרחש מהפך קטן-גדול.

לא, אני לא מדבר על מה שקרה (וקורה, בעת כתיבת הפוסט) באוקראינה. גם לא על נתח השוק שמאבדת מייקרוסופט בהתמדה, או על החדירה של אינטל ל high end האמיתי של החומרה. דווקא החברה שמפנה במקרה זה את מקומה היא לא אחרת מאשר הענק הכחול - IBM, והיא עשויה לא-להיות האחרונה.

בסיס נתונים לא-רלציוני, בשם MongoDB תפס את המקום החמישי הנחשק במדד הפופולריות של DB-Engine, ובעצם הפך לבסיס הנתונים הלא-רלציוני הראשון שנכנס לחמישייה הפותחת [א].


יש היגיון מסוים בכך שדווקא MongoDB הוא הראשון מבין בסיסי הנתונים מסוג NoSQL לפרוץ לרשימת בסיסי הנתונים הרלציוניים: מבחינות מסוימות הוא קצת יותר דומה להם: יש לו (יחסית ל NoSQL DB) עושר רב של יכולות, וותק, וזמינות של תמיכה מסודרת (לאחרונה גם בישראל) - הכוללת "גרסאות Enterprise".

האופן בו אנשי מונגו אוהבים להציג את בסיס הנתונים: מעט פחות יכולות והרבה יותר Scalability מ RDBMS + הרבה-הרבה יותר יכולות מבסיסי נתונים NoSQL אחרים...

מקור השם של מונגו הוא מהמילה humongous (גדול, אדיר), אולם היכולת לטפל בכמויות גדולות של Data היא כנראה לא התכונה הבולטת, המושכת אליו כ"כ הרבה מפתחים. בעולם ה Internet Scale דווקא אני שומע קולות שמחשיבים את Mongo כבסיס נתונים בעל יכולות Scale מתונות.

הפופולריות של MongoDB (בקיצור: "מונגו") נובעת מהקלות שהוא מספק למפתחים בעזרת הסכֶמה (db schema) הגמישה שלו. לא עוד Migrations, או יותר גרוע: תקלות migration על סביבת production (המוכרות כנראה היטב לכל מי שעבד עם SQL בסביבת ה Enterprise).
מונגו הופך להיות מועמד לקחת את מקומו של MySQL כבסיס נתונים זול ויעיל ל"משימות רגילות" - ויש הרבה מאוד כאלו.

הבהרה חשובה: מונגו הוא לא תחליף ישיר ל RDBMS. הוא שונה. כאשר זקוקים להרבה joins על נתונים, ופעולות רוחביות ("SELECT") על הרבה אובייקטים - מונגו (או Document-Based Database אחר) כנראה לא יתאים. מונגו מתאים כאשר יש אובייקטים עצמאיים ("לקוח", "אתר", "משלוח"), ורוב הפעולות נעשות על האובייקטים הבודדים - ורק מיעוט הפעולות הן רוחביות ("כל המשלוחים שיצאו אתמול ויגיעו מחר").

סקר המראה מה מושך מפתחים ב NoSQL: לא, זהו לא ה Scale במקום הראשון - אלא דווקא הגמישות. מקור

בפוסט זה אינני מתכוון לגעת בנושאים של Scalability או Availability של מונגו או בכלל ,לא הקדמה כללית ל NoSQL (נתתי כזו פעם, היא עדיין רלוונטית דייה) ולא דיון על ה CAP theorem.

בפוסט זה אני רוצה לגעת רק בפן הפונקציונלי של מונגו, כזה שעוזר לפתח (ולתחזק) אפליקציה יותר מהר. לספק תחושה כללית כיצד זה לעבוד עם MongoDB.



... עוד קיטלוג של בסיסי נתונים NoSql 


מונגו DB - מי זה הבחור הזה? 

ראשית הייתי רוצה להזכיר בכמה מילים את מודל ה Document-Based Database בכלל, ואת זה של מונגו בפרט.
אם נשווה את המודל של Mongo לבסיס נתונים רלציוני ההשוואה תראה בערך כך:


מונגו מנהל מספר בסיסי נתונים, ממש כמו RDBMS. יש אוספים של "מסמכים" (ניתן לחשוב עליהם בשלב זה כמחרוזות של JSON). בתוך ה JSON יש ערכי key:value (= שדות) שמונגו מודע להם (בניגוד לבסיס נתונים K/V - שם הוא לא).

ניתן ליישם הדמייה ל KVDB (בסיסי נתונים מסוג K/V) על גבי RDBMS בכך שמייצרים טבלה עם 2 עמודות: ID ו BLOB של נתונים (למשל: קובץ JSON).

באופן עקרוני, בסיס הנתונים הרלציוני יספק את היכולת הבסיסית של KVDB, אך ללא הביצועים / Scalability / זמינות שניתן לקבל מבסיס נתונים KVDB. הוא עתיד "להתפרק" אחרי מאות מיליונים ספורים של רשומות והקצב בו הוא ישרת את לקוחותיו לא הולך להיות מרשים במיוחד.


בסיס נתונים מבוסס מסמכים (בקיצור DBDB) הוא כמו[ב] KVDB עם 2 הבדלים משמעותיים נפוצים:
  • הוא מכיר את תוכן המסמך, והפורמט שלו - והוא מאנדקס חלקים ממנו, לצורך Queries רוחביים יעילים ו/או מתוחכמים.
  • לעתים קרובות: הוא מאפשר הגדרת קשרים (למשל: היררכי) בין המסמכים.
אפשר גם "לקמבן" התנהגות כזו בעזרת RDBMS בעזרת כמה stored procedures, מה שעשוי ליצור יישום יעיל אפילו פחות מ"חיקוי ה KVDB" שתארתי למעלה.



דייט ראשון - התקנה וכלים ראשוניים

ההתקנה של מונגו היא פשוטה למדי:
  • פתיחת ZIP הכולל את קבצי ה exe. לאיזו תיקיה (ב"חלונות").
  • יצירת תיקיה ריקה בשם data/db/ (יחסית לכונן בו מונגו מותקן) - התיקייה בה מונגו מאכסן את ה data שלו. ניתן לקנפג תיקיה אחרת, כמובן.

בין קובצי ה exe ניתן למצוא כמה מעניינים:
  • mongod.exe - השדון החרוץ (daemon [ג]) של בסיס הנתונים. כלומר: התהליך המרכזי.
  • mongo.exe - ה Admin Shell, דרכו ניתן לבצע פעולות רבות.
  • mongoexport.exe - פעולות import/export של קבצי נתונים (BSON).
  • mongodump.exe/mongorestore.exe - גיבוי / אחזור של בסיס הנתונים (כקובץ בינארי דחוס).
  • mongostat.exe - תהליך שאוסף נתונים על השימוש ב mongo.

בגדול, כדי להפעיל ולנסות את mongo בצורה קלה יש פשוט להפעיל את mongod.exe ואחריו את mogno.exe. דרך ה console ניתן לבצע / לנסות את רוב הפעולות המשמעותיות של בסיס הנתונים - ולקבל פידבק מהיר. 


בואו ננסה משהו:



  1. ביקשנו לראות אלו בסיסי נתונים קיימים בעזרת פקודת show dbs. בסיס הנתונים היחידי הקיים הוא local, למרות שמונגו חיבר אותנו בכניסה ל test - בסיס נתונים ריק. מונגו הוא לעתים רבות עצלן, וייצור אובייקטים מבנים רק כאשר יש בהם תוכן ממשי.
  2. השתמשנו ב use בכדי לעבור לבסיס נתונים חדש (שעדיין לא קיים). מונגו יזכור אותו, אך עדיין לא ייצור אותו.
  3. פקודת db בודקת באיזה בסיס נתונים אנו כעת.
  4. נתחיל בהכנסות: נכניס לתוך אוסף שעדיין לא קיים (people), בבסיס הנתונים שעדיין לא קיים ("db" הוא האובייקט המייצג את בסיס הנתונים הנוכחי) רשומה - כלומר מסמך, המתאר את ג'ון.
    ברגע זה מונגו ייצור את בסיס הנתונים ואת האוסף (collection) בשם people - ויכניס לתוכו את הרשומה.

    שימו לב שמונגו יקצה קובץ בשם data/db/myNewDb.0 בו הוא יאכסן את בסיס הנתונים. למרות שיש לי רק מסמך אחד קטן - מונגו בחר להקצות אצלי כ 200MB בדיסק - מה שעשוי להראות קצת מבהיל. הסברים - בהמשך הפוסט.

    לאחר שלמונגו יש קובץ המייצג את בסיס הנתונים - הוא יכניס במהירות את המסמך השני - לו סכמה דומה אך רחבה יותר.
  5. בשלב זה אני בודק אלו אוספים יש בבסיס הנתונים: מכיוון שבסיס הנתונים נוצר - האוספים כבר קיימים (people ואוסף האינדקסים - שמכיל רשומות ראשוניות בלבד). בדומה ל RDBMS, מונגו משתמש בעצמו כדי לנהל מידע מערכת.
  6. אני אבצע שאילתה (מקבילה ל * SELECT) על אוסף האנשים.
    הממ... אני לא זוכר שהכנסתי שדה בשם id_, אתם זוכרים?!

סביבת ה shell, כדרך אגב, היא javaScript לכל דבר - וניתן להשתמש ביכולות ה JavaScript שאתם מכירים (var, פונקציות ועוד).

בלינק זה תוכלו למצוא עוד פקודות רבות שניתן להפעיל ב shell. בעיקרון ה shell הוא כלי מצוין ללמידה, ניסוי, או תפעול של mongoDB.



שדה ה id_


כפי ששמתם לב מונגו הוסיף לנו שדה בתוך המסמך בשם id_. הוא יוסיף שדה זה אם לא הגדרנו אותו בעצמנו (בד"כ נשאיר זאת לו).

מבנה ה id_

תכונה מעניינת של ה id_ (נקרא גם object id) היא שה timestamp הוא ראשון, מה שמאפשר לבצע מיון של אובייקטים ע"פ זמן היצירה, בקירוב. הזמן נשמר ע"פ epoch[ה] של unix.


בבסיסי נתונים מסוג KVDB ו DBDB יש חשיבות לא-מעטה לדרך בה בונים את ה ids/keys:
  • ה key הוא לעתים קרובות המפתח ל partitioning, ויכול להשפיע רבות על הביצועים.
  • יעילות של ה hash function יכול להיות משמעותי כאשר מדברים על המון פעולות בשנייה.
  • מה קורה כאשר שני ids מתמפים לאותו אובייקט? זה לא סביר כאשר יש אלפי ערכים - אך סביר יותר, כאשר יש מיליארדים. 


חיפוש ואינדקסים


במונגו, ניתן לחפש אחר ערכים בתוך collection בעזרת פקודת find, למשל:

db.persons.find({ lastname: 'Smith' });

החיפוש נעשה ע"י "דוגמה" או prototype: אנו מספקים את הערכים שאנו מחפשים ומונגו יחזיר לנו cursor המצביע על האובייקטים שמכילים את הערכים הללו.

ניתן לעשות חיפוש קצת יותר כללי, בעזרת query modifiers המספקים יכולות מספריות / לוגיות, למשל:

db.persons.find( { childrenCount: { $gt: 3 } } );

gt$ הוא קיצור של greater than, כלומר: אנשים בעלי 3 ילדים או יותר. שדה שלא הוגדר באובייקט (כלומר: מסמך) מסוים, יהיה בעל ערך 0 - לצורך העניין. יש גם query modifiers נוספים כגון min$ או or$ ועוד.

אפשר לחפש על אובייקטים מקוננים, למשל address.city או להגדיר שאילותות מורכבות הכוללת מספר רב של שדות או query modifiers לדוגמה:

db.persons.find( { childrenCount: { $gt: 3, $lt 20 } } , { 'address.city': 'Holon' } );

הערה: הסיבה ששמתי את address.city בתוך מרכאות נובעת מג'אווהסקריפט. שימוש בנקודה איננו תקני במפתח של אובייקט.

אפשר גם לצמצם את התשובה לשדות מסוים (בדומה ל RDBMS select) בעזרת אובייקט projection - אותו מגדירים בפרמטר השני של הפקודה find:

db.persons.find({ lastname: 'Smith' }, { tile:1, lastname: 1 });

בדוגמה זו אנו רוצים לקבל רק "תואר" ושם משפחה. ערכי ה "1" הם דרך מקוצרת לכתוב true - כלומר: החזר לי את השדה הזה. שימו לב: השדות צריכים להיות כולם true או כולם false - לא ניתן לערבב. יוצא הדופן היחיד הוא השדה id_, אותו ניתן להשמיט גם כאשר יש רשימה "פוזיטיבית" של שדות בהם מעוניינים.

מה עוד ניתן לעשות ב queries? ניתן לעשות הרבה. אציין שליפת מספר קבוע-מראש של ערכים מתוך מערך במסמך (נאמר תגובות בבלוג: רק 10 תגובות ראשונות מכל פוסט), או את היכולת לעשות שאילתות קיבוץ מורכבות (db.collection.group) - ולהוסיף להן פונקציות פילטור בג'אווהסקריפט, ad-hoc.

התיעוד של מונגו על חיפוש הוא מצויין - ואין טעם שאשכפל אותו, מעבר להצגת היכולות העקרוניות.


בעולם ה NoSQL עושים הפרדה בין "planned Queries" ו "Ad-hoc Queries".
Planned Queries הן כאלו שהתכוננו אליהן, לרוב בעזרת יצירת אינדקסים (פנימיים או חיצוניים לבסיס הנתונים), בעוד Ad-hoc Queries הן כאלו שעושים בצורה "ספונטנית" - ללא קיום של אינדקסים, pre-fetch או כל הכנה מקדימה.

בעוד ב RDBMS אינדקסים משפרים את הביצועים, במונגו הם כמעט-הכרחיים בכדי לבצע שאילתה על collection גדול - ולסיים בזמן סביר. ההמלצה הכללית במונגו היא לא לבצע שאילתה על collection לא-קטן (עשרות אלפי אובייקטים או יותר) - מבלי שיש עליו אינדקס, פשוט לא.


אינדקסים

הגדרה של אינדקסים היא פשוטה למדי, דומה להגדרת החיפוש:

db.persons.ensureIndex( { firstname: 1 } );

ייצור אינדקס לשדה ה firstname.

db.person.ensureIndex( { "address.city" : 1 } );

ייצור אינדקס לשדה city בתוך תת-האובייקט (או המסמך) address. כמו כן:

db.person.ensureIndex( { lastname: 1, firstname: 1} );


ייצור אינדקס של מפתח המורכב מ2 שדות: שם פרטי, ושם משפחה.
התחביר ensureIndex מצביע על כך שאם אינדקס קיים - מונגו לא ישכפל אותו.

יש עוד אפשרויות אינדוקס רבות, עליהן ניתן לקרוא בתיעוד הרשמי.




מבני הנתונים המשמשים את מונגו. מקור


אכסון (Persistency)


שאלה שבוודאי מעניינת מאוד את מי שמגיע מרקע והבנה כיצד עובד RDBMS היא "מה קורה שם בתוך מונגו?" או "כיצד ממימשו זאת?".
בחלק זה אנסה לענות על שאלה זו בקיצור.

מונגו כתוב ב ++C וש לו הפצות למערכות הפעלה שונות. מערכת ההפעלה אליה הוא תוכנן במקור היא Unix/Linux (כלומר: מערכת POSIX). יוצרי מונגו לקחו כמה החלטות Design מהותיות:
  1. מונגו יעבוד קודם כל עם זיכרון (עבור המהירות). מונגו אוהב זיכרון, והרבה!
    כל בסיס נתונים שמכבד את עצמו מרוויח מזיכרון, אך כשלמונגו חסר זיכרון - נפילת הביצועים היא משמעותית מאוד.
  2. עדכון של הזיכרון לדיסק יינתן (delegated) למערכת ההפעלה. התכנון המקורי התבסס על הדרך בה מערכת Linux עובדת, וספציפית פקודת mmap למיפוי קבצים - ישירות לתוך הזיכרון. המימוש עבור מערכת ההפעלה "חלונות" הוא כנראה דומה (במסגרת הכלים ש"חלונות" מספקת), אך ידוע כפחות יעיל.
    היתרונות של גישה זו היא קלות פיתוח שמונגו הרוויח, ואי שכפול caches בין מערכת ההפעלה למונגו (מה שמנצל את הזיכרון בצורה יעילה יותר). החיסרון: המנגנון מאחורי mmap הוא כללי ואינו האופטימלי-ביותר לצרכים של מונגו.
  3. למונגו יש רמות "durability" שונות הניתנות לקנפוג. האם כתיבה לבסיס הנתונים תכנס לתור של כתיבה לדיסק ("fire and forget" - דומה ל INSERT_DELAYED של MySQL) או האם כל כתיבה נכתבת במקום ל journal על הדיסק ("fully safe"). ברירת המחדל היא "fire and forget".





מונגו מאכסן כל Database בסדרת קבצים. הקבצים מתחילים בקובץ בגודל 64MB (למשל db.0) ומכפילים את עצמם בכל פעם (128MB - עבור db.1 ואז 256MB עבור db.2) וכו' עד שמגיעים לגודל של 2GB - ונשארים שם. מונגו מקצה יותר מקום ממה שנדרש למידע בפועל - כדי לא "להתקע" באמצע רצף של כתיבות. במערכת ההפעלה שלי הוא מקצה 200MB לכל בסיס נתונים שרק נוצר.

את הקבצים עצמם מונגו מחלק ל Extents - מן בלוקים הכוללים data או אינדקסים של collection מסוים (לא מערבבים סוגים ולא מערבבים collections).
גם בתוך extent של data, מונגו שומר לכל document מעט Padding - כלומר: יותר מקום ממה שנדרש, במידה והמסמך יגדל במעט. לדוגמה: עדכון של שדה לערך ארוך יותר או הוספה של שדה. המסמכים וה extents הם רציפים בדיסק / זכרון.

db.collection.dataSize();

יספק לנו מידע מה גודל ה collection בבתים, עבור המסמכים וה paddings שלהם.

db.collection.storageSize();

יספק לנו את גודל ה data extents של ה collection. כלומר ()dataSize ועוד מקום שהוקצה בדיסק ועדיין לא בשימוש.

db.collection.totalIndexSize();

יספק לנו את גודל ה index extents של ה collection, כולל שטחים ב extents שעדיין לא בשימוש.

סטטיסטיקות נוסף ניתן לקבל בעזרת אובייקט ה dbStat.

מונגו משתמש בפקודת mmap של לינוקס בכדי למפות extents (בלוקים בקבצים) לזיכרון. הקבצים הם כמו "גדר הגנה" בפני fragmentation שיכולה להיווצר באופן טבעי במערכת הקבצים.

כאשר מסמכים גדלים מעבר ל padding הקיים - הם מועברים למקום חדש עם padding חדש. מונגו לומד את קצב השינויים ב collection ולפיו מבסס את ה padding factor לפיו יקבע גודל padding חדש. גדילות תכופות של מסמכים --> paddings גדולים יותר.

כאשר מסמכים מועברים ממקומם או נמחקים - נוצרים "רווחים ללא שימוש". עד כמה שידוע לי - מונגו לא מנסה למלא אותם. רווחים אלו הם לא-טובים לביצועים של מונגו - כפי שניתן לראות בניתוח הזה.



fragments בתוך extents של מונגו. מקור.


הפתרון בעיקרון הוא לבצע de-fragmantation בעזרת פקודת compact.
הקושי:  למונגו יש thread יחיד לכתיבות, ופעולת compact תתקע את בסיס הנתונים כולו לזמן מה (כלומר: down time). אפשר לתזמן פעולות תחזוקה אלו או אפשר, בעזרת replication של nodes של מונגו - לעשות את זה offline.
זה כנראה לא מה שהייתם מצפים מ"בסיס הנתונים מהדור החדש" - אבל ככה זה.

עוד נקודה כואבת במונגו היא נעילות (לצורך consistency):
נעילות הן ברמה של בסיס נתונים (database). עד גרסה 2.1 - נעילות בכלל היו ברמת כל ה instance של מונגו. ישנן תוכניות לעתיד לשפר את רמת הנעילה במונגו.
המשמעות היא כמובן מגבלה משמעותית לביצוע כתיבות ב scale, לפחות כאשר אנו עובדים ברמת durability (או safety) גבוהה.
הברירה היא בין לוותר על מידה מסוימת של durability, לצמצם כתיבות, או להשתמש ב very-high-end SSD עם IOPS (פעולות IO בשנייה) גבוה במיוחד: איזה חצי מיליון IOPS, הייתי אומר. האפשרות השלישית - חסומה בחומרה הקיימת כיום, כמובן.

ה consistency במונגו מובטח רק ברמת פעולה על מסמך - ואין להניח על סדר הפעולות בכלל המערכת (isolation).
מה קורה כאשר רוצים לבצע שינוי במסמך א' בצורה אטומית (למשל: להגדיל ערך מספרי ב 3)?

או שמוכנים לספוג חוסר consistency מסוים, או שאפשר להשתמש בכלי מיוחד של מונגו בשם collection.findAndModify המקבלת הוראות ומבצעת שינויים במסמכים בצורה אטומית.
הפקודה תבצע שינוי אטומי במסמך אחר מסמך, אך כל פעם - במסמך בודד.

מה קורה כאשר רוצים לבצע שינוי במסמך ב' על בסיס של נתונים ממסמך א'?
לבעיה זו אין למונגו פתרון מובנה, ויש כל מיני פתרונות מקובלים / "design patterns" כיצד לפתור את הבעיה. בגדול סט הפתרונות מתבסס על יצירת מסמך שלישי זמני בשם (מפתיע:) transaction שמסייע לנהל את הפעולה.


נקודה אחרונה מעניינת היא נושא גודל המידע. לחוסר הסכמה של מונגו יש חסרון אחד ברור: יש לאכסן את ה keys בכל פעם, מה שיכול להגדיל את גודל המידע בצורה משמעותית הן בדיסק והן ברשת (מול ה clients של מונגו).

אמנם JSON הוא מבנה נתונים יחסית יעיל, וכאשר מונגו שולח / מאכסן מסמכים - הוא בעצם משתמש בצורה בינרית יותר יעילה שנקראת BSON [ד] (קיצור של Binary JSON), אך עדיין מדובר בבזבוז.




סיכום


מונגו הוא לא בסיס נתונים מושלם, אך כנראה שהוא מספיק טוב למערכות רבות, ויכול לסייע בקיצור זמני פיתוח ותחזוקה.
ב technology stacks כמו MEAN (קיצור של Mongo, Express, Angular and Node.js) - השימוש הטבעי של מונגו ב javaScript ו JSON מאפשר לכתוב פתרון קצה-אל-קצה בשפה יחידה: javaScript.

למרות שמונגו יחסית עשיר ביכולות, יש לא מעט ספריות ODM (קיצור של Object-Document Mapping), כגון mongoose או doctrine, המסייעות במידול הנתונים או בהעשרת היכולות בשימוש במונגו.

מונגו, ומודל ה documents בכלל, טבעי מאוד לפיתוח מערכות בהן היינו משתמשים בכלי ORM: מסמך (document) הוא סוג של אובייקט, ועבודת התרגום בין שפת התכנות לשפת בסיס הנתונים, ובחזרה - נחסכת מאיתנו. ביוחד כאשר יש לפרק/להרכיב "אובייקט" יחיד מטבלאות רבות.
מונגו הוא בד"כ מהיר מ RDBMSs, לא בגלל שהמפתחים שלו חכמים יותר - אלא בגלל שהם עשו בחירות תכנוניות שמעדיפות מהירות על דברים אחרים (למשל: ההחלטה ל durability לא מושלם, כברירת מחדל). בטלו את הבחירות הללו - והפערים יצטמצמו. בעוד מימוש של KVDB הוא יחסית פשוט על גבי RDBMSs קיים (עם scale מוגבל יותר), מימוש של DBDB על גבי RDBMS הוא קשה - בגלל תהליכי האינדוקס.

כמו בסיס NoSQL רבים אחרים, מונגו מוסיף אחריות על המפתח: לדאוג לשלמות ואחידות הנתונים. מונגו העלים סט בעיות אחד מהמפתח - אך חושף אותו לסט אחר (בשאיפה: מצומצם יותר) של בעיות אחרות.
כשאתם ניגשים לעבוד במונגו - על תצפו ל Internet Scale. הוא כנראה טוב ב Scale בהרבה מ RDBMS - אך לא טוב כמו Riak או Cassandra. כמו כן אל תצפו לבגרות של RDBMS - המבוססים על מודל שכבר בעבודה במשך עשרות שנים.

בכל זאת, אם אתם מתחילים לפתח מערכת חדשה, במיוחד מערכות ווב (במידה, "המגרש הביתי של מונגו") ובמיוחד כאלו שאינן מוגדרות היטב עדיין - יש סיכוי טוב שעם מונגו תתקדמו מהר וטוב יותר מאשר עם MySQL או PostgresDB.


שיהיה בהצלחה!




זמן קצר לאחר שחרור הפוסט שוחררה גרסה משמעותית, 2.6, של מונגו. ניתן לקרוא highlights בלינק הבא.


נעשה עדכון ב 30/3 בכדי לחדד את הנקודה שמונגו הוא לא "RDBMS מהיר" או "גמיש יותר" - הוא שונה, והוא יעיל לסוגים מסויימים של בעיות.



---

לינקים רלוונטיים:
מצגת מעניינת על מידול נתונים במונגו
השוואה (מעט פרובוקטיבית) בין הביצועים של מונגו ו MS SQL

----

[א] DB-Engines קיים "כולו" שנתיים ומשהו, אבל ע"פ כל מדד מקובל - לא נראה שבסיס נתונים לא-רלציוני היה איפשהו ברשימה הפותחת ב 30 שנה האחרונות.

עוד הערה: Cassandra ומונגו נמצאים ברשימת ה top10 מאז המדד החל, אולם Sybase ASE "נדחף" ל Cassandra באמצע והחזיר אותו למקום העשירי. הסיבה היא כנראה החיזוק ש ASE קיבל מרכישת SAP את Sybase, אולם אין בכך לגרוע מהמגמה הכללית: ASE צמח בעיקר על חשבון אורקל.

[ב] ב NoSQL, כמו ב NoSQL - אין הגדרות מוסכמות או חד-משמעיות. התייחסו לכל הגדרה כ "נקודת מבט אפשרית".

[ג] מקור המונח daemon הוא מניסוי מחשבתי בתחום התרמודינמיקה בשם Maxwell's demon בו יש שדון שעובד ללא הפסקה ברקע ופותח דלת קטנה לחלקיקים שיעברו דרכה... daemon היא צורת כתיבה עתיקה של המילה demon (שד).

[ד] פורמט BSON גם מאפשר ניהול טיפוסים שלא קיימים ב javaScript כגון object id או date כפרמיטיב.

[ה] epoch (תאריך בסיס) הוא נקודת ייחוס ממנה שומרים תאריך בצורה מספרית, בדרך כלל בשניות. MS DOS, היתה מערכת 16bit ולא רצו לשמור תאריך, בשניות, משנת 0 (או אפילו מ 1970, כמו unix) - ולכן ה epoch הוא 1/1/1980.

כדי לתאר את התאריך 2/1/1980 (יום אחרי) - יש לשמור בשדה התאריך את המספר 24*60*60 = 86,400.

במערכת הקבצים NTFS (חלונות NT) זמן הייחוס הוא שנת 1601 - השנה הראשונה במחזור 400-השנים של לוח השנה הגרוגאני בו, שימו לב: שוחררה חלונות NT!. אפל (כתגובה?) קבעה את ה epoch של OS X לשנת 2001 - השנה בה יצאה מערכת ההפעלה OS X. בגרסאות קודמות של MAC OS, ה epoch היה שנת 1904 - שנקבעה בגלל שזו הייתה "השנה המעוברת הראשונה במאה ה-20", ואולי כדי להתבשם בעובדה שזו הייתה מערכת שרצה על מעבדי 24 ביט (בזמן ש DOS הייתה תלויה בעבדי 16 ביט, ו epoch כזה היה מגביל אותה).

2014-03-16

תוכנה רב-לשונית - חלק ב'

בפוסט הקודם עסקנו במינוחים, תיאור תהליך והתרגום והצגנו בעייה אחת של קוד המטפל ב i18n: מיון טקסט (והסברנו גם איך להתמודד).

בפוסט הזה אדבר על עוד מספר בעיות: חיפוש, עיבוד מחרוזות (בשפות שונות), תאריכים, מידות ועוד...
עבור שני הנושאים הראשונים, חשוב להבין קצת איך בנוי טקסט ב Unicode ולכן אפתח את הפוסט ברקע על encoding ו Unicode.

מטרת הפוסט (ביחד עם הפוסט הקודם) היא לספק מקור משמעותי וטוב לנושאי המרת תוכנה לרב-לשוניות: בעיות - ופתרונות.




מבוא ל Unicode


בגדול, ישנן 4 משפחות נפוצות של קידודי טקסט:
  • ASCII, קידוד 7 ביט. עליו דיברנו בפוסט הקודם.
  • ANSI, הרחבות ל ASCII ול 8 ביט כאשר מוסיפים 128 תווים המתארים לרוב שפה נוספת לאנגלית.
    עד כמה שידוע לי אין שום תקן רשמי של ארגון התקנים האמריקאי (ANSI) לגבי קידוד. משום מה השם "ANSI" דבק בסדרה זו של תקנים שדווקא מתחילים בשמות כגון Windows או ISO (אלו ספציפית - באמת תקני ISO).
  • כל מיני קידודים שונים של שפות אסייתיות (GB, EUC, Big5 ועוד), כאשר הגיוון שלהן רחב מאוד.
  • Unicode - תקן יחיד המתאר מספר רב מאוד של שפות (בפועל: כל השפות המוכרות) תחת מרחב אותיות/סימנים אחד. כבר כמה שנים הוא משמש כתקן הבולט.
בעוד שבמסמכים המקודדים ב ASCII או ANSI מוגבלים במספר השפות שיופיעו במסמך (אנגלית + שפה נוספת כגון עברית או רוסית), ב Unicode ניתן לכתוב מסמך המכיל טקסט בכל השפות. בנוסף, שימוש ב Unicode מאפשר לכתוב תוכנה רב-לשונית בעוד הקוד (או המתכנתים) צריכים להכיר קידוד אחד בלבד - ולא עשרות קידודים שונים.

הותיקים ביננו בטח זוכרים כיצד אתרי אינטרנט בעברית שלא הצהירו על הקידוד שלהם אלצו את המשתמש לנסות קידוד אחר קידוד עד שראו עברית.
"אולי הפעם זה ISO-VISUAL...?!"

תקן Unicode יש רק אחד.
בתוך עולם ה Unicode הגדירו מרחב של אותיות / סימנים (כ 110,000 מהם - נכון לשלב זה), המתארים, פחות או יותר, את כל השפות / תחבירים המוכרים לאדם.
תקן ה Unicode ממשיך להתפתח (לינק: מה חדש בגרסאות אחרונות). בעוד רוב העדכונים כוללים שפות ביזריות נוספות (למשל: ארמית עתיקה), הם כוללים גם פה ושם הוספה של סימנים לשפות קיימות, או סימנים כלליים (צורות מיוחדות, סימנים או מטבעות וכו'). סימן המטבע "רופי", למשל, נכנס לתקן בגרסה 6 ולא זמין עדיין ברוב המערכות.

כל סימן ב Unicode מתואר ע"י קוד שנקרא Code Point ונכתב בצורה U+xxxx, כאשר x מתאר ספרה הקסדצימלית. למשל: ה code point של הסימן  הוא U+0164 וה code point של הסימן  ﴿  (סוגריים מעוטרים בערבית?!) הוא U+FD3F.
בגלל צורת הכתיבה הכוללת 4 ספרות ניתן להסיק ש Unicode מוגבל ל 65K סימנים - אך זה לא נכון: מעבר למרחב הבסיסי המתאר את הסימנים של השפות ה "נפוצות", הנקרא BMP (קיצור של Basic Multilingual Plane), ניתן להיתקל באותיות וסימנים ממרחבים נוספים (נקראים supplementary planes) - שם מרחב הסימנים, בפועל, איננו מוגבל.

קידודים

Unicode יש אחד, אבל דרכים לקודד אותו (Encodings) - יש כמה וכמה:
הנפוצים שבהם הם: UTF-32, UTF-16, UTF-8 ואולי גם נתקלתם ב UTF-7.
המחשבה שב UTF-8, קידוד 8 ביט, יש פחות אותיות מאשר ב UTF-32, קידוד 32 ביט, היא שגוייה: בעצם מדובר ב-2 דרכים לתאר את אותו מרחב סימנים של תקן ה Unicode.




הקידוד שהכי פשוט להסביר הוא UTF-32: כל אות מיוצגת ע"י אורך קבוע - 4 בתים / 32 ביט. קידוד זה הוא פשוט ומאפשר לבצע כמה פעולות חישוביות על הטקסט בצורה פשוטה למדי (לדוגמה: לקפוץ לאות ה n-ית). מצד שני, הוא "בזבזני" למדי במקום, מכיוון שהרוב הגדול של הסימנים שבשימוש (BMP) יכולים להיות מתוארים ע"י 16 ביט בלבד.

UTF-8, לעומת זאת הוא יותר חסכוני (והנפוץ מבין השלושה). את האותיות הנפוצות ביותר (בעת הגדרת הסטנדרט: לטיניות, מבוססות ASCII) - מתארים בעזרת בית אחד (8 ביט). אותיות נפוצות פחות (שפות מזרח אירופאיות, עברית ועוד), מתארים בעזרת שני בתים. אותיות יותר נדירות - בעזרת 3 או 4 בתים [א].
מצד אחד חוסכים מקום, מצד שני אורך האותיות הוא משתנה (1 עד 4 בתים) - מה שגורם להתעסקות נוספת.

UTF-16 הוא פתרון ביניים (הוא אגב, הקידוד הראשון שהופיע): כל סימן Unicode מיוצג ע"י 1 או 2 "words" של 16 ביט.
כאשר סימן אינו חלק מה BMP ואינו יכול להיות מתואר ע"י "מילה" בודדת של 16 ביט, UTF-16 ישתמש ב2 מלים של 16 ביט בכדי לתאר את הסימן. צמד מלים נקרא Surrogate Pair (תרגום לעברית של Surrogate: "בא-כח"), כאשר יש lead Surrogate ו trail Surrogate (או high ו low - גם מינוח שבשימוש). למרות שהרעיון של UTF-8 דומה מאוד, לא משתמשים שם, משום מה, במונח Surrogate.
שפת ג'אווה וסביבת NET. מקודדות את כל המחרוזות ב UTF-16 וכך גם מערכת ההפעלה Windows.

UTF-7, מקדד הכל ב 7-ביט בכדי לתאום למערכות ישנות שהניחו הנחות על אורך אות של 7 ביט בהתבסס על קידוד ה ASCII. השימוש הסביר היחידי שלו הוא לקידוד הודעות אימייל, אם השתמשתם בו לצורך אחר - קרוב לוודאי שעשיתם משהו לא נכון.

הערך המספרי הסופי של אות יכולה להיות זהה או שונה בין קידודים שונים. למשל, אומגה בתרשים לעיל זהה בין UTF-16 ו UTF-32, אבל שונה בין UTF-8 ל UTF-16.

שאלה: באיזה סדר שולחים את הערכים על הרשת? בשניהם, כמובן!
בימים הראשונים של Unicode התעקשו שיהיה גם ייצוג BE (קיצור של Big Endian) וגם ייצוג LE (קיצור של Little Endian, או "אינדיאני קטן" - כמו שקראנו לו באוניברסיטה) כדי לנצל מעבדי LE ו BE בצורה יעילה יותר. על כן נוסף לתקן זוג סימנים מיוחד הנקרא BOM (קיצור של Byte Order Mark) - המופיע בתחילת הטקסט ומתאר את הכיוון.
ב UTF-16, לדוגמה, 0xFEFF מתאר את BE ו 0xFFEF מתאר את LE.

יש גם גרסאות של UTF המצהירות מראש את מיקום ה Endian, למשל UTF-16LE או UTF-16BE (אם אתם נתקלים אי פעם בשמות הללו).


לעתים מתייחסים ל UTF-16 ו UTF-32 כ UCS-2 ו UCS-4, בהתאמה. UCS הוא קיצור של Universal Character Set.


את התפריט הזה של ++TextPad אתם אמורים להבין, בשלב זה


לינק: טבלת Code Points של Unicode



השפעות בקוד - עיבוד מחרוזות


כל זה טוב ויפה, מדוע זה אכפת לנו / כיצד זה קשור ל i18n?

נתחיל עם תרגיל ראשון: charAt.
אנו יודעים שבג'אווה השיטה (charAt(n מחזירה את הסימן ה n-י במחרוזת (כל אות היא 16 ביט).
זה נכון לאנגלית, רוסית, יוונית ותאילנדית - אבל יכול להיות לא-נכון עבור שפות אסייתיות (יפנים, סינית בתחביריהם השונים) או סימנים שונים של שפות שונות - שם אנו משתמשים ב Surrogate Pair של 2 chars לתאר סימן בודד!
כלומר, אם יש סימן המיוצג כ Surrogate Pair במקום ה n-4 וה n-3 (זהו זוג סימנים), אזי הפקודה (charAt(n תחזיר בעצם את האות ה n-1. אאוץ של באג!

דוגמה נוספת: אם אתם רוצים לספור כמה אותיות יש במחרוזת - לא ניתן לסמוך על התוצאה של השיטה ()length - אולי רק כערך מקסימום (וגם זה לא כ"כ פשוט... יש סימנים ב Unicode שבהגדרה צריכים לתפוס מקום של 2 אותיות, אבל זה עניין נדיר אחר).

סיפור מהחיים: היה לנו קוד בו הגבלנו את הקלט בשדה מסוים ל25 תווים - משיקולי מקום לפלט על המסך.
לקוח יפני פתח לנו באג שהוא לא מצליח להכניס בשדה הזה יותר מ 10-12 תווים, ובכלל נראה לו מאוד מוזר שבמילה מסוימת אחת - המערכת חוסמת אותו פעם אחרי 10 אותיות, ובמילה אחרת - רק אחרי 12 אותיות.

כנראה שכל התווים בהם השתמש היו surrogate pairs - ועל כן בדיקת ()length החזירה 26 כאשר הוא כתב מילה בת 13 אותיות. כיצד length קיבל ערך של 25 כאשר יש 10 אותיות בלבד? - על זה נדבר בהמשך.

מה עושים? הפתרון פשוט:


כאשר עובדים עם מחרוזות טקסט בשפה שאיננה אנגלית (אלו כנראה מיעוט המחרוזות בקוד), עברו להשתמש בשיטות של String שהן Unicode Aware: למשל codePointAt או CodePointCount.

ביתר פירוט:
  1. חיברנו 2 אותיות יפניות שדורשות לתאורן Surrogate Pairs, כלומר 2 chars כ"א. בג'אווה ניתן לקודד מילים של UTF-16 בעזרת u\.
  2. שימוש ב length יציג אורך 4, למרות שיש לי במחרוזת רק 2 אותיות.
    codePointCount סופר את מספר הסימנים (codePoints) של Unicode בתת-מחרוזת של ה String. אני רוצה לספור את התווים בכל המחרוזת.
  3. בעיה: charAt במקום ה 0 מחזיר סימן מוזר! גם charAt במקום ה 1, 2 או 3!
    הסיבה לכך היא שאין משמעות לחצי של Surrogate Pair, סוג של "חצי סימן" - וריבוע חלול הוא הודעת השגיאה של חלונות למצב שגוי שזה.
    codePointAt יתנהג כפי שהיינו מצפים מ charAt להתנהג במחרוזת אנגלית (ASCII/ANSI) טיפוסית.

עוד טיפ שאזרוק על הדרך הוא השימוש ב toUpperCase / toLowerCase.

לא לכל השפות יש Case (למשל: עברית) - וזו לא בעיה.

מצד שני, יש שפות בהן חוקי ה Case שונים מאנגלית - ואז שימוש בשיטות הנ"ל ייצר שגיאות.
שפה לדוגמה היא טורקית בה upperCase של i הוא לא I, אלא מין i גדול. מצד שני, ה lowerCase של I הוא לא i אלא מין I קטן. אני לא יודע להסביר מדוע זה כך - אבל זו ההתנהגות הרצויה.
הפתרון הוא פשוט: להשתמש בחתימה של ()toUpperCase עם אובייקט Locale. יש כזו - והיא תעשה את העבודה.


עיבוד מחרוזות Unicode - הפתעות לא צפויות


בכדי להבין את התכונה הבאה של Unicode, נפתח בניסוי הקטן הבא:
  1. פתחו אפליקציית notepad והקלידו משפט בעברית.
  2. העמידו את סמן העכבר לאחר אחת האותיות.
  3. וודאו שכפתור NUM LOCK דלוק (כלומר: יש אור).
  4. החזיקו את מקש ה Alt למטה והקלידו את המספר 0198 (כולל האפס לפני).
מה קיבלתֶם?

אם הניסוי עבר בהצלחה, אות שלפני הסמן אמורה לקבל ניקוד סֶגול. זו איננה יכולת של Notepad אלא של "חלונות" (ניתן לקרוא עוד פרטים ושיטות לנקד בלינק הבא)
מה שמעניין אותנו הוא דווקא כיצד מייצג תקן Unicode את האות המנוקדת.
אפשרות אחת הייתה ליצור פרמוטציה של כל האותיות העבריות (27 כולל סופיות) וכל הניקודים (15, על כל הצורות) - מה שידרוש כ 400 תווים ב Unicode.

מילא עברית, אבל מה עם הינדית - לה פי 3 אותיות, והרבה צורות חיבור שונות? מה עם סינית, יפנית, קוריאנית?!

דוגמה לכתיב הודי

מגדירי ה Unicode החליטו להימנע מהכפלת פרמוטציות ויצרו מכניזם יעיל יותר: מה שעשוי להראות למשתמש כאות/סימן אחד על המסך יכול להיות בעצם הרכבה של כמה סימני Unicode (כלומר code points) שונים.

שימו לב: זהו לא Surrogate!
יכול להיות code point יחיד המתואר ע"י שני characters - זהו Surrogate.
יכול להיות סימן ויזואלי אחד (מה שנקרא בעולם הזה grapheme), המתואר ע"י כמה code points, חלקם אולי surrogate - ואחרים לא.

מבלבל? הנה כמה דוגמאות:
  • grapheme של "A" מתואר ע"י char אחד
  • grapheme של האות היפנית "姶" מתואר ע"י 2 chars - כלומר surrogate Pair, מכיוון שהאות לא נמצאת במרחב הסימנים הבסיסי, ה BMP.
  • הסימן המוזר " X͏֞ " הוא grapheme המתואר ע"י שני chars (בעצם, שני code points) אחד הוא האות X שכולנו מכירים, והשני הוא מין גרשיים שמתלבשים על האות הקודמת במקום שהוקצה לה, מה שנקרא combining character - סימן ה"מתנחל" במרחב של הסימן שקדם לו.
הנה כמה דוגמאות ל combining characters:

Graphemes המורכבים מ 2, 3 וארבעה אותיות/סימנים. איך בדיוק הרכיבו את הדוגמה האחרונה - אני לא מבין, אבל אני מאמין שזה נכון.

שימו לב שלסימן ë יש 2 ייצוגים שונים: או כסימן (code point) בודד, או כהרכבה של שתי סימנים, האות e ועוד combining character (כל אחד הוא code point עצמאי).

כדי לסבך מעט העניין, grapheme שונים מצוירים לעתים בצורה שונה ע"פ ההקשר בו הם מופיעים. למשל יכלו להחליט לתאר אותיות סופיות (ם,ן,ץ,ף) בשפה העברית כ glyph (ייצוג ויזואלי מסוים ל grapheme) - אך לא עשו זאת, במקרה.
בשפות שונות יש כל סימנים שמוצגים אחרת ע"פ המיקום שלהם במילה/משפט/אותיות שכנות - למרות שזהו אותו ה code point או אותו ה grapheme. עניין זה נוגע יותר לפונטים ולא לייצוג של Unicode - אך כדאי להכיר את התופעה ולדעת שאם האות נראית אחרת - היא לא בהכרח סימן שונה ב Unicode.


משמעות מיידית למפתחים: עיבוד מחרוזות בשפות-שונות


ראינו קודם איך לבדוק את מספר ה Code Point - ושיטה זו אכן תעבוד ב 95% מהמקרים. מעטים מאוד המקרים בהם משתמשים ב combining characters לכתוב בשפה אירופאית (משתמשים בגרסה המשולבת, למשל ë), אבל בשפות אסייתיות לפעמים אין ברירה אחרת. לפעמים מישהו השתמש בכלי שיצר לו צרופים כאלו.

מה עושים?

הנה מתודה שכתבתי בג'אווה שעושה את העבודה:


המפתח לפתרון הוא שימוש ב breakIterator מסוג Character שהולך תו נראה (grapheme) אחרי תו נראה על המחרוזת, יהיה זה combining character, surrogate או שילוב של שניהם, עד שהוא מגיע לסופה (ואז הוא מחזיר מין ערך ריק - לכן ה 1-).


השימוש ב breakIterators עשוי להיות חשוב מאוד - אין לכם מושג אלו שפות מוזרות יש בעולם:
  • יש שפות, כמו אמהרית או בורמזית, בהן רווח הוא לא המפריד בין מילה למילה. נסו להשתמש ב (" ")split - והגורל יצחק לכם בפנים.
  • בטיבטית, למשל, יש סימן מיוחד לרדת שורה. מי שכותב טיבטית לא יקליד n\ כי אם סימן אחר.
  • ביפנית וסינית משתמשים ברווחים להפריד בין ביטויים / משפטים, ולא בין מלים.
יש breakIterators לרמות תחביריות שונות: מילים, משפטים, שורות וכו'. אם אתם רציניים לגבי i18n ועיבוד מחרוזות בשפות שונות - כדאי שתשתמשו בכלי זה.

אני ממליץ גם להציץ על הטבלה המעניינת הבאה, המציגה רשימת "פיצ'רים" של שפות. אלו הפתעות צפויות - והיכן. הרשימה איננה מלאה.


חיפוש ו Normalization


טוב, בשלב זה עניין ה unicode אמור להיות דיי מובן. אנסה לכסות בקצרה עוד כמה pitfalls אפשריים.
נניח שיש לנו אפליקציית TODO List בה המשתמש מקליד משימות שעליו לבצע. המשימות נשמרות בבסיס נתונים, והמשתמש יכול מאוחר יותר לחפש ע"פ הטקסט שהקליד.

אנו יכולים לספק exact match search - חיפוש שעובד ע"פ שיוון רצפים של characters במחרוזת. מימוש נאיבי שכזה ייכשל למצוא את "Do" כאשר אנו מחפשים את "do".

הנירמול הבסיס ביותר אם כן עבור חיפוש הוא מעבר ל upper case (או lower - לא משנה). כפי שראינו קודם, חשוב לבצע אותו עם אובייקט Locale עם השפה הנכונה.
למשל: מהו ה upper case של Fuβ? אם אתם זוכרים β הוא קיצור של שני s (ב lower case) בגרמנית. צורת ה UpperCase בגרמנית היא לכן FUSS - טקסט בעל אורך אחר של אותיות.

עוד הבחנה מעניינת ב Unicode היא בין שוויון קאנוני ושוויון תאימות (או סמטני):


אם המשתמש שלכם מחפש אחר טקסט עם הסימן Ä, האם אכפת לו איך הוקלד הטקסט במקור / כיצד הוא מיוצג בבסיס הנתונים??
האם כ U+00C4, או כ combination sequence של U+0041 ו U+0308? - כנראה שלא.

אם הוא לא ימצא את הטקסט ויגלה שמשהו שנראה בדיוק אותו הדבר נמצא ברשימה - הוא פשוט יפתח לכם באג. "התוכנה לא עובדת כמו שצריך".

הערה: כאשר אנשים מקלידים את הטקסט אין ממש סיבה שיהיה שימוש ב combination sequence, אלא אם הם יכולים לכתוב את הטקסט בעורך טקסט לא ידוע. כלומר: מבחינת ניהול סיכונים, אם אתם כותבים אפליקציה ל iPhone - יש סיכוי טוב שאתם יכולים להימנע מטיפול normalization. אם המערכת שלכם היא מערכת שאותו טקסט משמש משתמשים רבים, שיכולים לכתוב אותו בעורכים שונים - אזי עניין זה נהיה ממשי.

לגבי שוויון תאימות (compatibility equivalence), שהוא סוג של שוויון סמנטי - ייתכן ומשתמשים רבים יקבלו את רוע הגזרה: חוץ מהאמריקאים, כנראה שכל העמים חווים תוכנות שלא עבודות בצורה מושלמת עם השפה שלהם. בכל זאת, אם כבר עושים טיפול - שווה לכסות כבר את האזור הזה גם כן.
קחו לדוגמה את השפה הערבית, בה יש סימנים שונים של Unicode לאותיות מחוברות () ולא מחוברות (ﻦ , ﻨ). דיי מרגיז לבצע חיפוש ולא למצוא מילה כי אות אחת הייתה מחוברת או לא מחוברת.
עוד דוגמה נפוצה (ומעצבנת) היא ההבדל הסמנטי בין מרכאות ניטראליות ( " ) למרכאות בעלות כיוון ( , ‟ ). יש להן code points שונים בתכלית - אך משמעות סמנטית זהה.

שימו לב ששוויון תאימות (למשל הדוגמה למעלה מהשפה הצרפתית) יוצר טקסט באורך שונה מבחינת מספר ה graphemes - למרות שהוא סמנטית זהה. כלומר: גם BreakIterator אינו חסין במקרים אלו בפני טעויות.

תקן ה Unicode מגדיר 4 צורות לנרמל טקסט:

מקור: וויקיפדיה

האם זה משנה באיזו צורה בוחרים?
ובכן: כן. אם מישהו מקליד טקסט בצורה האלגנטית, תוך שימוש בקיצור כגון   או כתב מחובר ואתם מנרמלים את הטקסט ל fi (שני סימנים) - הוא עלול להתבאס.
ברירת המחדל היא לנרמל ל NFC, אך יש שיקולים שונים לבחור בצורות נרמול אחרות.

בסופו של דבר, את הנרמול ניתן לבצע בקלות בעזרת מחלקה בשם UnicodeNormalizer המגיעה כחלק מספריית ICU, הנה דוגמת קוד לשימוש.

עוד נקודה חשובה: כל צורות הנרמול הן "בטוחות" - לא תפגעו בטקסט אם תנרמלו אותו, באותה השיטה, פעם נוספת.

קישור: FAQs אודות normalization ב Unicode.







BiDi ו RtL ב Unicode

נושא זה הוא לא כ"כ בסיסי, אך בגלל שאנו דוברי עברית - החלטתי לתאר בקצרה כיצד עניין ה Bi-Directional (בקיצור BiDi) עובד בקידוד של Unicode.
מילת המפתח היא Bi-Directional ולא RtL (קרי Right-to-Left). עברית וערבית הן שפות שנכתבות מימין לשמאל, אך מספרים וקטעי טקסט לטיני (למשל: אנגלית) - נכתבים בהן משמאל לימין.

תקן Unicode ממפה את כל האותיות / סימנים ל 3 קבוצות:
  • סימנים שהם LtR - רובם המכריע של הסימנים.
  • סימנים שהם RtL - עברית, ערבית ואולי עוד כמה שפות (בעיקר עתיקות?!)
  • סימנים ניטראליים - כמו סימני פיסוק ("!"), רווחים ועוד.
ברגע שהמחשב נתקל ברצף סימני ה Unicode הבא (סדר לוגי):

פייל40טסאל

הוא מציג אותו על המסך בצורה הבאה (סדר ויזואלי):


לאסט40לייפ

מי שמטפל בקידוד ה Unicode מזהה את טווח הסימנים העבריים שמסומן כ RtL ומציג אותם על המסך מימין לשמאל. כאשר הוא נתקל בסימן "4", המסומן כ LtR הוא הופך את כיוון התצוגה, כנ"ל לגבי האות "ל" לאחר ה "0".

מה קורה עם סימנים ניטראליים? הם מקבלים את הכיוון האחרון שיושם.

ומה אם "כוונת המשורר" היא אחרת (כזו שנשלטת ע"י עורך הטקסט - כמובן)?

ישנם מספר סימנים "בלתי נראים" ב Unicode הקובעים סדר ויזואלי.
למשל:

A?ג

יתפרש באופן טבעי כ ג?A, אך אם נוסיף סימן מפורש של RtL:

A\u200F

אזי הטקסט יוצג כ ?גA. כלומר: ציינו ש "?" מסודר ויזואלית מימין לשמאל.

מלבד סימני RtL ו LtR, יש סימני-כיוון מתקדמים יותר, לא מפורשים, המאפשרים לנהל רצפים של שינויי כיוון ולהתעלם מהכיוון המוגדרת על האות. הלוגיקה שלהם מורכבת יותר, ואם זה רלוונטי עבורכם אתם יכולים לקרוא בלינק הבא מאמר טוב ומסכם.

לינק רלוונטי (זהירות: hardcore): תיאור מלא של האלגוריתם לפענוח BiDi ב Unicode.




סרגל להמרת מידות. משהו שהיה בשימוש עד שנות ה-70


מידות ומדידות


הנושא הבא נוגע יותר לאזורים גאוגרפיים, מאשר שפות.
הביטו בביטוי המספרי הבא:

1,000

בכמה מדובר?
קרוב לוודאי שתאמרו שזהו "אלף". כך גם יאמר האמריקאי או היפני, אך סביר שגרמני דווקא יסיק שמדובר במספר "אחד". אחד?! ... כן.

ישנן 3 צורות נפוצות לתאר את המספר 10000 (עשר אלף):
  1. 10,000.00
  2. 10.000,00
  3. 000,00 10
הצורה הראשונה מקובלת בישראל, ארה"ב, יפן, אנגליה, אוסטרליה ועוד.
הצורה השנייה מקובלת בגרמניה, איטליה, ספרד, ברזיל ועוד.
הצורה השלישית מקובלת ברוסיה, צרפת ועוד.
ויש לפחות עוד כמה צורות אחרות... בשוויץ, כותבים עשר-אלף כך: 000.00'10

ניסיון אישי קצר: יוצא לי להשתמש בתוכנה גרמנית שאינה מותאמת לצורת הכתיבה הישראלית, ואני נדרש לכתוב את המספרים בצורה השנייה. זה אפשרי, אך מבלבל ולא נוח....

הפתרון לבעיה המוצגת למעלה הוא פשוט ביותר, להשתמש באובייקט NumberFormat בג'אווה (אני מניח שיש מקבילה, אולי כספרייה נוספת, בכל השפות הנפוצות). אובייקט זה מקבל בעת יצירת המופע לוקאל ועל פיו הוא יבצע formatting נכון של המספרים.

החכמה פה, כמו בדוגמאות אחרות - היא בעיקר להכיר את הבעיה, ולהיערך אליה נכון.


דוגמה נוספת: תאריך ושעה.
תאריך מושפע מ 3 גורמים עקריים:
  1. לוח השנה: למשל, גרוגאני (מערבי), עברי , יפני ,סיני ועוד.
  2. ה Time zone - היכן נמצא המשתמש הספציפי על הגלובוס, וה day light saving.
  3. הפורמט בו בקובל לכתוב תאריך באותו האזור (region).
יש מדינות בהן היום האחרון במילניום ייכתב כ 31/12/1999 או 12/31/1999 או 1999/12/31.
כאשר מדובר ביום וחודש ששניהם קטנים מ12 - הטעות יכולה להיות קריטית.
יש מדינות בהן יש העדפה ברורה לצורת ההפרדה: נקודה, קו נטוי, מקף או רווח. הנה סיכום יפה של האפשרויות השונות.

בג'אווה, האובייקטים Calendar, TimeZone ו DateFormat יעשו עבורכם את העבודה.

כאשר שומרים תאריך/זמן שייקרא אח"כ פוטנציאלית ע"י משתמש אחר / אותו המשתמש באזור זמן אחר - כדאי לשמור את כל הזמנים ע"י אזור זמן אחיד (בד"כ UTC) ולהתאים אח"כ, בזמן ריצה, לכל משתמש ע"פ הלוקאל הנוכחי שלו.

יש עוד נושאים הדורשים התאמה (Localization) לאזורים מסוימים: מטבע (כיצד קובעים שער חליפין), שיטות מדידה (מטרית, אניצ'ית) הדרך בה כותבים מספרי טלפון, כתובת דואר או מיקוד.
לצערי, אין לי תובנות מיוחדות כיצד להתמודד עם נושאים אלו. פשוט לעשות וללמוד, ללמוד ולעשות.



השלמה: תרגום טקסט עם פרמטרים.

בפוסט הראשון דיברתי לא-מעט על תרגום טקסט וטיפול בקבצי-תרגום, אך שכחתי לציין נקודה חשובה בהכנת הקוד לתרגום שנוטים ליפול בה: לחלק את ה user string לתרגום לחלקים.

כמה דוגמאות לטקסט באנגלית, וצורת העברה לתרגום (i18n) - שהיא בעייתית:
  1. "Do you want to delete item " + getId() + " ?"  --> "{KEY_DEL_APPROVAL_TRNSLT} {0} ?", getId()
  2. "An error occurred with " + "the cooling system" --> "{ERROR_OCCR_W_TRNSLT} {COOLING_SYS_TRNSLT}"
  3. "There are 2 more steps for completion" --> "{TRNSLT_1} 2 {TRNSLT_2}" 


מה הבעיה?
  1. בדוגמה זו יש משתנה שנכנס למשפט. המתכנת (הדמיוני) חילק את המשפט ל"קטע מתורגם", "מספר" ו"סימן שאלה".
    אבל.... בשפות שונות מבנה שאלה הוא אחר:
    בתאילנדית (סתם אני זוכר מהטיול) - על המשפט להתחיל במילת שאלה.
    בגרמנית - הפועל "מחיקה" הוא המילה האחרונה במשפט.
    בספרדית - בנוסף לסימן השאלה, המשפט יכול להתחיל בסימן שאלה הפוך (¿).הכי גרוע: המתרגם עלול לאבד את ההקשר.
  2. במקרה זה המתכנת ניסה לעשות "שימוש חוזר" בחלק המשפט שחוזר על עצמו, אולי בכדי לחסוך בתרגום (מתכנת דמיוני - זוכרים?). חלוקה זו ל 2 משפטים יכולה להתפרק בקלות בשפות שאינן אנגלית, ושוב - המתרגם מאבד את ההקשר של המשפט השלם.
  3. המספר 2 אינו אוניברסלי לכל שפה, ואינו מופיע באותו המקום באותו השפה.
    מספרים ותאריכים - כדאי שיהיו חלק מהטקסט המתורגם.
תרגום לצרפתית של משפטים פשוטים, הכוללים מספרים. אולי לא מה שהייתם מצפים.

על כן, הדרך המומלצת היא להכיל את כל המשתנים בתוך הטקסט המתורגם:
  1. "Do you want to delete item {ITEM_ID} ?"  --> "{TRANSLATED_TEXT1}"
  2. "An error occurred with the cooling system" --> "{TRANSLATED_TEXT2}
  3. "There are {NUMBER} more steps for completion" --> "{TRANSLATED_TEXT3}" 
המתרגם עצמו ינהל היכן הביטוי ("ITEM_ID") נכנס במשפט, וחשוב לתת לו הקשר - באיזה סוג של מידע מדובר (מספר, תאריך, טקסט - ובאיזה צורה) וכו'.



ווב ו SEO

אם מדובר במערכת ווב, זה פשוט: וודאו שיש לכם תגיות lang ו charset תקינות.


סיכום


פווו.... זה היה פוסט קשה לכתיבה!
מרחתי את הכתיבה לאורך שבועיים, כל יום כתבתי איזו פסקה. החלק הקשה: לחפש דוגמאות, ולאמת אותן. להתעסק עם כל הסימנים המוזרים האלו. זה היה מתיש...

ייתכן ואקבל תגובות נוסח "בחרת את הנושא הכי משעמם ביקום, כאילו!". אבל עבורי, למרות הקושי, הנושא היה דווקא דיי מעניין. אני מניח שזה עניין של פתרון בעיות: אחרי שנתקלים בהן ומשקיעים בהן זמן - נושא הפתרון נעשה מעניין, ולא קשור באיזה תחום הוא.

כפי שכתבתי בהקדמה, ניסיתי לתת בסיס ידע משמעותי על נושא ה i18n - כזה שלפי דעתי אין היום בעברית (וקשה גם למצוא באנגלית). אני מקווה שזה יהיה שימושי.

שיהיה בהצלחה!



----

לינקים רלוונטיים

כיצד Etsy מציגה כסף (במדינות שונות בעולם) https://codeascraft.com/2016/04/19/how-etsy-formats-currency/

----

[א] פעם התקן של UTF-8 אפשר עד 6 בתים בכדי לתאר סימן, אך פישטו אותו למקסימים של 4 בתים בלבד.


2014-03-04

לכבוש את העולם: תוכנה רב-לשונית

נניח שיש לכם מוצר בשם "FlappyWare", והוא מוצלח למדי. התחלתם למכור אותו בארה"ב (דא!) והמכירות כבר זורמות מעצמן. עכשיו אתם רוצים להתרחב לשוק גדול אחר: אירופה. אולי לשוק קטן ומקומי: ישראל, ואולי למזרח הרחוק.

מה בעצם הבעיה לקחת את המערכת שנכתבה לשוק האמריקאי, ולהתאים אותה למדינות נוספות כגון: אנגליה, גרמניה, ספרד וישראל?

אנגליה: השפה היא כמו שפת המערכת הקיימת, אנגלית. גרמניה: יש לכם מתרגם שיתרגם בקלות לגרמנית ומישהו בחברה שיודע ספרדית היטב. תוך כמה ימים אתם יוצרים אקסל עם כל הודעות המערכת למשמש ("user strings") בכל 4 השפות.
סביבת הפיתוח מאפשרת ניהול מספר שפות ל User Strings (תהיה זו ג'אווה עם Resource Bundle property files או iOS עם NSLocalizedString).
האם זה יותר מעניין של כמה ימים - בכדי לכבוש את העולם המערבי המפותח? (פרגון לישראל)


אני כותב את הפוסט בהמשך להאזנה לפרק של מובייל ובירה ששמעתי לאחרונה - פודקאסט עברי מוצלח על פיתוח מובייל שאני שמח להמליץ עליו בהזדמנות זאת. אני שומע קבוע והוא עוזר לי להישאר בעניינים בנושאי פיתוח למובייל.

אחד מחברי הפודקאסט סיפר שאפליקציית מובייל שהם כתבו הצליחה באופן מפתיע בשוק הרוסי והשוק הסיני - והם החליטו לתרגם את האפליקציה לשפות אלו. אני מכיר את נושאי רב-הלשוניות (Multi-lingual) מעולם ה Enterprise, אך נראה שהוא רלוונטי למדי גם לאפליקציות מובייל.

נושא ריבוי-הלשונות הוא נושא לא-מורכב, אך עשוי להיות מעט מפתיע. נראה לי שהוא גם לא כ"כ מוכר. בבדיקה קצרה בגוגל לא מצאתי עליו חומר איכותי, לא ספרים ובטח לא חומר טוב בעברית.

אני אנסה לשנות זאת... :)






מושגים


בתחום זה יש 3 מושגים מבלבלים:
  • Internationalization (בקיצור: i18n)
  • Localization (בקיצור: l10n)
  • Globalization (בקיצור: g11n)
ראשית הסבר על מהות הקיצורים המשונים:
המינוח Internationalization הוא דיי ארוך. מכיוון שאנשים מזהים מילים בעיקר בעזרת האות הפותחת והמסיימת, אפשר לקצר ל "i---n" ולשמור על היכולת של לקשר את הקיצור למילה המקורית. כמה אותיות יש באמצע? 18. לכן: i18n. (לא מאמינים? תספרו).

מה הקשר בין שלושת המושגים? האם Globalization הוא ההיפך מ Localization? האם זה אותו הדבר?

אפשר לסכם את הקשר בעזרת הנוסחה הבאה: g11n = i18n + l10n.

Internationalization הוא התהליך של הכנת התוכנה לריבוי שפות ואזורים (regions).

Localization הוא התהליך של התאמה לאזור / שפה מסוימת. התאמה כוללת אלמנטים של שפה (עברית, אנגלית) כללים של האזור (מטבע, שיטה מטרית, צורה לכתוב תאריך וכו') ורגולציות (למשל: חוקי פרטיות בגרמניה המחייבים למחוק פרטים היכולים לזהות אדם ספציפי מלוגים של מערכת).
עוד אלמנט שנוגע לאזור הוא עיצוב: צבעים, אייקונים וסמלים יכולים להתפרש במדינה אחת בצורה שונה מאשר במדינה אחרת.

Globalization הוא התהליך הכולל של Internationalization ו Localization, הכולל הכנה לריבוי-שפות ואז התאמה למספר שפות/אזורים ספציפיים.


שיטה אחרת (עמוד 2 במסמך) מתארת את הקשרים מעט אחרת ומדברת גם על Localizability, L12y). אם האנשים שעובדים אתכם נוהגים ע"פ המודל השני, פשוט הפכו את שמות המושגים i18n ו g11n - כל השאר נשאר בערך אותו הדבר.


מונח נפוץ אחר, בעיקר ב API של שפות תכנות, הוא Locale (תרגום לעברית: מקום).

Locale הוא אובייקט המתאר קומבינציה של שפה ("עברית"), אזור ("קנדה") ולעתים גם codeset (שזה בעצם encoding, כמו UTF-8 - נושא שאפשר להקדיש לו פוסט משלו). בד"כ יהיה לנו על ה session אובייקט locale שבעזרתו נדע כיצד לשרת את המשתמש הנוכחי.

נהוג לקדד את ה Locale בעזרת שפה ע"פ תקן ISO 639-1 ואזור ע"פ תקן ISO 3166-1.
למשל: en_US (אנגלית, ארה"ב), en_GB (אנגלית, ארה"ב) או zh_CN (סינית בסין) או אפשר גם he_CN (עברית בסין).

Locale, אפרופו, מבטאים כ"לו-קאל" (כמו "לוראל" - חברת קוסמטיקה) ולא "לוקייל", כפי שלעתים נהוג.



חזרה ל FlappyWare


הכנסתם למערכת את כל משפטי הטקסט ב-3 השפות החדשות (גרמנית, צרפתית ועברית). אבל הלקוחות מתלוננים:
  • הישראלים רוצים שכל התפריטים ועימודי הדף יהיו מימין לשמאל. העימוד הנוכחי פשוט לא מתאים!
    אולי למתכנת, עימוד משמאל לימין לא מפריע (או עדיף), אך פועל במפעל, או אמא שיש לה מספיק נושאי-עניין בחיים - פשוט רוצים שהממשק יהיה קל ונוח ("למה לא עשו את זה בסדר? הם לא רואים שזה עקום?").
  • הבריטים רוצים שהמערכת תשאל: "?what is your favourite colour" ולא "?what is your favorite color". הבדל בין אנגלית-אמריקאית לאנגלית-בריטית, שנתפס אצל הבריטים כמאוד-לא-קטנוני.
  • ישנם באגים לא צפויים במערכת בספרדית בתחום החיפוש. משום מה גם הטבלאות התחרבשו... מה זה קשור לתרגום?!
  • הגרמנים פשוט לא מרוצים ונמנעים משימוש באפליקציה. לא הצלחתם לייצר איתם קשר טוב מספיק להבין מה בכלל הבעיה (?!).

כאשר המשתמשים שלכם מגיעים מתרבות שונה ודוברים שפה שונה, גם הטיפול בכל case/bug נהייה קשה יותר. כשלקוח טוען ש"בהונגרית זה צריך להיות ככה...", ואתם לא מבינים על מה הוא מדבר / למה זה יצא אחרת - זה יכול להיות מאמץ לא קטן להבין ולתקן.







להבין את תהליך התרגום


לפני שנבחן "מה יכול להשתבש?" (זה החלק המשעשע), כדאי לדבר גם על תהליך התרגום, שגם הוא עשוי מעט להפתיע. בגדול, ניתן לחלק את מה שנרצה לתרגם ל 2 סוגים ב 2 קטגוריות.
הקטגוריות הן:
  1. תוכן מערכת - כל הודעות טקסט במסופקות כחלק מהאפליקציה: כותרות, תפריטים, הודעות שגיאה וכו'. סוג זה הוא הנפוץ ביותר.
  2. תוכן לקוח - תוכן שהלקוח יצר בעצמו והוא רוצה לתרגם. סוג זה של תוכן רלוונטי כנראה רק לתוכנה עסקית, ורק לחלק קטן ממנה. לדוגמה: מערכת שיווק אונליין הנמכרת ללקוח (קמעונאי כלשהו) רוצה למכור סחורה במדינות שונות - לכן עליו לתרגם את קטלוג המוצרים, כתבי אחריות, מפרטים וכו' - תוכן שהוא יצר.
שני סוגי התוכן הם:
  1. מחרוזות (Strings), קרי "טקסט נטו" - אותן קל למדי לתרגם.
  2. כל סוג אחר של מדיה: תמונות, מסמכים (למשל PDF), קישורים וכו'.
    תמונות (כמו ה screenshot ב appstore או כפתורים באפליקציה) לעתים מכילות טקסט - ועל כן יש גם ליצור להן עותק מתורגם. ייתכנו תמונות שונות ללוקאלים שונים, למשל תמונה שונה לשוק החרדי ולשוק החילוני בארץ (כן... ניתן לראות את השוק החרדי כסוג של Locale שונה, למרות שהוא במדינת ישראל).
    לעתים אלו קישורים (URLs) לאתרים שונים, כאשר נקשר אתרים שונים לשפות / לוקאלים שונים.
את התרגום נוהגים לעשות מתוך ה Master Language. כלומר, בוחרים שפה "מובילה" (בארץ זו תהיה בד"כ אנגלית, אך במדינות אירופיות זו עשויה בהחלט להיות השפה המקומית) בה ייכתבו הטקסטים המקוריים וממנה יתורגמו כל השפות - בכדי לצמצם טעויות.

אפילו אם המערכת שאותה נרצה לתרגם היא מערכת online (למשל ממשק וובי בענן), את התרגום נהוג לא לבצע בתוך המערכת אלא במערכות תרגום ייחודיות. למשל: במקרה של תרגום תוכן-לקוח, עשוי להראות סביר לספק את היכולת בתוך המערכת, הרי יש כבר ממשק ווב. מה יותר נוח מזה?!

לרוב זה לא יהיה נוח מ 2 סיבות:
א. למתרגם לא תמיד תהיה גישה למערכת ה Online.
ב. כלי התרגום מספקים יתרונות משמעותיים (למשל מילון-מונחים שכבר בשימוש במערכת, Thesaurus ועוד) - והמתרגמים עשויים דווקא לגבות מחיר גבוה יותר על שימוש במערכת אונליין.






בעיניים עצומות

פרט קטן לגבי התרגום לא ציינתי: המתרגמים לרוב עושים את עבודתם ללא שהם מכירים לעומק את האפליקציה שהם מתרגמים.
"לעומק"? תנו לי לתקן: הרבה פעמים הם מעולם לא ראו אותה או לא אפילו לא יודעים מהי.

רוצים להימנע מתרגום נוסח Google Translate או תרגומים מביכים כגון "רפידת הטקסט" (TextPad)? - אם כן, כדאי שתעזרו למתרגמים.

מקור: גיקטיים

מתרגמים קצת יותר רציניים יבקשו מסמך המתאר את האפליקציה, הסיטואציה והמשתמשים - או יקבעו פגישה כדי ללמוד ולכתוב כזה בעצמם למתרגמים האחרים. המתרגמים יכולים לעבוד בכל העולם ותרגום מחרוזות האפליקציה שלכם (אם היא לא גדולה) יכולה להיות אפליקציה אחת מתוך ארבעה שהם מתרגמים באותו היום. במיוחד כאשר מדובר בעדכוני-גרסה, ולא תוכנה שעוברת תרגום בפעם הראשונה.
עם מסמכים בפורמטים שונים (Pdf, תמונות, תמונות וקטוריות וכו') - המתרגמים יכולים להסתבך, ולשלוח לקבלן משנה בעל הבנה פחותה יותר במה מדובר. איך תאמדו את איכות התוצאה?

מה שעושים בדרך כלל (כשרוצים תוצאות יותר טובות) הוא מפרקים את הטקסטים מתוך קבצי המדיה השונים ושולחים אותם כ Strings. משאירים את הטיפול בפורמטים השונים של קבצי המדיה in-house.
אם המערכת שלכם מספקת יכולת לתרגם תוכן-לקוח - זה אומר לאפשר להוסיף מחרוזות שלא נמצאות במערכת.

דבר שני וחשוב יותר הוא הוספת תיאור קצר שמספק את ההקשר לכל מחרוזת. לדוגמה על "Hall of Fame" ניתן לציין שזוהי כותרת מסך עם תוצאות המשחק הטובות ביותר או דף באתר האינטרנט המאתר את מורשת החברה. התרגום - יהיה שונה ע"פ ההקשר שיסופק.

את ההערות מוסיפים פעמים רבות המתכנתים תוך כדי שהם כותבים את הקוד. כל מחרוזת שהיא טקסט לתרגום (לא כל המחרוזות בתוכנה הן כאלו, כמובן) - מתווספת עם הערה. יש סביבות בהן מוסיפים את ההערות בקוד (iOS?) וכאלו שבעזרת כלים נלווים ב IDE (למשל בג'אווה).

אחרי שמסיימים פרויקט תרגום, נראה הגיוני מאוד לסמן את המחרוזות למשתמש ("User Strings") בפרויקט הבא מהיום הראשון, כך שלא יהיה צריך לעבור מחרוזת-מחרוזת בקוד ולאתר את כל הטקסטים למשתמש. כך גם הומלץ בפרק של "מובייל ובירה" שעסק בנושא.
מניסיוני, אפשר לעשות זאת אם כלי הפיתוח באמת נוחים. Localization הוא קצת מעבר ל User Strings ולא כדאי להתחיל להשקיע לפני שברור שזו המטרה.

את המחרוזות לתרגום, נוהגים לנהל באחד מ-2 פורמטים נפוצים:
PO/POT - שמקובל בעולם ה Unix. מערכת Wordpress, למשל משתמשת בו.
XLIFF - מקובל יותר בעולם הג'אווה / אנטרפרייז. אפשר להמיר בקלות קבצי properties שמייצר Eclipse ל XLIFF.

עוד בסיס מידע חשוב הוא ה Translation Memory, מין "בסיס נתונים" (קובץ TMX או TBX) של המונחים בהם השתמשו במערכת. כאשר מתרגם יקבל מכם עוד עבודה אחרי חצי שנה שלא נגע במערכת (כאילו שהוא זוכר אותה) - ה Translation Memory יזכירו לו באלו מונחים הוא השתמש בעבר והוא יוכל לשמור על קונסיסטנטיות.

כדי שהמתרגמים יוכלו להיות עקביים, גם עליכם להיות עקביים במונחים בהם אתם משתמשים בשפת ה Master Language, כמובן.


כיצד נראה התרגום בקוד


זה כמובן תלוי בשפת וסביבת הפיתוח. אני אצמד לדוגמאות ב Java - השפה בה אני מכיר את הנושא בצורה הטובה ביותר.
בג'אווה יש מנגנון בשם Resource Bundles שעוזר לנהל user strings בשפות שונות.
את ה Resource Bundle ניתן לייצר בעזרת Wizard ב Eclipse:



באותו הרגע נוצרים הקבצים הבאים:
  • קובץ plugin.properties שכולל את טקסטי ברירת-המחדל / ה Master Language.
  • לכל שפה, אזור או צימוד של השניים שהגדרנו - ייווצר קובץ נוסף, לדוגמה plugin_en.properties או plugin_en_GB.properties.


לאחר מכן ניתן לערוך את קבצי ה properties, כאשר כל key מייצג מחרוזת בכל שפה.


הערה: אצלנו בחברה היה Editor אחר, אולי פרי פיתוח פנימי. אנא סלחו אם אני מפספס פה איזה פרט.

בזמן ריצה של הקוד ישלוף את המחרוזת המתורגמת המתאימה:


ResourceBundle bundle = ResourceBundle.getBundle("properties.namespace", someLocale);

String translatedString = bundle.getString("someKey");


שימו לב שלג'אווה יש מנגנון fallback אם הוא לא מוצא את הלוקאל המדויק:
אם למשל הלוקאל הוא "en_US" אך אין קובץ כזה, הוא ינסה להשתמש ב "en". אם אין גם קובץ כזה - הוא ילך לקובץ ברירת המחדל (למשל: plugin.properties).



מה יכול להשתבש ב Localization?  - מיון


אם האפליקציה שלכם מקבלת קלט בשפה שאיננה אנגלית ומנסה למיין אותו - היא עלולה להיכשל.
נתחיל בכך שמתודת ()compareTo של המחלקה String בג'אווה עלולה כבר לגרום לטעות, אפילו בשפה האנגלית.
מדוע? בגלל הקידוד (להלן כמה יסודות שחשובים בכדי להבין).

בגדול, כל מחרוזת טקסט היא חסרת משמעות - אלא אם ידוע הקידוד שלה. הקידוד הוא כמו מפתח שמסביר כיצד לקרוא את המחרוזת. אם השתמשתם בקידוד שגוי, אתם עשויים לראות במקום טקסט - רצף של קשקושים או ריבועים חלולים (שמשמעם ב"חלונות": אות שהייתה תקלה בפענוח שלה ולא יודעים כיצד לפענח).

הקידוד הבסיסי ביותר הוא קידוד בשם ASCII:
ASCII הוא קידוד 7-ביט. כלומר: כל אות יכולה להיות אחת מתוך 128 סימנים או אותיות.
32 התווים הראשונים הם control characters: טאב, רווח, מחיקה ועוד סימנים שרובם חסרי משמעות גרפית (למשל: סוף שורה). שאר התווים הם מספרים, אותיות וכמה סימני-שפה.
ישנן הרחבות לASCII של 8-ביט המוסיפות עוד כ 128 תווים המותאמים לשפות נוספות (כל הרחבה - לשפה אחרת). כלומר כל סימן/אות (character) הוא בית אחד / 8-ביט.


משפחת הסימנים הנראים של ASCII 8bit, בהתאמה לעברית (סימנים במסגרת הירוקה), מוצגים בתוכנת Character Map של "חלונות".

מיון בסיסי בשפת ג'אווה (וברוב השפות האחרות) מתבסס על הערך המספרי של האותיות במחרוזת.
כפי שניתן לראות, הערך המספרי של האות "Z" (גדולה) הוא 90, בעוד שהערך של "a" (קטנה) הוא גבוה יותר (97) - ולכן מיון יסדר מחרוזות בסדר הבא:
  1. Ant
  2. Zoo
  3. ambulance
...למרות שהיינו מצפים שהמילה ambulance תהיה במקום הראשון (סדר לקסיקוגרפי - כמו מילון).

פתרון נפוץ לאנגלית: לעשות מיון ע"פ צורת ה upperCase של המחרוזות.

אך זה לא עובד בשפות רבות אחרות....
  • בשוודית, ישנה אות הנכתבת כ ae (או סימן æ) - מקומה בסדר הלקסיקוגרפי הוא אחרי האות "z".
  • בגרמנית, כאשר האות s מופיעה ברצף פעמיים - מקצרים את הצמד לכתיבת ß (כמו ביטא). סידור ע"פ ערכי ASCII ימיין את המילה groß (שקול ל gross = "גדול") לאחר המילה grost - מכיוון שערך ה ASCII של ß גדול יותר. זו כמובן טעות צורמת לדובר גרמנית.
  • ע"פ חוקי הדקדוק של שפות אירופאיות שונות, אותיות מוטעמות ("accent") צריכות לבוא אחרי אותיות דומות לא מוטעמות. צרפתית גרועה אפילו יותר: אות מוטעמת מאוחרת היא חזקה יותר, כלומר: יש לבדוק את האותיות המוטעמות מימין לשמאל.


הצרפתים, כידוע, רגישים לשפה שלהם. לא תרכשו ידידות עם לקוח צרפתי בניסיון להסביר לו שמיון ע"פ הטעמה בחוקים הצרפתיים זו דקות מוזרה או קטנה - ושיסתדר.


אז מה עושים?
אפשר להתייאש. להגיד ללקוחות שככה זה אצלכם ושיעברו לאנגלית, אם זה מפריע להם.
אפשר לנסות ללמוד את כל הבעיות בכל השפות השונות - ולממש אלגוריתם מיון, אך לא כדאי.
אפשר להשתמש בכלי קיים להתמודד עם בעיות ה internationalization - וזה מה שאמליץ לעשות.

ג'אווה, כבר מגרסאות מוקדמות, כללה סט כלים עשיר ל i18n, רובם תחת החבילה java.text.
במשך השנים, התחזוקה והעדכון של ספריות אלו מעט הוזנח וכיום הן נחשבות עדיין טובות - אך לא "state of the art".
אם נושא ה i18n חשוב לכם באמת - הייתי ממליץ להשתמש בספריית ICU של IBM - ספריית open source בג'אווה (יש גם גרסת ++C) ל g11n המתוחזקת ברמה טובה.
יש גם רשימה של ספריות ג'אווהסקריפט לתמיכה ב i18n - אולם אין לי ניסיון עמן ואני לא יודע להמליץ.

בג`אווה, הביטוי הבא יבצע השוואה נכונה, ע"פ השפה הנתונה:


Collator.getInstance(Locale.FRENCH).compare(stringA, stringB)


ל ICU יש גרסה משלה ל Collator - מדויקת יותר.
אם אתם מתכוונים לעשות הרבה השוואות על אותה קבוצת מחרוזות (למשל: מיון תכוף), collator איננו מאוד יעיל ועדיך להשתמש ב CollatorKeys.



סיכום

טוב, כיסינו רק את נושא המיון - אך יש עוד מספר מקרים שכדאי להתייחס אליהם (חיפוש, עיבוד מחרוזות, html וכו') אותם אני מקווה לכסות בפוסט המשך.

התאמה של אפליקציה לשפות אחרות ("ריבוי-לשונות") היא לא רק "כאב ראש" ו"סיבוך נוסף" - זוהי הזדמנות להגיע לפלח שוק גדול בהרבה, מבלי להוסיף למערכת הרבה פיצ'רים. בעוד אנשי מקצוע מסוימים (למשל: מתכנתים) "מחויבים" לשלוט בשפה האנגלית, עובדים במגזרים שונים ואנשים פרטיים - לא זקוקים לכך. מעבר לכך: הם יכולים לשלוט באנגלית, אך עדיין לרצות תוכנה בשפה שלהם.
ריבוי-לשונות עשוי להיות פיצ'ר מכניס מאוד למערכת שכבר הוכיחה את עצמה.

כמובן שיש לתהליך globalization מלא יש עלות, במיוחד אם מתחילים לעשות שינויים בקוד (עבור חיפוש, מיון וכו'). כדאי להכיר את הנושאים ולהשתמש בפוסט(ים) כ reference לאלו בעיות יכולות לצוץ, ולבחור ע"פ מצב השוק - כמה רחוק ללכת.


שיהיה בהצלחה!



----

לינקים רלוונטיים:

סקירה מקיפה על פורמטים וכלים על תרגום (אם המערכת שלכם תנהל תרגום): http://edutechwiki.unige.ch/en/Software_localization
התרגומיה, בלוג עברי על תרגום :http://yaeltranslation.com



2014-03-01

הכנס להוראת הנדסת תוכנה בישראל - רשמים

רציתי לשתף כמה חוויות מכנס הנדסת התוכנה שהתקיים במכללת כנרת.
אני מפרסם פוסט זה כשבוע וחצי לאחר שהכנס התקיים - רק בגלל עומס שאני שרוי בו לאחרונה.
בתקופות כאלו קשה לי יותר "להרים את המקלדת" ולהתחיל את הכתיבה. הרבה יותר מפתה לראות איזה סרט אקשן רדוד וללכת לישון מוקדם...

אם אתם זוכרים, המצגת שהכנתי לכנס (פוסט: "תואר החלומות בהנדסת מערכות מידע") גררה לא מעט תגובות (פוסט: "הסערה") וחשפה בפני, באופן מפתיע, עד כמה הנושא בוער (לכאן או לכאן) בעצמותיהם של אנשים:
  • לחיוב, מישהו הציע לבוא ולחזק את דברי (לא הרגשתי שנזדקתתי לסיוע).
  • לשלילה, מישהו אחר הודיע שיגיע כדי לסתור ולהפריע (אני משתמש במילה זו מכיוון שהוא לא התבטא בצורה ידידותית). הוא לא הגיע בסוף.

תמונה שלקחתי בשטח המכללה. באמת נוף מהמם!

הרשמויות

אתחיל בקטנות:
  1. ראשית ראיתי קבוצה של חבר'ה (סטודנטים) דתיים ממש ליד קבוצה של חבר'ה (סטודנטים) שדיברו ערבית. אמנם זו לא הייתה שיחה משותפת - אבל הקרבה הפיסית בין הקבוצות עוררה תחושת נוחות שאני לא מורגל להתקל בה ביום-יום.
  2. הכנס כלל בעיקר אנשי אקדמיה ורק אנשי תעשייה בודדים (כך התרשמתי).
  3. אנשים מהאקדמיה סיפרו על ניכור מצד התעשייה. מישהי אחת סיפרה שכאשר היא באה לבצע מחקרים על תהליכי עבודה בארגונים היא נוהגת להציג את עצמה כשייכת לחברה x אחרת ולא לאקדמייה - וזה שיפר בצורה משמעותית את שיתוף הפעולה שהיא זוכה לו.
    אני כנראה ביום-יום שותף או תורם (מצד התעשיה) לאווירת הניכור שהיא נתקלה בה - והסיפור הזה עשה לי משהו. האם באמת התעשייה "לא ממש זקוקה" למחקר האקדמי?! אני לא חושב שזו נקודת מבט גורפת, אך בהחלט אני מכיר את נקודת המבט שרואה את המחקר האקדמי כתורם קטן לתעשיית התוכנה בכלל.
  4. הופתעתי לטובה מהרצון הרב של כמה מהמציגים ללכת בדרך לא-קלה בכדי לספק תואר יותר מעשי ויותר מועיל לסטודטים ולתעשייה. אפילו היו שם מרצים מהטכניון (!).
    למי שלא מכיר, הבדיחה המוכרת אומרת שהטכניון הוא המוסד האופטימלי לרכוש מיומנויות של למידה-עצמית: יש מבחנים קשים ומרצים גרועים - כך שהדרך היחידה לשרוד היא פשוט ללמוד לבד.
  5. שמחתי לראות פרוייקטים במכללה (כנרת, נדמה-לי) בהם דורשים מהסטודנטים לעבוד בצוות של ממש (7-8 סטודנטים כאשר כל זוג או שלישיה אחראי על מודול - ולא קל להסתדר), לעבוד עם קוד שלא הם כתבו, ולדלוור משהו - למרות שהוא לא "מושלם". זה נשמע לי כמו צעד משמעותי קדימה בהכשרת מהנדסי תוכנה.
  6. הסשן שלי עבר בשקט יחסי ובהקשבה. אוהד, אפילו, הייתי אומר. ככלל, אנשי אקדמיה הם אנשים תרבותיים :)
  7. ראיתי גם אנשי אקדמיה שנראו לי מנותקים. כאלו שטענו שזה לא תפקידה של האקדמיה לחנך מהנדסים, אלא רק חוקרים ("שילכו ללמוד במקום אחר"). או כאלו שלא ממש מבינים את כוחות השוק (פיתחו חלופה עדיפה, לכאורה, ל UML, אבל התעלמו מעוצמת הסטנדרט. כמו לשכנע אנשים לעבור מאנגלית לאספרנטו "כי זו שפה טובה יותר").
  8. למדתי שהמכללות מנסות לקדם את מעמדן של ההכשרות המקצועיות (Certifications, כמו CSDP).
    מדוע? כנראה כדי להעמיד פ'יצר בולט שאין לאוניברסיטאות ולבדל מכללות יותר טובות מאלו הפחות טובות. עבורנו, חלק מאנשי התעשייה (הפלצנים?) - כל המכללות נראות אותו הדבר.
  9. שוחחתי מעט לאחר הכנס על הרשמים שלי עם מנהלת גיוס של חברת הייטק כלשהי. היא סיפרה לי שהיא נתקלה בבוגרי מכללה (בארודה? - לא מכיר) שהועדפו ע"י בוגרי טכניון שהתמודדו על אותה המשרה. "הם פשוט היו טובים יותר" - היא אמרה.

הנוף מפריע! לא פעם תפסתי את עצמי בוהה בנוף ומאבד קשב למה שנאמר.



הנה המצגת שהצגתי - היא יצאה שונה ממה שפרסמתי פה בבלוג.
ככה זה: אני סוגר מצגות לילה לפני.


ליאור